├── core
├── lib
│ ├── deps.d.ts
│ ├── gensource.ts
│ ├── gensource.web.ts
│ ├── bit.ts
│ ├── stores
│ │ ├── index.ts
│ │ ├── readonly.ts
│ │ ├── splitwrites.ts
│ │ ├── poll.ts
│ │ ├── singlemem.ts
│ │ ├── onekey.ts
│ │ ├── ot.ts
│ │ ├── map.ts
│ │ └── opmem.ts
│ ├── iterGuard.ts
│ ├── simple.ts
│ ├── err.ts
│ ├── index.ts
│ ├── sel.ts
│ ├── typeregistry.ts
│ ├── subvalues.ts
│ ├── version.ts
│ ├── types
│ │ ├── field.ts
│ │ └── map.ts
│ └── opcache.ts
├── test
│ ├── kvmem.ts
│ ├── map.ts
│ └── router.ts
├── README.md
├── LICENSE
└── package.json
├── demos
├── bp
│ ├── output.wasm
│ ├── public
│ │ ├── icon.png
│ │ ├── Play.woff
│ │ ├── toolpanel.css
│ │ ├── editor.css
│ │ └── editor.html
│ ├── rustpng.d.ts
│ ├── rustpng_bg.js
│ ├── browserclient
│ │ ├── render.ts
│ │ └── index.ts
│ ├── rustpng.js
│ └── server.ts
├── universalclient
│ ├── README.md
│ ├── package.json
│ ├── examplestore.ts
│ ├── main.ts
│ └── public
│ │ └── index.html
├── midi
│ ├── public
│ │ └── index.html
│ ├── package.json
│ ├── state.ts
│ ├── server.ts
│ └── client.ts
├── bidirectional
│ ├── public
│ │ └── index.html
│ ├── 1.ts
│ ├── server.ts
│ └── client.ts
├── monitor
│ ├── deps.d.ts
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── monitor.ts
│ ├── dashboard.ts
│ └── server.ts
├── contentfulgraphql
│ ├── README.md
│ ├── post.tsx
│ ├── public
│ │ └── style.css
│ └── server.ts
├── text
│ ├── package.json
│ ├── public
│ │ ├── editorstyle.css
│ │ └── mdstyle.css
│ └── tsconfig.json
└── Makefile
├── package.json
├── prozess
├── README.md
├── package.json
├── prozess.ts
├── playground.ts
├── prozess-mock.ts
└── prozessops.ts
├── .gitignore
├── filestore
├── README.md
└── package.json
├── contentful
├── README.md
└── playground.ts
├── graphql
├── README.md
├── mapreduce.js
└── lib
│ └── gql.ts
├── lmdb
├── README.md
├── package.json
├── dumpdb.js
├── yarn.lock
├── playground.ts
├── deps.d.ts
├── test.ts
└── tsconfig.json
├── net
├── playground.ts
├── lib
│ ├── index.ts
│ ├── tcpserver.ts
│ ├── tcpclient.ts
│ ├── util.ts
│ ├── httpserver.ts
│ ├── wsclient.ts
│ ├── wsserver.ts
│ ├── clientservermux.ts
│ ├── tinystream.ts
│ ├── netmessages.ts
│ └── reconnectingclient.ts
├── package.json
├── README.md
├── test.ts
├── yarn.lock
└── tsconfig.json
└── foundationdb
├── LICENSE
├── package.json
├── yarn.lock
├── playground.ts
├── README.md
└── test.ts
/core/lib/deps.d.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demos/bp/output.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephg/statecraft/HEAD/demos/bp/output.wasm
--------------------------------------------------------------------------------
/demos/bp/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephg/statecraft/HEAD/demos/bp/public/icon.png
--------------------------------------------------------------------------------
/demos/bp/public/Play.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/josephg/statecraft/HEAD/demos/bp/public/Play.woff
--------------------------------------------------------------------------------
/demos/bp/rustpng.d.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | export function convert(arg0: string, arg1: number, arg2: number, arg3: number): Uint8Array;
3 |
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": ["core", "net", "lmdb", "foundationdb", "demos/text", "demos/midi", "demos/universalclient", "demos/monitor"]
4 | }
5 |
--------------------------------------------------------------------------------
/core/lib/gensource.ts:
--------------------------------------------------------------------------------
1 | import {randomBytes} from 'crypto'
2 |
3 | export default function genSource() {
4 | const sourceBytes = randomBytes(12)
5 | return sourceBytes.toString('base64')
6 | }
7 |
--------------------------------------------------------------------------------
/demos/universalclient/README.md:
--------------------------------------------------------------------------------
1 | # Universal Client
2 |
3 | This is a client which will happily connect to any backing store (over TCP or WebSockets) and expose a simple UI to view the contents of the given store
--------------------------------------------------------------------------------
/prozess/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft prozess connector
2 |
3 | This is an experimental statecraft operation log. Ignore this for now - it is not currently maintained. It will be moved out of this repo in a future commit.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .*.swp
3 | db
4 | .DS_Store
5 | rust/target
6 | rust/Cargo.lock
7 | dist
8 | bundle.js
9 | *.tmp-browserify*
10 | oldlib
11 | oldtest
12 | yarn-error.log
13 | scratch*
14 | *.cluster
15 | testdb
16 | .vscode
17 |
--------------------------------------------------------------------------------
/filestore/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft file store
2 |
3 | This provides a simple store which watches a JSON file and exposes the file's contents as a store.
4 |
5 | All writes replace the entire contents of the file (so its really not efficient), but it works great for managing application configuration.
6 |
--------------------------------------------------------------------------------
/filestore/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/filestore",
3 | "version": "0.1.0",
4 | "description": "Statecraft file store",
5 | "main": "index.js",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "dependencies": {
9 | "chokidar": "^2.1.5"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/prozess/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/prozess",
3 | "version": "0.1.0",
4 | "description": "Statecraft prozess binding",
5 | "main": "index.js",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "dependencies": {
9 | "prozess-client": "*"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/contentful/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft contentful store
2 |
3 | This is a proof-of-concept graphql-based contentful store using the graphql map reduce store.
4 |
5 | This existed at one point as part of a contentful demo. It is experimental and will either be deleted or moved into a different repository in the future.
6 |
--------------------------------------------------------------------------------
/core/lib/gensource.web.ts:
--------------------------------------------------------------------------------
1 | // require('./util')
2 | const alpha = 'abcdefghijklmnopqrstuvwxyz'
3 | const alphabet = alpha + alpha.toUpperCase() + '0123456789'
4 |
5 | export default function genSource() {
6 | let out = ''
7 | for (let i = 0; i < 12; i++) out += alphabet[(Math.random() * alphabet.length)|0]
8 | return out
9 | }
--------------------------------------------------------------------------------
/demos/midi/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MIDI demo
5 |
6 | The midi store is running. To see your midi controller state, connect the universal client:
7 |
8 |
npx ts-node demos/universalclient/main.ts tcp://localhost:2002
9 |
--------------------------------------------------------------------------------
/demos/bidirectional/public/index.html:
--------------------------------------------------------------------------------
1 |
2 | Mouse tracker
3 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/demos/monitor/deps.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module 'cpu-stats' {
3 | type CPUInfo = {
4 | cpu: number,
5 | user: number,
6 | nice: number,
7 | sys: number,
8 | idle: number,
9 | irq: number
10 | }
11 | function stats(samplems: number, cb: (err: Error, result: CPUInfo[]) => void): void;
12 | export = stats
13 | }
14 |
--------------------------------------------------------------------------------
/demos/bp/rustpng_bg.js:
--------------------------------------------------------------------------------
1 |
2 | const path = require('path').join(__dirname, 'output.wasm');
3 | const bytes = require('fs').readFileSync(path);
4 | let imports = {};
5 | imports['./rustpng'] = require('./rustpng');
6 |
7 | const wasmModule = new WebAssembly.Module(bytes);
8 | const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
9 | module.exports = wasmInstance.exports;
10 |
--------------------------------------------------------------------------------
/graphql/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft GraphQL support
2 |
3 | Eventually I want an official graphql statecraft store whereby stores can explicitly define a graphql schema and then we have a store which wraps a KV / range store, exposing that store via graphql.
4 |
5 | Currently this only really exists as a couple experiments. This code may not currently run. It will be expanded on at a later time.
6 |
--------------------------------------------------------------------------------
/demos/contentfulgraphql/README.md:
--------------------------------------------------------------------------------
1 | # Contentful Graphql demo
2 |
3 | This is a blog which uses data from contentful and serves it through a graphql rendering function.
4 |
5 | The blog content is stored in contentful, and we use the contentful streaming API to pull down changes into an in-memory store as they happen. The data is run through a map-reduce to render, and the HTML is served straight out of memory.
--------------------------------------------------------------------------------
/lmdb/README.md:
--------------------------------------------------------------------------------
1 | # LMDB Statecraft bindings
2 |
3 | This is a simple statecraft binding for [LMDB](https://symas.com/lmdb/technical/). LMDB is a simple, fast embedded database.
4 |
5 | The LDMB implementation currently does not act as an operation log. That is, the store depends on an externally provided operation log; which it subscribes to on startup. All changes are streamed into a local on-disk key-value store (lmdb).
6 |
--------------------------------------------------------------------------------
/core/lib/bit.ts:
--------------------------------------------------------------------------------
1 | // Simple helpers for bitsets (eg supported query type fields)
2 |
3 | // TODO: Consider exporting these into a separate tiny package.
4 | export const bitSet = (...bits: number[]) => {
5 | let field = 0
6 | for (let i = 0; i < bits.length; i++) field |= (1< (
11 | !!(field & (1 << bitNum))
12 | )
13 |
--------------------------------------------------------------------------------
/core/test/kvmem.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import kvmem from '../lib/stores/kvmem'
3 | import runTests from './common'
4 | import assert from 'assert'
5 |
6 | // describe('maths', () => {
7 | // it.only('Everest test', () => {
8 |
9 |
10 | // // assert(3 - 2 == 1)
11 |
12 |
13 | // // assert(3 - 3 == 0)
14 |
15 |
16 | // })
17 | // })
18 |
19 | describe('kvmem', () => {
20 | runTests(async () => kvmem())
21 | })
--------------------------------------------------------------------------------
/core/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft core
2 |
3 | This is the core statecraft repository.
4 |
5 | It contains some simple stores and combinators - enough to get started with very few dependancies.
6 |
7 | You will probably want to use the statecraft core with other modules to provide interoperability with your data sources, provide network interoperability and so on. These modules are packaged separately to reduce the weight of dependancies in your project.
8 |
9 |
--------------------------------------------------------------------------------
/demos/bp/browserclient/render.ts:
--------------------------------------------------------------------------------
1 | import html from 'nanohtml'
2 |
3 | export default function render(mimetype: string, obj: any) {
4 | switch (mimetype) {
5 | case 'image/png':
6 | case 'image/jpeg':
7 | // return html` `
8 | return html` `
9 |
10 | case 'application/json':
11 | return html`${JSON.stringify(obj, null, 2)} `
12 |
13 | default:
14 | throw Error('Unknown mimetype ' + mimetype)
15 | }
16 | }
--------------------------------------------------------------------------------
/net/playground.ts:
--------------------------------------------------------------------------------
1 | import {I, stores} from '@statecraft/core'
2 | import server from './lib/tcpserver'
3 | import remoteStore from './lib/tcpclient'
4 |
5 | const listen = async () => {
6 | const localStore = await stores.kvmem()
7 | server(localStore).listen(3334)
8 | console.log('listening on 3334')
9 | }
10 |
11 | const testNet = async () => {
12 | await listen()
13 |
14 | const store = await remoteStore(3334, 'localhost')
15 | const results = await store.fetch({type: I.QueryType.KV, q:new Set(['x'])})
16 | console.log(results)
17 | }
18 |
--------------------------------------------------------------------------------
/core/lib/stores/index.ts:
--------------------------------------------------------------------------------
1 | import opmem from './opmem'
2 | import kvmem from './kvmem'
3 | import singlemem from './singlemem'
4 |
5 | import ot from './ot'
6 | import map from './map'
7 | import onekey from './onekey'
8 | import readonly from './readonly'
9 | import router from './router'
10 | import splitwrites from './splitwrites'
11 |
12 | import poll from './poll'
13 |
14 | export default {
15 | opmem, kvmem, singlemem,
16 |
17 | // TODO: These should probably beCapitalizedLikeThis.
18 | ot, map, onekey, readonly, router, splitwrites,
19 |
20 | poll,
21 | }
--------------------------------------------------------------------------------
/contentful/playground.ts:
--------------------------------------------------------------------------------
1 |
2 | const contentful = async () => {
3 | const keys = JSON.parse(readFileSync('keys.json', 'utf8'))
4 | const ops = createContentful(await kvStore(), {
5 | space: keys.space,
6 | accessToken: keys.contentAPI,
7 |
8 | })
9 |
10 | const store = await kvStore(undefined, {inner: ops})
11 | const sub = store.subscribe({type: I.QueryType.StaticRange, q: [{
12 | low: sel(''),
13 | // low: sel('post/'),
14 | high: sel('post/\xff'),
15 | }]})
16 | // for await (const r of sub) {
17 | // console.log('results', ins(r))
18 | // }
19 | for await (const r of subValues(I.ResultType.Range, sub)) {
20 | console.log('results', ins(r))
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/demos/monitor/README.md:
--------------------------------------------------------------------------------
1 | # Monitor Demo
2 |
3 | This is a simple demo showing how statecraft can be used in a devops context.
4 |
5 | There's 2 processes:
6 |
7 | - The core server, which hosts a simple website showing a dashboard
8 | - A worker process which is installed on each client, that connects to the dashboard.
9 |
10 | To run this demo:
11 |
12 | - In one tab start the monitoring dashboard with `yarn start`
13 | - In another terminal, run `yarn run monitor`. You can also run this from another machine by setting the `HOST` environment variable to point to the machine running the dashboard.
14 |
15 | The server will listen for connections. Monitors can connect to the server and stream information about CPU usage.
--------------------------------------------------------------------------------
/core/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Joseph Gentle
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
--------------------------------------------------------------------------------
/foundationdb/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Joseph Gentle
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
--------------------------------------------------------------------------------
/net/lib/index.ts:
--------------------------------------------------------------------------------
1 | import wsserver, {wrapWebSocket} from './wsserver'
2 | import wsclient, {connect as connectToWS} from './wsclient'
3 | import tcpserver, {serveToSocket} from './tcpserver'
4 | import tcpclient, {connectToSocket, createStreams as createTCPStreams} from './tcpclient'
5 | import reconnectingclient from './reconnectingclient'
6 | import connectMux, {BothMsg} from './clientservermux'
7 | import WebSocket from 'ws'
8 |
9 | import * as tinyStream from './tinystream'
10 |
11 | export {
12 | WebSocket, wsserver, wrapWebSocket,
13 | tcpserver, serveToSocket,
14 |
15 | wsclient, connectToWS,
16 | tcpclient, connectToSocket, createTCPStreams,
17 |
18 | reconnectingclient,
19 |
20 | connectMux, BothMsg,
21 |
22 | tinyStream,
23 | }
--------------------------------------------------------------------------------
/lmdb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/lmdb",
3 | "version": "0.1.1",
4 | "main": "dist/lmdb.js",
5 | "types": "dist/lmdb.d.ts",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "files": [
9 | "dist/*",
10 | "lmdb.ts",
11 | "deps.d.ts"
12 | ],
13 | "scripts": {
14 | "prepare": "npm run build",
15 | "build": "tsc"
16 | },
17 | "dependencies": {
18 | "@josephg/resolvable": "^1.0.0",
19 | "@statecraft/core": "^0.1.1",
20 | "debug": "^4.1.1",
21 | "node-lmdb": "^0.7.0"
22 | },
23 | "devDependencies": {
24 | "@types/debug": "^4.1.3",
25 | "@types/node": "^11.13.0",
26 | "mocha": "^6.1.1",
27 | "ts-node": "^8.0.3",
28 | "typescript": "^3.4.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/lib/stores/readonly.ts:
--------------------------------------------------------------------------------
1 | // Read only store wrapper
2 | import * as I from '../interfaces'
3 |
4 | export default function readOnly | I.Store>(inner: S): S {
5 | // This is pretty messy.. :/
6 | const outer: S = {
7 | storeInfo: {
8 | ...inner.storeInfo,
9 | capabilities: {
10 | ...inner.storeInfo.capabilities,
11 | mutationTypes: new Set()
12 | }
13 | },
14 |
15 | mutate() {
16 | throw Error('Mutation not allowed')
17 | },
18 | } as any as S
19 |
20 | for (const k in inner) {
21 | if (k !== 'mutate' && k !== 'storeInfo') {
22 | const v = inner[k]
23 | if (typeof v === 'function') outer[k] = v.bind(inner)
24 | }
25 | }
26 |
27 | return outer
28 | }
--------------------------------------------------------------------------------
/demos/universalclient/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/demos-universalclient",
3 | "version": "1.0.0",
4 | "description": "Statecraft univeral debugger",
5 | "main": "main.js",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "scripts": {
9 | "build": "browserify -p tsify client.ts -o public/bundle.js"
10 | },
11 | "dependencies": {
12 | "@statecraft/core": "^0.1.0",
13 | "@statecraft/net": "^0.1.0",
14 | "@types/express": "^4.16.1",
15 | "@types/node": "^11.13.0",
16 | "choo": "^6.13.3",
17 | "express": "^4.16.4",
18 | "ot-json1": "^0.2.4",
19 | "ot-text-unicode": "^3.0.0",
20 | "ts-node": "^8.0.3",
21 | "typescript": "^3.4.2"
22 | },
23 | "devDependencies": {
24 | "browserify": "^16.2.3",
25 | "tsify": "^4.0.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demos/monitor/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/monitor-demo",
3 | "version": "1.0.0",
4 | "description": "Statecraft server monitor demo",
5 | "main": "server.ts",
6 | "license": "ISC",
7 | "scripts": {
8 | "build": "browserify -p tsify dashboard.ts -o public/bundle.js",
9 | "start": "ts-node server.ts",
10 | "start-monitor": "ts-node monitor.ts"
11 | },
12 | "dependencies": {
13 | "@statecraft/core": "*",
14 | "@statecraft/net": "*",
15 | "@types/express": "^4.16.1",
16 | "@types/node": "^11.13.0",
17 | "choo": "^6.13.3",
18 | "cpu-stats": "^1.0.0",
19 | "express": "^4.16.4"
20 | },
21 | "devDependencies": {
22 | "browserify": "^16.2.3",
23 | "tinyify": "^2.5.0",
24 | "ts-node": "^8.1.0",
25 | "tsify": "^4.0.1",
26 | "typescript": "^3.4.5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/foundationdb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/foundationdb",
3 | "version": "0.1.3",
4 | "description": "Statecraft foundationdb bindings",
5 | "main": "dist/fdb.js",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "scripts": {
9 | "prepare": "npm run build",
10 | "build": "tsc",
11 | "test": "mocha -r ts-node/register test.ts"
12 | },
13 | "files": [
14 | "dist/*",
15 | "fdb.ts"
16 | ],
17 | "dependencies": {
18 | "@statecraft/core": "^0.1.1",
19 | "msgpack-lite": "^0.1.26"
20 | },
21 | "peerDependencies": {
22 | "foundationdb": "^0.10.2"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^11.13.0",
26 | "foundationdb": "^0.10.2",
27 | "mocha": "^6.1.1",
28 | "ts-node": "^8.0.3",
29 | "typescript": "^3.4.2"
30 | },
31 | "public": true
32 | }
33 |
--------------------------------------------------------------------------------
/net/lib/tcpserver.ts:
--------------------------------------------------------------------------------
1 | import {I} from '@statecraft/core'
2 | import serve from './server'
3 | import {wrapReader, wrapWriter} from './tinystream'
4 |
5 | import net, { Socket } from 'net'
6 | import msgpack from 'msgpack-lite'
7 | import { write } from 'fs';
8 |
9 | export const serveToSocket = (store: I.Store, socket: Socket) => {
10 | const writer = msgpack.createEncodeStream()
11 | writer.pipe(socket)
12 | socket.on('end', () => {
13 | // This isn't passed through for some reason.
14 | writer.end()
15 | })
16 |
17 | const reader = msgpack.createDecodeStream()
18 | socket.pipe(reader)
19 |
20 | serve(wrapReader(reader), wrapWriter(writer), store)
21 | }
22 |
23 | export default (store: I.Store) => {
24 | return net.createServer(c => {
25 | // console.log('got connection')
26 | serveToSocket(store, c)
27 | })
28 | }
--------------------------------------------------------------------------------
/demos/midi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/midi-demo",
3 | "version": "1.0.0",
4 | "description": "Statecraft midi demo",
5 | "main": "server.ts",
6 | "author": "Seph Gentle ",
7 | "license": "ISC",
8 | "scripts": {
9 | "build": "browserify -p tsify client.ts -o public/bundle.js",
10 | "start": "ts-node server.ts"
11 | },
12 | "dependencies": {
13 | "@statecraft/core": "0.1.0",
14 | "@statecraft/net": "0.1.0",
15 | "@types/node": "^11.13.0",
16 | "@types/webmidi": "^2.0.3",
17 | "express": "^4.16.4",
18 | "ot-json1": "^0.2.4",
19 | "ts-node": "^8.0.3"
20 | },
21 | "devDependencies": {
22 | "@types/express": "^4.16.1",
23 | "@types/node": "^11.13.0",
24 | "browserify": "^16.2.3",
25 | "tinyify": "^2.5.0",
26 | "tsify": "^4.0.1",
27 | "typescript": "^3.4.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/net/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/net",
3 | "version": "0.1.1",
4 | "description": "Statecraft network server & client",
5 | "main": "dist/lib/index.js",
6 | "types": "dist/lib/index.d.js",
7 | "author": "Seph Gentle ",
8 | "license": "ISC",
9 | "scripts": {
10 | "prepare": "npm run build",
11 | "build": "tsc"
12 | },
13 | "browser": {
14 | "./dist/lib/tcpserver.js": false,
15 | "./dist/lib/wsserver.js": false,
16 | "./dist/lib/tcpclient.js": false
17 | },
18 | "dependencies": {
19 | "@statecraft/core": "^0.1.1",
20 | "@josephg/resolvable": "^1.0.0",
21 | "isomorphic-ws": "^4.0.1",
22 | "ministreamiterator": "^1.0.0",
23 | "ws": "^6.2.1"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^11.13.0",
27 | "@types/ws": "^6.0.1",
28 | "typescript": "^3.4.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/test/map.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../lib/interfaces'
2 | import 'mocha'
3 | import kvmem from '../lib/stores/kvmem'
4 | import runTests from './common'
5 | import map from '../lib/stores/map'
6 | import splitWrites from '../lib/stores/splitwrites'
7 | import assert from 'assert'
8 | import { setKV } from '../lib/simple'
9 |
10 | describe('map', () => {
11 | it('maps simple values', async () => {
12 | const root = await kvmem()
13 | const v = await setKV(root, 'x', 5)
14 |
15 | const store = map(root, x => x + 1)
16 | const result = await store.fetch({type: I.QueryType.KV, q: new Set(['x'])}, {minVersion: v})
17 | assert.strictEqual(result.results.get('x'), 6)
18 | })
19 |
20 | runTests(async () => {
21 | const store = await kvmem()
22 | const read = map(store, i => i)
23 |
24 | return splitWrites(read, store)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/lmdb/dumpdb.js:
--------------------------------------------------------------------------------
1 | const msgpack = require('msgpack-lite')
2 | const lmdb = require('node-lmdb')
3 | const {inspect} = require('util')
4 |
5 | const env = new lmdb.Env()
6 | env.open({ path: 'db', maxDbs: 2, noTls: true, })
7 | const snapDb = env.openDbi({name:'snapshots', create:true})
8 |
9 | const txn = env.beginTxn({readOnly:true})
10 |
11 | const cursor = new lmdb.Cursor(txn, snapDb)
12 |
13 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
14 | cursor.getCurrentBinary((k, v) => {
15 | if (k[0] === 1) {
16 | console.log('reserved', k.slice(1).toString('utf8'), inspect(msgpack.decode(v), {colors:true}))
17 | } else {
18 | const [lastMod, data] = msgpack.decode(v)
19 | console.log(`${k.toString('utf8')}: ${inspect(data, {colors:true})} (v ${lastMod})`)
20 | }
21 | })
22 | }
23 |
24 | cursor.close()
25 | txn.commit()
26 |
27 |
--------------------------------------------------------------------------------
/core/lib/iterGuard.ts:
--------------------------------------------------------------------------------
1 | // This is a helper for async iterators which calls the cleanup function when the iterator is returned.
2 | export default function iterGuard(inner: AsyncIterator, cleanupFn: () => void): AsyncIterableIterator {
3 | let isDone = false
4 | let donefn: (v: IteratorResult) => void
5 | const doneP = new Promise>(resolve => { donefn = resolve })
6 |
7 | const iter: AsyncIterableIterator = {
8 | next() {
9 | return isDone
10 | ? Promise.resolve({done: true, value: undefined as any as T})
11 | : Promise.race([inner.next(), doneP])
12 | },
13 | return(v: T) {
14 | isDone = true
15 | const result = {value: v, done: true}
16 | donefn(result)
17 | cleanupFn()
18 | return Promise.resolve(result)
19 | },
20 | [Symbol.asyncIterator]() { return iter }
21 | }
22 | return iter
23 | }
24 |
--------------------------------------------------------------------------------
/demos/contentfulgraphql/post.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react'
2 | import commonmark from 'commonmark'
3 |
4 | const renderMarkdown = (content: string) => {
5 | const parser = new commonmark.Parser({smart: true})
6 | const writer = new commonmark.HtmlRenderer({smart: true, safe: true})
7 |
8 | const tree = parser.parse(content)
9 | return {__html: writer.render(tree)}
10 | }
11 |
12 | export default function Post({updatedAt, title, content, author: {fullName}}: any): ReactElement {
13 | console.log('updat', updatedAt)
14 | return
15 |
16 | {title}
17 |
18 |
19 |
23 | {fullName} / {(new Date(updatedAt)).toLocaleDateString()}
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/core/lib/simple.ts:
--------------------------------------------------------------------------------
1 | // This contains some simple helpers to get / set values.
2 | import * as I from './interfaces'
3 |
4 | export const getKV = async (store: I.Store, k: I.Key): Promise => {
5 | const r = await store.fetch({type: I.QueryType.KV, q: new Set([k])})
6 | return r.results.get(k)
7 | }
8 |
9 | export const setKV = (store: I.Store, k: I.Key, v: Val): Promise => (
10 | store.mutate(I.ResultType.KV, new Map([[k, {type: 'set', data: v}]]))
11 | )
12 |
13 | export const rmKV = (store: I.Store, k: I.Key): Promise => (
14 | store.mutate(I.ResultType.KV, new Map([[k, {type: 'rm'}]]))
15 | )
16 |
17 | export const getSingle = async (store: I.Store) => (
18 | (await store.fetch({type: I.QueryType.Single, q:true})).results
19 | )
20 |
21 | export const setSingle = (store: I.Store, value: Val) => (
22 | store.mutate(I.ResultType.Single, {type: 'set', data: value})
23 | )
24 |
--------------------------------------------------------------------------------
/core/lib/stores/splitwrites.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../interfaces'
2 |
3 | // This store simply sends all reads to one store, and all writes to another.
4 | export default function splitWrites(readStore: I.Store, writeStore: I.Store): I.Store {
5 | return {
6 | storeInfo: {
7 | uid: readStore.storeInfo.uid, // TODO: Is this ok?
8 | sources: readStore.storeInfo.sources,
9 | sourceIsMonotonic: readStore.storeInfo.sourceIsMonotonic,
10 | capabilities: {
11 | queryTypes: readStore.storeInfo.capabilities.queryTypes,
12 | mutationTypes: writeStore.storeInfo.capabilities.mutationTypes,
13 | }
14 | },
15 |
16 | fetch: readStore.fetch,
17 | catchup: readStore.catchup,
18 | getOps: readStore.getOps,
19 | subscribe: readStore.subscribe,
20 |
21 | mutate: writeStore.mutate,
22 |
23 | close() {
24 | readStore.close()
25 | writeStore.close()
26 | },
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demos/text/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/text-demo",
3 | "version": "1.0.0",
4 | "description": "Statecraft collaborative text editor demo",
5 | "main": "server.ts",
6 | "license": "ISC",
7 | "scripts": {
8 | "build": "browserify -p tsify editor.ts -o public/bundle.js",
9 | "start": "ts-node server.ts"
10 | },
11 | "dependencies": {
12 | "@statecraft/core": "*",
13 | "@statecraft/net": "*",
14 | "@types/commonmark": "^0.27.3",
15 | "@types/express": "^4.16.1",
16 | "@types/fresh": "^0.5.0",
17 | "@types/jsesc": "^0.4.29",
18 | "@types/node": "^11.13.0",
19 | "commonmark": "^0.28.1",
20 | "express": "^4.16.4",
21 | "fresh": "^0.5.2",
22 | "jsesc": "^2.5.2",
23 | "msgpack-lite": "^0.1.26",
24 | "nanohtml": "^1.5.0",
25 | "ot-text-unicode": "^3.0.0"
26 | },
27 | "devDependencies": {
28 | "browserify": "^16.2.3",
29 | "tinyify": "^2.5.0",
30 | "tsify": "^4.0.1",
31 | "typescript": "^3.5.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/graphql/mapreduce.js:
--------------------------------------------------------------------------------
1 |
2 | // Given a schema..
3 |
4 | const schema = buildSchema(`
5 | type Post {
6 | title: String,
7 | content: String,
8 | author: Author,
9 | }
10 |
11 | type Author {
12 | fullName: String,
13 | }
14 |
15 |
16 | type Query {
17 | postById(id: String!): Post
18 | }
19 | `)
20 |
21 | const store = augment(kvStore(new Map([
22 | ['auth', {fullName: 'Seph'}],
23 | ['post1', {title: 'Yo', content: 'omg I am clever', author: 'auth'}],
24 | ])))
25 |
26 |
27 | // Might also need to pass a map-reduce function version number, so we know when to regenerate stuff.
28 | const fullposts = gqlMapReduce(store, schema, 'Post', gql`{title, author {fullName} }`, post => {
29 | // Or whatever. The fields named by the graphql query are tagged.
30 | return `Post ${post.title} author ${post.author.fullName}`
31 | })
32 |
33 |
34 | ////
35 |
36 | // Also need a structured way to slurp up the contents of a store into another local store. LMDB would be good here
37 |
38 |
--------------------------------------------------------------------------------
/foundationdb/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | foundationdb@^0.9.2:
6 | version "0.9.2"
7 | resolved "https://registry.yarnpkg.com/foundationdb/-/foundationdb-0.9.2.tgz#9881732ef24f32cfc20f90cad437049c19048b92"
8 | integrity sha512-3fM9Tsm82ljkGpDML8kcbbSNmCfPytjVj/+TqXqXHGCAO1dUOUkSiyvBVGrSkkJVByMbftGyeXguoApQp9cF6A==
9 | dependencies:
10 | nan "^2.10.0"
11 | node-gyp-build "^3.3.0"
12 |
13 | nan@^2.10.0:
14 | version "2.13.2"
15 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
16 | integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
17 |
18 | node-gyp-build@^3.3.0:
19 | version "3.8.0"
20 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.8.0.tgz#0f57efeb1971f404dfcbfab975c284de7c70f14a"
21 | integrity sha512-bYbpIHyRqZ7sVWXxGpz8QIRug5JZc/hzZH4GbdT9HTZi6WmKCZ8GLvP8OZ9TTiIBvwPFKgtGrlWQSXDAvYdsPw==
22 |
--------------------------------------------------------------------------------
/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@statecraft/core",
3 | "version": "0.1.2",
4 | "description": "",
5 | "main": "dist/lib/index.js",
6 | "types": "dist/lib/index.d.ts",
7 | "scripts": {
8 | "prepare": "npm run build",
9 | "test": "mocha -r ts-node/register test/*.ts",
10 | "build": "tsc"
11 | },
12 | "browser": {
13 | "./lib/gensource.ts": "./lib/gensource.web.ts",
14 | "./dist/lib/gensource.js": "./dist/lib/gensource.web.js"
15 | },
16 | "files": [
17 | "dist/lib/**",
18 | "lib/**"
19 | ],
20 | "author": "Joseph Gentle ",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@josephg/resolvable": "^1.0.0",
24 | "binary-search": "^1.3.2",
25 | "es6-error": "^4.1.1",
26 | "ministreamiterator": "^1.0.0"
27 | },
28 | "devDependencies": {
29 | "@types/mocha": "^5.2.5",
30 | "@types/msgpack-lite": "^0.1.6",
31 | "@types/node": "^10.12.5",
32 | "mocha": "^5.2.0",
33 | "ts-node": "^8.0.2",
34 | "typescript": "^3.4.1"
35 | },
36 | "public": true
37 | }
38 |
--------------------------------------------------------------------------------
/demos/universalclient/examplestore.ts:
--------------------------------------------------------------------------------
1 | // This is an example store that can be connected to by the universal client
2 |
3 | import net from 'net'
4 |
5 | import {I, stores, subValues, setKV, rmKV} from '@statecraft/core'
6 | import {tcpserver} from '@statecraft/net'
7 | // import serve from '../../lib/net/tcpserver'
8 | // import makeMap from '../../lib/stores/map'
9 |
10 | const {kvmem, map: makeMap} = stores
11 |
12 | process.on('unhandledRejection', err => {
13 | console.error((err as any).stack)
14 | process.exit(1)
15 | })
16 |
17 | ;(async () => {
18 | const store = await kvmem(new Map([
19 | ['hi', {a:3, b:4}],
20 | ['yo', {a:10, b:30}]
21 | ]))
22 |
23 | setInterval(() => {
24 | setKV(store, 'x', {a: Math.random(), b: Math.random()})
25 | }, 1000)
26 |
27 | const server = tcpserver(store)
28 | // const mapStore = makeMap(store, ({a, b}) => {
29 | // return "a + b is " + (a+b)
30 | // })
31 |
32 | // const server = tcpserver(mapStore)
33 | server.listen(2002)
34 | console.log('listening on TCP port 2002')
35 | })()
--------------------------------------------------------------------------------
/demos/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all clean watch-bp watch-text watch-bidirectional watch-monitor
2 |
3 | all: demos/text/public/bundle.js
4 | clean:
5 | rm demos/*/public/bundle.js
6 |
7 |
8 | demos/text/public/bundle.js: demos/text/*.ts lib/*.ts lib/*/*.ts
9 | npx browserify -p tsify -p tinyify demos/text/editor.ts -o $@
10 |
11 | watch-bp:
12 | npx watchify -v -p tsify demos/bp/browserclient/index.ts -o demos/bp/public/bundle.js
13 |
14 | watch-text:
15 | npx watchify -v -p tsify demos/text/editor.ts -o demos/text/public/bundle.js
16 |
17 | demos/bidirectional/public/bundle.js: demos/bidirectional/*.ts lib/*.ts lib/*/*.ts
18 | npx browserify -p tsify -p tinyify demos/bidirectional/client.ts -o $@
19 |
20 | watch-bidirectional:
21 | npx watchify -v -p tsify demos/bidirectional/client.ts -o demos/bidirectional/public/bundle.js
22 |
23 | watch-monitor:
24 | npx watchify -v -p tsify demos/monitor/dashboard.ts -o demos/monitor/public/bundle.js
25 |
26 | watch-universalclient:
27 | npx watchify -v -p tsify demos/universalclient/client.ts -o demos/universalclient/public/bundle.js
28 |
--------------------------------------------------------------------------------
/core/lib/err.ts:
--------------------------------------------------------------------------------
1 | import ExtendableError from 'es6-error'
2 |
3 | export class VersionTooOldError extends ExtendableError {}
4 | export class WriteConflictError extends ExtendableError {}
5 | export class UnsupportedTypeError extends ExtendableError {}
6 | export class AccessDeniedError extends ExtendableError {}
7 | export class InvalidDataError extends ExtendableError {}
8 | export class StoreChangedError extends ExtendableError {}
9 |
10 | const constructors = {VersionTooOldError, WriteConflictError, UnsupportedTypeError, AccessDeniedError, InvalidDataError, StoreChangedError}
11 | export default constructors
12 |
13 | export interface ErrJson {msg: string, name: string}
14 |
15 | export const errToJSON = (err: Error): ErrJson => {
16 | // console.warn('Sending error to client', err.stack)
17 | return {msg: err.message, name: err.name}
18 | }
19 | export const errFromJSON = (obj: ErrJson) => {
20 | const Con = (constructors as {[k: string]: typeof ExtendableError})[obj.name]
21 | if (Con) return new Con(obj.msg)
22 | else {
23 | const err = new Error(obj.msg)
24 | err.name = obj.name
25 | return err
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demos/monitor/public/index.html:
--------------------------------------------------------------------------------
1 |
2 | Dashboard
3 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/core/lib/stores/poll.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../interfaces'
2 | // import kvmem from './kvmem'
3 | import singlemem from './singlemem'
4 | import readonly from './readonly'
5 | import {setSingle} from '../simple'
6 |
7 | export interface PollOpts {
8 | periodMS?: number,
9 | initialVersion?: number,
10 | source?: I.Source,
11 | }
12 |
13 | const wait = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout))
14 |
15 | // Mmmmmm I wonder if this should actually also wrap a backing store to write
16 | // into...
17 | export default async function poller(
18 | poll: () => Promise,
19 | opts: PollOpts = {}): Promise> {
20 | const initial = await poll()
21 | const inner = singlemem(initial, opts.source, opts.initialVersion)
22 |
23 | const periodMS = opts.periodMS || 3000
24 | ;(async () => {
25 | while (true) {
26 | await wait(periodMS)
27 | const newVal = await poll()
28 | // console.log('..', newVal)
29 | await setSingle(inner, newVal)
30 | }
31 | })()
32 |
33 | // TODO: Read only wrapper!
34 | return readonly(inner)
35 | }
36 |
--------------------------------------------------------------------------------
/demos/text/public/editorstyle.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | background-color: #9d35db;
8 | }
9 |
10 | textarea {
11 | position: fixed;
12 | height: calc(100vh - 4em);
13 | width: calc(100vw - 4em);
14 | margin: 2em;
15 | resize: none;
16 | padding: 1.2em;
17 | border: 5px solid #0f0d6b;
18 |
19 | font-family: monospace;
20 | font-size: 16px;
21 |
22 | color: #87001d;
23 | background-color: #f9fff9;
24 | }
25 |
26 | textarea:focus {
27 | outline: none;
28 | }
29 |
30 | #connstatus {
31 | position: fixed;
32 | left: 50%;
33 | transform: translateX(-50%);
34 | padding: 4px;
35 | font-family: monospace;
36 | font-size: 16px;
37 | font-weight: bold;
38 | background-color: rgba(244, 76, 255, 0.6);
39 | }
40 |
41 | #connstatus.waiting {
42 | background-color: red;
43 | }
44 | #connstatus.connecting {
45 | background-color: yellow;
46 | }
47 |
48 | #connstatus.connected:before {
49 | content: 'Connected'
50 | }
51 | #connstatus.connecting:before {
52 | content: 'Connecting'
53 | }
54 | #connstatus.waiting:before {
55 | content: 'Not connected! Waiting to reconnect'
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/core/lib/index.ts:
--------------------------------------------------------------------------------
1 | import * as I from './interfaces'
2 | import stores from './stores'
3 | import err, {errFromJSON, errToJSON, ErrJson} from './err'
4 |
5 | import otDoc from './otdoc'
6 | import genSource from './gensource'
7 | import {getSingle, setSingle, getKV, rmKV, setKV} from './simple'
8 | import sel from './sel'
9 | import * as version from './version'
10 | import subValues, {catchupStateMachine, subResults} from './subvalues'
11 |
12 | import transaction from './transaction'
13 |
14 | import {register as registerType, supportedTypes, typeOrThrow} from './typeregistry'
15 | import {queryTypes, resultTypes, wrapQuery} from './qrtypes'
16 | import {bitHas, bitSet} from './bit'
17 |
18 | import makeSubGroup from './subgroup'
19 |
20 | export {
21 | I, // TODO: Its weird exposing this as types.
22 | stores,
23 | catchupStateMachine, subValues, subResults,
24 | err, errFromJSON, errToJSON, ErrJson,
25 | genSource,
26 | otDoc,
27 |
28 | getSingle, setSingle, getKV, rmKV, setKV,
29 | sel,
30 | version,
31 |
32 | registerType, supportedTypes, typeOrThrow,
33 | queryTypes, resultTypes, wrapQuery,
34 |
35 | transaction,
36 | makeSubGroup,
37 |
38 | bitSet, bitHas,
39 | }
--------------------------------------------------------------------------------
/core/test/router.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import kvmem from '../lib/stores/kvmem'
3 | import router, {ALL} from '../lib/stores/router'
4 | import runTests from './common'
5 |
6 | describe('router', () => {
7 | runTests(async () => {
8 | const root = await kvmem()
9 | const store = router()
10 |
11 | // This is really dumb - we're just mounting it all the way through.
12 | store.mount(root, '', ALL, '', true)
13 |
14 | // TODO: Also run all the tests with a double-mounted store
15 | // TODO: Also run all the tests via a split read / writer.
16 | return store
17 | })
18 |
19 | it('contains sources of all mounted stores')
20 | it('maps keys in fetch both ways')
21 | it('groups subscriptions based on the sources')
22 |
23 | describe('getops', () => {
24 | // Example inputs:
25 | // [[source:x, v:1]] => [source:x, v:1]
26 | // [[source:x, v:1], [source:y, v:1]] => [{source:x, v:1}, {source:y, v:1}]
27 | // [[v:{x:1, y:2}], [v:{y:1}]] => [{v:{y:1}}, {v:{x:1, y:2}}]
28 | // [[v:{x:1, y:1}], [v:{y:2}]] => [{v:{x:1, y:1}}, {v:{y:2}}]
29 | // [[v:{x:1}], [v:{x:1}]] => [v:{x:1}] with ops merged
30 | // ... And variations.
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/demos/midi/state.ts:
--------------------------------------------------------------------------------
1 | // Stolen from @types/webmidi
2 | type MIDIPortDeviceState = "disconnected" | "connected";
3 |
4 | type MIDIPortConnectionState = "open" | "closed" | "pending";
5 |
6 | export interface MIDIPort {
7 | /**
8 | * A unique ID of the port. This can be used by developers to remember ports the
9 | * user has chosen for their application.
10 | */
11 | id: string;
12 |
13 | /**
14 | * The manufacturer of the port.
15 | */
16 | manufacturer?: string;
17 |
18 | /**
19 | * The system name of the port.
20 | */
21 | name?: string;
22 |
23 | /**
24 | * The version of the port.
25 | */
26 | version?: string;
27 |
28 | /**
29 | * The state of the device.
30 | */
31 | state: MIDIPortDeviceState;
32 |
33 | /**
34 | * The state of the connection to the device.
35 | */
36 | // connection: MIDIPortConnectionState;
37 | }
38 |
39 | export interface MIDIInput extends MIDIPort {
40 | keys: {[k: string]: { // The keys are actually numbers, but :/
41 | held: boolean,
42 | pressure: number,
43 | timestamp: number
44 | }},
45 | pots: number[],
46 | sliders: number[],
47 | pitch: number,
48 | modulation: number,
49 | }
50 |
51 | export default interface State {
52 | timeOrigin: number,
53 | inputs: MIDIInput[],
54 | outputs: MIDIPort[],
55 | }
--------------------------------------------------------------------------------
/net/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft networking
2 |
3 | > *Status:* Beta. Working & tested, but I want to simplify the protocol slightly before 1.0.
4 |
5 | The goal of this module is to allow clients to transparently use remote statecraft stores as if they were local.
6 |
7 | There are currently two transports available:
8 |
9 | - TCP (generally for node <-> node communication)
10 | - WebSockets (for node <-> browser communication)
11 |
12 | (I'd also like to add an ICP based comms layer implementation here for multi-language support).
13 |
14 | With each transport one computer must act as the network server, and one must act as the client. But that decision is orthogonal to which of the two machines exposes a statecraft store to its remote.
15 |
16 | You can have:
17 |
18 | - Network server exposes a store which each client consumes. This is the most common architecture and currently the most tested & supported.
19 | - Each network client exposes a store. The server consumes all the client stores (TODO: Document how to do this)
20 | - The server and each client create and expose a store. This is useful for example to have clients expose some local state back to a governing SC server. This architecture works but is currently lacking an easy way to automatically reconnect. See the bidirectional example on how to implement this.
21 |
22 |
--------------------------------------------------------------------------------
/lmdb/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | bindings@^1.2.1:
6 | version "1.5.0"
7 | resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
8 | integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
9 | dependencies:
10 | file-uri-to-path "1.0.0"
11 |
12 | file-uri-to-path@1.0.0:
13 | version "1.0.0"
14 | resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
15 | integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
16 |
17 | nan@^2.12.0:
18 | version "2.13.2"
19 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
20 | integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
21 |
22 | node-lmdb@^0.6.2:
23 | version "0.6.2"
24 | resolved "https://registry.yarnpkg.com/node-lmdb/-/node-lmdb-0.6.2.tgz#2adc8b3907b067b25a3cfd2ea093253aada54ebd"
25 | integrity sha512-j9K0qVBSZ1YqP2hg++zxvxKL8mDYAhaprlH2VdfCI2Y+Pfr11kFXLfZ9lXs/H5gMq+Xy39zSR2Kh/DWU8+bUUQ==
26 | dependencies:
27 | bindings "^1.2.1"
28 | nan "^2.12.0"
29 |
--------------------------------------------------------------------------------
/net/test.ts:
--------------------------------------------------------------------------------
1 | // import 'mocha'
2 | // import kvStore from '../lib/stores/kvmem'
3 | // import createServer from '../lib/net/tcpserver'
4 | // import connectStore from '../lib/stores/tcpclient'
5 | // import runTests from './common'
6 | // import * as I from '../lib/interfaces'
7 | // import {AddressInfo} from 'net'
8 |
9 | // // TODO: This is currently using a simple TCP server & client and sending
10 | // // messages over msgpack. We should also test the JSON encoding path, because
11 | // // there are a bunch of errors which can show up there that we don't catch
12 | // // here. (Eg, version encoding bugs)
13 | // describe('net', () => {
14 | // const serverForStore = new WeakMap, any>()
15 | // runTests(() => new Promise((resolve, reject) => kvStore().then(store => {
16 | // const server = createServer(store)
17 | // server.listen(0, () => {
18 | // const port = (server.address() as AddressInfo).port
19 | // connectStore(port, 'localhost').then(remoteStore => {
20 | // serverForStore.set(remoteStore!, server)
21 | // resolve(remoteStore!)
22 | // }, err => {
23 | // console.error('ERROR: Could not connect to local server', err)
24 | // server.close()
25 | // return reject(err)
26 | // })
27 | // })
28 | // })), (store) => serverForStore.get(store).close())
29 |
30 | // it('returns an error if connection fails before hello is received')
31 | // })
--------------------------------------------------------------------------------
/foundationdb/playground.ts:
--------------------------------------------------------------------------------
1 | import {I} from '@statecraft/core'
2 | import fdbStore from './fdb'
3 | import {Console} from 'console'
4 | import * as fdb from 'foundationdb'
5 |
6 | fdb.setAPIVersion(600)
7 |
8 | process.on('unhandledRejection', err => {
9 | console.error(err != null ? (err as any).stack : 'error')
10 | process.exit(1)
11 | })
12 |
13 | global.console = new (Console as any)({
14 | stdout: process.stdout,
15 | stderr: process.stderr,
16 | inspectOptions: {depth: null}
17 | })
18 |
19 | const testFDB = async () => {
20 | const fdbConn = fdb.openSync().at('temp-playground') // TODO: Directory layer stuff.
21 | const store = await fdbStore(fdbConn)
22 |
23 | const sub = store.subscribe({type:I.QueryType.KV, q:new Set(['x', 'q', 'y'])}, {})
24 | ;(async () => {
25 | for await (const data of sub) {
26 | console.log('subscribe data', data)
27 | }
28 | })()
29 |
30 | const txn = new Map([['a', {type:'inc', data: 10}]])
31 | // const txn = new Map([['x', {type:'set', data: {ddd: (Math.random() * 100)|0}}]])
32 | // const txn = new Map([['q', {type:'set', data: (Math.random() * 100)|0}]])
33 | console.log('source', store.storeInfo.sources)
34 | const v = await store.mutate(I.ResultType.KV, txn)
35 | console.log('mutate cb', v)
36 |
37 | const results = await store.fetch({type:I.QueryType.AllKV, q: true})
38 | console.log('fetch results', results)
39 |
40 | store.close()
41 | }
42 |
43 | testFDB()
44 |
--------------------------------------------------------------------------------
/lmdb/playground.ts:
--------------------------------------------------------------------------------
1 | import {I, stores} from '@statecraft/core'
2 | import lmdbStore from './lmdb'
3 | import {Console} from 'console'
4 |
5 | const {opmem} = stores
6 |
7 |
8 | process.on('unhandledRejection', err => {
9 | console.error(err != null ? (err as any).stack : 'error')
10 | process.exit(1)
11 | })
12 |
13 | global.console = new (Console as any)({
14 | stdout: process.stdout,
15 | stderr: process.stderr,
16 | inspectOptions: {depth: null}
17 | })
18 |
19 |
20 |
21 | const testLmdb = async () => {
22 | const ops = opmem()
23 | // const client = await connectProzess()
24 |
25 | const store = await lmdbStore(ops, process.argv[2] || 'testdb')
26 |
27 | const sub = store.subscribe({type:I.QueryType.KV, q:new Set(['x', 'q', 'y'])}, {})
28 | ;(async () => {
29 | for await (const data of sub) {
30 | console.log('subscribe data', data)
31 | }
32 | })()
33 |
34 | const txn = new Map([['a', {type:'inc', data: 10}]])
35 | // const txn = new Map([['x', {type:'set', data: {ddd: (Math.random() * 100)|0}}]])
36 | // const txn = new Map([['q', {type:'set', data: (Math.random() * 100)|0}]])
37 | console.log('source', store.storeInfo.sources)
38 | const v = await store.mutate(I.ResultType.KV, txn, [new Uint8Array()])
39 | console.log('mutate cb', v)
40 |
41 | const results = await store.fetch({type:I.QueryType.AllKV, q: true})
42 | console.log('fetch results', results)
43 |
44 | // store.close()
45 | }
46 |
47 | testLmdb()
48 |
--------------------------------------------------------------------------------
/foundationdb/README.md:
--------------------------------------------------------------------------------
1 | # Statecraft Foundationdb store
2 |
3 | This is a simple implementation of a statecraft wrapper for the [foundationdb](https://www.foundationdb.org/) database maintained by Apple.
4 |
5 | This SC wrapper supports running a horizontally scaled set of statecraft stores all pointing to the same backing foundationdb cluster.
6 |
7 | > Status: Ready for use, but see caveats below
8 |
9 |
10 | ## Usage
11 |
12 | ```javascript
13 | const fdb = require('foundationdb')
14 | const fdbStore = require('@statecraft/foundationdb')
15 |
16 | // Configure foundationdb and create a database
17 | fdb.setAPIVersion(600)
18 | const db = fdb.openSync().at('my-prefix')
19 |
20 | ;(async () => {
21 | // Wrap the foundationdb database with statecraft!
22 | const store = await fdbStore(db)
23 |
24 | // ...
25 | // store.fetch(...), etc.
26 | })()
27 | ```
28 |
29 |
30 | ## Supported queries
31 |
32 | The foundationdb store acts as a key-value store with range support. So, you can run both standard KV queries and range queries against the store.
33 |
34 |
35 | ## Caveats
36 |
37 | - There is currently no support for deleting old operations from the operation log in foundationdb.
38 | - No performance tuning has been done yet.
39 | - Large queries are handled by single FDB transactions. This means you will run into performance issues and transaction size limits (5M by default) if you're fetching a lot of data all at once. Please file an issue if this is important to you.
40 |
41 | ## License
42 |
43 | ISC
--------------------------------------------------------------------------------
/net/lib/tcpclient.ts:
--------------------------------------------------------------------------------
1 | import {I} from '@statecraft/core'
2 | import * as N from './netmessages'
3 | import createStore from './client'
4 | import {wrapReader, wrapWriter, TinyReader, TinyWriter} from './tinystream'
5 |
6 | import net, {Socket} from 'net'
7 | import msgpack from 'msgpack-lite'
8 |
9 | const wrap = (socket: Socket): [TinyReader, TinyWriter] => {
10 | const writer = wrapWriter(socket, msgpack.encode)
11 |
12 | const readStream = msgpack.createDecodeStream()
13 | socket.pipe(readStream)
14 |
15 | const reader = wrapReader(readStream)
16 | return [reader, writer]
17 | }
18 |
19 | // We're exporting so many different variants here because they're useful for
20 | // reconnecting clients and stuff like that.
21 | export function createStreams(port: number, host: string): Promise<[TinyReader, TinyWriter]> {
22 | const socket = net.createConnection(port, host)
23 | // return wrap(socket)
24 | const pair = wrap(socket)
25 |
26 | return new Promise((resolve, reject) => {
27 | socket.on('connect', () => resolve(pair))
28 | socket.on('error', reject)
29 | })
30 | }
31 |
32 | export function connectToSocket(socket: Socket): Promise> {
33 | const [reader, writer] = wrap(socket)
34 | return createStore(reader, writer)
35 | }
36 |
37 | export default function(port: number, host: string): Promise> {
38 | const socket = net.createConnection(port, host)
39 | return connectToSocket(socket)
40 | }
--------------------------------------------------------------------------------
/foundationdb/test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import {I} from '@statecraft/core'
3 | import fdbStore from './fdb'
4 |
5 | // TODO: Its a little gross depending on the common tests like this. It would be
6 | // better to pull this suite out into yet another module.
7 | import runTests from '@statecraft/core/dist/test/common'
8 |
9 | import * as fdb from 'foundationdb'
10 |
11 | fdb.setAPIVersion(600)
12 |
13 | const TEST_PREFIX = '__test'
14 | let _dbid = 1
15 | const storeDb = new Map, fdb.Database>()
16 |
17 | const create = async () => {
18 | const prefix = TEST_PREFIX + _dbid++
19 | const db = fdb.openSync().at(prefix)
20 | await db.clearRange('', Buffer.from([0xff])) // Hope there's no bugs in this one!
21 | const store = await fdbStore(db)
22 | // console.log('got store at prefix', prefix)
23 | storeDb.set(store, db)
24 | return store
25 | }
26 |
27 | const teardown = (store: I.Store) => { // teardown. Nuke it.
28 | const db = storeDb.get(store)!
29 | storeDb.delete(store)
30 | store.close()
31 | db.clearRange('', Buffer.from([0xff]))
32 | // TODO: And close the database.
33 | }
34 |
35 | describe('fdb', () => {
36 | try {
37 | const db = fdb.openSync()
38 | // db.close()
39 | } catch (e) {
40 | console.warn('Warning: Foundationdb instance not found. Skipping foundationdb tests. Error:', e.message)
41 | return
42 | }
43 |
44 | // it('hi', async () => {
45 | // const store = await create()
46 | // setTimeout(() => teardown(store), 10)
47 | // })
48 | runTests(create, teardown)
49 | })
--------------------------------------------------------------------------------
/core/lib/sel.ts:
--------------------------------------------------------------------------------
1 | import {StaticKeySelector as Sel, Key} from './interfaces'
2 |
3 | const sel = (k: Key, isAfter: boolean = false): Sel => ({k, isAfter})
4 |
5 | // A selector and a key can never be equal.
6 | const kLtSel = sel.kLt = (k: Key, s: Sel) => k < s.k || (k === s.k && s.isAfter)
7 | sel.kGt = (k: Key, s: Sel) => !kLtSel(k, s)
8 |
9 | sel.kWithin = (k: Key, s: Sel, e: Sel) => !kLtSel(k, s) && kLtSel(k, e)
10 |
11 | const selLtSel = sel.ltSel = (a: Sel, b: Sel) => a.k < b.k || (a.k === b.k && !a.isAfter && b.isAfter)
12 | const selGtSel = sel.gtSel = (a: Sel, b: Sel) => selLtSel(b, a)
13 | sel.LtESel = (a: Sel, b: Sel) => !selLtSel(b, a)
14 | sel.GtESel = (a: Sel, b: Sel) => !selLtSel(a, b)
15 |
16 | const minSel = sel.min = (a: Sel, b: Sel): Sel => (
17 | (a.k < b.k) ? a
18 | : (a.k > b.k) ? b
19 | : {k: a.k, isAfter: a.isAfter && b.isAfter}
20 | )
21 | const maxSel = sel.max = (a: Sel, b: Sel): Sel => (
22 | (a.k > b.k) ? a
23 | : (a.k < b.k) ? b
24 | : {k: a.k, isAfter: a.isAfter || b.isAfter}
25 | )
26 |
27 | sel.intersect = (as: Sel, ae: Sel, bs: Sel, be: Sel): [Sel, Sel] | null => {
28 | const start = maxSel(as, bs)
29 | const end = minSel(ae, be)
30 | return selLtSel(end, start) ? null : [start, end]
31 | }
32 |
33 | sel.union = (as: Sel, ae: Sel, bs: Sel, be: Sel): [Sel, Sel] | null => {
34 | // If the ranges aren't touching / overlapping, the returned union is invalid.
35 | return (selLtSel(ae, bs) || selLtSel(be, as)) ? null : [minSel(as, bs), maxSel(ae, be)]
36 | }
37 |
38 | sel.addPrefix = (prefix: string, s: Sel): Sel => ({k: prefix + s.k, isAfter: s.isAfter})
39 |
40 | export default sel
--------------------------------------------------------------------------------
/core/lib/typeregistry.ts:
--------------------------------------------------------------------------------
1 | // This is the master operation type documents.
2 | //
3 | // This is used for interpretting transactions sent to the server through
4 | // mutate() and transactions sent to client subscriptions.
5 | //
6 | // It implements set and remove, and forwards the actual changes to
7 | // child op types, which can be registered using the register function below.
8 | import {Type, AnyOTType} from './interfaces'
9 |
10 | export const typeRegistry: {[name: string]: AnyOTType} = {}
11 | export const supportedTypes = new Set(['rm', 'set'])
12 |
13 | export function register(type: AnyOTType) {
14 | typeRegistry[type.name] = type
15 | supportedTypes.add(type.name)
16 | }
17 |
18 | // register(require('../common/rangeops'))
19 | // register(require('../common/setops'))
20 |
21 |
22 |
23 | // The 'inc' type is a tiny dummy type.
24 | register({
25 | name: 'inc',
26 | create(data) { return data|0 },
27 | apply(snapshot, op) { // Op is a number
28 | console.log('inc apply', snapshot, op)
29 | if (/*typeof snapshot === 'object' ||*/ typeof op !== 'number') throw Error('Invalid data')
30 | return (snapshot|0) + op
31 | },
32 | })
33 |
34 | export function typeOrThrow(typeName: string): AnyOTType {
35 | const type = typeRegistry[typeName]
36 | if (!type) throw Error('Unsupported type ' + typeName)
37 | return type
38 | }
39 |
40 |
41 | // TODO: + Register string, JSON, etc.
42 |
43 |
44 | // I'm just going to export the utilities with the type. Its .. not ideal, but
45 | // there's no clear line between what should be part of the result set type and
46 | // what is just utility methods. This'll be fine for now.
47 |
--------------------------------------------------------------------------------
/net/lib/util.ts:
--------------------------------------------------------------------------------
1 | import {I, queryTypes} from '@statecraft/core'
2 | import * as N from './netmessages'
3 |
4 | export const queryToNet = (q: I.Query | I.ReplaceQuery): N.NetQuery => {
5 | if (q.type === I.QueryType.Single || q.type === I.QueryType.AllKV) return q.type
6 | else return [q.type, queryTypes[q.type].toJSON(q.q)]
7 | }
8 |
9 | export const queryFromNet = (nq: N.NetQuery): I.Query | I.ReplaceQuery => {
10 | if (typeof nq === 'number') return ({type: nq} as I.Query)
11 | else {
12 | const [qtype, data] = nq
13 | return {
14 | type: qtype,
15 | q: queryTypes[qtype].fromJSON(data)
16 | } as I.Query
17 | }
18 | }
19 |
20 | // I'd really much prefer to use base64 encoding here, but the browser doesn't
21 | // have a usable base64 encoder and they aren't small.
22 | // Msgpack handles this fine, but I also want it to work over JSON. Eh.. :/
23 | export const versionToNet = (v: I.Version): N.NetVersion => Array.from(v)
24 | export const versionFromNet = (v: N.NetVersion): I.Version => Uint8Array.from(v)
25 |
26 | export const versionRangeToNet = ({from, to}: I.VersionRange): N.NetVersionRange => [versionToNet(from), versionToNet(to)]
27 | export const versionRangeFromNet = (val: N.NetVersionRange): I.VersionRange => ({from: versionFromNet(val[0]), to: versionFromNet(val[1])})
28 |
29 | export const fullVersionToNet = (vs: I.FullVersion): N.NetFullVersion => vs.map(v => v ? versionToNet(v) : null)
30 | export const fullVersionFromNet = (nvs: N.NetFullVersion): I.FullVersion => nvs.map(nv => nv ? versionFromNet(nv) : null)
31 |
32 | export const fullVersionRangeToNet = (vs: I.FullVersionRange): N.NetFullVersionRange => (
33 | vs.map(v => v ? versionRangeToNet(v) : null)
34 | )
35 | export const fullVersionRangeFromNet = (nvs: N.NetFullVersionRange): I.FullVersionRange => (
36 | nvs.map(nv => nv ? versionRangeFromNet(nv) : null)
37 | )
38 |
--------------------------------------------------------------------------------
/net/lib/httpserver.ts:
--------------------------------------------------------------------------------
1 | import {I, bitHas} from '@statecraft/core'
2 | import http from 'http'
3 |
4 | const id = (x: T) => x
5 |
6 | export interface HttpOpts {
7 | // Defaults to id.
8 | urlToKey?: (url: string) => I.Key,
9 |
10 | // Defaults to JSON stringifying everything.
11 | valToHttp?: (val: any, key: I.Key, req: http.IncomingMessage) => {
12 | mimeType: string,
13 | content: string | Buffer,
14 | }
15 | }
16 |
17 | const defaultOpts: HttpOpts = {
18 | urlToKey: id,
19 | valToHttp: (val: any) => ({
20 | mimeType: 'application/json',
21 | content: JSON.stringify({success: val != null, value: val})
22 | })
23 | }
24 |
25 | // This function should really return an express / connect Router or
26 | // something.
27 | export default function handler(store: I.Store, optsIn?: HttpOpts) {
28 | if (!bitHas(store.storeInfo.capabilities.queryTypes, I.QueryType.KV)) {
29 | throw Error('Httpserver needs kv support')
30 | }
31 |
32 | const opts = Object.assign({}, defaultOpts, optsIn)
33 | // const urlToKey = (opts && opts.urlToKey) ? opts.urlToKey : id
34 | // const valToHttp = (opts && opts.valToHttp) ? opts.valToHttp : toJson
35 |
36 | return async (req: http.IncomingMessage, res: http.ServerResponse) => {
37 | const key = opts.urlToKey!(req.url!)
38 | const result = await store.fetch({type: I.QueryType.KV, q: new Set([key])})
39 | const value = result.results.get(key)
40 | // console.log('key', key, 'value', value, result)
41 |
42 | // TODO: Clean this up.
43 | // res.setHeader('x-sc-version', JSON.stringify(result.versions))
44 |
45 | // TODO: Add etag. Steal code from text demo for it.
46 |
47 | const {mimeType, content} = opts.valToHttp!(value, key, req)
48 |
49 | res.setHeader('content-type', mimeType)
50 | res.writeHead(value == null ? 404 : 200)
51 | res.end(content)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/graphql/lib/gql.ts:
--------------------------------------------------------------------------------
1 | // import {graphql, buildSchema, GraphQLFieldResolver} from 'graphql'
2 | import * as gql from 'graphql'
3 | import * as gqlTools from 'graphql-tools'
4 |
5 | import kvStore from './stores/kvmem'
6 | import doTxn, {Transaction} from './transaction'
7 | import express from 'express'
8 | import graphqlHTTP from 'express-graphql'
9 | import {inspect} from 'util'
10 |
11 | process.on('unhandledRejection', err => { throw err })
12 |
13 |
14 |
15 | const schema = gqlTools.makeExecutableSchema({
16 | typeDefs: `
17 | type Post {
18 | title: String,
19 | content: String,
20 | author: Author,
21 | }
22 |
23 | type Author {
24 | fullName: String,
25 | }
26 |
27 | type Query {
28 | postById(id: String!): Post
29 | foo: String,
30 | }
31 | `,
32 | resolvers: {
33 | Post: {
34 | author(post, args, {txn}: {txn: Transaction}) {
35 | return txn.get('authors/' + post.author)
36 | }
37 | },
38 |
39 | Query: {
40 | postById: async (self, {id}: {id: string}, {txn}: {txn: Transaction}) => {
41 | console.log('id', id)
42 | const post = await txn.get('posts/' + id)
43 | console.log('post', post)
44 | return post
45 | }
46 | }
47 | }
48 | })
49 |
50 |
51 |
52 |
53 | ;(async () => {
54 | const store = await kvStore(new Map([
55 | ['authors/auth', {fullName: 'Seph'}],
56 | ['posts/post1', {title: 'Yo', content: 'omg I am clever', author: 'auth'}],
57 | ]))
58 |
59 | const res = await doTxn(store, async txn => (
60 | await gql.graphql(schema, `{postById(id: "post1") {title, author {fullName}}}`, null, {txn})
61 | ))
62 | console.log(inspect(res, {depth: null, colors: true}))
63 |
64 | // const app = express()
65 | // app.use('/graphql', graphqlHTTP({
66 | // graphiql: true,
67 | // schema,
68 | // rootValue: root,
69 | // }))
70 | })()
71 |
--------------------------------------------------------------------------------
/lmdb/deps.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module 'node-lmdb' {
3 | interface Database {
4 | close(): void
5 | }
6 |
7 | type Key = Buffer | string | number
8 |
9 | interface Txn {
10 | getString(dbi: Database, key: Key): string
11 | getStringUmsafe(dbi: Database, key: Key): string
12 | getBinary(dbi: Database, key: Key): Buffer
13 | getBinaryUnsafe(dbi: Database, key: Key): Buffer
14 | getNumber(dbi: Database, key: Key): number
15 | getBoolean(dbi: Database, key: Key): boolean
16 |
17 | putBinary(dbi: Database, key: Key, val: Buffer): void
18 | putString(dbi: Database, key: Key, val: string): void
19 | putString(dbi: Database, key: Key, val: number): void
20 | putString(dbi: Database, key: Key, val: boolean): void
21 |
22 | del(dbi: Database, key: Key): void
23 | abort(): void
24 | commit(): void
25 | }
26 |
27 | class Env {
28 | open(opts: {
29 | path?: string,
30 | maxDbs?: number,
31 | noTls?: boolean,
32 | }): void
33 |
34 | openDbi(opts: {
35 | name: string | null,
36 | create?: boolean,
37 | }): Database
38 |
39 | beginTxn(opts?: {readOnly?: boolean}): Txn
40 | close(): void
41 | }
42 |
43 | class Cursor {
44 | constructor(txn: Txn, dbi: Database)
45 |
46 | goToFirst(): Key | null
47 | goToLast(): Key | null
48 | goToKey(k: Key): Key | null // Go to k. Returns null if not found.
49 | goToRange(k: Key): Key | null // Go to the next key >= k.
50 | goToNext(): Key | null
51 | goToPrev(): Key | null
52 |
53 | getCurrentString(): string
54 | getCurrentNumber(): number
55 | getCurrentBoolean(): boolean
56 | getCurrentBinary(): Buffer
57 |
58 | // Valid only until next put() or txn closed.
59 | getCurrentStringUnsafe(): string
60 | getCurrentBinaryUnsafe(): Buffer
61 |
62 | // Weirdly no put methods.
63 | del(): void
64 |
65 | close(): void
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/net/lib/wsclient.ts:
--------------------------------------------------------------------------------
1 | import {I} from '@statecraft/core'
2 | import * as N from './netmessages'
3 | import createStore from './client'
4 | import {TinyReader, TinyWriter, onMsg} from './tinystream'
5 | import WebSocket from 'isomorphic-ws'
6 |
7 | // const wsurl = `ws${window.location.protocol.slice(4)}//${window.location.host}/ws`
8 |
9 | export const connect = (wsurl: string): Promise<[TinyReader, TinyWriter]> => new Promise((resolve, reject) => {
10 | const ws = new WebSocket(wsurl)
11 |
12 | const reader: TinyReader = {buf: [], isClosed: false}
13 | ws.onmessage = (msg) => {
14 | const data = JSON.parse(msg.data.toString())
15 | // console.log('received', data)
16 | onMsg(reader, data)
17 | }
18 |
19 | ws.onclose = () => {
20 | console.warn('---- WEBSOCKET CLOSED ----')
21 | reader.isClosed = true
22 | reader.onClose && reader.onClose()
23 | }
24 |
25 | const writer: TinyWriter = {
26 | write(data) {
27 | if (ws.readyState === ws.OPEN) {
28 | // console.log('sending', data)
29 | ws.send(JSON.stringify(data))
30 | } else {
31 | // On regular connections this won't happen because the first
32 | // message sent is server -> client. So we can't send anything until
33 | // the connection is open anyway. Its a problem with clientservermux though
34 | console.log('websocket message discarded because ws closed')
35 | }
36 | },
37 | close() {
38 | ws.close()
39 | },
40 | }
41 |
42 | ws.onopen = () => {
43 | console.log('ws opened')
44 | resolve([reader, writer])
45 | }
46 | ws.onerror = (e) => {
47 | console.error('ws error', e)
48 | reject(e)
49 | }
50 | })
51 |
52 | // TODO: Implement automatic reconnection and expose a simple server
53 | // describing the connection state
54 | export default async function(wsurl: string): Promise> {
55 | const [r, w] = await connect(wsurl)
56 | return createStore(r, w)
57 | }
--------------------------------------------------------------------------------
/prozess/prozess.ts:
--------------------------------------------------------------------------------
1 | // I'm not sure the best way to structure this. This is some utility functions for interacting
2 | // with prozess stores.
3 |
4 | import * as I from './interfaces'
5 | import {Event, PClient, SubCbData, VersionConflictError} from 'prozess-client'
6 | import msgpack from 'msgpack-lite'
7 | import err from './err'
8 | import {V64, v64ToNum} from './version'
9 |
10 | export const encodeTxn = (txn: I.KVTxn, meta: I.Metadata) => msgpack.encode([Array.from(txn), meta])
11 | export const decodeTxn = (data: Buffer): [I.KVTxn, I.Metadata] => {
12 | const [txn, meta] = msgpack.decode(data)
13 | return [new Map>(txn), meta]
14 | }
15 |
16 | // TODO: This should work with batches.
17 | export const decodeEvent = (event: Event, source: I.Source): I.TxnWithMeta => {
18 | const [txn, meta] = decodeTxn(event.data)
19 | return { versions: [V64(event.version)], txn, meta }
20 | }
21 |
22 | export function sendTxn(client: PClient,
23 | txn: I.KVTxn,
24 | meta: I.Metadata,
25 | expectedVersion: I.Version,
26 | opts: object): Promise {
27 | const data = encodeTxn(txn, meta)
28 |
29 | // TODO: This probably doesn't handle a missing version properly.
30 | // TODO: Add read conflict keys through opts.
31 | return client.send(data, {
32 | // targetVersion: expectedVersion === -1 ? -1 : expectedVersion + 1,
33 | targetVersion: v64ToNum(expectedVersion) + 1,
34 | conflictKeys: Array.from(txn.keys()),
35 | }).then(V64).catch(e => {
36 | // console.warn('WARNING: prozess detected conflict', e.stack)
37 | return Promise.reject(e instanceof VersionConflictError
38 | ? new err.WriteConflictError(e.message)
39 | : e
40 | )
41 | })
42 | }
43 |
44 | // Returns ops in SC style - range (from, to].
45 | // export function getOps(client: PClient,
46 | // from: I.Version,
47 | // to: I.Version,
48 | // callback: I.Callback) {
49 | // client.getEvents(from + 1, to, {}, callback)
50 | // }
51 |
--------------------------------------------------------------------------------
/demos/bidirectional/1.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../../lib/interfaces'
2 | import * as N from '../../lib/net/netmessages'
3 | import singleStore from '../../lib/stores/singlemem'
4 | import {inspect} from 'util'
5 |
6 | import {TinyReader, TinyWriter, wrapReader, wrapWriter} from '../../lib/net/tinystream'
7 | import connectMux from '../../lib/net/clientservermux'
8 | // import createServer from '../../lib/net/tcpserver'
9 | // import connectStore from '../../lib/stores/tcpclient'
10 |
11 | process.on('unhandledRejection', err => { throw err })
12 |
13 | const store1 = singleStore('store1')
14 | const store2 = singleStore('store2')
15 |
16 | const createPair = (): [TinyReader, TinyWriter] => {
17 | const reader: TinyReader = {buf: [], isClosed: false}
18 | const writer: TinyWriter = {
19 | write(msg) {
20 | // console.log('msg', msg)
21 | process.nextTick(() => reader.onMessage!(msg))
22 | },
23 | close() {},
24 | }
25 | return [reader, writer]
26 | }
27 |
28 | type BothMsg = N.CSMsg | N.SCMsg
29 | const [r1, w2] = createPair()
30 | const [r2, w1] = createPair()
31 |
32 | ;(async () => {
33 | const remoteStore2P = connectMux(r1, w1, store1, true)
34 | const remoteStore1P = connectMux(r2, w2, store2, false)
35 |
36 | const [remoteStore2, remoteStore1] = await Promise.all([remoteStore2P, remoteStore1P])
37 |
38 | console.log('store1', await remoteStore1.fetch({type: I.QueryType.Single, q:true}))
39 | console.log('store2', await remoteStore2.fetch({type: I.QueryType.Single, q:true}))
40 |
41 | const sub = remoteStore1.subscribe({type: I.QueryType.Single, q: true}, {})
42 | const results = await sub.next()
43 | console.log('initial', results.value)
44 |
45 | ;(async () => {
46 | for await (const data of sub) {
47 | console.log('subscribe data', inspect(data, false, 10, true))
48 | }
49 | })()
50 |
51 | const v = await remoteStore1.mutate(I.ResultType.Single, {type:'set', data: {x: 10}})
52 | console.log('mutate run', v)
53 |
54 | })()
55 |
--------------------------------------------------------------------------------
/net/lib/wsserver.ts:
--------------------------------------------------------------------------------
1 | import {I} from '@statecraft/core'
2 | import serve from './server'
3 | import {Writable} from 'stream'
4 | import {TinyReader, TinyWriter, wrapWriter, onMsg} from './tinystream'
5 | import WebSocket from 'ws'
6 | import {IncomingMessage} from 'http'
7 | import { CSMsg, SCMsg } from './netmessages';
8 |
9 | const isProd = process.env.NODE_ENV === 'production'
10 |
11 | export const wrapWebSocket = (socket: WebSocket): [TinyReader, TinyWriter] => {
12 | // TODO: Consider using the code in wsclient instead to convert a socket to a reader/writer pair.
13 | const reader: TinyReader = {buf: [], isClosed: false}
14 |
15 | socket.on("message", data => {
16 | // if (!isProd) console.log('C->S', data)
17 | onMsg(reader, JSON.parse(data as any))
18 | })
19 |
20 | // I could just go ahead and make a TinyWriter, but this handles
21 | // backpressure.
22 | const writer = new Writable({
23 | objectMode: true,
24 | write(data, _, callback) {
25 | // if (!isProd) console.log('S->C', data)
26 | if (socket.readyState === socket.OPEN) {
27 | // TODO: Should this pass the callback? Will that make backpressure
28 | // work?
29 | socket.send(JSON.stringify(data))
30 | }
31 | callback()
32 | },
33 | })
34 |
35 | socket.on('close', () => {
36 | writer.end() // Does this help??
37 | reader.isClosed = true
38 | reader.onClose && reader.onClose()
39 | })
40 |
41 | return [reader, wrapWriter(writer)]
42 | }
43 |
44 | export const serveWS = (wsOpts: WebSocket.ServerOptions, store: I.Store | ((ws: WebSocket, msg: IncomingMessage) => I.Store | undefined | null)) => {
45 | const getStore = typeof store === 'function' ? store : () => store
46 |
47 | const wss = new WebSocket.Server(wsOpts)
48 |
49 | wss.on('connection', (socket, req) => {
50 | const [reader, writer] = wrapWebSocket(socket)
51 | const store = getStore(socket, req)
52 | if (store != null) serve(reader, writer, store)
53 | })
54 |
55 | return wss
56 | }
57 |
58 | export default serveWS
--------------------------------------------------------------------------------
/demos/text/public/mdstyle.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | body {
7 | margin: 0 auto;
8 | /*background-color: #ffd1fa;*/
9 | max-width: 40em;
10 | }
11 |
12 | #content {
13 |
14 | }
15 |
16 | /*! Typebase.less v0.1.0 | MIT License */
17 | /* Setup */
18 | html {
19 | /* Change default typefaces here */
20 | font-family: serif;
21 | /*font-size: 137.5%;*/
22 | font-size: 115.5%;
23 | color: #1e1e1e;
24 | -webkit-font-smoothing: antialiased;
25 | }
26 | /* Copy & Lists */
27 | p {
28 | line-height: 1.5rem;
29 | margin-top: 1.5rem;
30 | margin-bottom: 0;
31 | }
32 | ul,
33 | ol {
34 | margin-top: 1.5rem;
35 | margin-bottom: 1.5rem;
36 | }
37 | ul li,
38 | ol li {
39 | line-height: 1.5rem;
40 | }
41 | ul ul,
42 | ol ul,
43 | ul ol,
44 | ol ol {
45 | margin-top: 0;
46 | margin-bottom: 0;
47 | }
48 | blockquote {
49 | line-height: 1.5rem;
50 | margin-top: 1.5rem;
51 | margin-bottom: 1.5rem;
52 | }
53 | /* Headings */
54 | h1,
55 | h2,
56 | h3,
57 | h4,
58 | h5,
59 | h6 {
60 | /* Change heading typefaces here */
61 | font-family: sans-serif;
62 | margin-top: 1.5rem;
63 | margin-bottom: 0;
64 | line-height: 1.5rem;
65 | }
66 | h1 {
67 | font-size: 4.242rem;
68 | line-height: 4.5rem;
69 | margin-top: 3rem;
70 | }
71 | h2 {
72 | font-size: 2.828rem;
73 | line-height: 3rem;
74 | margin-top: 3rem;
75 | }
76 | h3 {
77 | font-size: 1.414rem;
78 | }
79 | h4 {
80 | font-size: 0.707rem;
81 | }
82 | h5 {
83 | font-size: 0.4713333333333333rem;
84 | }
85 | h6 {
86 | font-size: 0.3535rem;
87 | }
88 | /* Tables */
89 | table {
90 | margin-top: 1.5rem;
91 | border-spacing: 0px;
92 | border-collapse: collapse;
93 | }
94 | table td,
95 | table th {
96 | padding: 0;
97 | line-height: 33px;
98 | }
99 | /* Code blocks */
100 | code {
101 | vertical-align: bottom;
102 | }
103 | /* Leading paragraph text */
104 | .lead {
105 | font-size: 1.414rem;
106 | }
107 | /* Hug the block above you */
108 | .hug {
109 | margin-top: 0;
110 | }
111 |
--------------------------------------------------------------------------------
/demos/bp/public/toolpanel.css:
--------------------------------------------------------------------------------
1 | .toolpanel {
2 | position: fixed;
3 | left: 0;
4 | right: 0;
5 | bottom: 0;
6 | text-align: center;
7 | padding-bottom: 26px;
8 | color: white;
9 |
10 | -webkit-user-select: none;
11 | -moz-user-select: none;
12 | -ms-user-select: none;
13 | cursor: default;
14 | /*background-color: hsl(184, 49%, 7%);*/
15 | pointer-events: none;
16 | }
17 |
18 | .toolpanel > div {
19 | width: 80px;
20 | margin: 0px;
21 | display: inline-block;
22 | text-align: center;
23 | text-transform: uppercase;
24 | border-left: 2px hsl(0, 100%, 50%);
25 | cursor: pointer;
26 | pointer-events: auto;
27 | }
28 |
29 | .toolpanel > div:not(:first-child) {
30 | margin-left: 6px;
31 | }
32 | .toolpanel > div:not(:last-child) {
33 | margin-right: 6px;
34 | }
35 |
36 | .toolpanel > div:not(.selected) {
37 | box-shadow: 0px 8px hsl(0, 0%, 47%);
38 | padding: 20px 0px 20px 0px;
39 | position: relative;
40 | }
41 | .toolpanel > div:not(.selected)::after {
42 | content: '';
43 | z-index: 1;
44 | position: absolute;
45 | left:0;
46 | top: 0;
47 | width: 80px;
48 | height: 70px;
49 |
50 | /*background-color: hsla(0, 0%, 0%, 0.31);*/
51 | }
52 |
53 | .toolpanel > div.selected {
54 | /*box-shadow: 0px -8px white;*/
55 | padding: 20px 0px 20px 0px;
56 | position: relative;
57 | top: 8px;
58 | }
59 |
60 | .toolpanel > #move { color: black; background-color: hsl(44, 87%, 52%); }
61 |
62 | .toolpanel > #nothing { color: black; background-color: white; }
63 | .toolpanel > #thinsolid { color: black; background-color: hsl(0, 0%, 71%); }
64 | .toolpanel > #solid { background-color: hsl(184, 49%, 7%); }
65 | .toolpanel > #bridge { background-color: hsl(208, 78%, 47%); }
66 |
67 | .toolpanel > #positive { background-color: #5ECC5E; }
68 | .toolpanel > #negative { background-color: #D65A2B; }
69 |
70 | .toolpanel > #shuttle { background-color: #9429BF; }
71 | .toolpanel > #thinshuttle { color: black; background-color: hsl(283, 89%, 75%); }
72 |
73 | .toolpanel > #glue { background-color: #af8145; }
74 | .toolpanel > #cut { color: black; background-color: #d2c1ac; }
75 |
--------------------------------------------------------------------------------
/lmdb/test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha'
2 | import * as I from '../lib/interfaces'
3 |
4 | import fs from 'fs'
5 | import assert from 'assert'
6 |
7 | import createMock from './prozess-mock'
8 | import {PClient} from 'prozess-client'
9 | import lmdb from '../lib/stores/lmdb'
10 | import prozessOps from '../lib/stores/prozessops'
11 | import runTests from './common'
12 |
13 | const rmdir = (path: string) => {
14 | //console.log('rmdir path', path)
15 | require('child_process').exec('rm -r ' + path)
16 | }
17 |
18 | const pathOfDb = new Map
19 | let _dbid = 1
20 |
21 | process.on('exit', function() {
22 | for (let p of pathOfDb.values()) {
23 | rmdir(p)
24 | }
25 | })
26 |
27 | const create = async () => {
28 | let path: string
29 | do {
30 | // path = __dirname + '/_test' + _dbid++
31 | path = '_test' + _dbid++
32 | } while (fs.existsSync(path))
33 |
34 | const store = await lmdb(prozessOps(createMock()), path)
35 | pathOfDb.set(store, path)
36 | return store
37 | }
38 |
39 | const teardown = (store: I.Store) => { // teardown. Nuke it.
40 | const path = pathOfDb.get(store)
41 | rmdir(path)
42 | pathOfDb.delete(store)
43 | }
44 |
45 | describe('prozess mock', () => {
46 | beforeEach(function() {
47 | this.client = createMock()
48 | })
49 |
50 | it('conflicts at the right time', async function() {
51 | const client = this.client as PClient
52 | const base = await client.getVersion()
53 | await client.send("hi", {conflictKeys: ['a']})
54 |
55 | try {
56 | await client.send("hi", {
57 | conflictKeys: ['a'],
58 | targetVersion: 1 + base, // should conflict with a 1, pass with a 2.
59 | })
60 | } catch (e) {
61 | assert(e)
62 | }
63 |
64 | // Ok like this.
65 | await client.send("hi", {
66 | conflictKeys: ['a'],
67 | targetVersion: 2 + base, // should conflict with a 1, pass with a 2.
68 | })
69 | })
70 | })
71 |
72 | describe('lmdb on prozess', () => {
73 | it('supports two stores pointed to the same prozess backend')
74 | it('catches up on missing operations from prozess')
75 |
76 | runTests(create, teardown)
77 | })
--------------------------------------------------------------------------------
/demos/monitor/monitor.ts:
--------------------------------------------------------------------------------
1 | import cpuStats from 'cpu-stats'
2 | import net from 'net'
3 | import os from 'os'
4 |
5 | import {stores, setSingle} from '@statecraft/core'
6 | import {serveToSocket} from '@statecraft/net'
7 |
8 | process.on('unhandledRejection', err => {
9 | console.error((err as any).stack)
10 | process.exit(1)
11 | })
12 |
13 | type CPU = {user: number, sys: number}
14 | type ClientInfo = {
15 | hostname: string,
16 | cpus: CPU[]
17 | }
18 |
19 | ;(async () => {
20 | const port = process.env.PORT || 3003
21 | const host = process.env.HOST || 'localhost'
22 |
23 | const localStore = stores.singlemem(null)
24 |
25 | let listening = false
26 | const listen = async () => {
27 | listening = true
28 |
29 | // We want to keep trying to reconnect here, so the dashboard can be restarted.
30 | while (true) {
31 | console.log('Connecting to monitoring server at', host)
32 | const socket = net.createConnection(+port, host)
33 | serveToSocket(localStore, socket)
34 |
35 | socket.on('connect', () => {console.log('connected')})
36 |
37 | await new Promise(resolve => {
38 | socket.on('error', err => {
39 | console.warn('Error connecting to monitoring server', err.message)
40 | })
41 | socket.on('close', hadError => {
42 | // console.log('closed', hadError)
43 | resolve()
44 | })
45 | })
46 |
47 | // Wait 3 seconds before trying to connect again.
48 | await new Promise(resolve => setTimeout(resolve, 3000))
49 | }
50 | }
51 |
52 | const hostname = os.hostname()
53 |
54 | const pollCpu = () => {
55 | cpuStats(1000, async (err, results) => {
56 | if (err) throw err // crash.
57 |
58 | process.stdout.write('.')
59 |
60 | const cpus = results.map(({cpu, user, sys}) => ({user, sys}))
61 | const info = {hostname, cpus}
62 | await setSingle(localStore, info)
63 |
64 | // We'll only connect after we have some data, to save on null checks.
65 | if (!listening) listen()
66 |
67 | pollCpu()
68 | })
69 | }
70 |
71 | pollCpu()
72 | })()
--------------------------------------------------------------------------------
/prozess/playground.ts:
--------------------------------------------------------------------------------
1 |
2 | const connectProzess = (): Promise => new Promise((resolve, reject) => {
3 | const client = reconnecter(9999, 'localhost', err => {
4 | if (err) reject(err)
5 | else resolve(client)
6 | }) as PClient
7 | })
8 |
9 | const testProzess = async () => {
10 | const client = await connectProzess()
11 | const store = prozessOps(client)
12 |
13 | const sub = store.subscribe({type: I.QueryType.AllKV, q: true})
14 | ;(async () => {
15 | for await (const cu of sub) {
16 | console.log('cu', cu)
17 | }
18 | })
19 |
20 | const txn = new Map([['x', {type:'set', data: {x: 10}}]])
21 | const v = await store.mutate(I.ResultType.KV, txn, [new Uint8Array()])
22 | console.log('mutate cb', v)
23 | }
24 |
25 |
26 | const retryTest = async () => {
27 | const concurrentWrites = 10
28 |
29 | const client = await connectProzess()
30 | // const localStore = augment(await lmdbStore(prozessOps(client), process.argv[2] || 'testdb'))
31 | const localStore = await kvStore()
32 |
33 | // Using net to give it a lil' latency.
34 | // const s = server(localStore).listen(3334)
35 | // const store = await remoteStore(3334, 'localhost')
36 |
37 | const store = localStore
38 |
39 | await store.mutate(I.ResultType.KV, setSingle('x', 0))
40 | // await db.set('x', numToBuf(0))
41 |
42 | let txnAttempts = 0
43 | const sync = await Promise.all(new Array(concurrentWrites).fill(0).map((_, i) => (
44 | doTxn(store, async txn => {
45 | const val = await txn.get('x')
46 | txn.set('x', val+1)
47 | return txnAttempts++
48 | })
49 | )))
50 |
51 | const result = await doTxn(store, txn => txn.get('x'))
52 | assert.strictEqual(result, concurrentWrites)
53 |
54 | // This doesn't necessarily mean there's an error, but if there weren't
55 | // more attempts than there were increments, the database is running
56 | // serially and this test is doing nothing.
57 | assert(txnAttempts > concurrentWrites)
58 |
59 | console.log('attempts', txnAttempts)
60 | console.log(sync)
61 |
62 | client.close()
63 | store.close()
64 | // s.close()
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/demos/bp/browserclient/index.ts:
--------------------------------------------------------------------------------
1 | // This is the main entrypoint for the in-browser viewer of live data.
2 |
3 | import html from 'nanohtml'
4 | import render from './render'
5 |
6 | // Should be able to use an alias here, but its broken in tsify for some reason.
7 | import * as I from '../../../lib/interfaces'
8 | import connect from '../../../lib/stores/wsclient'
9 | import fieldOps from '../../../lib/types/field'
10 | import onekey from '../../../lib/stores/onekey'
11 |
12 |
13 | declare const config: {
14 | mimetype: string,
15 | key: string,
16 | initialValue: any,
17 | initialVersions: I.FullVersion,
18 | }
19 |
20 | const container = document.getElementById('content')
21 | if (container == null) throw Error('Could not find document #content div')
22 | const setObj = (data: any) => {
23 | console.log('setobj', data)
24 | if (typeof data === 'object' && data.type === 'Buffer') {
25 | const blob = new Blob([new Uint8Array(data.data)], {type: config.mimetype})
26 | data = URL.createObjectURL(blob)
27 | }
28 |
29 | const replace = (elem: HTMLElement | null) => {
30 | while (container.firstChild) container.removeChild(container.firstChild);
31 | if (elem != null) {
32 | container.appendChild(elem)
33 | }
34 | }
35 | const result = data == null ? null : render(config.mimetype, data)
36 | if (result instanceof HTMLImageElement) {
37 | if (result.complete) replace(result)
38 | else result.onload = () => replace(result)
39 | } else {
40 | replace(result)
41 | }
42 | }
43 |
44 | type World = any
45 |
46 | ;(async () => {
47 | const store = onekey(await connect('ws://localhost:2000/'), config.key)
48 |
49 | const sub = store.subscribe({type: I.QueryType.Single, q: true}, {
50 | fromVersion: config.initialVersions,
51 | })
52 |
53 | let last: any = null
54 |
55 | // const r = new Map()
56 | for await (const update of sub) { // TODO
57 | if (update.replace) {
58 | const val = update.replace.with
59 | setObj(val)
60 | last = val
61 | }
62 |
63 | update.txns.forEach(txn => {
64 | last = fieldOps.apply(last, txn.txn as I.SingleTxn)
65 | setObj(last)
66 | })
67 | }
68 | })()
--------------------------------------------------------------------------------
/net/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/events@*":
6 | version "3.0.0"
7 | resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
8 | integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
9 |
10 | "@types/node@*", "@types/node@^11.13.0":
11 | version "11.13.0"
12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.0.tgz#b0df8d6ef9b5001b2be3a94d909ce3c29a80f9e1"
13 | integrity sha512-rx29MMkRdVmzunmiA4lzBYJNnXsW/PhG4kMBy2ATsYaDjGGR75dCFEVVROKpNwlVdcUX3xxlghKQOeDPBJobng==
14 |
15 | "@types/ws@^6.0.1":
16 | version "6.0.1"
17 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28"
18 | integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==
19 | dependencies:
20 | "@types/events" "*"
21 | "@types/node" "*"
22 |
23 | async-limiter@~1.0.0:
24 | version "1.0.0"
25 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
26 | integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
27 |
28 | isomorphic-ws@^4.0.1:
29 | version "4.0.1"
30 | resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
31 | integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
32 |
33 | typescript@^3.4.2:
34 | version "3.4.2"
35 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.2.tgz#9ed4e6475d906f589200193be056f5913caed481"
36 | integrity sha512-Og2Vn6Mk7JAuWA1hQdDQN/Ekm/SchX80VzLhjKN9ETYrIepBFAd8PkOdOTK2nKt0FCkmMZKBJvQ1dV1gIxPu/A==
37 |
38 | ws@^6.2.1:
39 | version "6.2.1"
40 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
41 | integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
42 | dependencies:
43 | async-limiter "~1.0.0"
44 |
--------------------------------------------------------------------------------
/demos/monitor/dashboard.ts:
--------------------------------------------------------------------------------
1 | import choo from 'choo'
2 | import html from 'choo/html'
3 |
4 | import {I, subValues} from '@statecraft/core'
5 | import {reconnectingclient, connectToWS} from '@statecraft/net'
6 |
7 | type CPU = {user: number, sys: number}
8 | type ClientInfo = {
9 | ip?: string,
10 | hostname: string,
11 | connected: boolean,
12 | cpus?: CPU[]
13 | }
14 |
15 | const machine = (val: ClientInfo) => {
16 | console.log('mach', val)
17 | return html`
18 | ${val.hostname}
19 | ${val.ip}
20 | ${val.cpus && val.cpus.map(({user, sys}, i) => html`
21 |
22 |
23 |
24 | `)}
25 |
`
26 | }
27 |
28 | const mainView = (state: any) => {
29 | const machines = state.machines as Map
30 | // console.log('state', state)
31 | return html`
32 |
33 | Statecraft CPU monitoring dashboard
34 |
35 |
36 | ${Array.from(machines.values()).map(machine)}
37 |
38 |
39 | `
40 | }
41 |
42 | (async () => {
43 | const wsurl = `ws${window.location.protocol.slice(4)}//${window.location.host}/ws/`
44 | console.log('connecting to ws', wsurl, '...')
45 | const [statusStore, storeP] = reconnectingclient(() => connectToWS(wsurl))
46 |
47 | ;(async () => {
48 | for await (const status of subValues(I.ResultType.Single, statusStore.subscribe({type:I.QueryType.Single, q:true}))) {
49 | console.log('status', status)
50 | }
51 | })()
52 |
53 | const store = await storeP
54 | console.log('connected to', store.storeInfo.uid)
55 |
56 | const app = new choo()
57 | app.use((state, emitter) => {
58 | state.machines = new Map()
59 | ;(async () => {
60 | for await (const val of subValues(I.ResultType.KV, store.subscribe({type: I.QueryType.AllKV, q: true}))) {
61 | console.log('val', val)
62 | state.machines = val
63 | emitter.emit('render')
64 | }
65 | })()
66 | })
67 | app.route('/', mainView)
68 | app.mount('body')
69 |
70 | })()
71 |
--------------------------------------------------------------------------------
/core/lib/stores/singlemem.ts:
--------------------------------------------------------------------------------
1 | // This is a simple single value in-memory store.
2 | import * as I from '../interfaces'
3 | import fieldOps from '../types/field'
4 | import genSource from '../gensource'
5 | import err from '../err'
6 | import {V64, vCmp} from '../version'
7 | import {bitSet} from '../bit'
8 | import makeOpCache from '../opcache'
9 | import makeSubGroup from '../subgroup'
10 |
11 | const capabilities = {
12 | queryTypes: bitSet(I.QueryType.Single),
13 | mutationTypes: bitSet(I.ResultType.Single),
14 | // ops: 'none',
15 | }
16 |
17 | export default function singleStore(initialValue: Val, source: I.Source = genSource(), initialVersionNum: number = 0): I.Store {
18 | let version: number = initialVersionNum
19 | let data = initialValue
20 |
21 | const fetch: I.FetchFn = async (query, opts) => {
22 | if (query.type !== I.QueryType.Single) throw new err.UnsupportedTypeError()
23 |
24 | return {
25 | results: data,
26 | queryRun: query,
27 | versions: [{from:V64(version), to:V64(version)}],
28 | }
29 | }
30 |
31 | const opcache = makeOpCache()
32 | const subGroup = makeSubGroup({initialVersion: [V64(version)], fetch, getOps: opcache.getOps})
33 |
34 | const store: I.Store = {
35 | storeInfo: {
36 | uid: `single(${source})`,
37 | capabilities,
38 | sources: [source],
39 | sourceIsMonotonic: [true],
40 | },
41 |
42 | fetch,
43 | getOps: opcache.getOps,
44 |
45 | subscribe: subGroup.create.bind(subGroup),
46 |
47 | async mutate(type, _txn, versions, opts = {}) {
48 | if (type !== I.ResultType.Single) throw new err.UnsupportedTypeError()
49 | const txn = _txn as I.Op
50 |
51 | const expectv = versions && versions[0]
52 | const currentv = V64(version)
53 | if (expectv != null && vCmp(expectv, currentv) < 0) throw new err.VersionTooOldError()
54 |
55 | if (txn) data = fieldOps.apply(data, txn)
56 | const newv = V64(++version)
57 |
58 | opcache.onOp(0, currentv, newv, I.ResultType.Single, txn, opts.meta || {})
59 | subGroup.onOp(0, currentv, [{txn, meta: opts.meta || {}, versions: [newv]}])
60 | return [newv]
61 | },
62 |
63 | close() {},
64 | }
65 |
66 | return store
67 | }
68 |
--------------------------------------------------------------------------------
/net/lib/clientservermux.ts:
--------------------------------------------------------------------------------
1 | // This implements a mux server which symmetrically both exposes and consumes
2 | // a store.
3 | import {I} from '@statecraft/core'
4 | import * as N from './netmessages'
5 |
6 | import createStore from './client'
7 | import serve from './server'
8 |
9 | import {TinyReader, TinyWriter, onMsg} from './tinystream'
10 |
11 | export type BothMsg = N.CSMsg | N.SCMsg
12 |
13 | // Passes invariants inv(0) != 0, inv(inv(x)) == x.
14 | const inv = (x: number) => -x-1
15 |
16 | export default function connectMux(reader: TinyReader, writer: TinyWriter, localStore: I.Store, symmetry: boolean): Promise> {
17 | // To mux them both together I'm going to use the .ref property, which is on
18 | // all net messages except the hello message.
19 |
20 | // There's 4 streams going on here:
21 | // - Local store reader (c->s) / writer (s->c)
22 | // - Remote store reader (s->c) / writer (c->s)
23 |
24 | // One reader/writer pair will have all the .ref properties in messages
25 | // inverted based on symmetry property. If symmetry is true, we invert the
26 | // local store messages. If its false, we invert the remote store messages.
27 |
28 | const localReader = {} as TinyReader
29 | localReader.buf = []
30 |
31 | // If either writer closes, we'll close the whole tunnel.
32 | const localWriter: TinyWriter = {
33 | write(msg) {
34 | if (msg.a !== N.Action.Hello && symmetry) msg.ref = inv(msg.ref)
35 | writer.write(msg)
36 | },
37 | close() { writer.close() },
38 | }
39 |
40 | const remoteReader: TinyReader = {buf: [], isClosed: false}
41 | const remoteWriter: TinyWriter = {
42 | write(msg) {
43 | if (!symmetry) msg.ref = inv(msg.ref)
44 | writer.write(msg)
45 | },
46 | close() { writer.close() },
47 | }
48 |
49 | reader.onMessage = msg => {
50 | if (msg.a === N.Action.Hello) onMsg(remoteReader, msg)
51 | else {
52 | const neg = msg.ref < 0
53 | if (neg) msg.ref = inv(msg.ref)
54 | if (neg !== symmetry) onMsg(remoteReader, msg as N.SCMsg)
55 | else onMsg(localReader, msg as N.CSMsg)
56 | }
57 | }
58 |
59 | serve(localReader, localWriter, localStore)
60 | return createStore(remoteReader, remoteWriter)
61 | }
62 |
--------------------------------------------------------------------------------
/demos/bidirectional/server.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../../lib/interfaces'
2 | import express from 'express'
3 | import WebSocket from 'ws'
4 | import http from 'http'
5 |
6 | import {wrapWebSocket} from '../../lib/net/wsserver'
7 | import kvMem from '../../lib/stores/kvmem'
8 | // import singleMem, {setSingle} from '../../lib/stores/singlemem'
9 | import connectMux, { BothMsg } from '../../lib/net/clientservermux'
10 | import subValues from '../../lib/subvalues'
11 | import { rmKV, setKV } from '../../lib/kv';
12 | import serveTCP from '../../lib/net/tcpserver'
13 |
14 | process.on('unhandledRejection', err => {
15 | console.error(err.stack)
16 | process.exit(1)
17 | })
18 |
19 | ;(async () => {
20 | type Pos = {x: number, y: number}
21 | // type DbVal = {[id: string]: Pos}
22 |
23 | // The store is a kv store mapping from client ID (incrementing numbers) => latest position.
24 | const store = await kvMem()
25 |
26 | const app = express()
27 | app.use(express.static(`${__dirname}/public`))
28 |
29 | const server = http.createServer(app)
30 | const wss = new WebSocket.Server({server})
31 |
32 | let nextId = 1000
33 |
34 | wss.on('connection', async (socket, req) => {
35 | const id = `${nextId++}`
36 |
37 | const [reader, writer] = wrapWebSocket(socket)
38 | const remoteStore = await connectMux(reader, writer, store, false)
39 |
40 | // console.log(id, 'info', remoteStore.storeInfo)
41 | console.log(id, 'client connected')
42 | const sub = remoteStore.subscribe({type: I.QueryType.Single, q: true})
43 |
44 | reader.onClose = () => {
45 | console.log(id, 'client gone')
46 | // delete db[id]
47 | // console.log('db', db)
48 | rmKV(store, id)
49 | }
50 |
51 | for await (const val of subValues(I.ResultType.Single, sub)) {
52 | // console.log(id, 'cu', val)
53 | // await setKV(store, id, {x: val.x, y: val.y})
54 | await setKV(store, id, val)
55 | // console.log('db', db)
56 | }
57 | })
58 |
59 | const port = process.env.PORT || 2222
60 | server.listen(port, (err: any) => {
61 | if (err) throw err
62 | console.log('listening on', port)
63 | })
64 |
65 |
66 | if (process.env.NODE_ENV !== 'production') {
67 | const tcpServer = serveTCP(store)
68 | tcpServer.listen(2002, 'localhost')
69 | console.log('Debugging server listening on tcp://localhost:2002')
70 | }
71 | })()
--------------------------------------------------------------------------------
/net/lib/tinystream.ts:
--------------------------------------------------------------------------------
1 | // I'm not using node streams because they add about 150k of crap to the browser
2 | // bundle.
3 |
4 | // This is the simplest possible API around a stream that we can use in server
5 | // & client code.
6 |
7 | // TODO: Consider using streamToIter here, which we're already pulling in to
8 | // the client anyway.
9 | import {Readable, Writable} from 'stream'
10 |
11 | export interface TinyReader {
12 | buf: Msg[] // For messages before onmessage has been set
13 | onMessage?: (msg: Msg) => void
14 | onClose?: () => void
15 | isClosed: boolean,
16 | // In case of error, destroy the stream.
17 | destroy?(e?: Error): void,
18 | }
19 |
20 | export interface TinyWriter {
21 | write(data: Msg): void,
22 | close(): void,
23 | // In case of error, destroy the stream.
24 | destroy?(e?: Error): void,
25 | }
26 |
27 | export const wrapReader = (r: Readable) => {
28 | const reader: TinyReader = {buf: [], isClosed: false}
29 | let destroyed = false
30 |
31 | r.on('data', msg => {
32 | if (destroyed) return
33 |
34 | if (reader.onMessage) reader.onMessage!(msg as any as Msg)
35 | else reader.buf.push(msg)
36 | })
37 | r.on('end', () => {
38 | reader.isClosed = true
39 | if (reader.onClose) reader.onClose()
40 | })
41 | r.on('error', err => {
42 | // I think this'll hit end right afterwards, so we don't have to do
43 | // anything here. I think.
44 | console.warn('socket error', err)
45 | })
46 | reader.destroy = err => {
47 | r.destroy()
48 | destroyed = true
49 | }
50 |
51 | return reader
52 | }
53 |
54 | export const listen = (r: TinyReader, listener: (msg: Msg) => void) => {
55 | r.buf.forEach(listener)
56 | r.buf.length = 0
57 | r.onMessage = listener
58 | }
59 | export const onMsg = (r: TinyReader, msg: Msg) => {
60 | if (r.onMessage) r.onMessage(msg)
61 | else r.buf.push(msg)
62 | }
63 |
64 | export const wrapWriter = (w: Writable, encode?: (msg: Msg) => any) => {
65 | return {
66 | write(data) {
67 | if (w.writable) {
68 | w.write(encode ? encode(data) : data)
69 |
70 | // Work around this bug:
71 | // https://github.com/kawanet/msgpack-lite/issues/80
72 | if ((w as any).encoder) (w as any).encoder.flush()
73 | }
74 | },
75 | close() {
76 | w.end()
77 | },
78 | destroy(err) {
79 | // Not passing the error through because if we do it'll emit an error in the
80 | // stream.
81 | w.destroy()
82 | }
83 | } as TinyWriter
84 | }
85 |
--------------------------------------------------------------------------------
/demos/bp/rustpng.js:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | var wasm;
3 |
4 | const TextEncoder = require('util').TextEncoder;
5 |
6 | let cachedTextEncoder = new TextEncoder('utf-8');
7 |
8 | let cachegetUint8Memory = null;
9 | function getUint8Memory() {
10 | if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
11 | cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
12 | }
13 | return cachegetUint8Memory;
14 | }
15 |
16 | function passStringToWasm(arg) {
17 |
18 | const buf = cachedTextEncoder.encode(arg);
19 | const ptr = wasm.__wbindgen_malloc(buf.length);
20 | getUint8Memory().set(buf, ptr);
21 | return [ptr, buf.length];
22 | }
23 |
24 | function getArrayU8FromWasm(ptr, len) {
25 | return getUint8Memory().subarray(ptr / 1, ptr / 1 + len);
26 | }
27 |
28 | let cachedGlobalArgumentPtr = null;
29 | function globalArgumentPtr() {
30 | if (cachedGlobalArgumentPtr === null) {
31 | cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
32 | }
33 | return cachedGlobalArgumentPtr;
34 | }
35 |
36 | let cachegetUint32Memory = null;
37 | function getUint32Memory() {
38 | if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
39 | cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
40 | }
41 | return cachegetUint32Memory;
42 | }
43 | /**
44 | * @param {string} arg0
45 | * @param {number} arg1
46 | * @param {number} arg2
47 | * @param {number} arg3
48 | * @returns {Uint8Array}
49 | */
50 | module.exports.convert = function(arg0, arg1, arg2, arg3) {
51 | const [ptr0, len0] = passStringToWasm(arg0);
52 | const retptr = globalArgumentPtr();
53 | try {
54 | wasm.convert(retptr, ptr0, len0, arg1, arg2, arg3);
55 | const mem = getUint32Memory();
56 | const rustptr = mem[retptr / 4];
57 | const rustlen = mem[retptr / 4 + 1];
58 |
59 | const realRet = getArrayU8FromWasm(rustptr, rustlen).slice();
60 | wasm.__wbindgen_free(rustptr, rustlen * 1);
61 | return realRet;
62 |
63 |
64 | } finally {
65 | wasm.__wbindgen_free(ptr0, len0 * 1);
66 |
67 | }
68 |
69 | };
70 |
71 | const TextDecoder = require('util').TextDecoder;
72 |
73 | let cachedTextDecoder = new TextDecoder('utf-8');
74 |
75 | function getStringFromWasm(ptr, len) {
76 | return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
77 | }
78 |
79 | module.exports.__wbindgen_throw = function(ptr, len) {
80 | throw new Error(getStringFromWasm(ptr, len));
81 | };
82 |
83 | wasm = require('./rustpng_bg');
84 |
--------------------------------------------------------------------------------
/core/lib/subvalues.ts:
--------------------------------------------------------------------------------
1 | // This is a helper method for yielding the values out of a subscription, and
2 | // ignoring everything else.
3 | import * as I from './interfaces'
4 | import {resultTypes} from './qrtypes'
5 |
6 | /**
7 | * This is a state machine which can process catchup objects. Each time the
8 | * object transitions, the transition function returns the new state.
9 | *
10 | * TODO: Consider finding a way to return the initial value when the object is
11 | * first created
12 | *
13 | * @param type The expected result type of the incoming catchup data
14 | * @returns Transition function
15 | */
16 | export const catchupStateMachine = (type: I.ResultType) => {
17 | const rtype = resultTypes[type]
18 | let value = rtype.create()
19 | const versions: I.FullVersionRange = []
20 |
21 | return (update: I.CatchupData): I.FetchResults => {
22 | if (update.replace) {
23 | // TODO: Should we be pulling update.replace.versions in here?
24 |
25 | // console.log('replace', value, update.replace)
26 | value = rtype.updateResults(value, update.replace.q, update.replace.with)
27 | // console.log('->', value)
28 | update.replace.versions.forEach((v, si) => {
29 | if (v != null) versions[si] = {from:v, to:v}
30 | })
31 | }
32 |
33 | for (const txn of update.txns) {
34 | // This is like this because I haven't implemented apply for range results
35 | if (rtype.applyMut) rtype.applyMut!(value, txn.txn)
36 | else (value = rtype.apply(value, txn.txn))
37 |
38 | txn.versions.forEach((v, si) => {
39 | // The version *must* exist already in versions.
40 | if (v) versions[si]!.from = v
41 | })
42 | }
43 |
44 | update.toVersion.forEach((v, si) => {
45 | if (v != null) {
46 | if (versions[si] == null) versions[si] = {from:v, to:v}
47 | else versions[si]!.to = v
48 | }
49 | })
50 |
51 | return {results: value!, versions}
52 | }
53 | }
54 |
55 | // TODO: Refactor things so we don't need a ResultType passed in here.
56 | export async function* subResults(type: I.ResultType, sub: I.Subscription) {
57 | const u = catchupStateMachine(type)
58 | for await (const update of sub) {
59 | // console.log('upd', update)
60 | yield {...u(update), raw: update}
61 | }
62 | }
63 |
64 | export default async function* subValues(type: I.ResultType, sub: I.Subscription) {
65 | const u = catchupStateMachine(type)
66 | for await (const update of sub) {
67 | yield u(update).results // A Val if type is single, or a container of Vals
68 | }
69 | }
--------------------------------------------------------------------------------
/demos/universalclient/main.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import http from 'http'
3 | import net from 'net'
4 | import url from 'url'
5 | import {Console} from 'console'
6 |
7 | import {I, sel, subValues, registerType} from '@statecraft/core'
8 | import {tcpclient, createTCPStreams, wsclient, connectToWS, reconnectingclient, wsserver} from '@statecraft/net'
9 |
10 | // import {type as texttype} from 'ot-text-unicode'
11 | // import {type as jsontype} from 'ot-json1'
12 |
13 | // registerType(texttype)
14 | // registerType(jsontype)
15 |
16 | process.on('unhandledRejection', err => {
17 | console.error((err as any).stack)
18 | process.exit(1)
19 | })
20 |
21 | global.console = new (Console as any)({
22 | stdout: process.stdout,
23 | stderr: process.stderr,
24 | inspectOptions: {depth: null}
25 | })
26 |
27 | ;(async () => {
28 | const urlStr = process.argv[2]
29 | if (urlStr == null) throw Error('Missing URL argument')
30 |
31 | console.log(urlStr)
32 |
33 | const type = (urlStr.startsWith('ws://') || urlStr.startsWith('wss://')) ? 'ws'
34 | : urlStr.startsWith('tcp://') ? 'tcp'
35 | : null
36 | if (type == null) throw Error('URL must start with ws:// or tcp://')
37 | const {hostname, port} = url.parse(urlStr)
38 | if (hostname == null) throw Error('invalid URL')
39 |
40 | // TODO: Pick a default port number for statecraft
41 | // const store = type === 'ws' ? await wsClient(urlStr)
42 | // : await tcpClient(+(port || 3000), host!)
43 | while (true) {
44 | const [status, storeP, uidChanged] = reconnectingclient(type === 'ws'
45 | ? () => connectToWS(urlStr)
46 | : () => createTCPStreams(+(port || 3000), hostname)
47 | )
48 | const store = await storeP
49 |
50 | // TODO: Listen to status...
51 |
52 | console.log('got store', store.storeInfo)
53 |
54 | // Ok, now host the client app.
55 | const app = express()
56 | app.use(express.static(`${__dirname}/public`))
57 |
58 | const webServer = http.createServer(app)
59 |
60 | const wss = wsserver({server: webServer}, store)
61 |
62 | const listenPort = process.env.PORT || 3333
63 | webServer.listen(listenPort, () => {
64 | console.log('http server listening on', listenPort)
65 | })
66 |
67 | await uidChanged.catch(() => {})
68 | console.log('UID changed. Re-hosting.')
69 |
70 | // Closing the server entirely forces all ws clients to reconnect
71 | await new Promise(resolve => wss.close(resolve))
72 | await new Promise(resolve => webServer.close(resolve))
73 | }
74 |
75 | // const sub = store.subscribe({type: 'static range', q: [{low: sel(''), high: sel('\xff')}]})
76 | // for await (const cu of subValues('range', sub)) {
77 | // console.log('cu', cu)
78 | // }
79 | })()
--------------------------------------------------------------------------------
/demos/monitor/server.ts:
--------------------------------------------------------------------------------
1 | // This is the monitoring server / host.
2 |
3 | // It exposes 2 endpoints:
4 | // - TCP client for monitoring daemons to connect
5 | // - HTTP listener + websockets for the monitoring app
6 |
7 | import express from 'express'
8 | import http from 'http'
9 | import net from 'net'
10 |
11 | import {I, stores, subValues, setKV, rmKV} from '@statecraft/core'
12 | import {connectToSocket, wsserver} from '@statecraft/net'
13 |
14 | process.on('unhandledRejection', err => {
15 | console.error((err as any).stack)
16 | process.exit(1)
17 | })
18 |
19 | ;(async () => {
20 | type CPU = {user: number, sys: number}
21 | type ClientInfo = {
22 | ip?: string,
23 | hostname: string,
24 | connected: boolean,
25 | cpus?: CPU[]
26 | }
27 |
28 | const store = await stores.kvmem()
29 |
30 | // 1. Setup the TCP server to listen for incoming clients
31 |
32 | // We sort of want to do the opposite of what tcpserver does - instead of
33 | // serving a store, we want to consume stores from incoming network
34 | // connections.
35 | const tcpserver = net.createServer(async socket => {
36 | console.log('got client')
37 | const ip = socket.remoteAddress
38 |
39 | // Set to the client's hostname once the client first sends us information
40 | let id: string | null = null
41 |
42 | const remoteStore = await connectToSocket(socket)
43 | const sub = remoteStore.subscribe({type: I.QueryType.Single, q: true})
44 |
45 | let lastVal: ClientInfo | null = null
46 | for await (const _val of subValues(I.ResultType.Single, sub)) {
47 | const val = _val as ClientInfo
48 | val.ip = ip
49 | if (id == null) id = val.hostname
50 | val.connected = true
51 | // console.log('val', val)
52 | console.log('update from', id)
53 |
54 | lastVal = val
55 | await setKV(store, id, val)
56 | }
57 |
58 | socket.on('close', async () => {
59 | console.log('closed', id)
60 | // if (id != null) await rmKV(store, id)
61 | if (lastVal != null) {
62 | lastVal.connected = false
63 | delete lastVal.cpus
64 | await setKV(store, id!, lastVal)
65 | }
66 | })
67 | })
68 | tcpserver.listen(3003)
69 | console.log('tcp server listening on port 3003')
70 |
71 |
72 |
73 | // 2. Set up the HTTP server & websocket listener for the dashboard
74 | const app = express()
75 | app.use(express.static(`${__dirname}/public`))
76 |
77 | const webServer = http.createServer(app)
78 |
79 | wsserver({server: webServer}, store)
80 |
81 | const port = process.env.PORT || 3000
82 | webServer.listen(+port, ((err: any) => {
83 | if (err) throw err
84 | console.log('http server listening on', port)
85 | }) as any as () => void)
86 | })()
--------------------------------------------------------------------------------
/demos/contentfulgraphql/public/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | background-color: #f9f9f9;
8 | }
9 |
10 | body > * {
11 | margin: 0 auto;
12 | max-width: 700px;
13 | }
14 |
15 | .footer {
16 | margin-top: 10em;
17 | border-top: 4px solid #626161;
18 | padding-top: 1em;
19 | font-size: 14px;
20 | /* width: 100%; */
21 | /* padding: 0 auto; */
22 | /* max-width: initial; */
23 | /* height: 300vh; */
24 | /* padding: 1em; */
25 | font-family: monospace;
26 | }
27 |
28 | hr {
29 | border: 4px solid #626161;
30 | }
31 |
32 | blockquote {
33 | font-style: italic;
34 | color: #1f1f1f;
35 | background-color: #efefef;
36 |
37 | line-height: 1.5rem;
38 | margin-top: 1.5rem;
39 | margin-bottom: 1.5rem;
40 | margin-left: 0;
41 | border-left: 9px solid #8b8b8b;
42 | padding-left: 2em;
43 | }
44 | blockquote > *:first-child {
45 | margin-top: 0;
46 | }
47 |
48 | img {
49 | max-width: 100%;
50 | }
51 |
52 | /*! Typebase.less v0.1.0 | MIT License */
53 | /* Setup */
54 | html {
55 | /* Change default typefaces here */
56 | font-family: serif;
57 | /* font-size: 137.5%; */
58 | font-size: 115.5%;
59 | /* color: #6a6969; */
60 | }
61 | /* Copy & Lists */
62 | p {
63 | line-height: 1.5rem;
64 | margin-top: 1.5rem;
65 | margin-bottom: 0;
66 | }
67 | ul,
68 | ol {
69 | margin-top: 1.5rem;
70 | margin-bottom: 1.5rem;
71 | }
72 | ul li,
73 | ol li {
74 | line-height: 1.5rem;
75 | }
76 | ul ul,
77 | ol ul,
78 | ul ol,
79 | ol ol {
80 | margin-top: 0;
81 | margin-bottom: 0;
82 | }
83 | /* Headings */
84 | h1,
85 | h2,
86 | h3,
87 | h4,
88 | h5,
89 | h6 {
90 | /* Change heading typefaces here */
91 | font-family: sans-serif;
92 | margin-top: 1.5rem;
93 | margin-bottom: 0;
94 | line-height: 1.5rem;
95 | color: #4f4f4f;
96 | }
97 | h1 {
98 | font-size: 4.242rem;
99 | line-height: 4.5rem;
100 | padding-top: 3rem;
101 | margin-top: 0;
102 | }
103 | h2 {
104 | font-size: 2.828rem;
105 | line-height: 3rem;
106 | margin-top: 3rem;
107 | }
108 | h3 {
109 | font-size: 1.414rem;
110 | }
111 | h4 {
112 | font-size: 0.707rem;
113 | }
114 | h5 {
115 | font-size: 0.4713333333333333rem;
116 | }
117 | h6 {
118 | font-size: 0.3535rem;
119 | }
120 | /* Tables */
121 | table {
122 | margin-top: 1.5rem;
123 | border-spacing: 0px;
124 | border-collapse: collapse;
125 | }
126 | table td,
127 | table th {
128 | padding: 0;
129 | line-height: 33px;
130 | }
131 | /* Code blocks */
132 | code {
133 | vertical-align: bottom;
134 | }
135 | /* Leading paragraph text */
136 | .lead {
137 | font-size: 1.414rem;
138 | }
139 | /* Hug the block above you */
140 | .hug {
141 | margin-top: 0;
142 | }
143 |
--------------------------------------------------------------------------------
/demos/universalclient/public/index.html:
--------------------------------------------------------------------------------
1 |
2 | Universal client
3 |
135 |
136 |
--------------------------------------------------------------------------------
/demos/bidirectional/client.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../../lib/interfaces'
2 | import {connect} from '../../lib/stores/wsclient'
3 | import singleMem, {setSingle} from '../../lib/stores/singlemem'
4 | import connectMux, { BothMsg } from '../../lib/net/clientservermux'
5 | import subValues from '../../lib/subvalues'
6 |
7 | const wsurl = `ws${window.location.protocol.slice(4)}//${window.location.host}/ws`
8 |
9 | type Pos = {
10 | x: number,
11 | y: number,
12 | gamepad?: {
13 | id: string,
14 | buttons: number[],
15 | axes: readonly number[],
16 | }
17 | }
18 | // type DbVal = {[id: string]: Pos}
19 |
20 | let data = new Map()
21 |
22 | // The local store that holds our mouse location
23 | const localStore = singleMem({x:0, y:0})
24 |
25 | const canvas = document.getElementsByTagName('canvas')[0]
26 | let ctx: CanvasRenderingContext2D
27 |
28 | const resize = () => {
29 | canvas.width = canvas.clientWidth
30 | canvas.height = canvas.clientHeight
31 | ctx = canvas.getContext('2d', {alpha: false})!
32 | draw()
33 | }
34 | resize()
35 |
36 | const colors = ['red', 'green', 'blue', 'white', 'purple', 'yellow', 'orange']
37 |
38 | function draw() {
39 | ctx.fillStyle = 'black'
40 | // ctx.fillRect(0, 0, canvas.width, canvas.height)
41 | ctx.fillRect(0, 0, canvas.width, canvas.height)
42 |
43 | ctx.fillStyle = 'red'
44 | for (const [id, {x, y}] of data) {
45 | ctx.fillStyle = colors[parseInt(id) % colors.length]
46 | ctx.fillRect(x-5, y-5, 10, 10)
47 | }
48 | }
49 |
50 | let lastTS: number = -1
51 | let mouse: Pos = {x:0, y:0}
52 | const pollGamepads = () => {
53 | const gamepads = navigator.getGamepads()
54 | const g = gamepads[0]
55 | if (g == null) {
56 | return
57 | }
58 |
59 | console.log(g.timestamp)
60 | if (lastTS != g.timestamp) {
61 | mouse.gamepad = {
62 | id: g.id,
63 | buttons: g.buttons.map(b => b.value),
64 | axes: g.axes,
65 | }
66 |
67 | setSingle(localStore, mouse)
68 | lastTS = g.timestamp
69 | }
70 |
71 | setTimeout(pollGamepads, 16)
72 | // requestAnimationFrame(pollGamepads)
73 | }
74 |
75 | window.addEventListener("gamepadconnected", e => {
76 | console.log('gamepad connected')
77 | pollGamepads()
78 | // requestAnimationFrame(pollGamepads)
79 | })
80 |
81 |
82 | ;(async () => {
83 | const [reader, writer] = await connect(wsurl)
84 | const remoteStore = await connectMux(reader, writer, localStore, true)
85 |
86 | document.body.onmousemove = e => {
87 | // console.log('setting', e.clientX)
88 | // mouse = {x: e.clientX, y: e.clientY}
89 | mouse.x = e.clientX; mouse.y = e.clientY
90 | setSingle(localStore, mouse)
91 | }
92 |
93 | const sub = remoteStore.subscribe({type: I.QueryType.AllKV, q:true})
94 | for await (const d of subValues(I.ResultType.KV, sub)) {
95 | // console.log('d', d)
96 | data = d
97 | draw()
98 | }
99 | })()
100 |
101 |
--------------------------------------------------------------------------------
/demos/midi/server.ts:
--------------------------------------------------------------------------------
1 | import {I, stores, rmKV, setKV, subValues, registerType, resultTypes, catchupStateMachine} from '@statecraft/core'
2 | import {wrapWebSocket, connectMux, BothMsg, tcpserver} from '@statecraft/net'
3 | import express from 'express'
4 | import WebSocket from 'ws'
5 | import http from 'http'
6 |
7 | // import kvMem from '../../lib/stores/kvmem'
8 | import State from './state'
9 | import {type as jsonType, JSONOp} from 'ot-json1'
10 | registerType(jsonType)
11 |
12 | const {kvmem} = stores
13 |
14 | process.on('unhandledRejection', err => {
15 | console.error((err as any).stack)
16 | process.exit(1)
17 | })
18 |
19 | ;(async () => {
20 | // The store is a kv store mapping from client ID (incrementing numbers) => latest position.
21 | const store = await kvmem()
22 |
23 | const app = express()
24 | app.use(express.static(`${__dirname}/public`))
25 |
26 | const server = http.createServer(app)
27 | const wss = new WebSocket.Server({server})
28 |
29 | let nextId = 1000
30 |
31 | wss.on('connection', async (socket, req) => {
32 | const id = `${nextId++}`
33 |
34 | const [reader, writer] = wrapWebSocket(socket)
35 | const remoteStore = await connectMux(reader, writer, store, false)
36 |
37 | // console.log(id, 'info', remoteStore.storeInfo)
38 | console.log(id, 'client connected')
39 | const sub = remoteStore.subscribe({type: I.QueryType.Single, q: true})
40 |
41 | reader.onClose = () => {
42 | console.log(id, 'client gone')
43 | // delete db[id]
44 | // console.log('db', db)
45 | rmKV(store, id)
46 | }
47 |
48 | const sm = catchupStateMachine(I.ResultType.Single)
49 | for await (const cu of sub) {
50 | // Most updates are just going to be a single JSON transaction. If thats the case, just port the transaction.
51 |
52 | // TODO: This code is way more complicated than I'd like. A functionally
53 | // equivalent (but much simpler) version is commented below. But, doing it
54 | // this way keeps network traffic down. Consider making a utility method
55 | // out of this sort of thing in the future.
56 | const newVal = sm(cu).results
57 |
58 | // Each transaction in the catchup might be a list or a single item. Throw
59 | // everything away except the ops themselves.
60 | const ops = ([] as I.SingleOp[]).concat(...cu.txns.map(({txn}) => txn as I.SingleTxn))
61 | if (cu.replace || ops.find(op => op.type !== 'json1')) await setKV(store, id, newVal)
62 | else {
63 | // Flatten ops into a single json1 operation using compose() and send it.
64 | const op = ops.map(({data}) => data as JSONOp).reduce(jsonType.compose)
65 | await store.mutate(I.ResultType.KV, new Map([[id, {type: 'json1', data: op}]]))
66 | }
67 | }
68 | // for await (const val of subValues(I.ResultType.Single, sub)) {
69 | // await setKV(store, id, val)
70 | // }
71 | })
72 |
73 | const port = process.env.PORT || 2444
74 | server.listen(port, () => {
75 | console.log('listening on', port)
76 | })
77 |
78 |
79 | if (process.env.NODE_ENV !== 'production') {
80 | const tcpServer = tcpserver(store)
81 | tcpServer.listen(2002, 'localhost')
82 | console.log('Debugging server listening on tcp://localhost:2002')
83 | }
84 | })()
--------------------------------------------------------------------------------
/core/lib/version.ts:
--------------------------------------------------------------------------------
1 | import * as I from './interfaces'
2 |
3 | const TWO_32 = Math.pow(2, 32) // Cannot be constructed via bit operations
4 |
5 | // The easiest way to use versions is to use numbers internally and then use
6 | // this version constructor, which makes a byte array out of the big-endian
7 | // encoding of the specified number. The number must be an integer.
8 | export const V64 = (x: number): I.Version => {
9 | if (x > Number.MAX_SAFE_INTEGER) throw Error('Cannot use normal number encoding on version above 2^53')
10 |
11 | const ab = new ArrayBuffer(8)
12 | const dataview = new DataView(ab)
13 | dataview.setUint32(0, x / TWO_32)
14 | dataview.setUint32(4, x & 0xffffffff)
15 |
16 | return new Uint8Array(ab)
17 | }
18 |
19 | export const V_EMPTY = new Uint8Array()
20 |
21 | export const v64ToNum = (v: I.Version): number => {
22 | if (v.length !== 8) throw new Error('Invalid byte length in version')
23 | const dataview = new DataView(v.buffer, v.byteOffset, v.byteLength)
24 | return dataview.getUint32(0) * TWO_32 + dataview.getUint32(4)
25 | }
26 |
27 | export const vIncMut = (v: Uint8Array) => {
28 | let i = v.length
29 | while (i > 0 && v[--i]++ === 0xff);
30 | return v
31 | }
32 | export const vInc = (v: Uint8Array) => vIncMut(new Uint8Array(v))
33 | export const vDecMut = (v: Uint8Array) => {
34 | let i = v.length
35 | while (i > 0 && v[--i]-- === 0x00);
36 | return v
37 | }
38 | export const vDec = (v: Uint8Array) => vDecMut(new Uint8Array(v))
39 |
40 | // Lexographical comparison, though in reality versions should probably always
41 | // have the same length. This is included so we don't need to bundle all of
42 | // Buffer in the browser.
43 | export const vCmp = (a: Uint8Array, b: Uint8Array): number => {
44 | if (a === b) return 0 // Shortcut
45 | let i
46 | for (i = 0; i < a.length; i++) {
47 | if (i >= b.length) return 1
48 | const v = a[i] - b[i]
49 | if (v) return v
50 | }
51 | return (i < b.length) ? -1 : 0
52 | }
53 |
54 | export const vMax = (a: Uint8Array, b: Uint8Array) => vCmp(a, b) > 0 ? a : b
55 | export const vMin = (a: Uint8Array, b: Uint8Array) => vCmp(a, b) > 0 ? b : a
56 | export const vEq = (a: Uint8Array, b: Uint8Array) => vCmp(a, b) === 0
57 |
58 | // Modifies and returns dest = intersect(dest, src) if an intersection is valid.
59 | // Otherwise returns null if no intersection exists.
60 | export const vIntersectMut = (dest: I.FullVersionRange, src: I.FullVersionRange) => {
61 | for (let i = 0; i < src.length; i++) if (src[i] != null) {
62 | const {from: fromSrc, to: toSrc} = src[i]!
63 | if (dest[i] == null) dest[i] = {from: fromSrc, to: toSrc}
64 | else {
65 | const {from:fromDest, to:toDest} = dest[i]!
66 | // No intersection!
67 | if (vCmp(fromSrc, toDest) > 0 || vCmp(toSrc, fromDest) < 0) return null
68 | dest[i] = {from: vMax(fromDest, fromSrc), to: vMin(toDest, toSrc)}
69 | }
70 | }
71 | return dest
72 | }
73 |
74 | export const vRangeFrom = (vr: I.FullVersionRange) => vr.map(v => v == null ? null : v.from)
75 | export const vRangeTo = (vr: I.FullVersionRange) => vr.map(v => v == null ? null : v.to)
76 | export const vToRange = (v: I.FullVersion): I.FullVersionRange => v.map(v => v == null ? null : ({from: v, to: v}))
77 |
78 | export const vSparse = (i: number, val: V): (V | null)[] => {
79 | const result = []
80 | result[i] = val
81 | return result
82 | }
--------------------------------------------------------------------------------
/demos/bp/public/editor.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Play';
3 | font-style: normal;
4 | font-weight: 700;
5 | src: local('Play-Bold'), url(/Play.woff) format('woff');
6 | }
7 |
8 | body, html {
9 | width: 100%; height: 100%; padding: 0; margin: 0;
10 | font-family: 'Play', sans-serif;
11 | box-sizing: border-box;
12 |
13 | /* you see this if you try to scroll out of the module list */
14 | background-color: hsl(184, 49%, 7%);
15 | }
16 |
17 | * { box-sizing: inherit; }
18 |
19 | a {
20 | color: hsl(203, 6%, 70%);
21 | text-decoration: none;
22 | }
23 |
24 | a:before {
25 | content: '[';
26 | }
27 | a:after {
28 | content: ']';
29 | }
30 |
31 | .boilerplate {
32 | position: relative;
33 | width: 100%; height: 100%;
34 | }
35 |
36 | .boilerplate canvas {
37 | position: absolute;
38 | top: 0; left: 0;
39 | width: 100%; height: 100%;
40 | cursor: crosshair;
41 | }
42 |
43 | .overlay {
44 | position: fixed;
45 |
46 | padding: 5px;
47 | background-color: hsla(184, 49%, 7%, 0.5);
48 | cursor: default;
49 | color: white;
50 |
51 | -webkit-user-select: none;
52 | -moz-user-select: none;
53 | -ms-user-select: none;
54 | }
55 |
56 | .topleft { top: 0; left: 0; }
57 | /*.topright { top: 0; right: 0; }*/
58 | .botright { bottom: 0; right: 0; }
59 |
60 | #readonly {
61 | display: none;
62 | color: red;
63 | }
64 |
65 | #worldname {
66 | color: hsl(42, 95%, 69%);
67 | margin: 0;
68 | padding: 0 0 0 8px;
69 | border: 0;
70 | font-size: 1em;
71 | background-color: transparent;
72 | min-width: 20em;
73 | }
74 |
75 | #playpanel {
76 | padding-top: 7px;
77 | }
78 |
79 | #playpanel span {
80 | cursor: pointer;
81 | color: hsl(42, 95%, 69%);
82 | font-size: 1.5em;
83 | margin: 7px;
84 | display: inline-block;
85 | min-width: 1em;
86 | text-align: center;
87 | vertical-align: top; /* needed because play button is tall */
88 | }
89 | /*#playpanel.running #play { color: hsl(0, 0%, 78%); }*/
90 | /*#playpanel.stopped #pause { color: white; }*/
91 | #playpanel.running #step { color: gray; }
92 |
93 | #componentPanel {
94 | top: calc(-50% - 1px); /* No idea why the extra pixel is needed, but it is */
95 | right: 0;
96 | width: 300px;
97 | /*height: calc(50% + 1em + 2 * 5px);*/
98 | height: 50%;
99 | transition: top 0.6s;
100 | background-color: rgba(66, 66, 66, 0.61);
101 | padding: 0;
102 | }
103 |
104 | #componentPanel:hover { top: 0%; }
105 |
106 | #moduleList {
107 | height: 100%;
108 | overflow-y: scroll;
109 | }
110 |
111 | #componentPanel > .disclosure {
112 | /*color: hsl(42, 95%, 69%);*/
113 | color: white;
114 | position: absolute;
115 | right: 0;
116 | top: 100%;
117 | padding: 5px;
118 | width: 50%;
119 | text-align: right;
120 | transition: color 0.6s;
121 | background-color: hsla(184, 49%, 7%, 0.5);
122 | }
123 |
124 | #componentPanel:hover > .disclosure {
125 | /*color: white;*/
126 | color: hsl(42, 95%, 69%);
127 | }
128 |
129 | #componentPanel .module {
130 | width: 100%;
131 | height: 300px; /* Same as width of #componentPanel */
132 | padding: 10px 10px 0 10px;
133 | position: relative;
134 | }
135 |
136 | #componentPanel .module:last-child {
137 | padding-bottom: 10px;
138 | }
139 |
140 | .module > canvas {
141 | width: 100%;
142 | height: 100%;
143 | background-color: hsla(184, 47%, 3%, 0.47);
144 | }
145 |
146 | .module.selected > canvas {
147 | background-color: rgba(19, 65, 70, 0.82);
148 | }
149 |
150 | .module > .rm {
151 | font-size: 30px;
152 | color: hsl(42, 95%, 69%);
153 | position: absolute;
154 | top: 10px;
155 | right: 14px;
156 | text-rendering: geometricPrecision;
157 | background-color: rgba(15, 21, 22, 0.51);
158 | }
159 |
160 | #addmod {
161 | font-size: 240px;
162 | text-align: center;
163 | background-color: hsla(184, 47%, 3%, 0.47);
164 | transition: height 0.6s;
165 | }
166 |
--------------------------------------------------------------------------------
/core/lib/types/field.ts:
--------------------------------------------------------------------------------
1 | import {QueryType, ResultType, SingleOp, Op, ResultOps} from '../interfaces'
2 | import {typeOrThrow, supportedTypes, typeRegistry} from '../typeregistry'
3 |
4 | function appendMut(a: Op, b: SingleOp) {
5 | const {type} = b
6 | // Rm and set override preceding data in the stream.
7 | if (type === 'rm'
8 | || type === 'set'
9 | || Array.isArray(b) && b.length === 0) return b
10 |
11 | // If we have a type with a compose function, use it.
12 | const last = Array.isArray(a) ? a[a.length - 1] : a
13 | if (type === last.type) {
14 | const t = typeRegistry[type]
15 | if (t && t.compose) {
16 | if (Array.isArray(a)) {
17 | a[a.length - 1] = t.compose(last.data, b.data)
18 | return a
19 | } else {
20 | return t.compose(last.data, b.data)
21 | }
22 | }
23 | }
24 |
25 | // Otherwise just turn it into a composite op.
26 | if (Array.isArray(a)) {
27 | a.push(b)
28 | return a
29 | } else return [a, b]
30 | }
31 |
32 | const id = (x: T) => x
33 | const apply2 = (x: T, fn: (x: T, y: null) => R) => fn(x, null)
34 |
35 | const type: ResultOps> = {
36 | name: 'single',
37 | type: ResultType.Single,
38 |
39 | create(data) {
40 | // Remember, this is creating a document.
41 | return data
42 | },
43 |
44 | apply(snapshot: Val, op: Op) {
45 | if (Array.isArray(op)) {
46 | // Multi operation.
47 | for (let i = 0; i < op.length; i++) {
48 | // Recurse.
49 | snapshot = type.apply(snapshot, op[i])
50 | }
51 | return snapshot
52 | }
53 |
54 | const {type:typeName, data} = op
55 |
56 | switch (typeName) {
57 | case 'rm': return undefined
58 | case 'set': return data
59 | default: {
60 | return typeOrThrow(typeName).apply(snapshot, data)
61 | }
62 | }
63 | },
64 |
65 | // TODO: Add applyMut.
66 |
67 | compose(op1, op2) {
68 | if (Array.isArray(op2)) {
69 | // This is textbook reduce territory.
70 | let comp = op1
71 | for (let i = 0; i < op2.length; i++) {
72 | // Recurse.
73 | appendMut(comp, op2[i])
74 | }
75 | return comp
76 | } else {
77 | // Now just the single case.
78 | return appendMut(op1, op2)
79 | }
80 | },
81 |
82 | asOp(snap: any) {
83 | return (snap == null) ? {type:'rm'} : {type:'set', data:snap}
84 | },
85 |
86 | // from(type, data) {
87 | // switch(type) {
88 | // case 'single': return data
89 | // case 'kv': return data.get('content')
90 | // }
91 | // },
92 |
93 | checkOp(op, snap) {
94 | type.apply(snap, op) // this will throw if invalid. TODO: use check on subtype if available.
95 | },
96 |
97 | mapEntries: (v, fn) => {
98 | const v2 = fn(null, v)
99 | return v2 != null ? v2[1] : null
100 | },
101 | mapEntriesAsync: (v, fn) => fn(null, v).then(x => x == null ? null : x[1]),
102 |
103 | map: apply2,
104 | mapAsync: apply2,
105 | mapTxn: apply2, // TODO: Consider pulling out SingleOp / SingleOp[] in these functions.
106 | mapTxnAsync: apply2,
107 |
108 | mapReplace: apply2,
109 |
110 |
111 | snapToJSON: id,
112 | snapFromJSON: id,
113 | opToJSON: id,
114 | opFromJSON: id,
115 |
116 |
117 | getCorrespondingQuery(data) { return {type: QueryType.Single, q: true} },
118 |
119 | filterSupportedOps(txn, view, supportedTypes) {
120 | if (Array.isArray(txn)) {
121 | let hasAll = true
122 | for (let i = 0; i < txn.length; i++) {
123 | if (!supportedTypes.has(txn[i].type)) hasAll = false
124 | }
125 | return hasAll ? txn : type.asOp(view)
126 | } else {
127 | return supportedTypes.has(txn.type) ? txn : type.asOp(view)
128 | }
129 | },
130 |
131 | composeResultsMut(a, b) { return b },
132 |
133 | updateResults: (s, q, data) => data,
134 | }
135 |
136 | export default type
--------------------------------------------------------------------------------
/net/lib/netmessages.ts:
--------------------------------------------------------------------------------
1 | import {I, ErrJson} from '@statecraft/core'
2 |
3 | export type Ref = /*string |*/ number
4 |
5 | export type NetKVTxn = [I.Key, I.Op][]
6 | export type NetTxn = I.SingleTxn | NetKVTxn
7 |
8 | export type NetQuery = I.QueryType.Single | I.QueryType.AllKV | [I.QueryType, any]
9 |
10 | export type NetVersion = number[] // This makes me really sad. Not needed with msgpack; only for json.
11 | export type NetVersionRange = [NetVersion, NetVersion]
12 | export type NetFullVersion = (null | NetVersion)[]
13 | export type NetFullVersionRange = (null | NetVersionRange)[]
14 |
15 | export type SubscribeOpts = {
16 | // TODO: Add all the rest!!!
17 | st?: string[]
18 | fv?: NetFullVersion | 'c', // known at versions
19 | }
20 |
21 | export const enum Action {
22 | // Many of these are used by both the server and client.
23 | Hello = 0,
24 | Err = 1,
25 |
26 | Fetch = 2,
27 | GetOps = 3,
28 | Mutate = 4,
29 | SubCreate = 5,
30 | SubClose = 6,
31 | SubUpdate = 7,
32 | }
33 |
34 | // **************** Client -> Server messages
35 |
36 | export interface FetchRequest {
37 | a: Action.Fetch,
38 | ref: Ref,
39 | query: NetQuery,
40 | opts: I.FetchOpts,
41 | }
42 |
43 | export interface GetOpsRequest {
44 | a: Action.GetOps,
45 | ref: Ref,
46 | query: NetQuery,
47 | v: NetFullVersionRange,
48 | opts: I.GetOpsOptions,
49 | }
50 |
51 | export interface MutateRequest {
52 | a: Action.Mutate,
53 | ref: Ref,
54 | mtype: I.ResultType,
55 | txn: any,
56 | v: NetFullVersion,
57 | opts: I.MutateOptions,
58 | }
59 |
60 | export interface SubCreate {
61 | a: Action.SubCreate,
62 | ref: Ref,
63 | query: NetQuery,
64 | opts: SubscribeOpts
65 | }
66 |
67 | export interface SubClose {
68 | a: Action.SubClose,
69 | ref: Ref, // Ref of subscription
70 | }
71 |
72 | export type CSMsg =
73 | FetchRequest
74 | | MutateRequest
75 | | GetOpsRequest
76 | | SubCreate
77 | | SubClose
78 |
79 |
80 | // **************** Server -> Client messages
81 |
82 | export interface HelloMsg {
83 | a: Action.Hello,
84 | p: 'statecraft',
85 | pv: number,
86 | uid: string,
87 | sources: I.Source[], // ??? TODO: Still not sure whether to allow unknown sources.
88 | m: boolean[], // This is kinda wasteful over the wire, but probably not a big deal. Bitvector would be better.
89 | capabilities: number[]
90 | }
91 |
92 | export interface FetchResponse {
93 | a: Action.Fetch,
94 | ref: Ref,
95 | results: any, // Dependant on query.
96 |
97 | bakedQuery?: NetQuery,
98 | versions: NetFullVersionRange, // Range across which version is valid.
99 | }
100 |
101 | // txn, version, meta.
102 | export type NetTxnWithMeta = [any, NetFullVersion, any]
103 |
104 | export interface GetOpsResponse {
105 | a: Action.GetOps,
106 | ref: Ref,
107 | ops: NetTxnWithMeta[],
108 | v: NetFullVersionRange
109 | }
110 |
111 | export interface MutateResponse {
112 | a: Action.Mutate,
113 | ref: Ref,
114 | v: NetFullVersion,
115 | }
116 |
117 | export interface ResponseErr { // Used for fetch, mutate and getops
118 | a: Action.Err,
119 | ref: Ref,
120 | err: ErrJson,
121 | }
122 |
123 | export interface SubUpdate {
124 | a: Action.SubUpdate,
125 | ref: Ref,
126 |
127 | // Replace data. If r exists, q must exist too.
128 | q?: NetQuery, // active query diff
129 | r?: any, // replacement
130 | rv?: NetFullVersion,
131 | u: boolean,
132 |
133 | txns: NetTxnWithMeta[], // updates on top of replacement
134 |
135 | tv: NetFullVersion, // version diff
136 | }
137 |
138 | // The subscription has closed
139 | export interface SubRet {
140 | a: Action.SubClose,
141 | ref: Ref,
142 | }
143 |
144 |
145 | export type SCMsg =
146 | HelloMsg
147 | | FetchResponse
148 | | GetOpsResponse
149 | | MutateResponse
150 | | ResponseErr
151 | | SubUpdate
152 | | SubRet
153 |
154 | // export const flatten1 = (data: any) => (data instanceof Map || data instanceof Set) ? Array.from(data) : data
155 |
--------------------------------------------------------------------------------
/core/lib/opcache.ts:
--------------------------------------------------------------------------------
1 | import * as I from './interfaces'
2 | import * as err from './err'
3 | import {queryTypes} from './qrtypes'
4 |
5 | import binsearch from 'binary-search'
6 | import {vCmp, vEq} from './version'
7 |
8 | export interface OpCacheOpts {
9 | readonly qtype?: I.QueryType,
10 | readonly maxTime?: number, // in milliseconds. 0 = ignore.
11 | readonly maxNum?: number, // Max number of ops kept for each source. 0 = ignore. Defaults to 100.
12 | }
13 |
14 | interface OpsEntry {
15 | fromV: I.Version,
16 | toV: I.Version,
17 | txn: I.Txn,
18 | meta: I.Metadata,
19 | ctime: number,
20 | }
21 | const cmp = (item: OpsEntry, v: I.Version) => vCmp(item.toV, v)
22 |
23 | /**
24 | * Opcache is a helper object which keeps a set of recent operations in memory and can use it to respond to getOps calls.
25 | *
26 | * At the moment opcache only works with monotonically increasing operations.
27 | */
28 | const opcache = (opts: OpCacheOpts = {}): {
29 | onOp(sourceIdx: number, fromV: I.Version, toV: I.Version, type: I.ResultType, txn: I.Txn, meta: I.Metadata): void,
30 | getOps: I.GetOpsFn,
31 | } => {
32 | const maxNum = opts.maxNum || 100
33 | const maxTime = opts.maxTime || 0
34 | // List is sorted in order and accessed using binary search.
35 |
36 | // source => list of ops entries sorted by version
37 | const opsForSource: OpsEntry[][] = []
38 |
39 | const getOpsForSource = (si: number) => {
40 | let ops = opsForSource[si]
41 | if (ops == null) opsForSource[si] = ops = []
42 | return ops
43 | }
44 |
45 | return {
46 | onOp(source, fromV, toV, type, txn, meta) {
47 | const ops = getOpsForSource(source)
48 | if (ops.length && !vEq(ops[ops.length - 1].toV, fromV)) throw Error('Emitted versions don\'t match')
49 | ops.push({fromV, toV, txn, meta, ctime: Date.now()})
50 |
51 | while (maxNum !== 0 && ops.length > maxNum) ops.shift()
52 |
53 | const now = Date.now()
54 | while (maxTime !== 0 && ops.length && ops[0].ctime < now - maxTime) ops.shift()
55 | },
56 |
57 | getOps(query, versions, options = {}) {
58 | const qtype = query.type
59 | const qops = queryTypes[qtype]
60 | if (qops == null) throw Error('Missing qops for type ' + qtype)
61 |
62 | let limitOps = options.limitOps || -1
63 |
64 | const vOut: I.FullVersionRange = []
65 |
66 | const result: I.TxnWithMeta[] = []
67 | for (let si = 0; si < opsForSource.length; si++) {
68 | // This is a bit inefficient - if there's lots of sources
69 | // we're looking through all of them even if the user only
70 | // wants one. But that shouldn't happen much in practice (right?)
71 | const ops = opsForSource[si]
72 | if (ops == null) continue
73 | const vs = versions[si] //|| versions._other
74 | if (vs == null) continue
75 |
76 | const {from, to} = vs
77 | let fromidx: number
78 | if (from.length === 0) fromidx = 0 // From version known.
79 | else {
80 | const searchidx = binsearch(ops, from, cmp)
81 | fromidx = searchidx < 0 ? ~searchidx : searchidx + 1
82 | }
83 |
84 | if (fromidx >= ops.length) continue
85 |
86 | // Figure out the actual returned version range.
87 | const vFrom = ops[fromidx].fromV
88 | let vTo = vFrom
89 |
90 | for (let i = fromidx; i < ops.length; i++) {
91 | const item = ops[i]
92 | if (to.length && vCmp(item.toV, to) > 0) break
93 |
94 | // The transaction will be null if the operation doesn't match
95 | // the supplied query.
96 | const txn = qops.adaptTxn(item.txn, query.q)
97 | const versions = []
98 | versions[si] = item.toV
99 | if (txn != null) result.push({versions, txn, meta: item.meta})
100 |
101 | vTo = item.toV
102 | if (limitOps > 0 && --limitOps === 0) break
103 | }
104 | /*if (vTo !== vFrom)*/ vOut[si] = {from: vFrom, to: vTo}
105 |
106 | if (limitOps === 0) break
107 | }
108 |
109 | // console.log('opcache', result, vOut)
110 | return Promise.resolve({ops: result, versions: vOut})
111 | },
112 | }
113 | }
114 |
115 | export default opcache
--------------------------------------------------------------------------------
/demos/midi/client.ts:
--------------------------------------------------------------------------------
1 | import {I, stores, subValues, setSingle, registerType} from '@statecraft/core'
2 | import {connectToWS, connectMux, BothMsg} from '@statecraft/net'
3 | import State, {MIDIPort, MIDIInput} from './state'
4 |
5 | import {type as jsonType, insertOp, replaceOp, JSONOp, removeOp} from 'ot-json1'
6 | registerType(jsonType)
7 |
8 | const {singlemem} = stores
9 |
10 | const wsurl = `ws${window.location.protocol.slice(4)}//${window.location.host}/ws`
11 | console.log('wsurl', wsurl)
12 |
13 | // let data = new Map()
14 |
15 | // The local store that holds our mouse location
16 |
17 | ;(async () => {
18 | const access = await navigator.requestMIDIAccess()
19 | // Get lists of available MIDI controllers
20 |
21 | const inputs = Array.from(access.inputs.values())
22 | const outputs = Array.from(access.outputs.values())
23 |
24 | const portData = (port: WebMidi.MIDIPort): MIDIPort => ({
25 | id: port.id, // These IDs are really silly.
26 | manufacturer: port.manufacturer,
27 | name: port.name,
28 | version: port.version,
29 | state: port.state,
30 | })
31 | const inputPortData = (port: WebMidi.MIDIPort): MIDIInput => ({
32 | ...portData(port),
33 | keys: {},//new Array(128).fill(null),
34 | pots: new Array(8).fill(NaN), //[],
35 | sliders: [NaN],
36 | pitch: 64,
37 | modulation: 0,
38 | })
39 |
40 | let data: State = {
41 | timeOrigin: performance.timeOrigin,
42 | inputs: inputs.map(inputPortData),
43 | outputs: outputs.map(portData),
44 | }
45 |
46 | console.log('state', data)
47 | console.log(inputs[0].connection, inputs[0].state)
48 | const localStore = singlemem(data)
49 |
50 | const [reader, writer] = await connectToWS(wsurl)
51 | const remoteStore = await connectMux(reader, writer, localStore, true)
52 |
53 | const apply = (op: JSONOp) => {
54 | console.log('before', data, 'op', op)
55 | data = jsonType.apply(data, op)
56 | console.log('after', data)
57 | return localStore.mutate(I.ResultType.Single, {type: 'json1', data: op})
58 | }
59 |
60 | const subscribeToInput = (input: WebMidi.MIDIInput, i: number) => {
61 | // console.log(i)
62 | input.onmidimessage = m => {
63 | // console.log(input.name, m.data, m.timeStamp)
64 |
65 | const [mtype, oper1, oper2] = m.data
66 | const inputData = data.inputs[i]
67 | const {keys, pots, sliders} = inputData
68 | switch (mtype) {
69 | case 0x90: // Note ON
70 | apply(replaceOp(['inputs', i, 'keys', ''+oper1], keys[oper1], {held: true, pressure: oper2, timestamp: m.timeStamp}))
71 | break
72 | case 0x80: // Note OFF
73 | apply(replaceOp(['inputs', i, 'keys', ''+oper1], keys[oper1], {held: false, pressure: 0, timestamp: m.timeStamp}))
74 | break
75 | case 176: { // Pots and sliders
76 | const field = oper1 === 1 ? ['modulation']
77 | : (oper1 >= 0x15 && oper1 <= 0x1c) ? ['pots', oper1 - 0x15]
78 | : (oper1 === 0x7) ? ['sliders', 0]
79 | : null
80 | if (field == null) console.log('unknown CC /', oper1, oper2)
81 | else apply(replaceOp(['inputs', i, ...field], true, oper2))
82 | break
83 | }
84 | case 224: // Pitch slider
85 | apply(replaceOp(['inputs', i, 'pitch'], inputData.pitch, oper2))
86 | break
87 | default:
88 | console.log('unknown message', mtype, oper1, oper2)
89 | break
90 | }
91 | }
92 | }
93 | inputs.forEach(subscribeToInput)
94 |
95 | access.onstatechange = e => {
96 | // Print information about the (dis)connected MIDI controller
97 | const {port} = e
98 | const set = port.type === 'input' ? data.inputs : data.outputs
99 | const deviceIdx = set.findIndex(({id}) => id === port.id)
100 | if (deviceIdx >= 0) {
101 | set[deviceIdx].state = port.state
102 | } else {
103 | set.push(port.type === 'input' ? inputPortData(port) : portData(port))
104 | if (port.type === 'input') subscribeToInput(port as WebMidi.MIDIInput, set.length-1)
105 | }
106 | setSingle(localStore, data)
107 |
108 | // console.log('onstatechange', e.port.name, e.port.manufacturer, e.port.state);
109 | // console.log(e.port.type)
110 | }
111 | })()
112 |
113 |
--------------------------------------------------------------------------------
/demos/contentfulgraphql/server.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs'
2 | import http from 'http'
3 | import express from 'express'
4 | import * as gql from 'graphql'
5 | import * as gqlTools from 'graphql-tools'
6 |
7 | import * as I from '../../lib/interfaces'
8 | import createContentful from '../../lib/stores/contentful';
9 | import kvMem from '../../lib/stores/kvmem'
10 | import gqlmr, {Ctx} from '../../lib/stores/graphqlmapreduce'
11 | import {Console} from 'console'
12 |
13 | // For debugging
14 | import router, {ALL} from '../../lib/stores/router'
15 | import serveTCP from '../../lib/net/tcpserver'
16 |
17 | import Post from './post'
18 | import {renderToString} from 'react-dom/server'
19 | import fresh from 'fresh'
20 | import { vRangeFrom } from '../../lib/version';
21 |
22 |
23 | process.on('unhandledRejection', err => {
24 | console.error(err.stack)
25 | process.exit(1)
26 | })
27 |
28 | global.console = new (Console as any)({
29 | stdout: process.stdout,
30 | stderr: process.stderr,
31 | inspectOptions: {depth: null}
32 | })
33 |
34 | // Eventually this will be autogenerated from the schema.
35 | const schema = gqlTools.makeExecutableSchema({
36 | typeDefs: `
37 | type Post {
38 | title: String,
39 | content: String,
40 | author: Author!,
41 | slug: [String!],
42 | updatedAt: String,
43 | }
44 |
45 | type Author {
46 | fullName: String,
47 | }
48 |
49 | type Query {
50 | postById(id: String!): Post
51 | foo: String,
52 |
53 | selfPost: Post,
54 | selfAuthor: Author,
55 | }
56 | `,
57 | resolvers: {
58 | Post: {
59 | author: (post, args, {txn}: Ctx) => (
60 | txn.get('author/' + post.author)
61 | )
62 | },
63 |
64 | Query: {
65 | postById: (_, {id}: {id: string}, {txn}: Ctx) => (
66 | txn.get('post/' + id)
67 | ),
68 |
69 | selfPost: (_self, _args, {Post}) => Post,
70 | }
71 | }
72 | })
73 |
74 | const genEtag = (versions: I.FullVersion): string => {
75 | return versions.map(v => v == null ? '' : Buffer.from(v).toString('base64')).join('.')
76 | }
77 |
78 | ;(async () => {
79 | const keys = JSON.parse(fs.readFileSync('keys.json', 'utf8'))
80 | const syncStore = await kvMem()
81 | const ops = createContentful(syncStore, {
82 | space: keys.space,
83 | accessToken: keys.contentAPI,
84 | })
85 | // Ideally this wouldn't return until after we get the initial sync.
86 | const cfstore = await kvMem(undefined, {inner: ops})
87 |
88 | const store = await gqlmr(cfstore, {
89 | schema,
90 | mapReduces: [
91 | {
92 | type: 'Post',
93 | queryStr: `{selfPost {updatedAt, title, content, slug, author {fullName}}}`,
94 | reduce: ({selfPost}) => (
95 | renderToString(Post(selfPost))
96 | ),
97 | getOutputKey: (k, {selfPost}) => (`${selfPost.slug[0]}`)
98 | }
99 | ],
100 | prefixForCollection: new Map([
101 | // This is mapping graphql types -> backend key prefixes.
102 | ['Post', 'post/'],
103 | ['Author', 'author/'],
104 | ]),
105 | })
106 |
107 | const app = express()
108 | app.use(express.static(`${__dirname}/public`))
109 |
110 | app.get('/post/:slug', async (req, res, next) => {
111 | const r = await store.fetch({type: I.QueryType.KV, q: new Set([req.params.slug])})
112 | const html = r.results.get(req.params.slug)
113 | if (html == null) return next()
114 | else {
115 | // TODO: ETag, etc.
116 | const headers = {
117 | 'content-type': 'text/html',
118 | 'etag': genEtag(vRangeFrom(r.versions)),
119 | }
120 | if (fresh(req.headers, headers)) return res.sendStatus(304)
121 |
122 | res.set(headers)
123 | res.send('\n' + html)
124 | }
125 | })
126 |
127 | const server = http.createServer(app)
128 | const port = process.env['PORT'] || 2004
129 | server.listen(port, () => {
130 | console.log('listening on port', port)
131 | })
132 |
133 | if (process.env.NODE_ENV !== 'production') {
134 | const all = router()
135 | all.mount(cfstore, 'cf/', null, '', false)
136 | all.mount(store, 'rendered/', null, '', false)
137 | const tcpServer = serveTCP(all)
138 | tcpServer.listen(2002, 'localhost')
139 | console.log('Debugging server listening on tcp://localhost:2002')
140 | }
141 | })()
--------------------------------------------------------------------------------
/core/lib/types/map.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../interfaces'
2 | import fieldOps from './field'
3 |
4 | const mapMapEntry = (input: Map, fn: (key: I.Key, val: In) => [I.Key, Out] | null) => {
5 | const result = new Map()
6 | for (const [k, val] of input) {
7 | const newEntry = fn(k, val)
8 | if (newEntry != null) result.set(newEntry[0], newEntry[1])
9 | }
10 | return result
11 | }
12 |
13 | // Could just write this in terms of mapMapEntry above.
14 | const mapMapVal = (input: Map, fn: (val: In, key: I.Key) => Out) => {
15 | const result = new Map()
16 | for (const [k, val] of input) result.set(k, fn(val, k))
17 | return result
18 | }
19 |
20 | const mapEntryAsync = (input: Map, fn: (key: I.Key, val: In) => Promise<[I.Key, Out] | null>) => {
21 | const entries = Array.from(input.entries())
22 | const mapped = entries.map(([k, v]) => fn(k, v))
23 | return Promise.all(mapped).then((results) => new Map(results.filter(e => e != null) as [I.Key, Out][]))
24 | }
25 |
26 | const mapAsync = (input: Map, fn: (val: In, key: I.Key) => Promise) => (
27 | mapEntryAsync(input, (k, v) => fn(v, k).then(v2 => ([k, v2] as [I.Key, Out])))
28 | )
29 |
30 | type Val = any
31 | const type: I.ResultOps, I.KVTxn> = {
32 | name: 'kv',
33 | type: I.ResultType.KV,
34 |
35 | create(data) {
36 | return data instanceof Map ? data : new Map(data)
37 | },
38 |
39 | applyMut(snap, op) {
40 | for (var [k, docop] of op) {
41 | const oldval = snap.get(k)
42 | const newval = fieldOps.apply(oldval, docop)
43 |
44 | // Statecraft considers null / undefined to be the same as a document not existing.
45 | if (newval == null) snap.delete(k)
46 | else snap.set(k, newval)
47 | }
48 | },
49 |
50 | apply(snap, op) {
51 | const newdata = new Map(snap)
52 | type.applyMut!(newdata, op)
53 | return newdata
54 | },
55 |
56 | composeMut(txn, other) {
57 | for (const [k, op] of other) {
58 | const orig = txn.get(k)
59 | if (orig == null) txn.set(k, op)
60 | else {
61 | txn.set(k, fieldOps.compose(orig, op))
62 | }
63 | }
64 | },
65 |
66 | compose(a, b) {
67 | const result = new Map(a)
68 | type.composeMut!(result, b)
69 | return result
70 | },
71 |
72 | composeResultsMut(dest, src) {
73 | // For maps this is the same as copyInto.
74 | return type.copyInto!(dest, src)
75 | },
76 |
77 | copyInto(dest, src) {
78 | for (const [k, v] of src) dest.set(k, v)
79 | return dest
80 | },
81 |
82 | filter(snap: Map, query: Set): Map {
83 | const result = new Map()
84 | for (const k of query) {
85 | const v = snap.get(k)
86 | if (v !== undefined) result.set(k, v)
87 | }
88 | return result
89 | },
90 |
91 | // from(type, data) {
92 | // switch(type) {
93 | // case 'single': return new Map([['content', data]])
94 | // case 'resultmap': return data
95 | // }
96 | // },
97 |
98 | mapEntries: mapMapEntry,
99 | mapEntriesAsync: mapEntryAsync,
100 | map: mapMapVal,
101 | mapAsync,
102 | mapTxn: mapMapVal,
103 | mapTxnAsync: mapAsync,
104 |
105 | mapReplace: mapMapVal,
106 |
107 | snapToJSON(snap) { return Array.from(snap) },
108 | snapFromJSON(data) { return new Map(data) },
109 | opToJSON(op) { return Array.from(op) },
110 | opFromJSON(data) { return new Map(data) },
111 |
112 | getCorrespondingQuery(snap) {
113 | return {type: I.QueryType.KV, q: new Set(snap.keys())}
114 | },
115 |
116 | filterSupportedOps(op, values: Map, supportedTypes) {
117 | // console.log('fso', op, values)
118 | return mapMapVal(op, (o, k) => (
119 | fieldOps.filterSupportedOps(o, values.get(k), supportedTypes))
120 | )
121 | },
122 |
123 | updateResults(s: Map, q: I.ReplaceQuery, data: Map) {
124 | if (q.type === I.QueryType.KV) {
125 | for (const k of q.q) {
126 | if (data.has(k)) s.set(k, data.get(k))
127 | else s.delete(k)
128 | }
129 | return s
130 | } else return data // allkv.
131 | // I'm not sure if we should look at q.q for this ??
132 | // } else return q.q ? data : s // allkv.
133 | },
134 | }
135 | export default type
136 |
--------------------------------------------------------------------------------
/core/lib/stores/onekey.ts:
--------------------------------------------------------------------------------
1 | // This is a simple store that re-exposes a single key from the underlying
2 | // store as a single value.
3 |
4 | import * as I from '../interfaces'
5 | import err from '../err'
6 | import { bitSet, bitHas } from '../bit';
7 |
8 | const capabilities = {
9 | queryTypes: bitSet(I.QueryType.Single),
10 | mutationTypes: bitSet(I.ResultType.Single),
11 | // ops: 'none',
12 | }
13 |
14 | const onekey = (innerStore: I.Store, key: I.Key): I.Store => {
15 | const canMutate = bitHas(innerStore.storeInfo.capabilities.mutationTypes, I.ResultType.KV)
16 | // console.log('cm', canMutate, innerStore.storeInfo)
17 |
18 | const innerQuery: I.Query = {type: I.QueryType.KV, q: new Set([key])}
19 | if (!bitHas(innerStore.storeInfo.capabilities.queryTypes, I.QueryType.KV)) throw new err.UnsupportedTypeError('Inner store must support KV queries')
20 |
21 | const unwrapTxns = (txns: I.TxnWithMeta[]): I.TxnWithMeta[] => (
22 | txns.map(txn => {
23 | if(!(txn.txn instanceof Map)) throw new err.InvalidDataError()
24 | return {
25 | ...txn,
26 | txn: txn.txn.get(key),
27 | } as I.TxnWithMeta
28 | })
29 | )
30 |
31 | const unwrapCatchupData = (data: I.CatchupData): I.CatchupData => {
32 | let hasReplace = false
33 | if (data.replace) {
34 | const replaceQ = data.replace.q
35 | if (replaceQ.type !== I.QueryType.KV) throw new err.InvalidDataError()
36 | if (replaceQ.q.has(key)) hasReplace = true
37 | }
38 |
39 | return {
40 | ...data,
41 | replace: hasReplace ? {
42 | q: {type: I.QueryType.Single, q: true},
43 | with: data.replace!.with.get(key),
44 | versions: data.replace!.versions,
45 | } : undefined,
46 | txns: unwrapTxns(data.txns),
47 | }
48 | }
49 |
50 | return {
51 | storeInfo: {
52 | uid: `onekey(${innerStore.storeInfo.uid},${key})`,
53 | sources: innerStore.storeInfo.sources,
54 | sourceIsMonotonic: innerStore.storeInfo.sourceIsMonotonic,
55 | capabilities: {
56 | mutationTypes: canMutate ? capabilities.mutationTypes : new Set(),
57 | ...capabilities,
58 | }
59 | },
60 |
61 | async fetch(query, opts) {
62 | if (query.type !== I.QueryType.Single) throw new err.UnsupportedTypeError()
63 |
64 | const results = await innerStore.fetch(innerQuery, opts)
65 | // if (results.type !== 'kv') throw new err.InvalidDataError()
66 |
67 | return {
68 | results: results.results.get(key),
69 | queryRun: query,
70 | versions: results.versions,
71 | }
72 | },
73 |
74 | async mutate(type, txn, versions, opts = {}) {
75 | if (!canMutate) throw new err.UnsupportedTypeError('Underlying store is read only')
76 | if (type !== I.ResultType.Single) throw new err.UnsupportedTypeError()
77 |
78 | const innerTxn = new Map([[key, txn as I.Op]])
79 | return await innerStore.mutate(I.ResultType.KV, innerTxn, versions, {
80 | ...opts,
81 | // conflictKeys: opts.conflictKeys && opts.conflictKeys.includes(key) ? [''] : undefined,
82 | })
83 | },
84 |
85 | // Should probably pass a parameter on whether we take ownership of the underlying store or not.
86 | close() { innerStore.close() },
87 |
88 | async getOps(query, reqVersions, opts) {
89 | if (query.type !== I.QueryType.Single) throw new err.UnsupportedTypeError()
90 |
91 | const {ops, versions} = await innerStore.getOps(innerQuery, reqVersions, opts)
92 |
93 | // Just going to mutate the ops in place.
94 |
95 | return {
96 | ops: unwrapTxns(ops),
97 | versions,
98 | }
99 | },
100 |
101 | catchup: innerStore.catchup ? async (query, fromVersion, opts) => (
102 | unwrapCatchupData(await innerStore.catchup!(innerQuery, fromVersion, opts))
103 | ) : undefined,
104 |
105 | // TODO: Map catchup if it exists in the underlying store.
106 |
107 | subscribe(query, opts = {}) {
108 | if (query.type !== I.QueryType.Single) throw new err.UnsupportedTypeError()
109 |
110 | const innerSub = innerStore.subscribe(innerQuery, opts)
111 |
112 | // TODO: It'd be great to have a SubscriptionMap thing that lets you map
113 | // the resulting data you get back from a subscription. That'd be useful
114 | // in a few places and this is really wordy.
115 | return (async function*() {
116 | for await (const innerUpdates of innerSub) {
117 | yield unwrapCatchupData(innerUpdates)
118 | }
119 | // TODO: Does this pass through return() correctly?
120 | })() as I.AsyncIterableIteratorWithRet>
121 | },
122 |
123 | }
124 | }
125 |
126 | export default onekey
--------------------------------------------------------------------------------
/prozess/prozess-mock.ts:
--------------------------------------------------------------------------------
1 | // This is a simple in-memory mock for prozess. This is useful so we don't
2 | // have to spin up & down lots of prozess instances to run tests.
3 |
4 | // This can't be used outside of tests because its used as the central source
5 | // of truth, and it loses all content on restart. So if you back an LMDB store
6 | // with this, it'll correctly fail to restart.
7 |
8 | import {
9 | PClient,
10 | VersionConflictError,
11 | Event,
12 | } from 'prozess-client'
13 | import genSource from '../lib/gensource'
14 |
15 | const clamp = (x: T, a: T, b: T) => x < a ? a : (x > b ? b : x)
16 |
17 | export default function createMock(): PClient {
18 | interface MockEvent extends Event {
19 | conflictKeys: string[],
20 | }
21 |
22 | const base = 0xff01 // Pretend the mock starts at some offset in the file
23 | const events: MockEvent[] = []
24 | const nextVersion = () => base + events.length
25 |
26 | let subscribed = false
27 |
28 | const addEvent = (
29 | data: Buffer | string,
30 | targetVersion: number = events.length,
31 | conflictKeys: string[] = []) => {
32 |
33 | // debugger
34 | // This isn't super efficient - it'd be better to sort and walk together.
35 | // But this is just for testing so it shouldn't matter.
36 | const ckSet = new Set(conflictKeys)
37 |
38 | // Version of the event
39 | const ev = nextVersion()
40 |
41 | for (targetVersion = clamp(targetVersion, base, ev);
42 | targetVersion < ev;
43 | targetVersion++) {
44 | const e = events[targetVersion - base]
45 | for (const k of e.conflictKeys) {
46 | if (ckSet.has(k)) return null // Write conflict.
47 | }
48 | }
49 |
50 | const event = {
51 | version: ev,
52 | crc32: 0, // dummy.
53 | batch_size: 1,
54 | flags: 0,
55 | data: typeof data === 'string' ? Buffer.from(data) : data,
56 | conflictKeys: conflictKeys || [],
57 | }
58 |
59 | events.push(event)
60 | return event
61 | }
62 |
63 | return {
64 | source: genSource(),
65 | onevents: undefined,
66 | onclose: undefined,
67 | onunsubscribe: undefined,
68 |
69 | subscribe(from, opts, callback) {
70 | process.nextTick(() => {
71 | if (from == null || from < 0 || from > nextVersion()) from = nextVersion()
72 |
73 | // First forward a bunch of events
74 | const idx = from - base
75 | const evts = events.slice(idx)
76 | this.onevents && this.onevents(evts, from)
77 | subscribed = true
78 |
79 | callback && callback(null, {
80 | v_start: from,
81 | v_end: nextVersion(),
82 | size: evts.reduce((sum, e) => e.data.length + sum, 0),
83 | complete: false,
84 | oneshot: false,
85 | current: true,
86 | events: evts,
87 | })
88 | })
89 | },
90 |
91 | // This gets ops in the range [from, to].
92 | getEventsRaw(from, to, opts, callback) {
93 | process.nextTick(() => {
94 | const f = clamp(from - base, 0, events.length) // Allowed to fall off the end.
95 | const t = to === -1 ? undefined : clamp(to - base, 0, events.length-1)+1
96 | const evts = events.slice(f, t)
97 | callback && callback(null, {
98 | v_start: f+base,
99 | v_end: f+base + evts.length,
100 | complete: true,
101 | current: false,
102 | oneshot: true,
103 | size: evts.reduce((sum, e) => e.data.length + sum, 0),
104 | events: evts,
105 | })
106 | })
107 | },
108 |
109 | getEvents(from, to, opts) {
110 | return new Promise((resolve, reject) => {
111 | this.getEventsRaw(from, to, opts, (err, data) => {
112 | if (err) reject(err)
113 | else resolve(data)
114 | })
115 | })
116 | },
117 |
118 | sendRaw(data, opts, callback) {
119 | const {targetVersion, conflictKeys} = opts
120 | if (callback) process.nextTick(() => {
121 | const event = addEvent(data, targetVersion, conflictKeys)
122 | if (event == null) callback(new VersionConflictError('Rejected event due to conflict'))
123 | else {
124 | if (subscribed && this.onevents) this.onevents([event], event.version)
125 | callback(null, event.version)
126 | }
127 | })
128 | },
129 |
130 | send(data, opts = {}) {
131 | return new Promise((resolve, reject) => {
132 | this.sendRaw(data, opts, (err, version) => {
133 | if (err) reject(err)
134 | else resolve(version)
135 | })
136 | })
137 | },
138 |
139 | getVersion() {
140 | // base + events.length is next version.
141 | return Promise.resolve(nextVersion() - 1)
142 | },
143 |
144 | close() {},
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/core/lib/stores/ot.ts:
--------------------------------------------------------------------------------
1 | // This is a simple passthrough store which catches write conflicts and tries
2 | // to do operational tranformation to recover them.
3 | import * as I from '../interfaces'
4 | import err from '../err'
5 | import {queryTypes, resultTypes} from '../qrtypes'
6 | import {typeOrThrow} from '../typeregistry'
7 |
8 | const mapTxnWithPair = (type: I.ResultType, a: I.Txn, b: I.Txn, fn: (a: I.SingleTxn, b: I.SingleTxn) => I.SingleTxn) => {
9 | if (a instanceof Map) {
10 | const result = new Map>()
11 | if (!(b instanceof Map)) throw new err.InvalidDataError('Transactions are different types')
12 | for (const [k, av] of a.entries()) {
13 | const bv = b.get(k)
14 | if (bv != null) result.set(k, fn(av, bv))
15 | else result.set(k, av)
16 | }
17 | return result
18 | } else if (type === I.ResultType.Single) {
19 | return fn(a as I.Op, b as I.Op)
20 | } else {
21 | throw new err.UnsupportedTypeError('Type ' + type + ' not supported by ot store')
22 | // throw new err.InvalidDataError('Transactions are different types')
23 | }
24 | }
25 |
26 | const eachOp = (op: I.Op, fn: (op: I.SingleOp) => void) => {
27 | if (Array.isArray(op)) op.forEach(fn)
28 | else fn(op)
29 | }
30 |
31 | const otStore = (inner: I.Store /*, filter: (key: I.Key) => boolean */): I.Store => {
32 |
33 | return {
34 | ...inner,
35 |
36 | async mutate(type, txn, versions, opts = {}) {
37 | // First try to just submit the operation directly.
38 | while (true) {
39 | try {
40 | const result = await inner.mutate(type, txn, versions, opts)
41 | return result
42 | } catch (e) {
43 | // console.error('Caught error in mutate', e.stack)
44 | if (!(e instanceof err.WriteConflictError)
45 | || versions == null) {
46 | throw e
47 | }
48 |
49 | const uid = opts.meta ? opts.meta.uid : undefined
50 |
51 | // console.log('wowooo got a write conflict')
52 |
53 | // We don't know what conflicted, and the transaction could have a
54 | // lot of stuff in it.
55 | const q = resultTypes[type].getCorrespondingQuery(txn)
56 |
57 | // This is a terrible error message for a complex error. Should be
58 | // pretty rare in practice - basically the mutation type has to
59 | // match the type of the corresponding query. Which will basically
60 | // always be kv or single.
61 | if (queryTypes[q.type].resultType.type !== type) throw Error(`Mismatched query types unsupported ${queryTypes[q.type].resultType.name} != ${type}`)
62 |
63 | // empty to version == all.
64 | const catchupVersions: I.FullVersionRange = versions.map(v => v == null ? null : {from: v, to: new Uint8Array()})
65 | const {ops} = await inner.getOps(q, catchupVersions)
66 | if (ops.length === 0) throw e // Can't make progress.
67 |
68 | let madeProgress = false
69 | for (let i = 0; i < ops.length; i++) {
70 | if (uid != null && ops[i].meta.uid === uid) {
71 | // The transaction has actually already been applied - we just
72 | // found it. We don't need to do any more work; just return as
73 | // if this txn was the txn you tried to submit.
74 | return ops[i].versions
75 | }
76 |
77 | txn = mapTxnWithPair(type, txn, ops[i].txn, (a, b) => {
78 | // The mutation we recieved has not been collapsed together.
79 | // This requires an M*N transform, which is not implemented.
80 | if (Array.isArray(a)) throw e
81 | if (a.type === 'set' || a.type === 'rm') throw e
82 | const type = typeOrThrow(a.type)
83 | if (type.transform == null) throw e
84 |
85 | let {data} = a
86 | eachOp(b, (bop) => {
87 | // Can't transform if they're a different type
88 | if (a.type !== bop.type) throw e
89 | // console.log('xf', data, bop.data)
90 | data = type.transform!(data, bop.data, 'left')
91 | // console.log('->', data)
92 | madeProgress = true
93 | })
94 | return {type: a.type, data}
95 | })
96 |
97 | ops[i].versions.forEach((v, si) => {
98 | if (v != null && versions[si] != null) versions[si] = v
99 | })
100 | }
101 |
102 | // If we didn't make progress, abort to avoid an infinite loop.
103 | // The store is generating a conflict for some other reason. Let it be.
104 | if (!madeProgress) throw e
105 |
106 | // console.log('retrying with', txn)
107 | }
108 | }
109 | }
110 | }
111 | }
112 |
113 | export default otStore
114 |
--------------------------------------------------------------------------------
/prozess/prozessops.ts:
--------------------------------------------------------------------------------
1 | // This is a wrapper 'store' around prozess.
2 | //
3 | // The store type is raw. The 'document' is empty, and you can't
4 | // query anything.
5 |
6 | import {PClient} from 'prozess-client'
7 | import assert from 'assert'
8 |
9 | import * as I from '../interfaces'
10 | import err from '../err'
11 | import {decodeTxn, decodeEvent, sendTxn} from '../prozess'
12 | import {queryTypes} from '../qrtypes'
13 | import {V64, v64ToNum, vEq} from '../version'
14 | import { bitSet } from '../bit'
15 | import SubGroup from '../subgroup'
16 |
17 | // const codec = msgpack.createCodec({usemap: true})
18 |
19 | // const doNothing = () => {}
20 | const capabilities = {
21 | // ... Except you can't fetch. :/
22 | queryTypes: bitSet(I.QueryType.AllKV, I.QueryType.KV, I.QueryType.StaticRange, I.QueryType.Range),
23 | mutationTypes: bitSet(I.ResultType.KV),
24 | }
25 |
26 | // TODO: pick a better port.
27 | const prozessStore = (conn: PClient): I.Store => {
28 | const source = conn.source!
29 |
30 | const getOps: I.GetOpsFn = async (query, versions, opts) => {
31 | // We don't need to wait for ready here because the query is passed straight back.
32 | if (query.type !== I.QueryType.AllKV && query.type !== I.QueryType.KV && query.type !== I.QueryType.StaticRange) throw new err.UnsupportedTypeError()
33 | const qops = queryTypes[query.type]
34 |
35 | // We need to fetch ops in the range of (from, to].
36 | const vs = versions[0]
37 | if (!vs || vEq(vs.from, vs.to)) return {ops: [], versions: vs ? [{from:vs.from, to:vs.to}] : []}
38 |
39 | const {from, to} = vs
40 |
41 | const data = await conn.getEvents(from.length ? v64ToNum(from) + 1 : 0, to.length ? v64ToNum(to) : -1, {})
42 | // console.log('client.getEvents', from+1, to, data)
43 |
44 | // Filter events by query.
45 | let ops = data!.events.map(event => decodeEvent(event, source))
46 | .filter((data) => {
47 | const txn = qops.adaptTxn(data.txn, query.q)
48 | if (txn == null) return false
49 | else {
50 | data.txn = txn
51 | return true
52 | }
53 | })
54 |
55 | return {
56 | ops,
57 | versions: [{from:V64(data!.v_start - 1), to: V64(data!.v_end - 1)}]
58 | }
59 | }
60 |
61 | const subGroup = new SubGroup({
62 | getOps,
63 | start(v) {
64 | let currentVersion = -1
65 | conn.onevents = (events, nextVersion) => {
66 | // console.log('conn.onevents', events, nextVersion)
67 | const txns: I.TxnWithMeta[] = []
68 | if (events.length == 0) return
69 |
70 | let fromV: I.Version | null = null
71 | events.forEach(event => {
72 | if (currentVersion !== -1) assert.strictEqual(
73 | currentVersion, event.version - 1,
74 | 'Error: Prozess version consistency violation. This needs debugging'
75 | )
76 |
77 | if (fromV == null) fromV = V64(event.version - 1)
78 |
79 | // TODO: Pack & unpack batches.
80 | const [txn, meta] = decodeTxn(event.data)
81 |
82 | const nextVersion = event.version - 1 + event.batch_size
83 |
84 | txns.push({txn, meta, versions: [V64(nextVersion)]})
85 |
86 | // store.onTxn!(source, V64(event.version - 1), V64(nextVersion), 'kv', txn, meta)
87 |
88 | currentVersion = nextVersion
89 | })
90 |
91 | subGroup.onOp(0, fromV!, txns)
92 | }
93 |
94 | return new Promise((resolve, reject) => {
95 | const vs = v == null ? null : v[0]
96 | // Should this be vs or vs+1 or something?
97 | conn.subscribe(vs == null ? -1 : v64ToNum(vs) + 1, {}, (err, subdata) => {
98 | if (err) {
99 | // I'm not sure what to do here. It'll depend on the error
100 | console.error('Error subscribing to prozess store')
101 | return reject(err)
102 | }
103 |
104 | // console.log('prozess ops from', V64(subdata!.v_end + 1), subdata)
105 | // The events will all be emitted via the onevent callback. We'll do catchup there.
106 | resolve([V64(subdata!.v_end - 1)]) // +1 ???
107 | })
108 | })
109 | }
110 | })
111 |
112 | const store: I.Store = {
113 | storeInfo: {
114 | uid: `prozess(${source})`,
115 | sources: [source],
116 | capabilities,
117 | },
118 |
119 | async mutate(type, _txn, versions, opts = {}) {
120 | if (type !== I.ResultType.KV) throw new err.UnsupportedTypeError()
121 | const txn = _txn as I.KVTxn
122 |
123 | const version = await sendTxn(conn, txn, opts.meta || {}, (versions && versions[0]) || new Uint8Array(), {})
124 | return [version!]
125 | },
126 |
127 | fetch() {
128 | throw new err.UnsupportedTypeError()
129 | },
130 | getOps,
131 | subscribe: subGroup.create.bind(subGroup),
132 |
133 | close() {
134 | conn.close()
135 | },
136 | }
137 |
138 | return store
139 | }
140 |
141 | export default prozessStore
--------------------------------------------------------------------------------
/demos/bp/server.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../../lib/interfaces'
2 | import lmdbStore from '../../lib/stores/lmdb'
3 | import kvStore from '../../lib/stores/kvmem'
4 | import mapStore from '../../lib/stores/map'
5 | import router, {ALL} from '../../lib/stores/router'
6 | import createWss from '../../lib/net/wsserver'
7 |
8 | import render from './browserclient/render'
9 | // import createHttp from '../net/httpserver'
10 | import http from 'http'
11 |
12 | import fs from 'fs'
13 | import url from 'url'
14 |
15 | import express from 'express'
16 | import bodyParser from 'body-parser'
17 | import jsesc from 'jsesc'
18 |
19 | import mod from './rustpng'
20 | import opmem from '../../lib/stores/opmem';
21 | import { vRangeTo } from '../../lib/version';
22 |
23 | process.on('unhandledRejection', err => { throw err })
24 |
25 | // const store = augment(lmdbStore(
26 |
27 | type BPWorld = {
28 | v: 2,
29 | offx: number,
30 | offy: number,
31 | img: string
32 | }
33 |
34 | const data = new Map(
35 | fs.readFileSync('bp.json', 'utf8')
36 | .split('\n')
37 | .filter(x => x !== '')
38 | .map(data => JSON.parse(data))
39 | .map(([key, val]) => [key, val.data] as [string, BPWorld])
40 | )
41 | // console.log(data)
42 |
43 | ;(async () => {
44 | const rootStore = await kvStore(data, {inner: opmem({source: 'rootstore'})})
45 | const imgStore = mapStore(rootStore, (v, k) => {
46 | console.log('v', v)
47 | return Buffer.from(mod.convert(v.img, 8, 1, 2) as any)
48 | })
49 |
50 | const store = router()
51 | store.mount(rootStore, 'world/', ALL, '', true)
52 | store.mount(imgStore, 'img/', ALL, '', false)
53 |
54 | // const s2 = augment(kvStore())
55 | // s2.storeInfo.sources[0] = 's2'
56 | // store.mount(s2, 's2/', ALL, '', true)
57 |
58 | // const s3 = augment(kvStore())
59 | // s3.storeInfo.sources[0] = 's3'
60 | // // s3.storeInfo.sources.push(s2.storeInfo.sources[0])
61 | // // s3.storeInfo.sources.push(imgStore.storeInfo.sources[0])
62 | // store.mount(s3, 's3/', ALL, '', true)
63 | // console.log(store.storeInfo.sources)
64 |
65 | // store.subscribe({type: 'kv', q: new Set(['world/asdf', 'img/adf', 's3/asdf', 's2/sfd'])}, {})
66 |
67 |
68 | const app = express()
69 | app.use(express.static(`${__dirname}/public`))
70 | app.use(bodyParser.json())
71 |
72 |
73 | // TODO: Do all this work in another map() function.
74 | app.get('*', async (req, res, next) => {
75 | // Try to answer request using a rendered statecraft store
76 | const key = url.parse(req.url).pathname!.slice(1)
77 | // console.log('key', key)
78 | const result = await store.fetch({type: I.QueryType.KV, q: new Set([key])})
79 | // console.log('result', result)
80 | let value = result.results.get(key)
81 | if (value == null) return next()
82 |
83 | res.setHeader('content-type', 'text/html')
84 | res.setHeader('x-sc-version', JSON.stringify(result.versions))
85 | // TODO: generate an etag based off the version, and parse it back to pass to fetch.
86 |
87 | const versionRange = result.versions
88 | const versions: I.FullVersion = vRangeTo(versionRange)
89 |
90 | const mimetype = Buffer.isBuffer(value) ? 'image/png' : 'application/json'
91 | // if (Buffer.isBuffer(value)) value = value.toString('base64')
92 | if (Buffer.isBuffer(value)) value = `data:${mimetype};base64,${value.toString('base64')}`
93 |
94 | // const v = Buffer.isBuffer(value) ? value.toString('base64') : value
95 | res.send(`
96 | ${render(mimetype, value).toString()}
97 |
106 |
107 | `)
108 | })
109 |
110 | app.get('/:user/:key.json', async (req, res, next) => {
111 | const {user, key} = req.params
112 | const k = `world/${user}/${key}`
113 | const result = await store.fetch({type: I.QueryType.KV, q: new Set([k])})
114 | const value = result.results.get(k)
115 | if (value == null) return next()
116 | res.setHeader('x-sc-version', JSON.stringify(result.versions))
117 | res.setHeader('content-type', 'application/json')
118 | res.json({
119 | v: 0,
120 | data: value,
121 | readonly: false,
122 | })
123 | })
124 |
125 | app.put('/:user/:key.json', async (req, res, next) => {
126 | const {user, key} = req.params
127 | const txn = new Map([[`world/${user}/${key}`, {type:'set', data: req.body.data}]])
128 | const result = await store.mutate(I.ResultType.KV, txn)
129 | console.log(result)
130 | res.end()
131 | })
132 |
133 | app.get('/:user/:key', (req, res) => {
134 | res.sendFile('public/editor.html', {root: __dirname})
135 | })
136 |
137 | const server = http.createServer(app)
138 |
139 | const wss = createWss({server}, store)
140 |
141 | server.listen(2000)
142 | console.log('http server listening on port 2000')
143 | })()
--------------------------------------------------------------------------------
/core/lib/stores/map.ts:
--------------------------------------------------------------------------------
1 | import * as I from '../interfaces'
2 | import err from '../err'
3 | import {queryTypes} from '../qrtypes'
4 | import iterGuard from '../iterGuard'
5 | import {vRangeTo} from '../version'
6 |
7 | const supportedOpTypes = new Set(['rm', 'set'])
8 |
9 | export type MapFn = (v: In, k: I.Key | null, version: I.FullVersion) => Out
10 |
11 | const mapValues = (map: Map, fn: (v: In, k: K) => Out): Map => {
12 | const result = new Map()
13 | for (const [key, val] of map.entries()) {
14 | result.set(key, fn(val, key))
15 | }
16 | return result
17 | }
18 |
19 | const mapSingleOp = (op: I.SingleOp, key: I.Key | null, version: I.FullVersion, fn: MapFn): I.SingleOp => {
20 | switch (op.type) {
21 | case 'rm': return op as I.SingleOp as I.SingleOp
22 | case 'set': return {type: 'set', data: fn(op.data, key, version)}
23 | default: throw Error('Map function cannot process operation with type ' + op.type)
24 | }
25 | }
26 |
27 | const mapTxnWithMetas = (type: I.ResultOps>, txn: I.TxnWithMeta[], fn: MapFn): I.TxnWithMeta[] => (
28 | txn.map(({versions, txn, meta}) => ({
29 | txn: type.mapTxn(txn, (op: I.Op, k) => (
30 | Array.isArray(op)
31 | ? op.map(singleOp => mapSingleOp(singleOp, k, versions, fn))
32 | : mapSingleOp(op, k, versions, fn)
33 | )),
34 | versions,
35 | meta,
36 | }))
37 | )
38 |
39 | export interface MapOpts {
40 | fnUid?: string, // This is a UID that should change each time the function is edited.
41 | }
42 |
43 | // Syncronous map fn
44 | const map = (inner: I.Store, mapfn: MapFn, opts: MapOpts = {}): I.Store