@evolu/common 1.0.0

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/src/Config.d.ts +20 -0
  4. package/dist/src/Config.d.ts.map +1 -0
  5. package/dist/src/Config.js +8 -0
  6. package/dist/src/Crypto.d.ts +42 -0
  7. package/dist/src/Crypto.d.ts.map +1 -0
  8. package/dist/src/Crypto.js +44 -0
  9. package/dist/src/Db.d.ts +60 -0
  10. package/dist/src/Db.d.ts.map +1 -0
  11. package/dist/src/Db.js +118 -0
  12. package/dist/src/DbWorker.d.ts +78 -0
  13. package/dist/src/DbWorker.d.ts.map +1 -0
  14. package/dist/src/DbWorker.js +282 -0
  15. package/dist/src/Diff.d.ts +19 -0
  16. package/dist/src/Diff.d.ts.map +1 -0
  17. package/dist/src/Diff.js +86 -0
  18. package/dist/src/Errors.d.ts +19 -0
  19. package/dist/src/Errors.d.ts.map +1 -0
  20. package/dist/src/Errors.js +11 -0
  21. package/dist/src/Evolu.d.ts +71 -0
  22. package/dist/src/Evolu.d.ts.map +1 -0
  23. package/dist/src/Evolu.js +235 -0
  24. package/dist/src/MerkleTree.d.ts +15 -0
  25. package/dist/src/MerkleTree.d.ts.map +1 -0
  26. package/dist/src/MerkleTree.js +49 -0
  27. package/dist/src/Model.d.ts +143 -0
  28. package/dist/src/Model.d.ts.map +1 -0
  29. package/dist/src/Model.js +103 -0
  30. package/dist/src/Murmurhash.d.ts +2 -0
  31. package/dist/src/Murmurhash.d.ts.map +1 -0
  32. package/dist/src/Murmurhash.js +60 -0
  33. package/dist/src/Platform.d.ts +29 -0
  34. package/dist/src/Platform.d.ts.map +1 -0
  35. package/dist/src/Platform.js +17 -0
  36. package/dist/src/Protobuf.d.ts +130 -0
  37. package/dist/src/Protobuf.d.ts.map +1 -0
  38. package/dist/src/Protobuf.js +100 -0
  39. package/dist/src/Public.d.ts +7 -0
  40. package/dist/src/Public.d.ts.map +1 -0
  41. package/dist/src/Public.js +2 -0
  42. package/dist/src/Sql.d.ts +12 -0
  43. package/dist/src/Sql.d.ts.map +1 -0
  44. package/dist/src/Sql.js +100 -0
  45. package/dist/src/Sqlite.d.ts +30 -0
  46. package/dist/src/Sqlite.d.ts.map +1 -0
  47. package/dist/src/Sqlite.js +41 -0
  48. package/dist/src/Store.d.ts +9 -0
  49. package/dist/src/Store.d.ts.map +1 -0
  50. package/dist/src/Store.js +18 -0
  51. package/dist/src/SyncWorker.d.ts +73 -0
  52. package/dist/src/SyncWorker.d.ts.map +1 -0
  53. package/dist/src/SyncWorker.js +136 -0
  54. package/dist/src/Timestamp.d.ts +44 -0
  55. package/dist/src/Timestamp.d.ts.map +1 -0
  56. package/dist/src/Timestamp.js +76 -0
  57. package/dist/src/index.d.ts +19 -0
  58. package/dist/src/index.d.ts.map +1 -0
  59. package/dist/src/index.js +18 -0
  60. package/package.json +76 -0
  61. package/src/Config.ts +33 -0
  62. package/src/Crypto.ts +113 -0
  63. package/src/Db.ts +323 -0
  64. package/src/DbWorker.ts +683 -0
  65. package/src/Diff.ts +113 -0
  66. package/src/Errors.ts +33 -0
  67. package/src/Evolu.ts +527 -0
  68. package/src/MerkleTree.ts +83 -0
  69. package/src/Model.ts +212 -0
  70. package/src/Murmurhash.ts +70 -0
  71. package/src/Platform.ts +63 -0
  72. package/src/Protobuf.ts +204 -0
  73. package/src/Public.ts +6 -0
  74. package/src/Sql.ts +114 -0
  75. package/src/Sqlite.ts +92 -0
  76. package/src/Store.ts +32 -0
  77. package/src/SyncWorker.ts +344 -0
  78. package/src/Timestamp.ts +192 -0
  79. package/src/index.ts +18 -0
@@ -0,0 +1,192 @@
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
+ });
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
2
+ export * from "./Config.js";
3
+ export * from "./Crypto.js";
4
+ export * from "./Db.js";
5
+ export * from "./DbWorker.js";
6
+ export * from "./Diff.js";
7
+ export * from "./Errors.js";
8
+ export * from "./Evolu.js";
9
+ export * from "./MerkleTree.js";
10
+ export * from "./Model.js";
11
+ export * from "./Murmurhash.js";
12
+ export * from "./Platform.js";
13
+ export * from "./Protobuf.js";
14
+ export * from "./Sql.js";
15
+ export * from "./Sqlite.js";
16
+ export * from "./Store.js";
17
+ export * from "./SyncWorker.js";
18
+ export * from "./Timestamp.js";