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",
"version": "2.0.2",
"version": "2.1.0",
"license": "MIT",
"description": "CRDT layer of Actual",
"main": "dist/index.js",
......
......@@ -55,7 +55,7 @@ describe('merkle trie', () => {
trie2 = merkle.insert(trie2, messages[4].timestamp);
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',
);
......@@ -126,10 +126,10 @@ describe('merkle trie', () => {
// Case 0: It always returns a base time when comparing with an
// 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',
);
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',
);
......@@ -140,52 +140,52 @@ describe('merkle trie', () => {
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
]);
// Normal comparision works
expect(new Date(merkle.diff(trie1, trie)).toISOString()).toBe(
// Normal comparison works
expect(new Date(merkle.diff(trie1, trie)!).toISOString()).toBe(
'2018-11-01T00:54:00.000Z',
);
// 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(
'2018-11-01T00:45:00.000Z',
);
expect(
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
expect(new Date(merkle.diff(trie1, merkle.prune(trie))).toISOString()).toBe(
'2018-11-01T00:45:00.000Z',
);
expect(
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
// key is pruned away in both cases and it won't find a changed
// key so it bails at the point
expect(
new Date(
merkle.diff(merkle.prune(trie1), merkle.prune(trie)),
merkle.diff(merkle.prune(trie1), merkle.prune(trie))!,
).toISOString(),
).toBe('2018-11-01T00:45:00.000Z');
// Case 2: Add two messages similar to the above case, but 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, [
message('2018-11-01T00:59:00.000Z-0000-0123456789ABCDEF', 900),
message('2018-11-01T01:15:00.000Z-0000-0123456789ABCDEF', 1422),
]);
// Normal comparision works
expect(new Date(merkle.diff(trie2, trie)).toISOString()).toBe(
// Normal comparison works
expect(new Date(merkle.diff(trie2, trie)!).toISOString()).toBe(
'2018-11-01T00:54:00.000Z',
);
// Same as case 1
expect(new Date(merkle.diff(merkle.prune(trie2), trie)).toISOString()).toBe(
'2018-11-01T00:45:00.000Z',
);
expect(
new Date(merkle.diff(merkle.prune(trie2), trie)!).toISOString(),
).toBe('2018-11-01T00:45:00.000Z');
// Same as case 1
expect(new Date(merkle.diff(trie2, merkle.prune(trie))).toISOString()).toBe(
'2018-11-01T00:45:00.000Z',
);
expect(
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
// time that only covers the second message. Syncing will need
......@@ -194,7 +194,7 @@ describe('merkle trie', () => {
// ignores the first message.
expect(
new Date(
merkle.diff(merkle.prune(trie2), merkle.prune(trie)),
merkle.diff(merkle.prune(trie2), merkle.prune(trie))!,
).toISOString(),
).toBe('2018-11-01T01:12:00.000Z');
});
......
......@@ -19,12 +19,18 @@ export type TrieNode = {
hash?: number;
};
type NumberTrieNodeKey = keyof Omit<TrieNode, 'hash'>;
export function emptyTrie(): TrieNode {
return { hash: 0 };
}
export function getKeys(trie: TrieNode): ('0' | '1' | '2')[] {
return Object.keys(trie).filter(x => x !== 'hash') as ('0' | '1' | '2')[];
function isNumberTrieNodeKey(input: string): input is NumberTrieNodeKey {
return ['0', '1', '2'].includes(input);
}
export function getKeys(trie: TrieNode): NumberTrieNodeKey[] {
return Object.keys(trie).filter(isNumberTrieNodeKey);
}
export function keyToTimestamp(key: string): number {
......@@ -43,7 +49,7 @@ export function insert(trie: TrieNode, timestamp: Timestamp) {
let hash = timestamp.hash();
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);
}
......@@ -52,10 +58,11 @@ function insertKey(trie: TrieNode, key: string, hash: number): TrieNode {
return trie;
}
const c = key[0];
const n = trie[c] || {};
const t = isNumberTrieNodeKey(c) ? trie[c] : undefined;
const n = t || {};
return Object.assign({}, trie, {
[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[]) {
return trie;
}
export function diff(trie1: TrieNode, trie2: TrieNode): number {
export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
if (trie1.hash === trie2.hash) {
return null;
}
......@@ -126,6 +133,8 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number {
node1 = node1[diffkey] || emptyTrie();
node2 = node2[diffkey] || emptyTrie();
}
return null;
}
export function prune(trie: TrieNode, n = 2): TrieNode {
......@@ -141,7 +150,13 @@ export function prune(trie: TrieNode, n = 2): TrieNode {
// Prune child nodes.
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;
......@@ -156,7 +171,9 @@ export function debug(trie: TrieNode, k = '', indent = 0): string {
str +
getKeys(trie)
.map(key => {
return debug(trie[key], key, indent + 2);
const node = trie[key];
if (!node) return '';
return debug(node, key, indent + 2);
})
.join('')
);
......
......@@ -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 {
timestamp: MutableTimestamp.from(Timestamp.parse(data.timestamp)),
timestamp: MutableTimestamp.from(ts),
merkle: data.merkle,
};
}
......@@ -320,6 +326,13 @@ export class Timestamp {
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 {
......
......@@ -7,8 +7,7 @@
"module": "CommonJS",
"noEmit": false,
"declaration": true,
// TODO: enable
// "strict": true,
"strict": true,
"outDir": "dist"
},
"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