Skip to content
Snippets Groups Projects
Unverified Commit 8ee4768f authored by Matiss Janis Aboltins's avatar Matiss Janis Aboltins Committed by GitHub
Browse files

:recycle: (crdt) adding more strict typings (#1461)

Making the `crdt` package fully TypeScript-strict.
parent c581a801
No related branches found
No related tags found
No related merge requests found
{ {
"name": "@actual-app/crdt", "name": "@actual-app/crdt",
"version": "2.0.2", "version": "2.1.0",
"license": "MIT", "license": "MIT",
"description": "CRDT layer of Actual", "description": "CRDT layer of Actual",
"main": "dist/index.js", "main": "dist/index.js",
......
...@@ -55,7 +55,7 @@ describe('merkle trie', () => { ...@@ -55,7 +55,7 @@ describe('merkle trie', () => {
trie2 = merkle.insert(trie2, messages[4].timestamp); trie2 = merkle.insert(trie2, messages[4].timestamp);
expect(trie2.hash).toBe(108); expect(trie2.hash).toBe(108);
expect(new Date(merkle.diff(trie1, trie2)).toISOString()).toBe( expect(new Date(merkle.diff(trie1, trie2)!).toISOString()).toBe(
'2018-11-02T17:15:00.000Z', '2018-11-02T17:15:00.000Z',
); );
...@@ -126,10 +126,10 @@ describe('merkle trie', () => { ...@@ -126,10 +126,10 @@ describe('merkle trie', () => {
// Case 0: It always returns a base time when comparing with an // Case 0: It always returns a base time when comparing with an
// empty trie // empty trie
expect(new Date(merkle.diff(merkle.emptyTrie(), trie)).toISOString()).toBe( expect(new Date(merkle.diff(merkle.emptyTrie(), trie)!).toISOString()).toBe(
'1970-01-01T00:00:00.000Z', '1970-01-01T00:00:00.000Z',
); );
expect(new Date(merkle.diff(trie, merkle.emptyTrie())).toISOString()).toBe( expect(new Date(merkle.diff(trie, merkle.emptyTrie())!).toISOString()).toBe(
'1970-01-01T00:00:00.000Z', '1970-01-01T00:00:00.000Z',
); );
...@@ -140,52 +140,52 @@ describe('merkle trie', () => { ...@@ -140,52 +140,52 @@ describe('merkle trie', () => {
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900), message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
]); ]);
// Normal comparision works // Normal comparison works
expect(new Date(merkle.diff(trie1, trie)).toISOString()).toBe( expect(new Date(merkle.diff(trie1, trie)!).toISOString()).toBe(
'2018-11-01T00:54:00.000Z', '2018-11-01T00:54:00.000Z',
); );
// Comparing the pruned new trie is lossy, so it returns an even older time // Comparing the pruned new trie is lossy, so it returns an even older time
expect(new Date(merkle.diff(merkle.prune(trie1), trie)).toISOString()).toBe( expect(
'2018-11-01T00:45:00.000Z', new Date(merkle.diff(merkle.prune(trie1), trie)!).toISOString(),
); ).toBe('2018-11-01T00:45:00.000Z');
// Comparing the pruned original trie is just as lossy // Comparing the pruned original trie is just as lossy
expect(new Date(merkle.diff(trie1, merkle.prune(trie))).toISOString()).toBe( expect(
'2018-11-01T00:45:00.000Z', new Date(merkle.diff(trie1, merkle.prune(trie))!).toISOString(),
); ).toBe('2018-11-01T00:45:00.000Z');
// Pruning both tries is just as lossy as well, since the changed // Pruning both tries is just as lossy as well, since the changed
// key is pruned away in both cases and it won't find a changed // key is pruned away in both cases and it won't find a changed
// key so it bails at the point // key so it bails at the point
expect( expect(
new Date( new Date(
merkle.diff(merkle.prune(trie1), merkle.prune(trie)), merkle.diff(merkle.prune(trie1), merkle.prune(trie))!,
).toISOString(), ).toISOString(),
).toBe('2018-11-01T00:45:00.000Z'); ).toBe('2018-11-01T00:45:00.000Z');
// Case 2: Add two messages similar to the above case, but the // Case 2: Add two messages similar to the above case, but the
// second message modifies the 2nd key at the same level as the // second message modifies the 2nd key at the same level as the
// first message modifiying the 1st key // first message modifying the 1st key
let trie2 = insertMessages(trie, [ let trie2 = insertMessages(trie, [
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900), message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
message('2018-11-01T01:15:00.000Z-0000-0123456789ABCDEF', 1422), message('2018-11-01T01:15:00.000Z-0000-0123456789ABCDEF', 1422),
]); ]);
// Normal comparision works // Normal comparison works
expect(new Date(merkle.diff(trie2, trie)).toISOString()).toBe( expect(new Date(merkle.diff(trie2, trie)!).toISOString()).toBe(
'2018-11-01T00:54:00.000Z', '2018-11-01T00:54:00.000Z',
); );
// Same as case 1 // Same as case 1
expect(new Date(merkle.diff(merkle.prune(trie2), trie)).toISOString()).toBe( expect(
'2018-11-01T00:45:00.000Z', new Date(merkle.diff(merkle.prune(trie2), trie)!).toISOString(),
); ).toBe('2018-11-01T00:45:00.000Z');
// Same as case 1 // Same as case 1
expect(new Date(merkle.diff(trie2, merkle.prune(trie))).toISOString()).toBe( expect(
'2018-11-01T00:45:00.000Z', new Date(merkle.diff(trie2, merkle.prune(trie))!).toISOString(),
); ).toBe('2018-11-01T00:45:00.000Z');
// Pruning both tries is very lossy and this ends up returning a // Pruning both tries is very lossy and this ends up returning a
// time that only covers the second message. Syncing will need // time that only covers the second message. Syncing will need
...@@ -194,7 +194,7 @@ describe('merkle trie', () => { ...@@ -194,7 +194,7 @@ describe('merkle trie', () => {
// ignores the first message. // ignores the first message.
expect( expect(
new Date( new Date(
merkle.diff(merkle.prune(trie2), merkle.prune(trie)), merkle.diff(merkle.prune(trie2), merkle.prune(trie))!,
).toISOString(), ).toISOString(),
).toBe('2018-11-01T01:12:00.000Z'); ).toBe('2018-11-01T01:12:00.000Z');
}); });
......
...@@ -19,12 +19,18 @@ export type TrieNode = { ...@@ -19,12 +19,18 @@ export type TrieNode = {
hash?: number; hash?: number;
}; };
type NumberTrieNodeKey = keyof Omit<TrieNode, 'hash'>;
export function emptyTrie(): TrieNode { export function emptyTrie(): TrieNode {
return { hash: 0 }; return { hash: 0 };
} }
export function getKeys(trie: TrieNode): ('0' | '1' | '2')[] { function isNumberTrieNodeKey(input: string): input is NumberTrieNodeKey {
return Object.keys(trie).filter(x => x !== 'hash') as ('0' | '1' | '2')[]; return ['0', '1', '2'].includes(input);
}
export function getKeys(trie: TrieNode): NumberTrieNodeKey[] {
return Object.keys(trie).filter(isNumberTrieNodeKey);
} }
export function keyToTimestamp(key: string): number { export function keyToTimestamp(key: string): number {
...@@ -43,7 +49,7 @@ export function insert(trie: TrieNode, timestamp: Timestamp) { ...@@ -43,7 +49,7 @@ export function insert(trie: TrieNode, timestamp: Timestamp) {
let hash = timestamp.hash(); let hash = timestamp.hash();
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3); let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
trie = Object.assign({}, trie, { hash: trie.hash ^ hash }); trie = Object.assign({}, trie, { hash: (trie.hash || 0) ^ hash });
return insertKey(trie, key, hash); return insertKey(trie, key, hash);
} }
...@@ -52,10 +58,11 @@ function insertKey(trie: TrieNode, key: string, hash: number): TrieNode { ...@@ -52,10 +58,11 @@ function insertKey(trie: TrieNode, key: string, hash: number): TrieNode {
return trie; return trie;
} }
const c = key[0]; const c = key[0];
const n = trie[c] || {}; const t = isNumberTrieNodeKey(c) ? trie[c] : undefined;
const n = t || {};
return Object.assign({}, trie, { return Object.assign({}, trie, {
[c]: Object.assign({}, n, insertKey(n, key.slice(1), hash), { [c]: Object.assign({}, n, insertKey(n, key.slice(1), hash), {
hash: n.hash ^ hash, hash: (n.hash || 0) ^ hash,
}), }),
}); });
} }
...@@ -68,7 +75,7 @@ export function build(timestamps: Timestamp[]) { ...@@ -68,7 +75,7 @@ export function build(timestamps: Timestamp[]) {
return trie; return trie;
} }
export function diff(trie1: TrieNode, trie2: TrieNode): number { export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
if (trie1.hash === trie2.hash) { if (trie1.hash === trie2.hash) {
return null; return null;
} }
...@@ -126,6 +133,8 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number { ...@@ -126,6 +133,8 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number {
node1 = node1[diffkey] || emptyTrie(); node1 = node1[diffkey] || emptyTrie();
node2 = node2[diffkey] || emptyTrie(); node2 = node2[diffkey] || emptyTrie();
} }
return null;
} }
export function prune(trie: TrieNode, n = 2): TrieNode { export function prune(trie: TrieNode, n = 2): TrieNode {
...@@ -141,7 +150,13 @@ export function prune(trie: TrieNode, n = 2): TrieNode { ...@@ -141,7 +150,13 @@ export function prune(trie: TrieNode, n = 2): TrieNode {
// Prune child nodes. // Prune child nodes.
for (let k of keys.slice(-n)) { for (let k of keys.slice(-n)) {
next[k] = prune(trie[k], n); const node = trie[k];
if (!node) {
throw new Error(`TrieNode for key ${k} could not be found`);
}
next[k] = prune(node, n);
} }
return next; return next;
...@@ -156,7 +171,9 @@ export function debug(trie: TrieNode, k = '', indent = 0): string { ...@@ -156,7 +171,9 @@ export function debug(trie: TrieNode, k = '', indent = 0): string {
str + str +
getKeys(trie) getKeys(trie)
.map(key => { .map(key => {
return debug(trie[key], key, indent + 2); const node = trie[key];
if (!node) return '';
return debug(node, key, indent + 2);
}) })
.join('') .join('')
); );
......
...@@ -64,8 +64,14 @@ export function deserializeClock(clock: string): Clock { ...@@ -64,8 +64,14 @@ export function deserializeClock(clock: string): Clock {
}; };
} }
const ts = Timestamp.parse(data.timestamp);
if (!ts) {
throw new Timestamp.InvalidError(data.timestamp);
}
return { return {
timestamp: MutableTimestamp.from(Timestamp.parse(data.timestamp)), timestamp: MutableTimestamp.from(ts),
merkle: data.merkle, merkle: data.merkle,
}; };
} }
...@@ -320,6 +326,13 @@ export class Timestamp { ...@@ -320,6 +326,13 @@ export class Timestamp {
this.name = 'OverflowError'; this.name = 'OverflowError';
} }
}; };
static InvalidError = class InvalidError extends Error {
constructor(...args: unknown[]) {
super(['timestamp is not valid'].concat(args.map(String)).join(' '));
this.name = 'InvalidError';
}
};
} }
class MutableTimestamp extends Timestamp { class MutableTimestamp extends Timestamp {
......
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
"module": "CommonJS", "module": "CommonJS",
"noEmit": false, "noEmit": false,
"declaration": true, "declaration": true,
// TODO: enable "strict": true,
// "strict": true,
"outDir": "dist" "outDir": "dist"
}, },
"include": ["."], "include": ["."],
......
---
category: Bugfix
authors: [MatissJanis]
---
crdt: making the package fully TypeScript strict
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment