Skip to content

Commit 8e04672

Browse files
authored
Merge pull request #10 from jsonjoy-com/local-server-crud
Local repo, edit session and demo server improvements
2 parents f6e6c1c + f07e2ce commit 8e04672

File tree

88 files changed

+8348
-2127
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+8348
-2127
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ typedocs/
127127
out.bin
128128

129129
/gh-pages/
130+
/db/

package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"prettier:check": "prettier --ignore-path .gitignore --list-different 'src/**/*.{ts,tsx,js,jsx}'",
4444
"lint": "yarn tslint",
4545
"tslint": "tslint 'src/**/*.{js,jsx,ts,tsx}' -t verbose --project .",
46-
"clean": "rimraf lib typedocs coverage gh-pages yarn-error.log",
46+
"clean": "rimraf lib typedocs coverage gh-pages yarn-error.log db",
4747
"build": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib",
4848
"jest": "node -r ts-node/register ./node_modules/.bin/jest",
4949
"test": "jest --maxWorkers 7",
@@ -72,18 +72,23 @@
7272
},
7373
"dependencies": {
7474
"@jsonjoy.com/jit-router": "^1.0.1",
75-
"@jsonjoy.com/json-pack": "^1.0.2",
76-
"@jsonjoy.com/util": "^1.0.0",
77-
"json-joy": "^15.8.0",
78-
"memfs": "^5.0.0-next.1",
79-
"sonic-forest": "^1.0.0",
75+
"@jsonjoy.com/json-pack": "^1.1.0",
76+
"@jsonjoy.com/util": "^1.3.0",
77+
"abstract-level": "^2.0.0",
78+
"browser-level": "^1.0.1",
79+
"fs-zoo": "^1.1.0",
80+
"json-joy": "^16.24.0",
81+
"memfs": "^4.11.0",
82+
"memory-level": "^1.0.0",
83+
"sonic-forest": "^1.0.3",
8084
"thingies": "^2.1.0"
8185
},
8286
"devDependencies": {
8387
"@types/benchmark": "^2.1.5",
8488
"@types/jest": "^29.5.12",
8589
"@types/ws": "^8.5.10",
8690
"benchmark": "^2.1.4",
91+
"classic-level": "^1.4.1",
8792
"jest": "^29.7.0",
8893
"prettier": "^3.2.5",
8994
"rimraf": "^5.0.5",
@@ -95,7 +100,7 @@
95100
"tslint-config-common": "^1.6.2",
96101
"typedoc": "^0.25.13",
97102
"typescript": "^5.4.5",
98-
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.23.0",
103+
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.44.0",
99104
"websocket": "^1.0.34",
100105
"ws": "^8.16.0"
101106
},
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import {setup} from './setup';
1+
import {setupMemory, setupLevelMemory, setupLevelClassic} from './setup';
22
import {runBlockTests} from '../../../__tests__/json-crdt-server/block';
33
import type {ApiTestSetup} from '../../../common/rpc/__tests__/runApiTests';
44

5-
runBlockTests(setup as ApiTestSetup);
5+
describe('MemoryStore', () => {
6+
runBlockTests(setupMemory as ApiTestSetup);
7+
});
8+
9+
describe('LevelStore(memory-level)', () => {
10+
runBlockTests(setupLevelMemory as ApiTestSetup);
11+
});
12+
13+
describe('LevelStore(classic-level)', () => {
14+
runBlockTests(setupLevelClassic as ApiTestSetup);
15+
});
Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import {MemoryLevel} from 'memory-level';
12
import {buildE2eClient} from '../../../common/testing/buildE2eClient';
23
import {createCaller} from '../routes';
4+
import {LevelStore} from '../services/blocks/store/level/LevelStore';
35
import {Services} from '../services/Services';
6+
import {ClassicLevel} from 'classic-level';
7+
import {MemoryStore} from '../services/blocks/store/MemoryStore';
8+
import type {Store} from '../services/blocks/store/types';
49

5-
export const setup = async () => {
6-
const services = new Services();
10+
export const setup = async (store: Store = new MemoryStore(), close?: () => Promise<void>) => {
11+
const services = new Services({store});
712
const {caller} = createCaller(services);
813
const {client} = buildE2eClient(caller, {
914
writerDefaultBufferKb: [1, 32],
@@ -14,8 +19,31 @@ export const setup = async () => {
1419
});
1520
const call = client.call.bind(client);
1621
const call$ = client.call$.bind(client);
17-
const stop = () => {};
22+
const stop = async (): Promise<void> => {
23+
await close?.();
24+
};
1825
return {call, call$, stop};
1926
};
2027

28+
export const setupMemory = async () => {
29+
const store = new MemoryStore();
30+
return setup(store);
31+
};
32+
33+
export const setupLevelMemory = async () => {
34+
const kv = new MemoryLevel<string, Uint8Array>({
35+
keyEncoding: 'utf8',
36+
valueEncoding: 'view',
37+
});
38+
const store = new LevelStore(<any>kv);
39+
return setup(store);
40+
};
41+
42+
export const setupLevelClassic = async () => {
43+
const kv = new ClassicLevel<string, Uint8Array>('./db', {valueEncoding: 'view'});
44+
await kv.open();
45+
const store = new LevelStore(<any>kv);
46+
return setup(store, async () => kv.close());
47+
};
48+
2149
export type JsonCrdtTestSetup = typeof setup;

src/__demos__/json-crdt-server/main-uws.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ const app = new RpcApp<MyCtx>({
1515
port: +(process.env.PORT || 9999),
1616
});
1717
app.startWithDefaults();
18+
19+
// tslint:disable-next-line:no-console
20+
console.log(app + '');
Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import {new_} from './methods/new';
22
import {get} from './methods/get';
3+
import {view} from './methods/view';
34
import {upd} from './methods/upd';
45
import {del} from './methods/del';
56
import {scan} from './methods/scan';
67
import {listen} from './methods/listen';
78
import {
8-
Block,
99
BlockId,
1010
BlockPatch,
1111
BlockPatchPartial,
1212
BlockPatchPartialReturn,
1313
BlockCur,
14-
BlockNew,
1514
BlockSnapshot,
1615
NewBlockSnapshotResponse,
1716
BlockEvent,
17+
BlockBatch,
18+
BlockBatchPartial,
19+
BlockBatchPartialReturn,
20+
BlockBatchSeq,
21+
BlockSnapshotReturn,
22+
Block,
1823
} from './schema';
1924
import type {RouteDeps, Router, RouterBase} from '../types';
25+
import {pull} from './methods/pull';
2026

2127
export const block =
2228
(d: RouteDeps) =>
@@ -25,25 +31,32 @@ export const block =
2531

2632
system.alias('BlockId', BlockId);
2733
system.alias('BlockCur', BlockCur);
28-
system.alias('BlockNew', BlockNew);
34+
system.alias('BlockBatchSeq', BlockBatchSeq);
35+
2936
system.alias('Block', Block);
3037

38+
system.alias('BlockSnapshotReturn', BlockSnapshotReturn);
3139
system.alias('BlockSnapshot', BlockSnapshot);
3240
system.alias('NewBlockSnapshotResponse', NewBlockSnapshotResponse);
3341

3442
system.alias('BlockPatch', BlockPatch);
3543
system.alias('BlockPatchPartial', BlockPatchPartial);
3644
system.alias('BlockPatchPartialReturn', BlockPatchPartialReturn);
45+
system.alias('BlockBatch', BlockBatch);
46+
system.alias('BlockBatchPartial', BlockBatchPartial);
47+
system.alias('BlockBatchPartialReturn', BlockBatchPartialReturn);
3748

3849
system.alias('BlockEvent', BlockEvent);
3950

4051
// prettier-ignore
4152
return (
4253
( new_(d)
4354
( get(d)
55+
( view(d)
4456
( upd(d)
4557
( del(d)
4658
( listen(d)
4759
( scan(d)
48-
( r ))))))));
60+
( pull(d)
61+
( r ))))))))));
4962
};
Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {ResolveType} from 'json-joy/lib/json-type';
2-
import {BlockRef, BlockIdRef} from '../schema';
2+
import {BlockIdRef, BlockRef} from '../schema';
33
import type {RouteDeps, Router, RouterBase} from '../../types';
44

55
export const get =
@@ -12,27 +12,19 @@ export const get =
1212
}),
1313
);
1414

15-
const Response = t.Object(t.prop('block', BlockRef));
15+
// prettier-ignore
16+
const Response = t.Object(
17+
t.prop('block', BlockRef),
18+
);
1619

1720
const Func = t.Function(Request, Response).options({
1821
title: 'Read Block',
1922
intro: 'Retrieves a block by ID.',
2023
});
2124

2225
return r.prop('block.get', Func, async ({id}) => {
23-
const {snapshot} = await services.blocks.get(id);
24-
const response: ResolveType<typeof Response> = {
25-
block: {
26-
id: snapshot.id,
27-
ts: snapshot.created,
28-
snapshot: {
29-
blob: snapshot.blob,
30-
cur: snapshot.seq,
31-
ts: snapshot.created,
32-
},
33-
tip: [],
34-
},
35-
};
26+
const {block} = await services.blocks.get(id);
27+
const response: ResolveType<typeof Response> = {block};
3628
return response;
3729
});
3830
};

src/__demos__/json-crdt-server/routes/block/methods/listen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {map, switchMap, tap} from 'rxjs';
1+
import {map, switchMap} from 'rxjs';
22
import {BlockEventRef, BlockIdRef} from '../schema';
33
import type {RouteDeps, Router, RouterBase} from '../../types';
44

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
BlockIdRef,
3-
BlockPatchPartialRef,
4-
BlockPatchPartialReturnRef,
5-
BlockNewRef,
6-
NewBlockSnapshotResponseRef,
7-
} from '../schema';
1+
import {BlockIdRef, BlockBatchPartialRef, BlockSnapshotReturnRef} from '../schema';
82
import type {RouteDeps, Router, RouterBase} from '../../types';
93

104
export const new_ =
@@ -16,26 +10,20 @@ export const new_ =
1610
description:
1711
'The ID of the new block. Must be a unique ID, if the block already exists it will return an error.',
1812
}),
19-
t.prop('patches', t.Array(BlockPatchPartialRef)).options({
20-
title: 'Patches',
21-
description: 'The patches to apply to the document.',
13+
t.propOpt('batch', BlockBatchPartialRef).options({
14+
title: 'Batch',
15+
description: 'A collection of patches to apply to the new block.',
2216
}),
2317
);
2418

25-
const Response = t
26-
.Object(
27-
t.prop('block', BlockNewRef),
28-
t.prop('snapshot', NewBlockSnapshotResponseRef),
29-
t.prop('patches', t.Array(BlockPatchPartialReturnRef)).options({
30-
title: 'Patches',
31-
description: 'The list of patches to apply to the newly created block.',
32-
}),
33-
)
34-
.options({
35-
title: 'New block creation response',
36-
description:
37-
'The response object for the new block creation, contains server generated metadata without blobs supplied by the client.',
38-
});
19+
// prettier-ignore
20+
const Response = t.Object(
21+
t.prop('snapshot', BlockSnapshotReturnRef),
22+
).options({
23+
title: 'New block creation response',
24+
description:
25+
'The response object for the new block creation, contains server generated metadata without blobs supplied by the client.',
26+
});
3927

4028
const Func = t.Function(Request, Response).options({
4129
title: 'Create Block',
@@ -44,20 +32,15 @@ export const new_ =
4432
'Creates a new block out of supplied patches. A block starts empty with an `undefined` state, and patches are applied to it.',
4533
});
4634

47-
return r.prop('block.new', Func, async ({id, patches}) => {
48-
const res = await services.blocks.create(id, patches);
35+
return r.prop('block.new', Func, async ({id, batch}) => {
36+
const {block} = await services.blocks.create(id, batch);
37+
const snapshot = block.snapshot;
4938
return {
50-
block: {
51-
id: res.snapshot.id,
52-
ts: res.snapshot.created,
53-
},
5439
snapshot: {
55-
cur: res.snapshot.seq,
56-
ts: res.snapshot.created,
40+
id,
41+
seq: snapshot.seq,
42+
ts: snapshot.ts,
5743
},
58-
patches: res.patches.map((patch) => ({
59-
ts: patch.created,
60-
})),
6144
};
6245
});
6346
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {BlockIdRef, BlockCurRef, BlockBatchRef, BlockSnapshotRef} from '../schema';
2+
import type {RouteDeps, Router, RouterBase} from '../../types';
3+
4+
export const pull =
5+
({t, services}: RouteDeps) =>
6+
<R extends RouterBase>(r: Router<R>) => {
7+
// prettier-ignore
8+
const Request = t.Object(
9+
t.prop('id', BlockIdRef).options({
10+
title: 'Block ID',
11+
description: 'The ID of the block.',
12+
}),
13+
t.prop('seq', BlockCurRef).options({
14+
title: 'Last Known Sequence Number',
15+
description: 'The sequence number that the client is caught up to. If '
16+
+ 'the client is not caught up to the latest state of the block, the '
17+
+ 'server will return a list of batches that the client needs to apply '
18+
+ 'to get to the latest state. If the client is too far behind, the '
19+
+ 'server will return a snapshot of the block.'
20+
+ '\n\n'
21+
+ 'The initial value should be `-1`.',
22+
}),
23+
t.propOpt('create', t.bool).options({
24+
title: 'Create Block',
25+
description: 'Whether to create a new block if it does not exist.',
26+
}),
27+
);
28+
29+
// prettier-ignore
30+
const Response = t.Object(
31+
t.prop('batches', t.Array(BlockBatchRef)).options({
32+
title: 'Batches',
33+
description: 'List of batches that the client need to apply to the local state. ' +
34+
'Or, if `snapshot` is provided, the list of batches that the client need to apply to the snapshot to get to the latest state.',
35+
}),
36+
t.propOpt('snapshot', BlockSnapshotRef).options({
37+
title: 'Snapshot',
38+
description: 'The state of the block right before the first batch in the result.',
39+
}),
40+
);
41+
42+
const Func = t.Function(Request, Response).options({
43+
title: 'Pull Block',
44+
intro: 'Catch up to the latest state of a block.',
45+
description: 'Returns a list of most recent change batches or a snapshot of a block.',
46+
});
47+
48+
return r.prop('block.pull', Func, async ({id, seq, create}) => {
49+
return await services.blocks.pull(id, seq, !!create);
50+
});
51+
};

0 commit comments

Comments
 (0)