Skip to content

Commit c33a0a5

Browse files
committed
feat(json-crdt): 🎸 implement "bin" node deletion undo
1 parent 348ab2c commit c33a0a5

File tree

2 files changed

+123
-57
lines changed

2 files changed

+123
-57
lines changed

src/json-crdt/log/Log.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {AvlMap} from 'sonic-forest/lib/avl/AvlMap';
22
import {first, next} from 'sonic-forest/lib/util';
33
import {printTree} from 'tree-dump/lib/printTree';
4+
import {listToUint8} from '@jsonjoy.com/util/lib/buffers/concat';
45
import {Model} from '../model';
56
import {toSchema} from '../schema/toSchema';
67
import {
@@ -16,7 +17,7 @@ import {
1617
Timespan,
1718
compare,
1819
} from '../../json-crdt-patch';
19-
import {ObjNode, StrNode, ValNode, VecNode} from '../nodes';
20+
import {BinNode, ObjNode, StrNode, ValNode, VecNode} from '../nodes';
2021
import type {FanOutUnsubscribe} from 'thingies/lib/fanout';
2122
import type {Printable} from 'tree-dump/lib/types';
2223
import type {JsonNode} from '../nodes/types';
@@ -235,6 +236,17 @@ export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
235236
if (after2) after = after2;
236237
}
237238
builder.insStr(op.obj, after, str);
239+
} else if (rga instanceof BinNode) {
240+
const buffers: Uint8Array[] = [];
241+
for (const span of op.what) buffers.push(...rga.spanView(span));
242+
let after = op.obj;
243+
const firstDelSpan = op.what[0];
244+
if (firstDelSpan) {
245+
const after2 = rga.prevId(firstDelSpan);
246+
if (after2) after = after2;
247+
}
248+
const blob = listToUint8(buffers);
249+
builder.insBin(op.obj, after, blob);
238250
}
239251
}
240252
}

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

Lines changed: 110 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {DelOp, s} from '../../../json-crdt-patch';
1+
import {DelOp, InsStrOp, s} from '../../../json-crdt-patch';
22
import {Model} from '../../model';
33
import {Log} from '../Log';
44

@@ -121,65 +121,119 @@ describe('.advanceTo()', () => {
121121

122122
describe('.undo()', () => {
123123
describe('RGA', () => {
124-
test('can undo string insert', () => {
125-
const {log} = setup({str: ''});
126-
log.end.api.flush();
127-
log.end.api.str(['str']).ins(0, 'a');
128-
const patch = log.end.api.flush();
129-
expect(patch.ops.length).toBe(1);
130-
expect(patch.ops[0].name()).toBe('ins_str');
131-
const undo = log.undo(patch);
132-
expect(undo.ops.length).toBe(1);
133-
expect(undo.ops[0].name()).toBe('del');
134-
const del = undo.ops[0] as DelOp;
135-
expect(del.what.length).toBe(1);
136-
expect(del.what[0].sid).toBe(patch.ops[0].id.sid);
137-
expect(del.what[0].time).toBe(patch.ops[0].id.time);
138-
expect(del.what[0].span).toBe(1);
139-
expect(log.end.view()).toEqual({str: 'a'});
140-
log.end.applyPatch(undo);
141-
expect(log.end.view()).toEqual({str: ''});
124+
describe('str', () => {
125+
test('can undo string insert', () => {
126+
const {log} = setup({str: ''});
127+
log.end.api.flush();
128+
log.end.api.str(['str']).ins(0, 'a');
129+
const patch = log.end.api.flush();
130+
expect(patch.ops.length).toBe(1);
131+
expect(patch.ops[0].name()).toBe('ins_str');
132+
const undo = log.undo(patch);
133+
expect(undo.ops.length).toBe(1);
134+
expect(undo.ops[0].name()).toBe('del');
135+
const del = undo.ops[0] as DelOp;
136+
expect(del.what.length).toBe(1);
137+
expect(del.what[0].sid).toBe(patch.ops[0].id.sid);
138+
expect(del.what[0].time).toBe(patch.ops[0].id.time);
139+
expect(del.what[0].span).toBe(1);
140+
expect(log.end.view()).toEqual({str: 'a'});
141+
log.end.applyPatch(undo);
142+
expect(log.end.view()).toEqual({str: ''});
143+
});
144+
145+
test('can undo string delete', () => {
146+
const {log} = setup({str: 'a'});
147+
log.end.api.flush();
148+
log.end.api.str(['str']).del(0, 1);
149+
const patch = log.end.api.flush();
150+
expect(patch.ops.length).toBe(1);
151+
expect(patch.ops[0].name()).toBe('del');
152+
const undo = log.undo(patch);
153+
expect(undo.ops.length).toBe(1);
154+
expect(undo.ops[0].name()).toBe('ins_str');
155+
const op = undo.ops[0] as InsStrOp;
156+
expect(op.data).toBe('a');
157+
expect(op.obj.time).toBe(log.end.api.str(['str']).node.id.time);
158+
expect(op.obj.sid).toBe(log.end.api.str(['str']).node.id.sid);
159+
expect(op.ref.time).toBe(log.end.api.str(['str']).node.id.time);
160+
expect(op.ref.sid).toBe(log.end.api.str(['str']).node.id.sid);
161+
expect(log.end.view()).toEqual({str: ''});
162+
log.end.applyPatch(undo);
163+
expect(log.end.view()).toEqual({str: 'a'});
164+
});
165+
166+
test('can undo string delete - 2', () => {
167+
const {log} = setup({str: '12345'});
168+
log.end.api.flush();
169+
log.end.api.str(['str']).del(1, 1);
170+
const patch1 = log.end.api.flush();
171+
log.end.api.str(['str']).del(1, 2);
172+
const patch2 = log.end.api.flush();
173+
const undo2 = log.undo(patch2);
174+
const undo1 = log.undo(patch1);
175+
expect(log.end.view()).toEqual({str: '15'});
176+
log.end.applyPatch(undo2);
177+
expect(log.end.view()).toEqual({str: '1345'});
178+
log.end.applyPatch(undo1);
179+
expect(log.end.view()).toEqual({str: '12345'});
180+
});
142181
});
143182

144-
test('can undo blob insert', () => {
145-
const {log} = setup({bin: new Uint8Array()});
146-
log.end.api.flush();
147-
log.end.api.bin(['bin']).ins(0, new Uint8Array([1, 2, 3]));
148-
const patch = log.end.api.flush();
149-
expect(patch.ops.length).toBe(1);
150-
expect(patch.ops[0].name()).toBe('ins_bin');
151-
const undo = log.undo(patch);
152-
expect(undo.ops.length).toBe(1);
153-
expect(undo.ops[0].name()).toBe('del');
154-
const del = undo.ops[0] as DelOp;
155-
expect(del.what.length).toBe(1);
156-
expect(del.what[0].sid).toBe(patch.ops[0].id.sid);
157-
expect(del.what[0].time).toBe(patch.ops[0].id.time);
158-
expect(del.what[0].span).toBe(3);
159-
expect(log.end.view()).toEqual({bin: new Uint8Array([1, 2, 3])});
160-
log.end.applyPatch(undo);
161-
expect(log.end.view()).toEqual({bin: new Uint8Array([])});
183+
describe('bin', () => {
184+
test('can undo blob insert', () => {
185+
const {log} = setup({bin: new Uint8Array()});
186+
log.end.api.flush();
187+
log.end.api.bin(['bin']).ins(0, new Uint8Array([1, 2, 3]));
188+
const patch = log.end.api.flush();
189+
expect(patch.ops.length).toBe(1);
190+
expect(patch.ops[0].name()).toBe('ins_bin');
191+
const undo = log.undo(patch);
192+
expect(undo.ops.length).toBe(1);
193+
expect(undo.ops[0].name()).toBe('del');
194+
const del = undo.ops[0] as DelOp;
195+
expect(del.what.length).toBe(1);
196+
expect(del.what[0].sid).toBe(patch.ops[0].id.sid);
197+
expect(del.what[0].time).toBe(patch.ops[0].id.time);
198+
expect(del.what[0].span).toBe(3);
199+
expect(log.end.view()).toEqual({bin: new Uint8Array([1, 2, 3])});
200+
log.end.applyPatch(undo);
201+
expect(log.end.view()).toEqual({bin: new Uint8Array([])});
202+
});
162203
});
163204

164-
test('can undo array insert', () => {
165-
const {log} = setup({arr: []});
166-
log.end.api.flush();
167-
log.end.api.arr(['arr']).ins(0, [s.con(1)]);
168-
const patch = log.end.api.flush();
169-
expect(patch.ops.length).toBe(2);
170-
const insOp = patch.ops.find(op => op.name() === 'ins_arr')!;
171-
expect(log.end.view()).toEqual({arr: [1]});
172-
const undo = log.undo(patch);
173-
expect(undo.ops.length).toBe(1);
174-
expect(undo.ops[0].name()).toBe('del');
175-
const del = undo.ops[0] as DelOp;
176-
expect(del.what.length).toBe(1);
177-
expect(del.what[0].sid).toBe(insOp.id.sid);
178-
expect(del.what[0].time).toBe(insOp.id.time);
179-
expect(del.what[0].span).toBe(1);
180-
expect(log.end.view()).toEqual({arr: [1]});
181-
log.end.applyPatch(undo);
182-
expect(log.end.view()).toEqual({arr: []});
205+
describe('arr', () => {
206+
test('can undo array insert', () => {
207+
const {log} = setup({arr: []});
208+
log.end.api.flush();
209+
log.end.api.arr(['arr']).ins(0, [s.con(1)]);
210+
const patch = log.end.api.flush();
211+
expect(patch.ops.length).toBe(2);
212+
const insOp = patch.ops.find(op => op.name() === 'ins_arr')!;
213+
expect(log.end.view()).toEqual({arr: [1]});
214+
const undo = log.undo(patch);
215+
expect(undo.ops.length).toBe(1);
216+
expect(undo.ops[0].name()).toBe('del');
217+
const del = undo.ops[0] as DelOp;
218+
expect(del.what.length).toBe(1);
219+
expect(del.what[0].sid).toBe(insOp.id.sid);
220+
expect(del.what[0].time).toBe(insOp.id.time);
221+
expect(del.what[0].span).toBe(1);
222+
expect(log.end.view()).toEqual({arr: [1]});
223+
log.end.applyPatch(undo);
224+
expect(log.end.view()).toEqual({arr: []});
225+
});
226+
227+
test('can undo blob delete', () => {
228+
const {log} = setup({bin: new Uint8Array([1, 2, 3])});
229+
log.end.api.flush();
230+
log.end.api.bin(['bin']).del(1, 1);
231+
const patch = log.end.api.flush();
232+
const undo = log.undo(patch);
233+
expect(log.end.view()).toEqual({bin: new Uint8Array([1, 3])});
234+
log.end.applyPatch(undo);
235+
expect(log.end.view()).toEqual({bin: new Uint8Array([1, 2, 3])});
236+
});
183237
});
184238
});
185239

0 commit comments

Comments
 (0)