Skip to content

Commit 48d64e8

Browse files
authored
Merge pull request #6 from jsonjoy-com/remote
Remote
2 parents db004af + 0d6cd38 commit 48d64e8

File tree

5 files changed

+215
-64
lines changed

5 files changed

+215
-64
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ResolveType} from 'json-joy/lib/json-type';
22
import type {RouteDeps, Router, RouterBase} from '../../types';
3-
import {BlockCurRef, BlockIdRef, BlockPatchPartialRef, BlockPatchPartialReturnRef} from '../schema';
3+
import {BlockIdRef, BlockPatchPartialRef, BlockPatchPartialReturnRef} from '../schema';
44

55
export const upd =
66
({t, services}: RouteDeps) =>

src/__demos__/json-crdt-server/services/blocks/BlocksServices.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ export class BlocksServices {
106106
if (!limit || Math.round(limit) !== limit) throw RpcError.badRequest('INVALID_LIMIT');
107107
if (limit > 0) {
108108
min = Number(offset) || 0;
109-
max = min + limit;
109+
max = min + limit - 1;
110110
} else {
111111
max = Number(offset) || 0;
112-
min = max - limit;
112+
min = max - limit + 1;
113113
}
114114
if (min < 0) {
115115
min = 0;

src/json-crdt-repo/remote/DemoServerRemoteHistory.ts

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type {Observable} from 'rxjs';
21
import {CallerToMethods, TypedRpcClient} from '../../common';
2+
import type {Observable} from 'rxjs';
33
import type {JsonJoyDemoRpcCaller} from '../../__demos__/json-crdt-server';
44
import type {RemoteHistory, RemoteBlockSnapshot, RemoteBlockPatch, RemoteBlock} from './types';
55

@@ -17,84 +17,75 @@ export class DemoServerRemoteHistory
1717
constructor(protected readonly client: TypedRpcClient<Methods>) {}
1818

1919
public async read(id: string): Promise<{block: DemoServerBlock}> {
20-
throw new Error('Method not implemented.');
2120
const res = await this.client.call('block.get', {id});
22-
23-
// return {
24-
// cursor: model.seq,
25-
// model,
26-
// patches: [],
27-
// };
21+
return {
22+
block: {
23+
id: res.block.id,
24+
snapshot: res.block.snapshot,
25+
tip: [],
26+
},
27+
};
2828
}
2929

3030
public async scanFwd(id: string, cursor: Cursor): Promise<{patches: DemoServerPatch[]}> {
31-
throw new Error('Method not implemented.');
32-
// const limit = 100;
33-
// const res = await this.client.call('block.scan', {
34-
// id,
35-
// seq: cursor,
36-
// limit: cursor + limit,
37-
// });
38-
// if (res.patches.length === 0) {
39-
// return {
40-
// cursor,
41-
// patches: [],
42-
// };
43-
// }
44-
// return {
45-
// cursor: res.patches[res.patches.length - 1].seq,
46-
// patches: res.patches,
47-
// };
31+
const limit = 100;
32+
const res = await this.client.call('block.scan', {
33+
id,
34+
cur: cursor + 1,
35+
limit,
36+
});
37+
return res;
4838
}
4939

5040
public async scanBwd(
5141
id: string,
5242
cursor: Cursor,
53-
): Promise<{snapshot: DemoServerSnapshot; patches: DemoServerPatch[]}> {
54-
throw new Error('Method not implemented.');
55-
// const res = await this.client.call('block.scan', {
56-
// id,
57-
// seq: cursor,
58-
// limit: -100,
59-
// model: true,
60-
// });
43+
): Promise<{snapshot?: DemoServerSnapshot; patches: DemoServerPatch[]}> {
44+
if (cursor <= 0) {
45+
return {
46+
patches: [],
47+
};
48+
}
49+
const res = await this.client.call('block.scan', {
50+
id,
51+
cur: 0,
52+
limit: cursor,
53+
});
54+
return {
55+
patches: res.patches,
56+
};
6157
}
6258

6359
public async create(
6460
id: string,
6561
patches: Pick<DemoServerPatch, 'blob'>[],
6662
): Promise<{
67-
block: Omit<DemoServerBlock, 'data' | 'tip'>;
63+
block: Omit<DemoServerBlock, 'data' | 'tip' | 'snapshot'>;
6864
snapshot: Omit<DemoServerSnapshot, 'blob'>;
6965
patches: Omit<DemoServerPatch, 'blob'>[];
7066
}> {
71-
throw new Error('Method not implemented.');
72-
// await this.client.call('block.new', {
73-
// id,
74-
// patches: patches.map((patch) => ({
75-
// blob: patch.blob,
76-
// })),
77-
// });
67+
const res = await this.client.call('block.new', {
68+
id,
69+
patches: patches.map((patch) => ({
70+
blob: patch.blob,
71+
})),
72+
});
73+
return res;
7874
}
7975

8076
public async update(
8177
id: string,
82-
cursor: Cursor,
8378
patches: Pick<DemoServerPatch, 'blob'>[],
8479
): Promise<{patches: Omit<DemoServerPatch, 'blob'>[]}> {
85-
throw new Error('Method not implemented.');
86-
// const res = await this.client.call('block.upd', {
87-
// id,
88-
// patches: patches.map((patch, seq) => ({
89-
// seq,
90-
// created: Date.now(),
91-
// blob: patch.blob,
92-
// })),
93-
// });
94-
// return {
95-
// cursor: res.patches.length ? res.patches[res.patches.length - 1].seq : cursor,
96-
// patches: res.patches,
97-
// };
80+
const res = await this.client.call('block.upd', {
81+
id,
82+
patches: patches.map((patch) => ({
83+
blob: patch.blob,
84+
})),
85+
});
86+
return {
87+
patches: res.patches,
88+
};
9889
}
9990

10091
public async delete(id: string): Promise<void> {

src/json-crdt-repo/remote/__tests__/DemoServerRemoteHistory.spec.ts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {Model} from 'json-joy/lib/json-crdt';
22
import {buildE2eClient} from '../../../common/testing/buildE2eClient';
33
import {createCaller} from '../../../__demos__/json-crdt-server/routes';
44
import {DemoServerRemoteHistory} from '../DemoServerRemoteHistory';
5+
import {SESSION} from 'json-joy/lib/json-crdt-patch/constants';
6+
import {Value} from 'json-joy/lib/json-type-value/Value';
57

68
const setup = () => {
79
const {caller, router} = createCaller();
@@ -20,7 +22,7 @@ let cnt = 0;
2022
const genId = () => Math.random().toString(36).slice(2) + '-' + Date.now().toString(36) + '-' + cnt++;
2123

2224
describe('.create()', () => {
23-
test.skip('can create a block with a simple patch', async () => {
25+
test('can create a block with a simple patch', async () => {
2426
const {remote, caller} = await setup();
2527
const model = Model.withLogicalClock();
2628
model.api.root({foo: 'bar'});
@@ -29,8 +31,166 @@ describe('.create()', () => {
2931
const id = genId();
3032
await remote.create(id, [{blob}]);
3133
const {data} = await caller.call('block.get', {id}, {});
32-
// console.log(data.patches);
3334
const model2 = Model.fromBinary(data.block.snapshot.blob);
3435
expect(model2.view()).toEqual({foo: 'bar'});
3536
});
37+
38+
test('can create with empty model', async () => {
39+
const {remote, caller} = await setup();
40+
const id = genId();
41+
await remote.create(id, []);
42+
const {data} = await caller.call('block.get', {id}, {});
43+
const model2 = Model.fromBinary(data.block.snapshot.blob);
44+
expect(model2.view()).toBe(undefined);
45+
});
46+
47+
test('empty model uses global session ID', async () => {
48+
const {remote, caller} = await setup();
49+
const id = genId();
50+
await remote.create(id, []);
51+
const {data} = await caller.call('block.get', {id}, {});
52+
const model2 = Model.fromBinary(data.block.snapshot.blob);
53+
expect(model2.clock.sid).toBe(SESSION.GLOBAL);
54+
});
55+
});
56+
57+
describe('.read()', () => {
58+
test('can read a block with a simple patch', async () => {
59+
const {remote} = await setup();
60+
const model = Model.withLogicalClock();
61+
model.api.root({score: 42});
62+
const patch = model.api.flush();
63+
const blob = patch.toBinary();
64+
const id = genId();
65+
await remote.create(id, [{blob}]);
66+
const read = await remote.read(id);
67+
expect(read).toMatchObject({
68+
block: {
69+
id,
70+
snapshot: {
71+
blob: expect.any(Uint8Array),
72+
cur: 0,
73+
ts: expect.any(Number),
74+
},
75+
tip: [],
76+
},
77+
});
78+
const model2 = Model.fromBinary(read.block.snapshot.blob);
79+
expect(model2.view()).toEqual({score: 42});
80+
});
81+
82+
test('throws NOT_FOUND error on missing block', async () => {
83+
const {remote} = await setup();
84+
const id = genId();
85+
try {
86+
const read = await remote.read(id);
87+
throw new Error('not this error');
88+
} catch (error) {
89+
expect(error).toMatchObject({
90+
message: 'NOT_FOUND',
91+
});
92+
}
93+
});
94+
});
95+
96+
describe('.update()', () => {
97+
test('can apply changes to an empty document', async () => {
98+
const {remote} = await setup();
99+
const id = genId();
100+
await remote.create(id, []);
101+
const read1 = await remote.read(id);
102+
const model1 = Model.fromBinary(read1.block.snapshot.blob);
103+
expect(model1.view()).toBe(undefined);
104+
const model = Model.withLogicalClock();
105+
model.api.root({score: 42});
106+
const patch = model.api.flush();
107+
const blob = patch.toBinary();
108+
const update = await remote.update(id, [{blob}]);
109+
expect(update).toMatchObject({
110+
patches: [
111+
{
112+
ts: expect.any(Number),
113+
},
114+
],
115+
});
116+
const read2 = await remote.read(id);
117+
const model2 = Model.fromBinary(read2.block.snapshot.blob);
118+
expect(model2.view()).toEqual({score: 42});
119+
});
120+
});
121+
122+
describe('.scanFwd()', () => {
123+
test('can scan patches forward', async () => {
124+
const {remote} = await setup();
125+
const id = genId();
126+
const model1 = Model.withLogicalClock();
127+
model1.api.root({score: 42});
128+
const patch1 = model1.api.flush();
129+
const blob = patch1.toBinary();
130+
await remote.create(id, [{blob}]);
131+
const read1 = await remote.read(id);
132+
model1.api.obj([]).set({
133+
foo: 'bar',
134+
});
135+
const patch2 = model1.api.flush();
136+
const blob2 = patch2.toBinary();
137+
await remote.update(id, [{blob: blob2}]);
138+
const scan1 = await remote.scanFwd(id, read1.block.snapshot.cur);
139+
expect(scan1).toMatchObject({
140+
patches: [
141+
{
142+
blob: expect.any(Uint8Array),
143+
ts: expect.any(Number),
144+
},
145+
],
146+
});
147+
expect(scan1.patches[0].blob).toEqual(blob2);
148+
});
149+
});
150+
151+
describe('.scanBwd()', () => {
152+
test('can scan patches backward', async () => {
153+
const {remote} = await setup();
154+
const id = genId();
155+
const model1 = Model.withLogicalClock();
156+
model1.api.root({score: 42});
157+
const patch1 = model1.api.flush();
158+
const blob1 = patch1.toBinary();
159+
await remote.create(id, [{blob: blob1}]);
160+
const read1 = await remote.read(id);
161+
model1.api.obj([]).set({
162+
foo: 'bar',
163+
});
164+
const patch2 = model1.api.flush();
165+
const blob2 = patch2.toBinary();
166+
await remote.update(id, [{blob: blob2}]);
167+
const read2 = await remote.read(id);
168+
const scan1 = await remote.scanBwd(id, read2.block.snapshot.cur);
169+
expect(scan1.patches.length).toBe(1);
170+
expect(scan1).toMatchObject({
171+
patches: [
172+
{
173+
blob: expect.any(Uint8Array),
174+
ts: expect.any(Number),
175+
},
176+
],
177+
});
178+
expect(scan1.patches[0].blob).toEqual(blob1);
179+
});
180+
});
181+
182+
describe('.delete()', () => {
183+
test('can delete an existing block', async () => {
184+
const {remote, caller} = await setup();
185+
const id = genId();
186+
await remote.create(id, []);
187+
const get1 = await caller.call('block.get', {id}, {});
188+
await remote.delete(id);
189+
try {
190+
const get2 = await caller.call('block.get', {id}, {});
191+
throw new Error('not this error');
192+
} catch (err) {
193+
expect((err as Value<any>).data.message).toBe('NOT_FOUND');
194+
}
195+
});
36196
});

src/json-crdt-repo/remote/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface RemoteHistory<
5959
* @param id ID of the block.
6060
* @param cursor The cursor until which to scan.
6161
*/
62-
scanBwd(id: string, cursor: Cursor): Promise<{snapshot: S; patches: P[]}>;
62+
scanBwd(id: string, cursor: Cursor): Promise<{patches: P[]; snapshot?: S}>;
6363

6464
/**
6565
* Create a new block with the given patches.
@@ -71,7 +71,7 @@ export interface RemoteHistory<
7171
id: string,
7272
patches: Pick<P, 'blob'>[],
7373
): Promise<{
74-
block: Omit<B, 'data' | 'tip'>;
74+
block: Omit<B, 'snapshot' | 'tip'>;
7575
snapshot: Omit<S, 'blob'>;
7676
patches: Omit<P, 'blob'>[];
7777
}>;
@@ -83,7 +83,7 @@ export interface RemoteHistory<
8383
* @param cursor The cursor of the last known model state of the block.
8484
* @param patches A list of patches to apply to the block.
8585
*/
86-
update(id: string, cursor: Cursor, patches: Pick<P, 'blob'>[]): Promise<{patches: Omit<P, 'blob'>[]}>;
86+
update(id: string, patches: Pick<P, 'blob'>[]): Promise<{patches: Omit<P, 'blob'>[]}>;
8787

8888
/**
8989
* Delete the block. If not implemented, means that the protocol does not
@@ -120,7 +120,7 @@ export interface RemoteBlock<Cursor> {
120120
/**
121121
* The latest snapshot of the block.
122122
*/
123-
data: RemoteBlockSnapshot<Cursor>;
123+
snapshot: RemoteBlockSnapshot<Cursor>;
124124

125125
/**
126126
* The latest patches that have been stored, but not yet applied to the the

0 commit comments

Comments
 (0)