Skip to content

Commit

Permalink
Merge pull request #5 from jsonjoy-com/server
Browse files Browse the repository at this point in the history
Server
  • Loading branch information
streamich authored Apr 25, 2024
2 parents a9c7919 + 1d1a13b commit db004af
Show file tree
Hide file tree
Showing 22 changed files with 632 additions and 498 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@jsonjoy.com/jit-router": "^1.0.1",
"@jsonjoy.com/json-pack": "^1.0.2",
"@jsonjoy.com/util": "^1.0.0",
"json-joy": "^14.2.0",
"json-joy": "^15.4.1",
"memfs": "^4.8.1",
"sonic-forest": "^1.0.0"
},
Expand Down
19 changes: 13 additions & 6 deletions src/__demos__/json-crdt-server/routes/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {scan} from './methods/scan';
import {listen} from './methods/listen';
import {
Block,
BlockPartial,
BlockPartialReturn,
BlockId,
BlockPatch,
BlockPatchPartial,
BlockPatchPartialReturn,
BlockSeq,
BlockCur,
BlockNew,
BlockSnapshot,
NewBlockSnapshotResponse,
BlockEvent,
} from './schema';
import type {RouteDeps, Router, RouterBase} from '../types';

Expand All @@ -22,14 +24,19 @@ export const block =
const {system} = d;

system.alias('BlockId', BlockId);
system.alias('BlockSeq', BlockSeq);
system.alias('BlockCur', BlockCur);
system.alias('BlockNew', BlockNew);
system.alias('Block', Block);
system.alias('BlockPartial', BlockPartial);
system.alias('BlockPartialReturn', BlockPartialReturn);

system.alias('BlockSnapshot', BlockSnapshot);
system.alias('NewBlockSnapshotResponse', NewBlockSnapshotResponse);

system.alias('BlockPatch', BlockPatch);
system.alias('BlockPatchPartial', BlockPatchPartial);
system.alias('BlockPatchPartialReturn', BlockPatchPartialReturn);

system.alias('BlockEvent', BlockEvent);

// prettier-ignore
return (
( new_(d)
Expand Down
20 changes: 14 additions & 6 deletions src/__demos__/json-crdt-server/routes/block/methods/del.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ export const del =
}),
);

const Response = t.obj;
const Response = t.Object(
t.prop('success', t.bool).options({
title: 'Success',
description:
'Indicates whether the block was deleted successfully. Returns `false` if the block does not exist.',
}),
);

const Func = t.Function(Request, Response).options({
title: 'Read Block',
intro: 'Retrieves a block by ID.',
description: 'Fetches a block by ID.',
title: 'Delete Block',
intro: 'Deletes a block by ID.',
description: 'Deletes a block by ID. It will not rise an error if the block does not exist.',
});

return r.prop('block.del', Func, async ({id}) => {
await services.blocks.remove(id);
return {};
const success = await services.blocks.remove(id);
return {
success,
};
});
};
38 changes: 17 additions & 21 deletions src/__demos__/json-crdt-server/routes/block/methods/get.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
import {ResolveType} from 'json-joy/lib/json-type';
import {BlockRef, BlockIdRef} from '../schema';
import type {RouteDeps, Router, RouterBase} from '../../types';
import type {Block, BlockId, BlockPatch} from '../schema';

export const get =
({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
t.prop('id', BlockIdRef).options({
title: 'Block ID',
description: 'The ID of the block to retrieve.',
}),
t.propOpt('history', t.bool).options({
title: 'With History',
description: 'Whether to include the full history of patches in the response. Defaults to `false`.',
}),
);

const Response = t.Object(
t.prop('model', t.Ref<typeof Block>('Block')),
t.propOpt('patches', t.Array(t.Ref<typeof BlockPatch>('BlockPatch'))).options({
title: 'Patches',
description: 'The list of all patches.',
}),
);
const Response = t.Object(t.prop('block', BlockRef));

const Func = t.Function(Request, Response).options({
title: 'Read Block',
intro: 'Retrieves a block by ID.',
description: 'Fetches a block by ID.',
});

return r.prop('block.get', Func, async ({id, history}) => {
const {model} = await services.blocks.get(id);
const response: ResolveType<typeof Response> = {model};
if (history) {
const {patches} = await services.blocks.scan(id, 0, model.seq);
response.patches = patches;
}
return r.prop('block.get', Func, async ({id}) => {
const {snapshot} = await services.blocks.get(id);
const response: ResolveType<typeof Response> = {
block: {
id: snapshot.id,
ts: snapshot.created,
snapshot: {
blob: snapshot.blob,
cur: snapshot.seq,
ts: snapshot.created,
},
tip: [],
},
};
return response;
});
};
37 changes: 16 additions & 21 deletions src/__demos__/json-crdt-server/routes/block/methods/listen.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
import {switchMap} from 'rxjs';
import {map, switchMap, tap} from 'rxjs';
import {BlockEventRef, BlockIdRef} from '../schema';
import type {RouteDeps, Router, RouterBase} from '../../types';
import type {BlockId, BlockPatch, Block} from '../schema';

export const listen =
({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
t.prop('id', BlockIdRef).options({
title: 'Block ID',
description: 'The ID of the block to subscribe to.',
}),
);

const Response = t.Or(
t.Tuple(t.Const('del')),
t.Tuple(
t.Const('upd'),
t.Object(
t.propOpt('model', t.Ref<typeof Block>('Block')).options({
title: 'Block',
description: 'The whole block object, emitted only when the block is created.',
}),
t.propOpt('patches', t.Array(t.Ref<typeof BlockPatch>('BlockPatch'))).options({
title: 'Latest Patches',
description: 'Patches that have been applied to the block.',
}),
),
),
);
const Response = t.Object(t.prop('event', BlockEventRef));

const Func = t.Function$(Request, Response).options({
title: 'Listen for block changes',
description: 'Subscribe to a block to receive updates when it changes.',
description: `Subscribe to a block to receive updates when it changes.`,
});

return r.prop('block.listen', Func, (req$) => {
return req$.pipe(switchMap(({id}) => services.pubsub.listen$(`__block:${id}`))) as any;
const response = req$.pipe(
switchMap(({id}) => {
return services.blocks.listen(id);
}),
map((event) => {
return {
event,
};
}),
);
return response;
});
};
55 changes: 41 additions & 14 deletions src/__demos__/json-crdt-server/routes/block/methods/new.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
import {
BlockIdRef,
BlockPatchPartialRef,
BlockPatchPartialReturnRef,
BlockNewRef,
NewBlockSnapshotResponseRef,
} from '../schema';
import type {RouteDeps, Router, RouterBase} from '../../types';
import type {Block, BlockId, BlockPatchPartial, BlockPatchPartialReturn} from '../schema';

export const new_ =
({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
t.prop('id', BlockIdRef).options({
title: 'New block ID',
description: 'The ID of the new block.',
description:
'The ID of the new block. Must be a unique ID, if the block already exists it will return an error.',
}),
t.prop('patches', t.Array(t.Ref<typeof BlockPatchPartial>('BlockPatchPartial'))).options({
t.prop('patches', t.Array(BlockPatchPartialRef)).options({
title: 'Patches',
description: 'The patches to apply to the document.',
}),
);

const Response = t.Object(
t.prop('model', t.Ref<typeof Block>('Block')),
t.prop('patches', t.Array(t.Ref<typeof BlockPatchPartialReturn>('BlockPatchPartialReturn'))).options({
title: 'Patches',
description: 'The list of all patches.',
}),
);
const Response = t
.Object(
t.prop('block', BlockNewRef),
t.prop('snapshot', NewBlockSnapshotResponseRef),
t.prop('patches', t.Array(BlockPatchPartialReturnRef)).options({
title: 'Patches',
description: 'The list of patches to apply to the newly created block.',
}),
)
.options({
title: 'New block creation response',
description:
'The response object for the new block creation, contains server generated metadata without blobs supplied by the client.',
});

const Func = t.Function(Request, Response).options({
title: 'Create Block',
intro: 'Creates a new block or applies patches to it.',
description: 'Creates a new block or applies patches to it.',
intro: 'Creates a new block out of patches.',
description:
'Creates a new block out of supplied patches. A block starts empty with an `undefined` state, and patches are applied to it.',
});

return r.prop('block.new', Func, async ({id, patches}) => {
const res = await services.blocks.create(id, patches);
return res;
return {
block: {
id: res.snapshot.id,
ts: res.snapshot.created,
},
snapshot: {
cur: res.snapshot.seq,
ts: res.snapshot.created,
},
patches: res.patches.map((patch) => ({
ts: patch.created,
})),
};
});
};
27 changes: 12 additions & 15 deletions src/__demos__/json-crdt-server/routes/block/methods/scan.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type {BlockPatch, BlockId} from '../schema';
import {BlockIdRef, BlockCurRef, BlockPatchRef} from '../schema';
import type {RouteDeps, Router, RouterBase} from '../../types';

export const scan =
({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
t.prop('id', BlockIdRef).options({
title: 'Block ID',
description: 'The ID of the block.',
}),
t.propOpt('seq', t.num.options({format: 'u32'})).options({
t.propOpt('cur', BlockCurRef).options({
title: 'Starting Sequence Number',
description: 'The sequence number to start from. Defaults to the latest sequence number.',
}),
Expand All @@ -20,20 +20,13 @@ export const scan =
'When positive, returns the patches ahead of the starting sequence number. ' +
'When negative, returns the patches behind the starting sequence number.',
}),
t.propOpt('model', t.bool).options({
title: 'With Model',
description:
'Whether to include the model in the response. ' +
'Defaults to `false`, when `len` is positive; and, defaults to `true`, when `len` is negative.',
}),
);

const Response = t.Object(
t.prop('patches', t.Array(t.Ref<typeof BlockPatch>('BlockPatch'))).options({
t.prop('patches', t.Array(BlockPatchRef)).options({
title: 'Patches',
description: 'The list of patches.',
}),
t.propOpt('modelBlob', t.bin),
);

const Func = t.Function(Request, Response).options({
Expand All @@ -42,9 +35,13 @@ export const scan =
description: 'Returns a list of specified change patches for a block.',
});

return r.prop('block.scan', Func, async ({id, seq, limit = 10, model: returnModel = limit > 0}) => {
const {patches, model} = await services.blocks.scan(id, seq, limit, returnModel);
const modelBlob: Uint8Array | undefined = model?.toBinary();
return {patches, modelBlob};
return r.prop('block.scan', Func, async ({id, cur, limit = 10}) => {
const {patches} = await services.blocks.scan(id, cur, limit);
return {
patches: patches.map((p) => ({
blob: p.blob,
ts: p.created,
})),
};
});
};
24 changes: 9 additions & 15 deletions src/__demos__/json-crdt-server/routes/block/methods/upd.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import {ResolveType} from 'json-joy/lib/json-type';
import type {RouteDeps, Router, RouterBase} from '../../types';
import type {BlockId, BlockPatch} from '../schema';
import {BlockCurRef, BlockIdRef, BlockPatchPartialRef, BlockPatchPartialReturnRef} from '../schema';

export const upd =
({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const PatchType = t.Ref<typeof BlockPatch>('BlockPatch');

const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
t.prop('id', BlockIdRef).options({
title: 'Document ID',
description: 'The ID of the document to apply the patch to.',
}),
// This can be inferred from the "seq" of the first patch:
// t.prop('seq', t.Ref<typeof BlockSeq>('BlockSeq')).options({
// title: 'Last known sequence number',
// description:
// 'The last known sequence number of the document. ' +
// 'If the document has changed since this sequence number, ' +
// 'the response will contain all the necessary patches for the client to catch up.',
// }),
t.prop('patches', t.Array(PatchType)).options({
t.prop('patches', t.Array(BlockPatchPartialRef)).options({
title: 'Patches',
description: 'The patches to apply to the document.',
}),
);

const Response = t.Object(
t.prop('patches', t.Array(PatchType)).options({
t.prop('patches', t.Array(BlockPatchPartialReturnRef)).options({
title: 'Latest patches',
description: 'The list of patches that the client might have missed and should apply to the document.',
}),
Expand All @@ -40,8 +31,11 @@ export const upd =

return r.prop('block.upd', Func, async ({id, patches}) => {
const res = await services.blocks.edit(id, patches);
const patchesReturn: ResolveType<typeof BlockPatchPartialReturnRef>[] = res.patches.map((patch) => ({
ts: patch.created,
}));
return {
patches: res.patches,
patches: patchesReturn,
};
});
};
Loading

0 comments on commit db004af

Please sign in to comment.