Skip to content

Commit 92ddbba

Browse files
authored
Merge pull request #457 from streamich/sync-events
Sync events
2 parents 9a602f0 + 9661fb7 commit 92ddbba

File tree

17 files changed

+234
-76
lines changed

17 files changed

+234
-76
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"safe-stable-stringify": "^2.3.1",
145145
"secure-json-parse": "^2.4.0",
146146
"sorted-btree": "^1.8.1",
147-
"thingies": "^1.11.1",
147+
"thingies": "^1.14.1",
148148
"tinybench": "^2.4.0",
149149
"ts-jest": "^29.1.0",
150150
"ts-loader": "^9.4.3",

src/json-crdt/__demos__/getting-started.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Model, n} from '..';
1010
import {vec} from '../../json-crdt-patch';
1111

1212
// Create a new JSON CRDT document, 1234 is the session ID.
13-
const model = Model.withLogicalClock(1234) as Model<
13+
const model = Model.withLogicalClock(1234) as any as Model<
1414
n.obj<{
1515
counter: n.val<n.con<number>>;
1616
text: n.str;

src/json-crdt/__demos__/type-safety.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Model, n} from '..';
1010

1111
console.clear();
1212

13-
const model = Model.withLogicalClock(1234) as Model<
13+
const model = Model.withLogicalClock(1234) as any as Model<
1414
n.obj<{
1515
num: n.con<number>;
1616
text: n.str;

src/json-crdt/codec/indexed/binary/Encoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class Encoder {
1515
this.enc = new CborEncoder<CrdtWriter>(writer || new CrdtWriter());
1616
}
1717

18-
public encode(doc: Model, clockTable: ClockTable = ClockTable.from(doc.clock)): IndexedFields {
18+
public encode(doc: Model<any>, clockTable: ClockTable = ClockTable.from(doc.clock)): IndexedFields {
1919
this.clockTable = clockTable;
2020
const writer = this.enc.writer;
2121
writer.reset();

src/json-crdt/codec/structural/compact/Encoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class Encoder {
1111
protected clock?: ClockEncoder;
1212
protected model!: Model;
1313

14-
public encode(model: Model): t.JsonCrdtCompactDocument {
14+
public encode(model: Model<any>): t.JsonCrdtCompactDocument {
1515
this.model = model;
1616
const isServerTime = model.clock.sid === SESSION.SERVER;
1717
const clock = model.clock;

src/json-crdt/codec/structural/verbose/Encoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type * as types from './types';
88
export class Encoder {
99
protected model!: Model;
1010

11-
public encode(model: Model): types.JsonCrdtVerboseDocument {
11+
public encode(model: Model<any>): types.JsonCrdtVerboseDocument {
1212
this.model = model;
1313
const clock = model.clock;
1414
const isServerClock = clock.sid === SESSION.SERVER;

src/json-crdt/model/Model.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const enum ModelChangeType {
3737
* In instance of Model class represents the underlying data structure,
3838
* i.e. model, of the JSON CRDT document.
3939
*/
40-
export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printable {
40+
export class Model<N extends JsonNode = JsonNode> implements Printable {
4141
/**
4242
* Create a CRDT model which uses logical clock. Logical clock assigns a
4343
* logical timestamp to every node and operation. Logical timestamp consists
@@ -85,7 +85,7 @@ export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printabl
8585
* so that the JSON document does not necessarily need to be an object. The
8686
* JSON document can be any JSON value.
8787
*/
88-
public root: RootNode<RootJsonNode> = new RootNode<RootJsonNode>(this, ORIGIN);
88+
public root: RootNode<N> = new RootNode<N>(this, ORIGIN);
8989

9090
/**
9191
* Clock that keeps track of logical timestamps of the current editing session
@@ -115,13 +115,13 @@ export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printabl
115115
}
116116

117117
/** @ignore */
118-
private _api?: ModelApi<RootJsonNode>;
118+
private _api?: ModelApi<N>;
119119

120120
/**
121121
* API for applying local changes to the current document.
122122
*/
123-
public get api(): ModelApi<RootJsonNode> {
124-
if (!this._api) this._api = new ModelApi<RootJsonNode>(this);
123+
public get api(): ModelApi<N> {
124+
if (!this._api) this._api = new ModelApi<N>(this);
125125
return this._api;
126126
}
127127

@@ -302,29 +302,29 @@ export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printabl
302302
* @param sessionId Session ID to use for the new model.
303303
* @returns A copy of this model with a new session ID.
304304
*/
305-
public fork(sessionId: number = randomSessionId()): Model<RootJsonNode> {
306-
const copy = Model.fromBinary(this.toBinary());
305+
public fork(sessionId: number = randomSessionId()): Model<N> {
306+
const copy = Model.fromBinary(this.toBinary()) as unknown as Model<N>;
307307
if (copy.clock.sid !== sessionId && copy.clock instanceof ClockVector) copy.clock = copy.clock.fork(sessionId);
308308
copy.ext = this.ext;
309-
return copy as Model<RootJsonNode>;
309+
return copy;
310310
}
311311

312312
/**
313313
* Creates a copy of this model with the same session ID.
314314
*
315315
* @returns A copy of this model with the same session ID.
316316
*/
317-
public clone(): Model<RootJsonNode> {
317+
public clone(): Model<N> {
318318
return this.fork(this.clock.sid);
319319
}
320320

321321
/**
322322
* Resets the model to equivalent state of another model.
323323
*/
324-
public reset(to: Model<RootJsonNode>): void {
324+
public reset(to: Model<N>): void {
325325
this.index = new AvlMap<ITimestampStruct, JsonNode>(compare);
326326
const blob = to.toBinary();
327-
decoder.decode(blob, this);
327+
decoder.decode(blob, <any>this);
328328
this.clock = to.clock.clone();
329329
this.ext = to.ext.clone();
330330
const api = this._api;
@@ -337,7 +337,7 @@ export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printabl
337337
*
338338
* @returns JSON/CBOR of the model.
339339
*/
340-
public view(): Readonly<JsonNodeView<RootJsonNode>> {
340+
public view(): Readonly<JsonNodeView<N>> {
341341
return this.root.view();
342342
}
343343

@@ -386,7 +386,6 @@ export class Model<RootJsonNode extends JsonNode = JsonNode> implements Printabl
386386
);
387387
},
388388
nl,
389-
// (tab) => `View ${toTree(this.view(), tab)}`,
390389
(tab) =>
391390
`view${printTree(tab, [(tab) => String(JSON.stringify(this.view(), null, 2)).replace(/\n/g, '\n' + tab)])}`,
392391
nl,

src/json-crdt/model/__tests__/Model.events.spec.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {PatchBuilder} from '../../../json-crdt-patch';
12
import {Model, ModelChangeType} from '../Model';
23

34
describe('DOM Level 0, .onchange event system', () => {
@@ -8,13 +9,17 @@ describe('DOM Level 0, .onchange event system', () => {
89
cnt++;
910
};
1011
expect(cnt).toBe(0);
11-
model.api.root({foo: 'bar'});
12+
const builder = new PatchBuilder(model.clock.clone());
13+
const objId = builder.json({foo: 123});
14+
builder.root(objId);
15+
model.applyPatch(builder.flush());
1216
expect(cnt).toBe(1);
13-
model.api.obj([]).set({hello: 123});
17+
builder.insObj(objId, [['hello', builder.const(456)]]);
18+
model.applyPatch(builder.flush());
1419
expect(cnt).toBe(2);
1520
expect(model.view()).toStrictEqual({
16-
foo: 'bar',
17-
hello: 123,
21+
foo: 123,
22+
hello: 456,
1823
});
1924
});
2025

@@ -25,9 +30,12 @@ describe('DOM Level 0, .onchange event system', () => {
2530
cnt++;
2631
};
2732
expect(cnt).toBe(0);
28-
model.api.root({foo: 123});
33+
const builder = new PatchBuilder(model.clock.clone());
34+
builder.root(builder.json({foo: 123}));
35+
model.applyPatch(builder.flush());
2936
expect(cnt).toBe(1);
30-
model.api.obj([]).set({foo: 123});
37+
builder.root(builder.json({foo: 123}));
38+
model.applyPatch(builder.flush());
3139
expect(cnt).toBe(2);
3240
});
3341

@@ -38,9 +46,13 @@ describe('DOM Level 0, .onchange event system', () => {
3846
cnt++;
3947
};
4048
expect(cnt).toBe(0);
41-
model.api.root({foo: 123});
49+
const builder = new PatchBuilder(model.clock.clone());
50+
const objId = builder.json({foo: 123});
51+
builder.root(objId);
52+
model.applyPatch(builder.flush());
4253
expect(cnt).toBe(1);
43-
model.api.obj([]).set({foo: undefined});
54+
builder.insObj(objId, [['foo', builder.const(undefined)]]);
55+
model.applyPatch(builder.flush());
4456
expect(cnt).toBe(2);
4557
expect(model.view()).toStrictEqual({});
4658
});
@@ -52,9 +64,13 @@ describe('DOM Level 0, .onchange event system', () => {
5264
cnt++;
5365
};
5466
expect(cnt).toBe(0);
55-
model.api.root({foo: 123});
67+
const builder = new PatchBuilder(model.clock.clone());
68+
const objId = builder.json({foo: 123});
69+
builder.root(objId);
70+
model.applyPatch(builder.flush());
5671
expect(cnt).toBe(1);
57-
model.api.obj([]).set({bar: undefined});
72+
builder.insObj(objId, [['bar', builder.const(undefined)]]);
73+
model.applyPatch(builder.flush());
5874
expect(cnt).toBe(2);
5975
expect(model.view()).toStrictEqual({foo: 123});
6076
});
@@ -66,38 +82,28 @@ describe('DOM Level 0, .onchange event system', () => {
6682
cnt++;
6783
};
6884
expect(cnt).toBe(0);
69-
model.api.root({foo: 123});
85+
const builder = new PatchBuilder(model.clock.clone());
86+
const objId = builder.json({foo: 123});
87+
builder.root(objId);
88+
model.applyPatch(builder.flush());
7089
expect(cnt).toBe(1);
71-
model.api.root(123);
90+
builder.root(builder.json(123));
91+
model.applyPatch(builder.flush());
7292
expect(cnt).toBe(2);
73-
model.api.root('asdf');
93+
builder.root(builder.json('asdf'));
94+
model.applyPatch(builder.flush());
7495
expect(cnt).toBe(3);
7596
});
7697

7798
describe('event types', () => {
78-
it('should trigger the onchange event with a LOCAL event type', () => {
79-
const model = Model.withLogicalClock();
80-
let cnt = 0;
81-
model.onchange = (type) => {
82-
expect(type).toBe(ModelChangeType.LOCAL);
83-
cnt++;
84-
};
85-
expect(cnt).toBe(0);
86-
model.api.root({foo: 123});
87-
expect(cnt).toBe(1);
88-
model.api.obj([]).set({foo: 55});
89-
expect(cnt).toBe(2);
90-
expect(model.view()).toStrictEqual({foo: 55});
91-
});
92-
9399
it('should trigger the onchange event with a REMOTE event type', () => {
94100
const model = Model.withLogicalClock();
95101
let cnt = 0;
96102
model.onchange = (type) => {
97103
expect(type).toBe(ModelChangeType.REMOTE);
98104
cnt++;
99105
};
100-
const builder = model.api.builder;
106+
const builder = new PatchBuilder(model.clock.clone());
101107
builder.root(builder.json({foo: 123}));
102108
const patch = builder.flush();
103109
expect(cnt).toBe(0);

src/json-crdt/model/__tests__/Model.types.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Model} from '../Model';
22
import type {ConNode, ObjNode, StrNode} from '../../nodes';
33

44
test('can add TypeScript types to Model view', () => {
5-
const model = Model.withLogicalClock() as Model<
5+
const model = Model.withLogicalClock() as any as Model<
66
ObjNode<{
77
foo: StrNode;
88
bar: ConNode<number>;

src/json-crdt/model/api/ModelApi.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import {FanOut} from 'thingies/es2020/fanout';
12
import {VecNode, ConNode, ObjNode, ArrNode, BinNode, StrNode, ValNode} from '../../nodes';
23
import {ApiPath, ArrApi, BinApi, ConApi, NodeApi, ObjApi, StrApi, VecApi, ValApi} from './nodes';
34
import {Emitter} from '../../../util/events/Emitter';
45
import {Patch} from '../../../json-crdt-patch/Patch';
56
import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder';
67
import {ModelChangeType, type Model} from '../Model';
7-
import type {JsonNode} from '../../nodes';
8+
import {SyncStore} from '../../../util/events/sync-store';
9+
import type {JsonNode, JsonNodeView} from '../../nodes';
810

911
export interface ModelApiEvents {
1012
/**
@@ -26,7 +28,7 @@ export interface ModelApiEvents {
2628
*
2729
* @category Local API
2830
*/
29-
export class ModelApi<Value extends JsonNode = JsonNode> {
31+
export class ModelApi<N extends JsonNode = JsonNode> implements SyncStore<JsonNodeView<N>> {
3032
/**
3133
* Patch builder for the local changes.
3234
*/
@@ -42,15 +44,19 @@ export class ModelApi<Value extends JsonNode = JsonNode> {
4244
/**
4345
* @param model Model instance on which the API operates.
4446
*/
45-
constructor(public readonly model: Model<Value>) {
47+
constructor(public readonly model: Model<N>) {
4648
this.builder = new PatchBuilder(this.model.clock);
49+
this.model.onchange = this.queueChange;
4750
}
4851

52+
public readonly changes = new FanOut<ModelChangeType>();
53+
4954
/** @ignore */
5055
private queuedChanges: undefined | Set<ModelChangeType> = undefined;
5156

52-
/** @ignore */
57+
/** @ignore @deprecated */
5358
private readonly queueChange = (changeType: ModelChangeType): void => {
59+
this.changes.emit(changeType);
5460
let changesQueued = this.queuedChanges;
5561
if (changesQueued) {
5662
changesQueued.add(changeType);
@@ -67,19 +73,16 @@ export class ModelApi<Value extends JsonNode = JsonNode> {
6773
});
6874
};
6975

70-
/** @ignore */
71-
private et: undefined | Emitter<ModelApiEvents> = undefined;
76+
/** @ignore @deprecated */
77+
private et: Emitter<ModelApiEvents> = new Emitter();
7278

7379
/**
7480
* Event target for listening to {@link Model} changes.
81+
*
82+
* @deprecated
7583
*/
7684
public get events(): Emitter<ModelApiEvents> {
77-
let et = this.et;
78-
if (!et) {
79-
this.et = et = new Emitter();
80-
this.model.onchange = this.queueChange;
81-
}
82-
return et;
85+
return this.et;
8386
}
8487

8588
/**
@@ -277,4 +280,14 @@ export class ModelApi<Value extends JsonNode = JsonNode> {
277280
this.events.emit(event);
278281
return patch;
279282
}
283+
284+
// ---------------------------------------------------------------- SyncStore
285+
286+
public readonly subscribe = (callback: () => void) => {
287+
const listener = () => callback();
288+
this.events.on('change', listener);
289+
return () => this.events.off('change', listener);
290+
};
291+
292+
public readonly getSnapshot = () => this.view() as any;
280293
}

0 commit comments

Comments
 (0)