Skip to content

Commit 6770ffc

Browse files
committed
feat(json-crdt): 🎸 add ability to replay log until patch non-inclusively
1 parent 4e7b835 commit 6770ffc

File tree

2 files changed

+87
-57
lines changed

2 files changed

+87
-57
lines changed

src/json-crdt/log/Log.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {AvlMap} from 'sonic-forest/lib/avl/AvlMap';
22
import {first, next} from 'sonic-forest/lib/util';
3-
import type {FanOutUnsubscribe} from 'thingies/lib/fanout';
43
import {printTree} from 'tree-dump/lib/printTree';
5-
import {type ITimestampStruct, type Patch, compare} from '../../json-crdt-patch';
64
import {Model} from '../model';
5+
import {type ITimestampStruct, type Patch, compare} from '../../json-crdt-patch';
6+
import type {FanOutUnsubscribe} from 'thingies/lib/fanout';
77
import type {Printable} from 'tree-dump/lib/types';
88
import type {JsonNode} from '../nodes/types';
99

@@ -107,12 +107,18 @@ export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
107107
* with patches replayed up to the given timestamp.
108108
*
109109
* @param ts Timestamp ID of the patch to replay to.
110+
* @param inclusive If `true`, the patch at the given timestamp `ts` is included,
111+
* otherwise replays up to the patch before the given timestamp. Default is `true`.
110112
* @returns A new model instance with patches replayed up to the given timestamp.
111113
*/
112-
public replayTo(ts: ITimestampStruct): Model<N> {
114+
public replayTo(ts: ITimestampStruct, inclusive: boolean = true): Model<N> {
115+
// TODO: PERF: Make `.clone()` implicit in `.start()`.
113116
const clone = this.start().clone();
114-
for (let node = first(this.patches.root); node && compare(ts, node.k) >= 0; node = next(node))
117+
let cmp: number = 0;
118+
for (let node = first(this.patches.root); node && (cmp = compare(ts, node.k)) >= 0; node = next(node)){
119+
if (cmp === 0 && !inclusive) break;
115120
clone.applyPatch(node.v);
121+
}
116122
return clone;
117123
}
118124

src/json-crdt/log/__tests__/Log.spec.ts

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -37,60 +37,84 @@ test('can create a new log from a new model with right starting logical clock',
3737
expect(log.end.clock.time > 10).toBe(true);
3838
});
3939

40-
test('can replay to specific patch', () => {
41-
const {log} = setup({foo: 'bar'});
42-
const model = log.end.clone();
43-
model.api.obj([]).set({x: 1});
44-
const patch1 = model.api.flush();
45-
model.api.obj([]).set({y: 2});
46-
const patch2 = model.api.flush();
47-
log.end.applyPatch(patch1);
48-
log.end.applyPatch(patch2);
49-
const model2 = log.replayToEnd();
50-
const model3 = log.replayTo(patch1.getId()!);
51-
const model4 = log.replayTo(patch2.getId()!);
52-
expect(model.view()).toEqual({foo: 'bar', x: 1, y: 2});
53-
expect(log.end.view()).toEqual({foo: 'bar', x: 1, y: 2});
54-
expect(log.start().view()).toEqual(undefined);
55-
expect(model2.view()).toEqual({foo: 'bar', x: 1, y: 2});
56-
expect(model3.view()).toEqual({foo: 'bar', x: 1});
57-
expect(model4.view()).toEqual({foo: 'bar', x: 1, y: 2});
58-
});
40+
describe('.replayTo()', () => {
41+
test('can replay to specific patch', () => {
42+
const {log} = setup({foo: 'bar'});
43+
const model = log.end.clone();
44+
model.api.obj([]).set({x: 1});
45+
const patch1 = model.api.flush();
46+
model.api.obj([]).set({y: 2});
47+
const patch2 = model.api.flush();
48+
log.end.applyPatch(patch1);
49+
log.end.applyPatch(patch2);
50+
const model2 = log.replayToEnd();
51+
const model3 = log.replayTo(patch1.getId()!);
52+
const model4 = log.replayTo(patch2.getId()!);
53+
expect(model.view()).toEqual({foo: 'bar', x: 1, y: 2});
54+
expect(log.end.view()).toEqual({foo: 'bar', x: 1, y: 2});
55+
expect(log.start().view()).toEqual(undefined);
56+
expect(model2.view()).toEqual({foo: 'bar', x: 1, y: 2});
57+
expect(model3.view()).toEqual({foo: 'bar', x: 1});
58+
expect(model4.view()).toEqual({foo: 'bar', x: 1, y: 2});
59+
});
5960

60-
test('can advance the log from start', () => {
61-
const {log} = setup({foo: 'bar'});
62-
log.end.api.obj([]).set({x: 1});
63-
const patch1 = log.end.api.flush();
64-
log.end.api.obj([]).set({y: 2});
65-
const patch2 = log.end.api.flush();
66-
log.end.api.obj([]).set({foo: 'baz'});
67-
const patch3 = log.end.api.flush();
68-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
69-
expect(log.start().view()).toEqual(undefined);
70-
log.advanceTo(patch1.getId()!);
71-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
72-
expect(log.start().view()).toEqual({foo: 'bar', x: 1});
73-
log.advanceTo(patch2.getId()!);
74-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
75-
expect(log.start().view()).toEqual({foo: 'bar', x: 1, y: 2});
76-
expect(log.patches.size()).toBe(1);
77-
log.advanceTo(patch3.getId()!);
78-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
79-
expect(log.start().view()).toEqual({foo: 'baz', x: 1, y: 2});
80-
expect(log.patches.size()).toBe(0);
61+
test('can replay to just before a specific patch', () => {
62+
const {log} = setup({foo: 'bar'});
63+
const model = log.end.clone();
64+
model.api.obj([]).set({x: 1});
65+
const patch1 = model.api.flush();
66+
model.api.obj([]).set({y: 2});
67+
const patch2 = model.api.flush();
68+
log.end.applyPatch(patch1);
69+
log.end.applyPatch(patch2);
70+
const model2 = log.replayToEnd();
71+
const model3 = log.replayTo(patch1.getId()!, false);
72+
const model4 = log.replayTo(patch2.getId()!, false);
73+
expect(model.view()).toEqual({foo: 'bar', x: 1, y: 2});
74+
expect(log.end.view()).toEqual({foo: 'bar', x: 1, y: 2});
75+
expect(log.start().view()).toEqual(undefined);
76+
expect(model2.view()).toEqual({foo: 'bar', x: 1, y: 2});
77+
expect(model3.view()).toEqual({foo: 'bar'});
78+
expect(model4.view()).toEqual({foo: 'bar', x: 1});
79+
});
8180
});
8281

83-
test('can advance multiple patches at once', () => {
84-
const {log} = setup({foo: 'bar'});
85-
log.end.api.obj([]).set({x: 1});
86-
log.end.api.flush();
87-
log.end.api.obj([]).set({y: 2});
88-
const patch2 = log.end.api.flush();
89-
log.end.api.obj([]).set({foo: 'baz'});
90-
log.end.api.flush();
91-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
92-
expect(log.start().view()).toEqual(undefined);
93-
log.advanceTo(patch2.getId()!);
94-
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
95-
expect(log.start().view()).toEqual({foo: 'bar', x: 1, y: 2});
82+
describe('.advanceTo()', () => {
83+
test('can advance the log from start', () => {
84+
const {log} = setup({foo: 'bar'});
85+
log.end.api.obj([]).set({x: 1});
86+
const patch1 = log.end.api.flush();
87+
log.end.api.obj([]).set({y: 2});
88+
const patch2 = log.end.api.flush();
89+
log.end.api.obj([]).set({foo: 'baz'});
90+
const patch3 = log.end.api.flush();
91+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
92+
expect(log.start().view()).toEqual(undefined);
93+
log.advanceTo(patch1.getId()!);
94+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
95+
expect(log.start().view()).toEqual({foo: 'bar', x: 1});
96+
log.advanceTo(patch2.getId()!);
97+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
98+
expect(log.start().view()).toEqual({foo: 'bar', x: 1, y: 2});
99+
expect(log.patches.size()).toBe(1);
100+
log.advanceTo(patch3.getId()!);
101+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
102+
expect(log.start().view()).toEqual({foo: 'baz', x: 1, y: 2});
103+
expect(log.patches.size()).toBe(0);
104+
});
105+
106+
test('can advance multiple patches at once', () => {
107+
const {log} = setup({foo: 'bar'});
108+
log.end.api.obj([]).set({x: 1});
109+
log.end.api.flush();
110+
log.end.api.obj([]).set({y: 2});
111+
const patch2 = log.end.api.flush();
112+
log.end.api.obj([]).set({foo: 'baz'});
113+
log.end.api.flush();
114+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
115+
expect(log.start().view()).toEqual(undefined);
116+
log.advanceTo(patch2.getId()!);
117+
expect(log.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
118+
expect(log.start().view()).toEqual({foo: 'bar', x: 1, y: 2});
119+
});
96120
});

0 commit comments

Comments
 (0)