Skip to content

Commit b2ac900

Browse files
authored
Merge pull request #466 from streamich/events-2.0
JSON CRDT Events 2.0
2 parents acdaff9 + 6a1768e commit b2ac900

File tree

13 files changed

+426
-287
lines changed

13 files changed

+426
-287
lines changed

src/json-crdt/__demos__/events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const model = Model.withLogicalClock(1234); // 1234 is the session ID
2525

2626
// DOM Level 2 node events
2727
const root = model.api.r;
28-
root.events.on('view', () => {
28+
root.events.onViewChanges.listen(() => {
2929
console.log('Root value changed');
3030
});
3131

src/json-crdt/model/Model.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ import type {NodeApi} from './api/nodes';
1717

1818
export const UNDEFINED = new ConNode(ORIGIN, undefined);
1919

20-
export const enum ModelChangeType {
21-
/** When operations are applied through `.applyPatch()` directly. */
22-
REMOTE = 0,
23-
/** When local operations are applied through the `ModelApi`. */
24-
LOCAL = 1,
25-
/** When model is reset using the `.reset()` method. */
26-
RESET = 2,
27-
}
28-
2920
/**
3021
* In instance of Model class represents the underlying data structure,
3122
* i.e. model, of the JSON CRDT document.
@@ -140,15 +131,6 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
140131
*/
141132
public tick: number = 0;
142133

143-
/**
144-
* Callback called after every `applyPatch` call.
145-
*
146-
* When using the `.api` API, this property is set automatically by
147-
* the {@link ModelApi} class. In that case use the `mode.api.evens.on('change')`
148-
* to subscribe to changes.
149-
*/
150-
public onchange: undefined | ((type: ModelChangeType) => void) = undefined;
151-
152134
/**
153135
* Applies a batch of patches to the document.
154136
*
@@ -159,16 +141,27 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
159141
for (let i = 0; i < length; i++) this.applyPatch(patches[i]);
160142
}
161143

144+
/**
145+
* Callback called before every `applyPatch` call.
146+
*/
147+
public onbeforepatch?: (patch: Patch) => void = undefined;
148+
149+
/**
150+
* Callback called after every `applyPatch` call.
151+
*/
152+
public onpatch?: (patch: Patch) => void = undefined;
153+
162154
/**
163155
* Applies a single patch to the document. All mutations to the model must go
164156
* through this method.
165157
*/
166158
public applyPatch(patch: Patch) {
159+
this.onbeforepatch?.(patch);
167160
const ops = patch.ops;
168161
const {length} = ops;
169162
for (let i = 0; i < length; i++) this.applyOperation(ops[i]);
170163
this.tick++;
171-
this.onchange?.(ModelChangeType.REMOTE);
164+
this.onpatch?.(patch);
172165
}
173166

174167
/**
@@ -180,6 +173,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
180173
*
181174
* @param op Any JSON CRDT Patch operation
182175
* @ignore
176+
* @internal
183177
*/
184178
public applyOperation(op: JsonCrdtPatchOperation): void {
185179
this.clock.observe(op.id, op.span());
@@ -293,7 +287,7 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
293287
const node = this.index.get(value);
294288
if (!node) return;
295289
const api = node.api;
296-
if (api) (api as NodeApi).events.onDelete();
290+
if (api) (api as NodeApi).events.handleDelete();
297291
node.children((child) => this.deleteNodeTree(child.id));
298292
this.index.del(value);
299293
}
@@ -322,18 +316,40 @@ export class Model<N extends JsonNode = JsonNode> implements Printable {
322316
return this.fork(this.clock.sid);
323317
}
324318

319+
/**
320+
* Callback called before model isi reset using the `.reset()` method.
321+
*/
322+
public onbeforereset?: () => void = undefined;
323+
324+
/**
325+
* Callback called after model has been reset using the `.reset()` method.
326+
*/
327+
public onreset?: () => void = undefined;
328+
325329
/**
326330
* Resets the model to equivalent state of another model.
327331
*/
328332
public reset(to: Model<N>): void {
333+
this.onbeforereset?.();
334+
const index = this.index;
329335
this.index = new AvlMap<clock.ITimestampStruct, JsonNode>(clock.compare);
330336
const blob = to.toBinary();
331337
decoder.decode(blob, <any>this);
332338
this.clock = to.clock.clone();
333339
this.ext = to.ext.clone();
334-
const api = this._api;
335-
if (api) api.flush();
336-
this.onchange?.(ModelChangeType.RESET);
340+
this._api?.flush();
341+
index.forEach(({v: node}) => {
342+
const api = node.api as NodeApi | undefined;
343+
if (!api) return;
344+
const newNode = this.index.get(node.id);
345+
if (!newNode) {
346+
api.events.handleDelete();
347+
return;
348+
}
349+
api.node = newNode;
350+
newNode.api = api;
351+
});
352+
this.onreset?.();
337353
}
338354

339355
/**

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {until} from '../../../__tests__/util';
2+
import {schema} from '../../../json-crdt-patch';
23
import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder';
34
import {Model} from '../Model';
45

@@ -214,9 +215,24 @@ describe('reset()', () => {
214215
});
215216
doc2.api.str(['text']).ins(5, ' world');
216217
let cnt = 0;
217-
doc2.api.events.on('change', () => cnt++);
218+
doc2.api.onChanges.listen(() => cnt++);
218219
doc2.reset(doc1);
219220
await until(() => cnt > 0);
220221
expect(cnt).toBe(1);
221222
});
223+
224+
test('preserves API nodes when model is reset', async () => {
225+
const doc1 = Model.withLogicalClock().setSchema(
226+
schema.obj({
227+
text: schema.str('hell'),
228+
}),
229+
);
230+
const doc2 = doc1.fork();
231+
doc2.s.text.toApi().ins(4, 'o');
232+
const str = doc1.s.text.toApi();
233+
expect(str === doc2.s.text.toApi()).toBe(false);
234+
expect(str.view()).toBe('hell');
235+
doc1.reset(doc2);
236+
expect(str.view()).toBe('hello');
237+
});
222238
});

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

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {PatchBuilder} from '../../../json-crdt-patch';
2-
import {Model, ModelChangeType} from '../Model';
2+
import {Model} from '../Model';
33

44
describe('DOM Level 0, .onchange event system', () => {
55
it('should trigger the onchange event when a value is set', () => {
66
const model = Model.withLogicalClock();
77
let cnt = 0;
8-
model.onchange = () => {
8+
model.onpatch = () => {
99
cnt++;
1010
};
1111
expect(cnt).toBe(0);
@@ -26,7 +26,7 @@ describe('DOM Level 0, .onchange event system', () => {
2626
it('should trigger the onchange event when a value is set to the same value', () => {
2727
const model = Model.withLogicalClock();
2828
let cnt = 0;
29-
model.onchange = () => {
29+
model.onpatch = () => {
3030
cnt++;
3131
};
3232
expect(cnt).toBe(0);
@@ -42,7 +42,7 @@ describe('DOM Level 0, .onchange event system', () => {
4242
it('should trigger the onchange event when a value is deleted', () => {
4343
const model = Model.withLogicalClock();
4444
let cnt = 0;
45-
model.onchange = () => {
45+
model.onpatch = () => {
4646
cnt++;
4747
};
4848
expect(cnt).toBe(0);
@@ -60,7 +60,7 @@ describe('DOM Level 0, .onchange event system', () => {
6060
it('should trigger the onchange event when a non-existent value is deleted', () => {
6161
const model = Model.withLogicalClock();
6262
let cnt = 0;
63-
model.onchange = () => {
63+
model.onpatch = () => {
6464
cnt++;
6565
};
6666
expect(cnt).toBe(0);
@@ -78,7 +78,7 @@ describe('DOM Level 0, .onchange event system', () => {
7878
it('should trigger when root value is changed', () => {
7979
const model = Model.withLogicalClock();
8080
let cnt = 0;
81-
model.onchange = () => {
81+
model.onpatch = () => {
8282
cnt++;
8383
};
8484
expect(cnt).toBe(0);
@@ -96,29 +96,12 @@ describe('DOM Level 0, .onchange event system', () => {
9696
});
9797

9898
describe('event types', () => {
99-
it('should trigger the onchange event with a REMOTE event type', () => {
100-
const model = Model.withLogicalClock();
101-
let cnt = 0;
102-
model.onchange = (type) => {
103-
expect(type).toBe(ModelChangeType.REMOTE);
104-
cnt++;
105-
};
106-
const builder = new PatchBuilder(model.clock.clone());
107-
builder.root(builder.json({foo: 123}));
108-
const patch = builder.flush();
109-
expect(cnt).toBe(0);
110-
model.applyPatch(patch);
111-
expect(cnt).toBe(1);
112-
expect(model.view()).toStrictEqual({foo: 123});
113-
});
114-
11599
it('should trigger the onchange event with a RESET event type', () => {
116100
const model1 = Model.withLogicalClock();
117101
const model2 = Model.withLogicalClock();
118102
model2.api.root([1, 2, 3]);
119103
let cnt = 0;
120-
model1.onchange = (type) => {
121-
expect(type).toBe(ModelChangeType.RESET);
104+
model1.onreset = () => {
122105
cnt++;
123106
};
124107
expect(cnt).toBe(0);

0 commit comments

Comments
 (0)