Skip to content

Commit 8c2dcfe

Browse files
authored
Merge pull request #458 from streamich/fix-deleted-container-contents
fix: make sure obj, vec, arr, and val nodes don't throw on .view() or .toString() calls
2 parents f83faca + 397468d commit 8c2dcfe

File tree

5 files changed

+101
-13
lines changed

5 files changed

+101
-13
lines changed

src/json-crdt/model/__tests__/Model.node-deletion.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {s} from '../../../json-crdt-patch';
12
import {ValNode} from '../../nodes';
23
import {Model} from '../Model';
34

@@ -160,3 +161,74 @@ test('removes from index recursively after LWW register write', () => {
160161
expect(!!doc.index.get(val3)).toBe(false);
161162
expect(!!doc.index.get(val4)).toBe(false);
162163
});
164+
165+
test('calling .view() on dangling "obj" when it was deleted, should not throw', () => {
166+
const doc = Model.withLogicalClock().setSchema(
167+
s.obj({
168+
foo: s.obj({
169+
bar: s.obj({
170+
baz: s.con(123),
171+
qux: s.str('asdf'),
172+
}),
173+
}),
174+
}),
175+
);
176+
177+
const bar = doc.root.child()!.get('foo')!.get('bar')!;
178+
const baz = bar.get('baz')!;
179+
const qux = bar.get('qux')!;
180+
expect(bar.view()).toStrictEqual({
181+
baz: 123,
182+
qux: 'asdf',
183+
});
184+
doc.api.obj(['foo']).del(['bar']);
185+
expect(bar.view()).toStrictEqual({});
186+
expect((bar + '').includes(bar.id.time + '')).toBe(true);
187+
expect(baz.view()).toBe(123);
188+
expect(qux.view()).toBe('asdf');
189+
});
190+
191+
test('calling .view() on dangling "arr" when it was deleted, should not throw', () => {
192+
const doc = Model.withLogicalClock().setSchema(
193+
s.obj({
194+
foo: s.obj({
195+
bar: s.arr([s.con(123), s.str('asdf')]),
196+
}),
197+
}),
198+
);
199+
const bar = doc.root.child()!.get('foo')!.get('bar')!;
200+
expect(bar.view()).toStrictEqual([123, 'asdf']);
201+
doc.api.obj(['foo']).del(['bar']);
202+
expect(bar.view()).toStrictEqual([]);
203+
expect((bar + '').includes(bar.id.time + '')).toBe(true);
204+
});
205+
206+
test('calling .view() on dangling "vec" when it was deleted, should not throw', () => {
207+
const doc = Model.withLogicalClock().setSchema(
208+
s.obj({
209+
foo: s.obj({
210+
bar: s.vec(s.con(123), s.str('asdf')),
211+
}),
212+
}),
213+
);
214+
const bar = doc.root.child()!.get('foo')!.get('bar')!;
215+
expect(bar.view()).toStrictEqual([123, 'asdf']);
216+
doc.api.obj(['foo']).del(['bar']);
217+
expect(bar.view()).toStrictEqual([undefined, undefined]);
218+
expect((bar + '').includes(bar.id.time + '')).toBe(true);
219+
});
220+
221+
test('calling .view() on dangling "val" when it was deleted, should not throw', () => {
222+
const doc = Model.withLogicalClock().setSchema(
223+
s.obj({
224+
foo: s.obj({
225+
bar: s.val(s.str('asdf')),
226+
}),
227+
}),
228+
);
229+
const bar = doc.root.child()!.get('foo')!.get('bar')!;
230+
expect(bar.view()).toStrictEqual('asdf');
231+
doc.api.obj(['foo']).del(['bar']);
232+
expect(bar.view()).toStrictEqual(undefined);
233+
expect((bar + '').includes(bar.id.time + '')).toBe(true);
234+
});

src/json-crdt/nodes/arr/ArrNode.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,12 @@ export class ArrNode<Element extends JsonNode = JsonNode>
155155
for (let chunk = this.first(); chunk; chunk = this.next(chunk)) {
156156
if (chunk.del) continue;
157157
for (const node of chunk.data!) {
158-
const element = index.get(node)!.view() as JsonNodeView<Element>;
158+
const elementNode = index.get(node);
159+
if (!elementNode) {
160+
useCache = false;
161+
continue;
162+
}
163+
const element = elementNode.view() as JsonNodeView<Element>;
159164
if (_view[view.length] !== element) useCache = false;
160165
view.push(element);
161166
}
@@ -188,9 +193,10 @@ export class ArrNode<Element extends JsonNode = JsonNode>
188193
const index = this.doc.index;
189194
valueTree = printTree(
190195
tab,
191-
chunk.data!.map(
192-
(id, i) => (tab) => `[${pos + i}]: ${index.get(id)!.toString(tab + ' ' + ' '.repeat(String(i).length))}`,
193-
),
196+
chunk
197+
.data!.map((id) => index.get(id))
198+
.filter((node) => !!node)
199+
.map((node, i) => (tab) => `[${pos + i}]: ${node!.toString(tab + ' ' + ' '.repeat(String(i).length))}`),
194200
);
195201
}
196202
return (

src/json-crdt/nodes/obj/ObjNode.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,12 @@ export class ObjNode<Value extends Record<string, JsonNode> = Record<string, Jso
111111
const index = doc.index;
112112
let useCache = true;
113113
this.keys.forEach((id, key) => {
114-
const value = index.get(id)!.view();
114+
const valueNode = index.get(id);
115+
if (!valueNode) {
116+
useCache = false;
117+
return;
118+
}
119+
const value = valueNode.view();
115120
if (value !== undefined) {
116121
if (_view[key] !== value) useCache = false;
117122
(<any>view)[key] = value;
@@ -137,11 +142,13 @@ export class ObjNode<Value extends Record<string, JsonNode> = Record<string, Jso
137142
header +
138143
printTree(
139144
tab,
140-
[...this.keys.entries()].map(
141-
([key, id]) =>
142-
(tab) =>
143-
JSON.stringify(key) + printTree(tab + ' ', [(tab) => this.doc.index.get(id)!.toString(tab)]),
144-
),
145+
[...this.keys.entries()]
146+
.filter(([, id]) => !!this.doc.index.get(id))
147+
.map(
148+
([key, id]) =>
149+
(tab) =>
150+
JSON.stringify(key) + printTree(tab + ' ', [(tab) => this.doc.index.get(id)!.toString(tab)]),
151+
),
145152
)
146153
);
147154
}

src/json-crdt/nodes/val/ValNode.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ export class ValNode<Value extends JsonNode = JsonNode> implements JsonNode<Read
4545
* @returns The latest value of the node.
4646
*/
4747
public node(): Value {
48-
return this.val.sid === SESSION.SYSTEM ? <any>UNDEFINED : this.child()!;
48+
return this.val.sid === SESSION.SYSTEM ? <any>UNDEFINED : this.child();
4949
}
5050

5151
// ----------------------------------------------------------------- JsonNode
5252

5353
public view(): Readonly<JsonNodeView<Value>> {
54-
return this.node().view() as Readonly<JsonNodeView<Value>>;
54+
return this.node()?.view() as Readonly<JsonNodeView<Value>>;
5555
}
5656

5757
/**

src/json-crdt/nodes/vec/VecNode.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,15 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]>
183183
if (extNode) {
184184
return this.child()!.toString(tab);
185185
}
186+
const index = this.doc.index;
186187
return (
187188
header +
188189
printTree(tab, [
189190
...this.elements.map(
190191
(id, i) => (tab: string) =>
191-
`${i}: ${!id ? 'nil' : this.doc.index.get(id)!.toString(tab + ' ' + ' '.repeat(('' + i).length))}`,
192+
`${i}: ${
193+
!id ? 'nil' : index.get(id) ? index.get(id)!.toString(tab + ' ' + ' '.repeat(('' + i).length)) : 'nil'
194+
}`,
192195
),
193196
...(extNode ? [(tab: string) => `${this.child()!.toString(tab)}`] : []),
194197
])

0 commit comments

Comments
 (0)