Skip to content

Commit 6d3cdf7

Browse files
committed
feat(json-crdt): 🎸 when forking, detect if sid was already used
1 parent 2bd08bc commit 6d3cdf7

File tree

2 files changed

+39
-4
lines changed

2 files changed

+39
-4
lines changed

src/json-crdt/model/Model.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
3434
*/
3535
public static readonly sid = randomSessionId;
3636

37+
/**
38+
* Use this method to generate a random session ID for an existing document.
39+
* It checks for the uniqueness of the session ID given the current peers in
40+
* the document. This reduces the chance of collision substantially.
41+
*
42+
* @returns A random session ID that is not used by any peer in the current
43+
* document.
44+
*/
45+
public rndSid(): number {
46+
const clock = this.clock;
47+
const sid = clock.sid;
48+
const peers = clock.peers;
49+
while (true) {
50+
const candidate = randomSessionId();
51+
if (sid !== candidate && !peers.has(candidate)) return candidate;
52+
}
53+
}
54+
3755
/**
3856
* Create a CRDT model which uses logical clock. Logical clock assigns a
3957
* logical timestamp to every node and operation. Logical timestamp consists
@@ -46,7 +64,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
4664
* @deprecated Use `Model.create()` instead.
4765
*/
4866
public static readonly withLogicalClock = (clockOrSessionId?: clock.ClockVector | number): Model => {
49-
return Model.create(undefined, clockOrSessionId);
67+
return Model.create(void 0, clockOrSessionId);
5068
};
5169

5270
/**
@@ -62,7 +80,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
6280
* @deprecated Use `Model.create()` instead: `Model.create(undefined, SESSION.SERVER)`.
6381
*/
6482
public static readonly withServerClock = (time: number = 1): Model => {
65-
return Model.create(undefined, new clock.ServerClockVector(SESSION.SERVER, time));
83+
return Model.create(void 0, new clock.ServerClockVector(SESSION.SERVER, time));
6684
};
6785

6886
/**
@@ -193,7 +211,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
193211
const first = patches[0];
194212
const sid = first.getId()!.sid;
195213
if (!sid) throw new Error('NO_SID');
196-
const model = Model.withLogicalClock(sid);
214+
const model = Model.create(void 0, sid);
197215
model.applyBatch(patches);
198216
return model;
199217
}
@@ -436,7 +454,7 @@ export class Model<N extends JsonNode = JsonNode<any>> implements Printable {
436454
* @param sessionId Session ID to use for the new model.
437455
* @returns A copy of this model with a new session ID.
438456
*/
439-
public fork(sessionId: number = Model.sid()): Model<N> {
457+
public fork(sessionId: number = this.rndSid()): Model<N> {
440458
const copy = Model.fromBinary(this.toBinary()) as unknown as Model<N>;
441459
if (copy.clock.sid !== sessionId && copy.clock instanceof clock.ClockVector)
442460
copy.clock = copy.clock.fork(sessionId);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ describe('fork()', () => {
165165
expect(doc2.view()).toEqual([true, 1, 2, 'lol']);
166166
expect(doc1.clock.sid !== doc2.clock.sid).toBe(true);
167167
});
168+
169+
test('does not reuse existing session IDs when forking', () => {
170+
const rnd = Math.random;
171+
let i = 0;
172+
Math.random = () => {
173+
i++;
174+
return i < 20 ? 0.5 : i < 24 ? 0.1 : i < 30 ? 0.5 : rnd();
175+
};
176+
const model = Model.create();
177+
model.api.root(123);
178+
const model2 = model.fork();
179+
const model3 = model2.fork();
180+
expect(model.clock.sid).not.toBe(model2.clock.sid);
181+
expect(model3.clock.sid).not.toBe(model2.clock.sid);
182+
expect(model3.clock.sid).not.toBe(model.clock.sid);
183+
Math.random = rnd;
184+
});
168185
});
169186

170187
describe('reset()', () => {

0 commit comments

Comments
 (0)