Skip to content

Commit 83fc77b

Browse files
authored
Merge pull request #12 from jsonjoy-com/demo
Demo and fixes
2 parents 90cc182 + 2fdf462 commit 83fc77b

File tree

18 files changed

+3145
-740
lines changed

18 files changed

+3145
-740
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,5 @@ out.bin
128128

129129
/gh-pages/
130130
/db/
131+
132+
*storybook.log

package.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"demo:e2e:sample-api:uws": "ts-node src/__demos__/sample-api/main-uws.ts",
5656
"demo:e2e:json-crdt-server:http1": "ts-node src/__demos__/json-crdt-server/main-http1.ts",
5757
"demo:e2e:json-crdt-server:uws": "ts-node src/__demos__/json-crdt-server/main-uws.ts",
58+
"demo:block-sync-ui": "webpack serve --config ./src/__demos__/block-sync-ui/webpack.config.js",
5859
"start:json-crdt-server:http1": "NODE_ENV=production PORT=80 JSON_CRDT_STORE=level pm2 start lib/__demos__/json-crdt-server/main-http1.js",
5960
"start": "NODE_ENV=production PORT=80 JSON_CRDT_STORE=level pm2 start lib/__demos__/json-crdt-server/main-http1.js --exp-backoff-restart-delay=100",
6061
"coverage": "yarn test --collectCoverage",
@@ -64,45 +65,62 @@
6465
"publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages"
6566
},
6667
"peerDependencies": {
68+
"browser-level": "*",
6769
"rxjs": "7",
6870
"tslib": "2"
6971
},
7072
"peerDependenciesMeta": {
7173
"rxjs": {
7274
"optional": true
75+
},
76+
"browser-level": {
77+
"optional": true
7378
}
7479
},
7580
"dependencies": {
7681
"@jsonjoy.com/jit-router": "^1.0.1",
7782
"@jsonjoy.com/json-pack": "^1.1.0",
7883
"@jsonjoy.com/util": "^1.3.0",
7984
"abstract-level": "^2.0.0",
80-
"browser-level": "^1.0.1",
8185
"fs-zoo": "^1.1.0",
82-
"json-joy": "^16.24.0",
86+
"json-joy": "^16.25.0",
8387
"memfs": "^4.11.0",
8488
"memory-level": "^1.0.0",
89+
"rx-use": "^1.8.1",
8590
"sonic-forest": "^1.0.3",
8691
"thingies": "^2.1.0"
8792
},
8893
"devDependencies": {
8994
"@types/benchmark": "^2.1.5",
9095
"@types/jest": "^29.5.12",
96+
"@types/react": "^18.3.8",
97+
"@types/react-dom": "^18.3.0",
9198
"@types/ws": "^8.5.10",
9299
"benchmark": "^2.1.4",
100+
"browser-level": "^1.0.1",
93101
"classic-level": "^1.4.1",
102+
"clickable-json": "^1.14.0",
103+
"html-webpack-plugin": "^5.6.0",
94104
"jest": "^29.7.0",
105+
"nano-theme": "^1.4.2",
106+
"nice-ui": "^1.9.2",
95107
"prettier": "^3.2.5",
108+
"react": "^18.3.1",
109+
"react-dom": "^18.3.1",
96110
"rimraf": "^5.0.5",
97111
"rxjs": "^7.8.1",
98112
"ts-jest": "^29.1.2",
113+
"ts-loader": "^9.5.1",
99114
"ts-node": "^10.9.2",
100115
"tslib": "^2.6.2",
101116
"tslint": "^6.1.3",
102117
"tslint-config-common": "^1.6.2",
103118
"typedoc": "^0.25.13",
104119
"typescript": "^5.4.5",
105120
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.44.0",
121+
"webpack": "^5.94.0",
122+
"webpack-cli": "^5.1.4",
123+
"webpack-dev-server": "^5.1.0",
106124
"websocket": "^1.0.34",
107125
"ws": "^8.16.0"
108126
},
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom/client';
3+
import {JsonCrdtRepo} from '../../json-crdt-repo/JsonCrdtRepo';
4+
import {ClickableJsonCrdt} from 'clickable-json';
5+
import {Model, Patch} from 'json-joy/lib/json-crdt';
6+
7+
/* tslint:disable no-console */
8+
9+
const repo = new JsonCrdtRepo({
10+
wsUrl: 'wss://demo-iasd8921ondk0.jsonjoy.com/rpc',
11+
});
12+
const id = 'block-sync-ui-demo-id';
13+
const session = repo.make(id);
14+
15+
const model = session.model;
16+
17+
model.api.onPatch.listen((op) => {
18+
console.log('onPatch', op + '');
19+
});
20+
21+
model.api.onLocalChange.listen((op) => {
22+
console.log('onLocalChange', op);
23+
});
24+
25+
model.api.onFlush.listen((op) => {
26+
console.log('onFlush', op + '');
27+
});
28+
29+
model.api.onTransaction.listen((op) => {
30+
console.log('onTransaction', op);
31+
});
32+
33+
const Demo: React.FC = () => {
34+
const [remote, setRemote] = React.useState<Model | null>(null);
35+
36+
return (
37+
<div style={{padding: 32}}>
38+
<ClickableJsonCrdt model={model} showRoot />
39+
<hr />
40+
<button
41+
onClick={async () => {
42+
const {block} = await repo.remote.read(id);
43+
const model = Model.fromBinary(block.snapshot.blob);
44+
for (const batch of block.tip)
45+
for (const patch of batch.patches) model.applyPatch(Patch.fromBinary(patch.blob));
46+
setRemote(model);
47+
}}
48+
>
49+
Load remote state
50+
</button>
51+
<br />
52+
{!!remote && (
53+
<code style={{fontSize: 8}}>
54+
<pre>{remote.toString()}</pre>
55+
</code>
56+
)}
57+
</div>
58+
);
59+
};
60+
61+
const div = document.createElement('div');
62+
document.body.appendChild(div);
63+
const root = ReactDOM.createRoot(div);
64+
root.render(<Demo />);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const path = require('path');
2+
const HtmlWebpackPlugin = require('html-webpack-plugin');
3+
4+
module.exports = {
5+
mode: 'development',
6+
devtool: 'inline-source-map',
7+
entry: {
8+
bundle: __dirname + '/main',
9+
},
10+
plugins: [
11+
new HtmlWebpackPlugin({
12+
title: 'Development',
13+
}),
14+
],
15+
module: {
16+
rules: [
17+
{
18+
test: /\.tsx?$/,
19+
exclude: /node_modules/,
20+
loader: 'ts-loader',
21+
},
22+
],
23+
},
24+
resolve: {
25+
extensions: ['.tsx', '.ts', '.js'],
26+
},
27+
output: {
28+
filename: '[name].js',
29+
path: path.resolve(__dirname, '../../dist'),
30+
},
31+
devServer: {
32+
port: 9949,
33+
hot: false,
34+
},
35+
};

src/browser/createBinaryWsRpcClient.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import {CborJsonValueCodec} from '@jsonjoy.com/json-pack/lib/codecs/cbor';
22
import {Writer} from '@jsonjoy.com/util/lib/buffers/Writer';
3-
import {RpcPersistentClient, WebSocketChannel} from '../common';
3+
import {RpcPersistentClient} from '../common/rpc/RpcPersistentClient';
4+
import {WebSocketChannel} from '../common/channel/channel';
45
import {RpcCodec} from '../common/codec/RpcCodec';
56
import {BinaryRpcMessageCodec} from '../common/codec/binary';
67

78
/**
89
* Constructs a JSON Reactive RPC client.
10+
*
11+
* ```typescript
12+
* const client = createJsonWsRpcClient('wss://api.host.com', 'token');
13+
* ```
14+
*
915
* @param url RPC endpoint.
1016
* @param token Authentication token.
1117
* @returns An RPC client.
1218
*/
13-
export const createBinaryWsRpcClient = (url: string, token: string) => {
19+
export const createBinaryWsRpcClient = (url: string, token?: string) => {
1420
const writer = new Writer(1024 * 4);
1521
const msg = new BinaryRpcMessageCodec();
1622
const req = new CborJsonValueCodec(writer);
1723
const codec = new RpcCodec(msg, req, req);
24+
const protocols: string[] = [codec.specifier()];
25+
if (token) protocols.push(token);
1826
const client = new RpcPersistentClient({
1927
codec,
2028
channel: {
2129
newChannel: () =>
2230
new WebSocketChannel({
23-
newSocket: () => new WebSocket(url, [codec.specifier(), token]),
31+
newSocket: () => new WebSocket(url, protocols),
2432
}),
2533
},
2634
});

src/common/channel/channel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export interface PersistentChannelParams<T extends string | Uint8Array = string
195195

196196
/**
197197
* Channel which automatically reconnects if disconnected.
198+
* @todo Check if this is still used.
198199
*/
199200
export class PersistentChannel<T extends string | Uint8Array = string | Uint8Array> {
200201
/**

src/common/messages/messages.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {MsgPackEncoder} from '@jsonjoy.com/json-pack/lib/msgpack';
2-
import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder';
3-
import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder';
1+
import {MsgPackEncoder} from '@jsonjoy.com/json-pack/lib/msgpack'; // TODO: Should not statically import codecs
2+
import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder'; // TODO: Should not statically import codecs
3+
import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder'; // TODO: Should not statically import codecs
44
import {BinaryMessageType} from '../codec/binary/constants';
55
import {CompactMessageType} from '../codec/compact/constants';
66
import {validateId, validateMethod} from '../rpc/validation';

src/json-crdt-repo/JsonCrdtRepo.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {BrowserLevel} from 'browser-level';
2+
import {createBinaryWsRpcClient} from '../browser/createBinaryWsRpcClient';
3+
import {DemoServerClient, DemoServerRemoteHistory} from './remote/DemoServerRemoteHistory';
4+
import {EditSessionFactory} from './session/EditSessionFactory';
5+
import {BinStrLevel, LevelLocalRepoPubSubMessage} from './local/level/types';
6+
import {PubSubBC} from './pubsub';
7+
import {Locks} from 'thingies/lib/Locks';
8+
import {LevelLocalRepo, LevelLocalRepoOpts} from './local/level/LevelLocalRepo';
9+
import {Model} from 'json-joy/lib/json-crdt';
10+
import {onLine$} from 'rx-use/lib/onLine$';
11+
import type {EditSession} from './session/EditSession';
12+
13+
export interface JsonCrdtRepoOpts {
14+
name: string;
15+
wsUrl: string;
16+
}
17+
18+
export class JsonCrdtRepo {
19+
public readonly sessions: EditSessionFactory;
20+
public readonly opts: JsonCrdtRepoOpts;
21+
public readonly remote: DemoServerRemoteHistory;
22+
23+
constructor(opts: Partial<JsonCrdtRepoOpts>) {
24+
this.opts = {
25+
name: opts.name ?? 'json-crdt-repo',
26+
wsUrl: opts.wsUrl ?? 'ws://localhost:9999/rpc',
27+
...opts,
28+
};
29+
const client = createBinaryWsRpcClient(this.opts.wsUrl) as DemoServerClient;
30+
this.remote = new DemoServerRemoteHistory(client);
31+
const kv: BinStrLevel = new BrowserLevel(this.opts.name, {
32+
keyEncoding: 'utf8',
33+
valueEncoding: 'view',
34+
}) as any;
35+
const pubsub = new PubSubBC<LevelLocalRepoPubSubMessage>(this.opts.name);
36+
const locks = new Locks();
37+
const sid: number = this.readSid();
38+
const connected$ = onLine$ as LevelLocalRepoOpts['connected$'];
39+
const repo = new LevelLocalRepo({
40+
kv,
41+
locks,
42+
sid,
43+
rpc: this.remote,
44+
pubsub,
45+
connected$,
46+
});
47+
this.sessions = new EditSessionFactory({
48+
repo,
49+
sid,
50+
});
51+
}
52+
53+
public readSid(): number {
54+
const ls = window.localStorage;
55+
const key = this.opts.name + '-sid';
56+
const value = ls.getItem(key);
57+
if (value) return +value;
58+
const sid: number = Model.sid();
59+
ls.setItem(key, sid + '');
60+
return sid;
61+
}
62+
63+
public genId(): string {
64+
return 'id-' + Date.now().toString(36) + Math.random().toString(36).slice(2);
65+
}
66+
67+
public make(id: string = this.genId()): EditSession {
68+
const {session} = this.sessions.make({
69+
id: [id],
70+
});
71+
return session;
72+
}
73+
}

src/json-crdt-repo/__tests__/testbed.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {MemoryLevel} from 'memory-level';
66
import {pubsub as createPubsub} from '../pubsub';
77
import {BinStrLevel, LevelLocalRepoPubSub} from '../local/level/types';
88
import {EditSessionFactory} from '../session/EditSessionFactory';
9+
import {Model, Patch} from 'json-joy/lib/json-crdt';
910

1011
/* tslint:disable:no-console */
1112

@@ -26,6 +27,15 @@ export class Testbed {
2627
public createBrowser(): BrowserTestbed {
2728
return new BrowserTestbed(this);
2829
}
30+
31+
public readonly getModelFromRemote = async (id: string | string[]): Promise<Model> => {
32+
if (Array.isArray(id)) id = id.join('/');
33+
const res = await this.remote.client.call('block.get', {id});
34+
const model = Model.fromBinary(res.block.snapshot.blob);
35+
for (const batch of res.block.tip)
36+
for (const patch of batch.patches) model.applyPatch(Patch.fromBinary(patch.blob));
37+
return model;
38+
};
2939
}
3040

3141
export class BrowserTestbed {
@@ -38,6 +48,7 @@ export class BrowserTestbed {
3848
public readonly sid: number = 12345678,
3949
) {
4050
this.id = this.global.genId();
51+
// TODO: Namespace locks to a specific repo.
4152
this.locks = new Locks();
4253
this.kv = new MemoryLevel<string, Uint8Array>({
4354
keyEncoding: 'utf8',
@@ -93,6 +104,10 @@ export class LocalRepoTestbed {
93104
});
94105
}
95106

107+
public readonly getModelFromRemote = async (id: string | string[]): Promise<Model> => {
108+
return await this.tab.browser.global.getModelFromRemote(id);
109+
};
110+
96111
public readonly stop = async () => {
97112
await this.repo.stop();
98113
};

0 commit comments

Comments
 (0)