Skip to content

Commit 4f26c28

Browse files
committed
feat: 🎸 implement Log.clone() method
1 parent 28359d3 commit 4f26c28

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

src/json-crdt/log/Log.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {AvlMap} from 'sonic-forest/lib/avl/AvlMap';
22
import {first, next, prev} from 'sonic-forest/lib/util';
33
import {printTree} from 'tree-dump/lib/printTree';
44
import {listToUint8} from '@jsonjoy.com/util/lib/buffers/concat';
5+
import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone/cloneBinary';
56
import {Model} from '../model';
67
import {toSchema} from '../schema/toSchema';
78
import {
@@ -198,6 +199,23 @@ export class Log<N extends JsonNode = JsonNode<any>, Metadata extends Record<str
198199
};
199200
}
200201

202+
/**
203+
* Finds the latest patch for a given session ID.
204+
*
205+
* @param sid Session ID to find the latest patch for.
206+
* @return The latest patch for the given session ID, or `undefined` if no
207+
* such patch exists.
208+
*/
209+
public findMax(sid: number): Patch | undefined {
210+
let curr = this.patches.max;
211+
while (curr) {
212+
if (curr.k.sid === sid) return curr.v;
213+
curr = prev(curr);
214+
}
215+
return;
216+
}
217+
218+
201219
/**
202220
* Creates a patch which reverts the given patch. The RGA insertion operations
203221
* are reversed just by deleting the inserted values. All other operations

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {deepEqual} from '@jsonjoy.com/util/lib/json-equal/deepEqual';
12
import {type DelOp, type InsStrOp, s} from '../../../json-crdt-patch';
23
import {Model} from '../../model';
34
import {Log} from '../Log';
@@ -143,6 +144,89 @@ describe('.findMax()', () => {
143144
});
144145
});
145146

147+
describe('.clone()', () => {
148+
const setup = () => {
149+
const model = Model.create({foo: 'bar'});
150+
const log1 = Log.fromNewModel(model);
151+
log1.metadata = {time: 123};
152+
log1.end.api.obj([]).set({x: 1});
153+
log1.end.api.flush();
154+
log1.end.api.obj([]).set({y: 2});
155+
log1.end.api.flush();
156+
log1.end.api.obj([]).set({foo: 'baz'});
157+
log1.end.api.flush();
158+
const log2 = log1.clone();
159+
return {log1, log2};
160+
};
161+
162+
test('start model has the same view and clock', () => {
163+
const {log1, log2} = setup();
164+
expect(log1.start()).not.toBe(log2.start());
165+
expect(deepEqual(log1.start().view(), log2.start().view())).toBe(true);
166+
expect(log1.start().clock.sid).toEqual(log2.start().clock.sid);
167+
expect(log1.start().clock.time).toEqual(log2.start().clock.time);
168+
});
169+
170+
test('end model has the same view and clock', () => {
171+
const {log1, log2} = setup();
172+
expect(log1.end).not.toBe(log2.end);
173+
expect(deepEqual(log1.end.view(), log2.end.view())).toBe(true);
174+
expect(log1.end.clock.sid).toEqual(log2.end.clock.sid);
175+
expect(log1.end.clock.time).toEqual(log2.end.clock.time);
176+
});
177+
178+
test('metadata is the same but has different identity', () => {
179+
const {log1, log2} = setup();
180+
expect(log1.metadata).not.toBe(log2.metadata);
181+
expect(deepEqual(log1.metadata, log2.metadata)).toBe(true);
182+
});
183+
184+
test('patch log is the same', () => {
185+
const {log1, log2} = setup();
186+
expect(log1.patches.size()).toBe(log2.patches.size());
187+
expect(log1.patches.min!.v.toBinary()).toEqual(log2.patches.min!.v.toBinary());
188+
expect(log1.patches.max!.v.toBinary()).toEqual(log2.patches.max!.v.toBinary());
189+
expect(log1.patches.min!.v).not.toBe(log2.patches.min!.v);
190+
expect(log1.patches.max!.v).not.toBe(log2.patches.max!.v);
191+
});
192+
193+
test('can evolve logs independently', () => {
194+
const {log1, log2} = setup();
195+
log1.end.api.obj([]).set({a: 1});
196+
log1.end.api.flush();
197+
expect(log1.end.view()).toEqual({foo: 'baz', x: 1, y: 2, a: 1});
198+
expect(log2.end.view()).toEqual({foo: 'baz', x: 1, y: 2});
199+
log2.end.api.obj([]).set({b: 2});
200+
log2.end.api.flush();
201+
expect(log1.end.view()).toEqual({foo: 'baz', x: 1, y: 2, a: 1});
202+
expect(log2.end.view()).toEqual({foo: 'baz', x: 1, y: 2, b: 2});
203+
});
204+
});
205+
206+
describe('.rebase()', () => {
207+
test('can advance the log from start', () => {
208+
const model = Model.create();
209+
const sid0 = model.clock.sid;
210+
const sid1 = Model.sid();
211+
model.api.set({foo: 'bar'});
212+
const log = Log.fromNewModel(model);
213+
log.end.api.obj([]).set({x: 1});
214+
const patch1 = log.end.api.flush();
215+
log.end.setSid(sid1);
216+
log.end.api.obj([]).set({y: 2});
217+
const patch2 = log.end.api.flush();
218+
log.end.setSid(sid0);
219+
log.end.api.obj([]).set({foo: 'baz'});
220+
const patch3 = log.end.api.flush();
221+
const found0 = log.findMax(sid0);
222+
const found1 = log.findMax(sid1);
223+
const found2 = log.findMax(12345);
224+
expect(found0).toBe(patch3);
225+
expect(found1).toBe(patch2);
226+
expect(found2).toBe(void 0);
227+
});
228+
});
229+
146230
describe('.undo()', () => {
147231
describe('RGA', () => {
148232
describe('str', () => {

0 commit comments

Comments
 (0)