Skip to content

Commit 1d96827

Browse files
committed
feat(json-crdt): 🎸 implement .remove() method
1 parent 32d91e1 commit 1d96827

File tree

3 files changed

+139
-20
lines changed

3 files changed

+139
-20
lines changed

src/json-crdt/model/api/ModelApi.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,18 @@ export class ModelApi<N extends JsonNode = JsonNode> implements SyncStore<JsonNo
297297
return this.r.read(path);
298298
}
299299

300-
public add(path: ApiPath, value: unknown) {
300+
public add(path: ApiPath, value: unknown): boolean {
301301
return this.r.add(path, value);
302302
}
303303

304-
public replace(path: ApiPath, value: unknown) {
304+
public replace(path: ApiPath, value: unknown): boolean {
305305
return this.r.replace(path, value);
306306
}
307307

308+
public remove(path: ApiPath, length?: number): boolean {
309+
return this.r.remove(path, length);
310+
}
311+
308312
private inTx = false;
309313
public transaction(callback: () => void) {
310314
if (this.inTx) callback();

src/json-crdt/model/api/__tests__/NodeApi.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,5 +387,105 @@ describe('.replace()', () => {
387387
});
388388
});
389389

390+
describe('.remove()', () => {
391+
describe('"obj" node', () => {
392+
test('can remove value in "obj" node', () => {
393+
const doc = createTypedModel();
394+
expect(doc.api.read('/obj/str')).toBe('asdf');
395+
const success = doc.api.remove('/obj/str');
396+
expect(doc.api.read('/obj/str')).toBe(undefined);
397+
expect(success).toBe(true);
398+
});
399+
400+
test('returns false if key does not exist', () => {
401+
const doc = createTypedModel();
402+
expect(doc.api.read('/obj/str')).toBe('asdf');
403+
const success = doc.api.remove('/obj/nonexistent');
404+
expect(doc.api.read('/obj/str')).toBe('asdf');
405+
expect(success).toBe(false);
406+
});
407+
});
408+
409+
describe('"arr" node', () => {
410+
test('can remove value in "arr" node', () => {
411+
const doc = createTypedModel();
412+
expect(doc.api.read('/arr/1')).toBe(0);
413+
const success = doc.api.remove('/arr/1');
414+
expect(doc.api.read('/arr/1')).toBe(undefined);
415+
expect(success).toBe(true);
416+
});
417+
418+
test('returns false if index does not exist', () => {
419+
const doc = createTypedModel();
420+
expect(doc.api.read('/arr/1')).toBe(0);
421+
const success = doc.api.remove('/arr/9999');
422+
expect(doc.api.read('/arr/1')).toBe(0);
423+
expect(success).toBe(false);
424+
});
425+
426+
test('can remove two elements at once', () => {
427+
const doc = Model.create(s.arr([s.con(1), s.con(2), s.con(3), s.con(4), s.con(5)]));
428+
expect(doc.api.read()).toEqual([1, 2, 3, 4, 5]);
429+
const success = doc.api.remove('/1', 2);
430+
expect(doc.api.read()).toEqual([1, 4, 5]);
431+
expect(success).toBe(true);
432+
});
433+
});
434+
435+
describe('"str" node', () => {
436+
test('can remove text from "str" node', () => {
437+
const doc = createTypedModel();
438+
expect(doc.api.read('/obj/str')).toBe('asdf');
439+
const success = doc.api.remove('/obj/str/1', 2);
440+
expect(doc.api.read('/obj/str')).toBe('af');
441+
expect(success).toBe(true);
442+
});
443+
444+
test('can remove text from "str" node - 2', () => {
445+
const doc = createTypedModel();
446+
expect(doc.api.read('/obj/str')).toBe('asdf');
447+
const success = doc.api.remove(['obj', 'str', 0]);
448+
expect(doc.api.read('/obj/str')).toBe('sdf');
449+
expect(success).toBe(true);
450+
});
451+
});
452+
453+
describe('"bin" node', () => {
454+
test('can remove bytes from "bin" node', () => {
455+
const doc = createTypedModel();
456+
expect(doc.api.read('/bin')).toEqual(new Uint8Array([1, 2, 3]));
457+
const success = doc.api.remove('/bin/1', 2);
458+
expect(doc.api.read('/bin')).toEqual(new Uint8Array([1]));
459+
expect(success).toBe(true);
460+
});
461+
462+
test('can remove bytes from "bin" node - 2', () => {
463+
const doc = createTypedModel();
464+
expect(doc.api.read('/bin')).toEqual(new Uint8Array([1, 2, 3]));
465+
const success = doc.api.remove(['bin', 0]);
466+
expect(doc.api.read('/bin')).toEqual(new Uint8Array([2, 3]));
467+
expect(success).toBe(true);
468+
});
469+
});
470+
471+
describe('"vec" node', () => {
472+
test('can remove value from "vec" node', () => {
473+
const doc = createTypedModel();
474+
expect(doc.api.read('/vec/0')).toBe('asdf');
475+
const success = doc.api.remove('/vec/0');
476+
expect(doc.api.read('/vec/0')).toBe(undefined);
477+
expect(success).toBe(true);
478+
});
479+
480+
test('returns false if index does not exist', () => {
481+
const doc = createTypedModel();
482+
expect(doc.api.read('/vec/0')).toBe('asdf');
483+
const success = doc.api.remove('/vec/9999');
484+
expect(doc.api.read('/vec/0')).toBe('asdf');
485+
expect(success).toBe(false);
486+
});
487+
});
488+
});
489+
390490
test.todo('.merge()');
391491
test.todo('.shallowMerge()');

src/json-crdt/model/api/nodes.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,8 @@ export class NodeApi<N extends JsonNode = JsonNode> implements Printable {
213213

214214
public add(path: ApiPath, value: unknown): boolean {
215215
const [parent, key] = breakPath(path);
216-
let node: NodeApi<any> = this;
217-
try {
218-
node = parent ? this.in(parent) : this;
219-
while (node instanceof ValApi) node = node.in();
220-
} catch {
221-
return false;
222-
}
223-
ADD: {
216+
ADD: try {
217+
const node = this.select(parent, true);
224218
if (node instanceof ObjApi) {
225219
node.set({[key]: value});
226220
} else if (node instanceof ArrApi || node instanceof StrApi || node instanceof BinApi) {
@@ -247,20 +241,14 @@ export class NodeApi<N extends JsonNode = JsonNode> implements Printable {
247241
node.set([[~~key, value]]);
248242
} else break ADD;
249243
return true;
250-
}
244+
} catch {}
251245
return false;
252246
}
253247

254248
public replace(path: ApiPath, value: unknown): boolean {
255249
const [parent, key] = breakPath(path);
256-
let node: NodeApi<any> = this;
257-
try {
258-
node = parent ? this.in(parent) : this;
259-
while (node instanceof ValApi) node = node.in();
260-
} catch {
261-
return false;
262-
}
263-
REPLACE: {
250+
REPLACE: try {
251+
const node = this.select(parent, true);
264252
if (node instanceof ObjApi) {
265253
const keyStr = key + '';
266254
if (!node.has(keyStr)) break REPLACE;
@@ -285,7 +273,34 @@ export class NodeApi<N extends JsonNode = JsonNode> implements Printable {
285273
node.set([[~~key, value]]);
286274
} else break REPLACE;
287275
return true;
288-
}
276+
} catch {}
277+
return false;
278+
}
279+
280+
public remove(path: ApiPath, length: number = 1): boolean {
281+
const [parent, key] = breakPath(path);
282+
REMOVE: try {
283+
const node = this.select(parent, true);
284+
if (node instanceof ObjApi) {
285+
const keyStr = key + '';
286+
if (!node.has(keyStr)) break REMOVE;
287+
node.del([keyStr]);
288+
} else if (node instanceof ArrApi || node instanceof StrApi || node instanceof BinApi) {
289+
const len = node.length();
290+
let index: number = 0;
291+
if (typeof key === 'number') index = key;
292+
else if (key === '-') index = length;
293+
else {
294+
index = ~~key;
295+
if (index + '' !== key) break REMOVE;
296+
}
297+
if (index !== index || index < 0 || index > len) break REMOVE;
298+
node.del(index, Math.min(length, len - index));
299+
} else if (node instanceof VecApi) {
300+
node.set([[~~key, void 0]]);
301+
} else break REMOVE;
302+
return true;
303+
} catch {}
289304
return false;
290305
}
291306

0 commit comments

Comments
 (0)