@evolu/common 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Crdt.d.ts +78 -0
- package/dist/src/Crdt.d.ts.map +1 -0
- package/dist/src/Crdt.js +170 -0
- package/dist/src/Crypto.d.ts +1 -1
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +1 -2
- package/dist/src/DbWorker.d.ts.map +1 -1
- package/dist/src/DbWorker.js +1 -2
- package/dist/src/Errors.d.ts +1 -1
- package/dist/src/Errors.d.ts.map +1 -1
- package/dist/src/Evolu.d.ts +1 -1
- package/dist/src/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu.js +1 -1
- package/dist/src/Protobuf.d.ts +2 -3
- package/dist/src/Protobuf.d.ts.map +1 -1
- package/dist/src/SyncWorker.d.ts +1 -2
- package/dist/src/SyncWorker.d.ts.map +1 -1
- package/dist/src/SyncWorker.js +1 -1
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -2
- package/package.json +7 -7
- package/src/Crdt.ts +376 -0
- package/src/Crypto.ts +1 -1
- package/src/Db.ts +6 -2
- package/src/DbWorker.ts +22 -21
- package/src/Errors.ts +1 -1
- package/src/Evolu.ts +1 -1
- package/src/Protobuf.ts +2 -3
- package/src/SyncWorker.ts +7 -5
- package/src/index.ts +1 -2
- package/dist/src/MerkleTree.d.ts +0 -15
- package/dist/src/MerkleTree.d.ts.map +0 -1
- package/dist/src/MerkleTree.js +0 -49
- package/dist/src/Timestamp.d.ts +0 -44
- package/dist/src/Timestamp.d.ts.map +0 -1
- package/dist/src/Timestamp.js +0 -76
- package/src/MerkleTree.ts +0 -83
- package/src/Timestamp.ts +0 -192
package/dist/src/Timestamp.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import * as Schema from "@effect/schema/Schema";
|
|
2
|
-
import { Context, Effect, Either, Layer, Number, pipe } from "effect";
|
|
3
|
-
import { Config } from "./Config.js";
|
|
4
|
-
import { NanoId, NodeId } from "./Crypto.js";
|
|
5
|
-
import { murmurhash } from "./Murmurhash.js";
|
|
6
|
-
export const Millis = Schema.number.pipe(Schema.greaterThanOrEqualTo(0), Schema.brand("Millis"));
|
|
7
|
-
const initialMillis = Schema.parseSync(Millis)(0);
|
|
8
|
-
export const Counter = Schema.number.pipe(Schema.between(0, 65535), Schema.brand("Counter"));
|
|
9
|
-
const initialCounter = Schema.parseSync(Counter)(0);
|
|
10
|
-
export const timestampToString = (t) => [
|
|
11
|
-
new Date(t.millis).toISOString(),
|
|
12
|
-
t.counter.toString(16).toUpperCase().padStart(4, "0"),
|
|
13
|
-
t.node,
|
|
14
|
-
].join("-");
|
|
15
|
-
export const unsafeTimestampFromString = (s) => {
|
|
16
|
-
const a = s.split("-");
|
|
17
|
-
return {
|
|
18
|
-
millis: Date.parse(a.slice(0, 3).join("-")).valueOf(),
|
|
19
|
-
counter: parseInt(a[3], 16),
|
|
20
|
-
node: a[4],
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
export const timestampToHash = (t) => murmurhash(timestampToString(t));
|
|
24
|
-
const syncNodeId = Schema.parseSync(NodeId)("0000000000000000");
|
|
25
|
-
export const makeSyncTimestamp = (millis = initialMillis) => ({
|
|
26
|
-
millis,
|
|
27
|
-
counter: initialCounter,
|
|
28
|
-
node: syncNodeId,
|
|
29
|
-
});
|
|
30
|
-
export const makeInitialTimestamp = NanoId.pipe(Effect.flatMap(({ nanoidAsNodeId }) => nanoidAsNodeId), Effect.map((node) => ({
|
|
31
|
-
millis: initialMillis,
|
|
32
|
-
counter: initialCounter,
|
|
33
|
-
node,
|
|
34
|
-
})));
|
|
35
|
-
export const Time = Context.Tag("evolu/Time");
|
|
36
|
-
export const TimeLive = Layer.succeed(Time, Time.of({
|
|
37
|
-
now: Effect.sync(() => Date.now()),
|
|
38
|
-
}));
|
|
39
|
-
const getNextMillis = (millis) => Effect.gen(function* (_) {
|
|
40
|
-
const time = yield* _(Time);
|
|
41
|
-
const config = yield* _(Config);
|
|
42
|
-
const now = yield* _(time.now);
|
|
43
|
-
const next = Math.max(now, ...millis);
|
|
44
|
-
if (next - now > config.maxDrift)
|
|
45
|
-
yield* _(Effect.fail({
|
|
46
|
-
_tag: "TimestampDriftError",
|
|
47
|
-
now,
|
|
48
|
-
next,
|
|
49
|
-
}));
|
|
50
|
-
return next;
|
|
51
|
-
});
|
|
52
|
-
const incrementCounter = (counter) => pipe(Number.increment(counter), Schema.parseEither(Counter), Either.mapLeft(() => ({ _tag: "TimestampCounterOverflowError" })));
|
|
53
|
-
const counterMin = Schema.parseSync(Counter)(0);
|
|
54
|
-
export const sendTimestamp = (timestamp) => Effect.gen(function* (_) {
|
|
55
|
-
const millis = yield* _(getNextMillis([timestamp.millis]));
|
|
56
|
-
const counter = millis === timestamp.millis
|
|
57
|
-
? yield* _(incrementCounter(timestamp.counter))
|
|
58
|
-
: counterMin;
|
|
59
|
-
return { ...timestamp, millis, counter };
|
|
60
|
-
});
|
|
61
|
-
export const receiveTimestamp = ({ local, remote, }) => Effect.gen(function* (_) {
|
|
62
|
-
if (local.node === remote.node)
|
|
63
|
-
yield* _(Effect.fail({
|
|
64
|
-
_tag: "TimestampDuplicateNodeError",
|
|
65
|
-
node: local.node,
|
|
66
|
-
}));
|
|
67
|
-
const millis = yield* _(getNextMillis([local.millis, remote.millis]));
|
|
68
|
-
const counter = yield* _(millis === local.millis && millis === remote.millis
|
|
69
|
-
? incrementCounter(Math.max(local.counter, remote.counter))
|
|
70
|
-
: millis === local.millis
|
|
71
|
-
? incrementCounter(local.counter)
|
|
72
|
-
: millis === remote.millis
|
|
73
|
-
? incrementCounter(remote.counter)
|
|
74
|
-
: Either.right(counterMin));
|
|
75
|
-
return { ...local, millis, counter };
|
|
76
|
-
});
|
package/src/MerkleTree.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { Brand, Option } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
Millis,
|
|
4
|
-
Timestamp,
|
|
5
|
-
TimestampHash,
|
|
6
|
-
timestampToHash,
|
|
7
|
-
} from "./Timestamp.js";
|
|
8
|
-
|
|
9
|
-
// Technically, it's not Merkle Tree but “merkleized” prefix tree (trie).
|
|
10
|
-
// https://decomposition.al/blog/2019/05/31/how-i-learned-about-merklix-trees-without-having-to-become-a-cryptocurrency-enthusiast/#fnref:1
|
|
11
|
-
// TODO: Add Schema and use it in Evolu Server.
|
|
12
|
-
export interface MerkleTree {
|
|
13
|
-
readonly hash?: TimestampHash;
|
|
14
|
-
readonly "0"?: MerkleTree;
|
|
15
|
-
readonly "1"?: MerkleTree;
|
|
16
|
-
readonly "2"?: MerkleTree;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type MerkleTreeString = string & Brand.Brand<"MerkleTreeString">;
|
|
20
|
-
|
|
21
|
-
export const initialMerkleTree = Object.create(null) as MerkleTree;
|
|
22
|
-
|
|
23
|
-
const timestampToKey = (timestamp: Timestamp): string =>
|
|
24
|
-
Math.floor(timestamp.millis / 1000 / 60).toString(3);
|
|
25
|
-
|
|
26
|
-
const insertKey = (
|
|
27
|
-
tree: MerkleTree,
|
|
28
|
-
key: string,
|
|
29
|
-
hash: TimestampHash,
|
|
30
|
-
): MerkleTree => {
|
|
31
|
-
if (key.length === 0) return tree;
|
|
32
|
-
const childKey = key[0] as "0" | "1" | "2";
|
|
33
|
-
const child = tree[childKey] || {};
|
|
34
|
-
return {
|
|
35
|
-
...tree,
|
|
36
|
-
[childKey]: {
|
|
37
|
-
...child,
|
|
38
|
-
...insertKey(child, key.slice(1), hash),
|
|
39
|
-
// @ts-expect-error undefined is OK
|
|
40
|
-
hash: child.hash ^ hash,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const insertIntoMerkleTree =
|
|
46
|
-
(timestamp: Timestamp) =>
|
|
47
|
-
(tree: MerkleTree): MerkleTree => {
|
|
48
|
-
const key = timestampToKey(timestamp);
|
|
49
|
-
const hash = timestampToHash(timestamp);
|
|
50
|
-
// @ts-expect-error undefined is OK
|
|
51
|
-
return insertKey({ ...tree, hash: tree.hash ^ hash }, key, hash);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const keyToTimestamp = (key: string): Millis =>
|
|
55
|
-
(parseInt(key.length > 0 ? key : "0", 3) * 1000 * 60) as Millis;
|
|
56
|
-
|
|
57
|
-
export const diffMerkleTrees = (
|
|
58
|
-
tree1: MerkleTree,
|
|
59
|
-
tree2: MerkleTree,
|
|
60
|
-
): Option.Option<Millis> => {
|
|
61
|
-
if (tree1.hash === tree2.hash) return Option.none();
|
|
62
|
-
for1: for (let node1 = tree1, node2 = tree2, key = ""; ; ) {
|
|
63
|
-
for (const k of ["0", "1", "2"] as const) {
|
|
64
|
-
const next1 = node1[k];
|
|
65
|
-
const next2 = node2[k];
|
|
66
|
-
if (!next1 && !next2) continue;
|
|
67
|
-
if (!next1 || !next2) break;
|
|
68
|
-
if (next1.hash !== next2.hash) {
|
|
69
|
-
key += k;
|
|
70
|
-
node1 = next1;
|
|
71
|
-
node2 = next2;
|
|
72
|
-
continue for1;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return Option.some(keyToTimestamp(key));
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export const merkleTreeToString = (m: MerkleTree): MerkleTreeString =>
|
|
80
|
-
JSON.stringify(m) as MerkleTreeString;
|
|
81
|
-
|
|
82
|
-
export const unsafeMerkleTreeFromString = (m: MerkleTreeString): MerkleTree =>
|
|
83
|
-
JSON.parse(m) as MerkleTree;
|
package/src/Timestamp.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import * as Schema from "@effect/schema/Schema";
|
|
2
|
-
import { Brand, Context, Effect, Either, Layer, Number, pipe } from "effect";
|
|
3
|
-
import { Config } from "./Config.js";
|
|
4
|
-
import { NanoId, NodeId } from "./Crypto.js";
|
|
5
|
-
import { murmurhash } from "./Murmurhash.js";
|
|
6
|
-
|
|
7
|
-
// https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
|
|
8
|
-
// https://jaredforsyth.com/posts/hybrid-logical-clocks/
|
|
9
|
-
// https://github.com/clintharris/crdt-example-app_annotated/blob/master/shared/timestamp.js
|
|
10
|
-
export interface Timestamp {
|
|
11
|
-
readonly node: NodeId;
|
|
12
|
-
readonly millis: Millis;
|
|
13
|
-
readonly counter: Counter;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const Millis = Schema.number.pipe(
|
|
17
|
-
Schema.greaterThanOrEqualTo(0),
|
|
18
|
-
Schema.brand("Millis"),
|
|
19
|
-
);
|
|
20
|
-
export type Millis = Schema.Schema.To<typeof Millis>;
|
|
21
|
-
|
|
22
|
-
const initialMillis = Schema.parseSync(Millis)(0);
|
|
23
|
-
|
|
24
|
-
export const Counter = Schema.number.pipe(
|
|
25
|
-
Schema.between(0, 65535),
|
|
26
|
-
Schema.brand("Counter"),
|
|
27
|
-
);
|
|
28
|
-
export type Counter = Schema.Schema.To<typeof Counter>;
|
|
29
|
-
|
|
30
|
-
const initialCounter = Schema.parseSync(Counter)(0);
|
|
31
|
-
|
|
32
|
-
export type TimestampHash = number & Brand.Brand<"TimestampHash">;
|
|
33
|
-
|
|
34
|
-
export type TimestampString = string & Brand.Brand<"TimestampString">;
|
|
35
|
-
|
|
36
|
-
export const timestampToString = (t: Timestamp): TimestampString =>
|
|
37
|
-
[
|
|
38
|
-
new Date(t.millis).toISOString(),
|
|
39
|
-
t.counter.toString(16).toUpperCase().padStart(4, "0"),
|
|
40
|
-
t.node,
|
|
41
|
-
].join("-") as TimestampString;
|
|
42
|
-
|
|
43
|
-
export const unsafeTimestampFromString = (s: TimestampString): Timestamp => {
|
|
44
|
-
const a = s.split("-");
|
|
45
|
-
return {
|
|
46
|
-
millis: Date.parse(a.slice(0, 3).join("-")).valueOf() as Millis,
|
|
47
|
-
counter: parseInt(a[3], 16) as Counter,
|
|
48
|
-
node: a[4] as NodeId,
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const timestampToHash = (t: Timestamp): TimestampHash =>
|
|
53
|
-
murmurhash(timestampToString(t)) as TimestampHash;
|
|
54
|
-
|
|
55
|
-
const syncNodeId = Schema.parseSync(NodeId)("0000000000000000");
|
|
56
|
-
|
|
57
|
-
export const makeSyncTimestamp = (
|
|
58
|
-
millis: Millis = initialMillis,
|
|
59
|
-
): Timestamp => ({
|
|
60
|
-
millis,
|
|
61
|
-
counter: initialCounter,
|
|
62
|
-
node: syncNodeId,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export const makeInitialTimestamp = NanoId.pipe(
|
|
66
|
-
Effect.flatMap(({ nanoidAsNodeId }) => nanoidAsNodeId),
|
|
67
|
-
Effect.map(
|
|
68
|
-
(node): Timestamp => ({
|
|
69
|
-
millis: initialMillis,
|
|
70
|
-
counter: initialCounter,
|
|
71
|
-
node,
|
|
72
|
-
}),
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
export interface Time {
|
|
77
|
-
readonly now: Effect.Effect<never, never, Millis>;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const Time = Context.Tag<Time>("evolu/Time");
|
|
81
|
-
|
|
82
|
-
export const TimeLive = Layer.succeed(
|
|
83
|
-
Time,
|
|
84
|
-
Time.of({
|
|
85
|
-
now: Effect.sync(() => Date.now() as Millis),
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
export type TimestampError =
|
|
90
|
-
| TimestampDriftError
|
|
91
|
-
| TimestampCounterOverflowError
|
|
92
|
-
| TimestampDuplicateNodeError;
|
|
93
|
-
|
|
94
|
-
export interface TimestampDriftError {
|
|
95
|
-
readonly _tag: "TimestampDriftError";
|
|
96
|
-
readonly next: Millis;
|
|
97
|
-
readonly now: Millis;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface TimestampCounterOverflowError {
|
|
101
|
-
readonly _tag: "TimestampCounterOverflowError";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const getNextMillis = (
|
|
105
|
-
millis: Millis[],
|
|
106
|
-
): Effect.Effect<Time | Config, TimestampDriftError, Millis> =>
|
|
107
|
-
Effect.gen(function* (_) {
|
|
108
|
-
const time = yield* _(Time);
|
|
109
|
-
const config = yield* _(Config);
|
|
110
|
-
|
|
111
|
-
const now = yield* _(time.now);
|
|
112
|
-
const next = Math.max(now, ...millis) as Millis;
|
|
113
|
-
|
|
114
|
-
if (next - now > config.maxDrift)
|
|
115
|
-
yield* _(
|
|
116
|
-
Effect.fail<TimestampDriftError>({
|
|
117
|
-
_tag: "TimestampDriftError",
|
|
118
|
-
now,
|
|
119
|
-
next,
|
|
120
|
-
}),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
return next;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const incrementCounter = (
|
|
127
|
-
counter: Counter,
|
|
128
|
-
): Either.Either<TimestampCounterOverflowError, Counter> =>
|
|
129
|
-
pipe(
|
|
130
|
-
Number.increment(counter),
|
|
131
|
-
Schema.parseEither(Counter),
|
|
132
|
-
Either.mapLeft(() => ({ _tag: "TimestampCounterOverflowError" })),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const counterMin = Schema.parseSync(Counter)(0);
|
|
136
|
-
|
|
137
|
-
export const sendTimestamp = (
|
|
138
|
-
timestamp: Timestamp,
|
|
139
|
-
): Effect.Effect<
|
|
140
|
-
Time | Config,
|
|
141
|
-
TimestampDriftError | TimestampCounterOverflowError,
|
|
142
|
-
Timestamp
|
|
143
|
-
> =>
|
|
144
|
-
Effect.gen(function* (_) {
|
|
145
|
-
const millis = yield* _(getNextMillis([timestamp.millis]));
|
|
146
|
-
const counter =
|
|
147
|
-
millis === timestamp.millis
|
|
148
|
-
? yield* _(incrementCounter(timestamp.counter))
|
|
149
|
-
: counterMin;
|
|
150
|
-
return { ...timestamp, millis, counter };
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
export interface TimestampDuplicateNodeError {
|
|
154
|
-
readonly _tag: "TimestampDuplicateNodeError";
|
|
155
|
-
readonly node: NodeId;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export const receiveTimestamp = ({
|
|
159
|
-
local,
|
|
160
|
-
remote,
|
|
161
|
-
}: {
|
|
162
|
-
readonly local: Timestamp;
|
|
163
|
-
readonly remote: Timestamp;
|
|
164
|
-
}): Effect.Effect<
|
|
165
|
-
Time | Config,
|
|
166
|
-
| TimestampDriftError
|
|
167
|
-
| TimestampCounterOverflowError
|
|
168
|
-
| TimestampDuplicateNodeError,
|
|
169
|
-
Timestamp
|
|
170
|
-
> =>
|
|
171
|
-
Effect.gen(function* (_) {
|
|
172
|
-
if (local.node === remote.node)
|
|
173
|
-
yield* _(
|
|
174
|
-
Effect.fail<TimestampDuplicateNodeError>({
|
|
175
|
-
_tag: "TimestampDuplicateNodeError",
|
|
176
|
-
node: local.node,
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const millis = yield* _(getNextMillis([local.millis, remote.millis]));
|
|
181
|
-
const counter = yield* _(
|
|
182
|
-
millis === local.millis && millis === remote.millis
|
|
183
|
-
? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
|
|
184
|
-
: millis === local.millis
|
|
185
|
-
? incrementCounter(local.counter)
|
|
186
|
-
: millis === remote.millis
|
|
187
|
-
? incrementCounter(remote.counter)
|
|
188
|
-
: Either.right(counterMin),
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
return { ...local, millis, counter };
|
|
192
|
-
});
|