Skip to content

Commit afa9d5c

Browse files
committed
feat(json-crdt): 🎸 implement deep schema equality check
1 parent 6b8de6f commit afa9d5c

File tree

3 files changed

+125
-3
lines changed

3 files changed

+125
-3
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Model} from '../../model';
2+
import {equalSchema} from '..';
3+
import {s} from '../../../json-crdt-patch';
4+
5+
describe('equalSchema()', () => {
6+
const assertSchemasEqual = (a: unknown): void => {
7+
const model1 = Model.create(a);
8+
const model2 = Model.create(a);
9+
const result = equalSchema(model1.root, model2.root);
10+
expect(result).toBe(true);
11+
};
12+
13+
const assertSchemasDifferent = (a: unknown, b: unknown): void => {
14+
const model1 = Model.create(a);
15+
const model2 = Model.create(b);
16+
const result = equalSchema(model1.root, model2.root);
17+
expect(result).toBe(false);
18+
};
19+
20+
test('returns true for identical nodes', () => {
21+
assertSchemasEqual(new Uint8Array([1, 2, 3]));
22+
assertSchemasEqual({foo: new Uint8Array([1, 2, 3])});
23+
assertSchemasEqual([[[{a: [1, [2, 3]]}]]]);
24+
assertSchemasEqual([[[{a: [1, s.vec(s.con(2), s.json(3))]}]]]);
25+
assertSchemasEqual({
26+
foo: 'bar',
27+
num: 123,
28+
obj: {key: 'value'},
29+
arr: [1, 2, 3, false],
30+
bool: true,
31+
});
32+
});
33+
34+
test('returns false for slightly different nodes', () => {
35+
assertSchemasDifferent({
36+
foo: 'bar',
37+
num: 123,
38+
obj: {key: 'value'},
39+
arr: [1, 2, 3],
40+
bool: true,
41+
}, {
42+
foo: 'bar',
43+
num: 124,
44+
obj: {key: 'value'},
45+
arr: [1, 2, 3],
46+
bool: true,
47+
});
48+
});
49+
50+
test('returns false for slightly different nodes - 2', () => {
51+
assertSchemasDifferent({
52+
foo: 'baz',
53+
num: 123,
54+
obj: {key: 'value'},
55+
arr: [1, 2, 3],
56+
bool: true,
57+
}, {
58+
foo: 'baz',
59+
num: 123,
60+
obj: {key: 'valee'},
61+
arr: [1, 2, 3],
62+
bool: true,
63+
});
64+
});
65+
66+
test('returns false for slightly different nodes - 3', () => {
67+
assertSchemasDifferent({
68+
bin: new Uint8Array([1, 2, 3]),
69+
}, {
70+
bin: new Uint8Array([1, 0, 3]),
71+
});
72+
});
73+
});

src/json-crdt/equal/index.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {deepEqual} from '@jsonjoy.com/util/lib/json-equal/deepEqual';
2+
import {ArrNode, BinNode, ConNode, JsonNode, ObjNode, StrNode, ValNode, VecNode} from '../nodes';
3+
4+
/**
5+
* Deeply checks if two JSON nodes have the same schema and values. Does not
6+
* verify that the CRDT metadata (like timestamps) are the same, only that
7+
* the structure and values are equal.
8+
*
9+
* @param a The first JSON CRDT node.
10+
* @param b The second JSON CRDT node.
11+
* @returns True if the schemas and values are equal, false otherwise.
12+
*/
13+
export const equalSchema = <A extends JsonNode<any>>(a: A, b: unknown): b is A => {
14+
if (a === b) return true;
15+
if (a instanceof ConNode) return b instanceof ConNode && deepEqual(a.val, b.val);
16+
else if (a instanceof ValNode) return b instanceof ValNode && equalSchema(a.node(), b.node());
17+
else if (a instanceof StrNode) return b instanceof StrNode && a.length() === b.length() && a.view() === b.view();
18+
else if (a instanceof ObjNode) {
19+
if (!(b instanceof ObjNode)) return false;
20+
const keys1 = a.keys;
21+
const keys2 = b.keys;
22+
const length1 = keys1.size;
23+
const length2 = keys2.size;
24+
if (length1 !== length2) return false;
25+
for (const key of keys1.keys()) {
26+
if (!keys2.has(key)) return false;
27+
if (!equalSchema(a.get(key), b.get(key))) return false;
28+
}
29+
return true;
30+
} else if (a instanceof ArrNode) {
31+
if (!(b instanceof ArrNode)) return false;
32+
const length = a.length();
33+
if (length !== b.length()) return false;
34+
for (let i = 0; i < length; i++) if (!equalSchema(a.getNode(i)!, b.getNode(i))) return false;
35+
return true;
36+
} else if (a instanceof VecNode) {
37+
if (!(b instanceof VecNode)) return false;
38+
const length = a.length();
39+
if (length !== b.length()) return false;
40+
for (let i = 0; i < length; i++) if (!equalSchema(a.get(i), b.get(i))) return false;
41+
return true;
42+
} else if (a instanceof BinNode)
43+
return b instanceof BinNode && a.length() === b.length() && deepEqual(a.view(), b.view());
44+
return false;
45+
};

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]> implements JsonNode<
3131
public readonly id: ITimestampStruct,
3232
) {}
3333

34+
public length(): number {
35+
return this.elements.length;
36+
}
37+
3438
/**
3539
* Retrieves the ID of an element at the given index.
3640
*
3741
* @param index Index of the element to get.
3842
* @returns ID of the element at the given index, if any.
3943
*/
40-
public val(index: number): undefined | ITimestampStruct {
44+
public val(index: number): ITimestampStruct | undefined {
4145
return this.elements[index] as ITimestampStruct | undefined;
4246
}
4347

@@ -47,7 +51,7 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]> implements JsonNode<
4751
* @param index Index of the element to get.
4852
* @returns JSON CRDT node at the given index, if any.
4953
*/
50-
public get<Index extends number>(index: Index): undefined | Value[Index] {
54+
public get<Index extends number>(index: Index): Value[Index] | undefined {
5155
const id = this.elements[index] as ITimestampStruct | undefined;
5256
if (!id) return undefined;
5357
return this.doc.index.get(id);
@@ -56,7 +60,7 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]> implements JsonNode<
5660
/**
5761
* @ignore
5862
*/
59-
public put(index: number, id: ITimestampStruct): undefined | ITimestampStruct {
63+
public put(index: number, id: ITimestampStruct): ITimestampStruct | undefined {
6064
if (index > CRDT_CONSTANTS.MAX_TUPLE_LENGTH) throw new Error('OUT_OF_BOUNDS');
6165
const currentId = this.val(index);
6266
if (currentId && compare(currentId, id) >= 0) return;

0 commit comments

Comments
 (0)