@evolu/common 1.0.9 → 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.
@@ -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
- });