@evolu/common 4.1.0 → 5.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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Config.d.ts +26 -27
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +35 -7
  5. package/dist/src/Crdt.d.ts +5 -4
  6. package/dist/src/Crdt.d.ts.map +1 -1
  7. package/dist/src/Crdt.js +27 -24
  8. package/dist/src/Crypto.d.ts +16 -10
  9. package/dist/src/Crypto.d.ts.map +1 -1
  10. package/dist/src/Crypto.js +24 -11
  11. package/dist/src/Db.d.ts +58 -78
  12. package/dist/src/Db.d.ts.map +1 -1
  13. package/dist/src/Db.js +383 -185
  14. package/dist/src/Diff.d.ts +9 -3
  15. package/dist/src/Diff.d.ts.map +1 -1
  16. package/dist/src/Diff.js +12 -11
  17. package/dist/src/Error.d.ts +15 -0
  18. package/dist/src/Error.d.ts.map +1 -0
  19. package/dist/src/Error.js +14 -0
  20. package/dist/src/Evolu.d.ts +139 -91
  21. package/dist/src/Evolu.d.ts.map +1 -1
  22. package/dist/src/Evolu.js +267 -239
  23. package/dist/src/Model.d.ts +52 -0
  24. package/dist/src/Model.d.ts.map +1 -1
  25. package/dist/src/Model.js +47 -5
  26. package/dist/src/Owner.d.ts.map +1 -1
  27. package/dist/src/Owner.js +14 -8
  28. package/dist/src/Platform.d.ts +24 -34
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +9 -31
  31. package/dist/src/Public.d.ts +3 -5
  32. package/dist/src/Public.d.ts.map +1 -1
  33. package/dist/src/Public.js +2 -3
  34. package/dist/src/Sqlite.d.ts +25 -15
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +28 -7
  37. package/dist/src/Store.d.ts.map +1 -1
  38. package/dist/src/Sync.d.ts +70 -0
  39. package/dist/src/Sync.d.ts.map +1 -0
  40. package/dist/src/Sync.js +126 -0
  41. package/dist/src/index.d.ts +2 -5
  42. package/dist/src/index.d.ts.map +1 -1
  43. package/dist/src/index.js +2 -5
  44. package/package.json +12 -10
  45. package/src/Config.ts +82 -39
  46. package/src/Crdt.ts +61 -76
  47. package/src/Crypto.ts +83 -60
  48. package/src/Db.ts +802 -370
  49. package/src/Diff.ts +23 -21
  50. package/src/Error.ts +29 -0
  51. package/src/Evolu.ts +599 -499
  52. package/src/Model.ts +102 -6
  53. package/src/Owner.ts +24 -24
  54. package/src/Platform.ts +33 -81
  55. package/src/Public.ts +3 -5
  56. package/src/Sqlite.ts +81 -27
  57. package/src/Sync.ts +306 -0
  58. package/src/index.ts +2 -5
  59. package/dist/src/DbWorker.d.ts +0 -82
  60. package/dist/src/DbWorker.d.ts.map +0 -1
  61. package/dist/src/DbWorker.js +0 -335
  62. package/dist/src/ErrorStore.d.ts +0 -29
  63. package/dist/src/ErrorStore.d.ts.map +0 -1
  64. package/dist/src/ErrorStore.js +0 -13
  65. package/dist/src/OnCompletes.d.ts +0 -14
  66. package/dist/src/OnCompletes.d.ts.map +0 -1
  67. package/dist/src/OnCompletes.js +0 -25
  68. package/dist/src/SyncWorker.d.ts +0 -80
  69. package/dist/src/SyncWorker.d.ts.map +0 -1
  70. package/dist/src/SyncWorker.js +0 -150
  71. package/dist/src/Types.d.ts +0 -13
  72. package/dist/src/Types.d.ts.map +0 -1
  73. package/dist/src/Types.js +0 -1
  74. package/src/DbWorker.ts +0 -721
  75. package/src/ErrorStore.ts +0 -46
  76. package/src/OnCompletes.ts +0 -51
  77. package/src/SyncWorker.ts +0 -361
  78. package/src/Types.ts +0 -11
@@ -2,18 +2,15 @@ export * from "./Config.js";
2
2
  export * from "./Crdt.js";
3
3
  export * from "./Crypto.js";
4
4
  export * from "./Db.js";
5
- export * from "./DbWorker.js";
6
5
  export * from "./Diff.js";
7
- export * from "./ErrorStore.js";
6
+ export * from "./Error.js";
8
7
  export * from "./Evolu.js";
9
8
  export * from "./Model.js";
10
9
  export * from "./Murmurhash.js";
11
- export * from "./OnCompletes.js";
12
10
  export * from "./Owner.js";
13
11
  export * from "./Platform.js";
14
12
  export * from "./Protobuf.js";
15
- export * from "./Public.js";
16
13
  export * from "./Sqlite.js";
17
14
  export * from "./Store.js";
18
- export * from "./SyncWorker.js";
15
+ export * from "./Sync.js";
19
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC"}
package/dist/src/index.js CHANGED
@@ -2,17 +2,14 @@ export * from "./Config.js";
2
2
  export * from "./Crdt.js";
3
3
  export * from "./Crypto.js";
4
4
  export * from "./Db.js";
5
- export * from "./DbWorker.js";
6
5
  export * from "./Diff.js";
7
- export * from "./ErrorStore.js";
6
+ export * from "./Error.js";
8
7
  export * from "./Evolu.js";
9
8
  export * from "./Model.js";
10
9
  export * from "./Murmurhash.js";
11
- export * from "./OnCompletes.js";
12
10
  export * from "./Owner.js";
13
11
  export * from "./Platform.js";
14
12
  export * from "./Protobuf.js";
15
- export * from "./Public.js";
16
13
  export * from "./Sqlite.js";
17
14
  export * from "./Store.js";
18
- export * from "./SyncWorker.js";
15
+ export * from "./Sync.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Local-first platform designed for privacy, ease of use, and no vendor lock-in to sync and backup people's lifetime data",
5
5
  "keywords": [
6
6
  "evolu",
@@ -44,28 +44,30 @@
44
44
  "README.md"
45
45
  ],
46
46
  "dependencies": {
47
- "@noble/ciphers": "^0.5.1",
47
+ "@noble/ciphers": "^0.5.2",
48
48
  "@noble/hashes": "^1.4.0",
49
49
  "@protobuf-ts/runtime": "^2.9.4",
50
- "@scure/bip39": "^1.2.2",
50
+ "@scure/bip39": "^1.3.0",
51
51
  "kysely": "^0.27.3",
52
- "nanoid": "^5.0.6"
52
+ "nanoid": "^5.0.7"
53
53
  },
54
54
  "devDependencies": {
55
- "@effect/schema": "^0.64.9",
55
+ "@effect/platform": "^0.50.7",
56
+ "@effect/schema": "^0.66.10",
56
57
  "@protobuf-ts/plugin": "^2.9.4",
57
58
  "@protobuf-ts/protoc": "^2.9.4",
58
59
  "array-shuffle": "^3.0.0",
59
- "effect": "2.4.9",
60
+ "effect": "^3.0.7",
60
61
  "eslint": "^8.57.0",
61
- "typescript": "^5.4.3",
62
- "vitest": "^1.4.0",
62
+ "typescript": "^5.4.5",
63
+ "vitest": "^1.5.2",
63
64
  "@evolu/tsconfig": "0.0.2",
64
65
  "eslint-config-evolu": "1.0.0"
65
66
  },
66
67
  "peerDependencies": {
67
- "@effect/schema": "^0.64.9",
68
- "effect": "2.4.9"
68
+ "@effect/platform": "^0.50.2",
69
+ "@effect/schema": "^0.64.20",
70
+ "effect": "^3.0.0"
69
71
  },
70
72
  "publishConfig": {
71
73
  "access": "public"
package/src/Config.ts CHANGED
@@ -1,60 +1,103 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Layer from "effect/Layer";
3
- import { CreateIndexBuilder } from "kysely";
3
+ import * as LogLevel from "effect/LogLevel";
4
+ import * as Logger from "effect/Logger";
5
+ import * as ManagedRuntime from "effect/ManagedRuntime";
6
+ import * as Match from "effect/Match";
4
7
 
5
8
  export interface Config {
6
- /** Alternate URL to Evolu sync and backup server. */
7
- readonly syncUrl: string;
8
-
9
9
  /**
10
- * Alternate URL to reload browser tabs after {@link Owner} reset or restore.
10
+ * URL to reload browser tabs after {@link Owner} reset or restore.
11
+ *
11
12
  * The default value is `/`.
12
13
  */
13
- readonly reloadUrl: string;
14
+ reloadUrl: string;
14
15
 
15
16
  /**
16
- * Maximum physical clock drift allowed in ms. The default value is 5 * 60 *
17
- * 1000 (5 minutes).
17
+ * URL for Evolu sync and backup server.
18
+ *
19
+ * The default value is `https://evolu.world`.
18
20
  */
19
- readonly maxDrift: number;
21
+ syncUrl: string;
20
22
 
21
23
  /**
22
- * Evolu application name. For now, this is only useful for localhost
23
- * development, where we want each application to have its own database. The
24
- * default value is: "Evolu".
24
+ * Evolu is multitenant - it can run more instances concurrently. Every Evolu
25
+ * instance has to have its own unique name. Database files are separated and
26
+ * invisible to each other.
27
+ *
28
+ * The default value is: `Evolu`.
25
29
  */
26
- readonly name: string;
30
+ name: string;
27
31
 
28
32
  /**
29
- * Use the `indexes` property to define SQLite indexes.
30
- *
31
- * Table and column names are not typed because Kysely doesn't support it.
33
+ * Maximum physical clock drift allowed in ms.
32
34
  *
33
- * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
34
- *
35
- * @example
36
- * const indexes = [
37
- * createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
35
+ * The default value is 5 * 60 * 1000 (5 minutes).
36
+ */
37
+ maxDrift: number;
38
+
39
+ /**
40
+ * Setting the minimum log level. The default value is `none`.
38
41
  *
39
- * createIndex("indexTodoCategoryCreatedAt")
40
- * .on("todoCategory")
41
- * .column("createdAt"),
42
- * ];
42
+ * For development, use `trace` to log all events and `debug` to log only
43
+ * events with values. For production, use `warning`.
43
44
  */
44
- readonly indexes: ReadonlyArray<CreateIndexBuilder<any>>;
45
+ minimumLogLevel: "none" | "trace" | "debug" | "warning";
45
46
  }
46
47
 
47
- export const Config = Context.GenericTag<Config>("@services/Config");
48
-
49
- export const ConfigLive = (config?: Partial<Config>): Layer.Layer<Config> =>
50
- Layer.succeed(
51
- Config,
52
- Config.of({
53
- syncUrl: "https://evolu.world",
54
- maxDrift: 5 * 60 * 1000,
55
- reloadUrl: "/",
56
- name: "Evolu",
57
- indexes: [],
58
- ...config,
59
- }),
48
+ export const Config = Context.GenericTag<Config>("Config");
49
+
50
+ export const defaultConfig: Config = {
51
+ reloadUrl: "/",
52
+ syncUrl: "https://evolu.world",
53
+ name: "Evolu",
54
+ maxDrift: 5 * 60 * 1000,
55
+ minimumLogLevel: "none",
56
+ };
57
+
58
+ /** https://effect.website/docs/guides/runtime */
59
+ export const createRuntime = (
60
+ partialConfig?: Partial<Config>,
61
+ ): ManagedRuntime.ManagedRuntime<Config, never> => {
62
+ const config = { ...defaultConfig, ...partialConfig };
63
+ const ConfigLive = Layer.succeed(Config, config);
64
+
65
+ const minimumLogLevel = Match.value(config.minimumLogLevel).pipe(
66
+ Match.when("debug", () => LogLevel.Debug),
67
+ Match.when("none", () => LogLevel.None),
68
+ Match.when("trace", () => LogLevel.Trace),
69
+ Match.when("warning", () => LogLevel.Warning),
70
+ Match.exhaustive,
60
71
  );
72
+
73
+ const evoluLayer =
74
+ config.minimumLogLevel === "none"
75
+ ? ConfigLive
76
+ : Layer.mergeAll(
77
+ ConfigLive,
78
+ Logger.minimumLogLevel(minimumLogLevel),
79
+ Logger.replace(Logger.defaultLogger, makeEvoluLogger(config.name)),
80
+ );
81
+
82
+ return ManagedRuntime.make(evoluLayer);
83
+ };
84
+
85
+ // TODO: Spans and Effect native variadic log args.
86
+ const makeEvoluLogger = (name: string) =>
87
+ Logger.make(({ logLevel, message, date }) => {
88
+ const fn =
89
+ logLevel._tag === "Warning"
90
+ ? "warn"
91
+ : logLevel._tag === "Error"
92
+ ? "error"
93
+ : "log";
94
+
95
+ const hours = date.getHours().toString().padStart(2, "0");
96
+ const minutes = date.getMinutes().toString().padStart(2, "0");
97
+ const seconds = date.getSeconds().toString().padStart(2, "0");
98
+ const milliseconds = date.getMilliseconds().toString().padStart(3, "0");
99
+ const formattedDate = `${hours}:${minutes}:${seconds}:${milliseconds}`;
100
+
101
+ const messages: unknown[] = Array.isArray(message) ? message : [message];
102
+ globalThis.console[fn](`${formattedDate} [${name}]`, ...messages);
103
+ });
package/src/Crdt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as S from "@effect/schema/Schema";
2
+ import * as Arr from "effect/Array";
2
3
  import * as Brand from "effect/Brand";
3
4
  import * as Context from "effect/Context";
4
5
  import * as Effect from "effect/Effect";
@@ -7,7 +8,6 @@ import { pipe } from "effect/Function";
7
8
  import * as Layer from "effect/Layer";
8
9
  import * as Number from "effect/Number";
9
10
  import * as Option from "effect/Option";
10
- import * as ReadonlyArray from "effect/ReadonlyArray";
11
11
  import * as String from "effect/String";
12
12
  import { Config } from "./Config.js";
13
13
  import { NanoIdGenerator, NodeId } from "./Crypto.js";
@@ -36,7 +36,7 @@ export const AllowedTimeRange = {
36
36
  * the device clock is out of range, Evolu will not store data until it's
37
37
  * fixed.
38
38
  */
39
- export const Millis = S.number.pipe(
39
+ export const Millis = S.Number.pipe(
40
40
  S.greaterThan(AllowedTimeRange.greaterThan),
41
41
  S.lessThan(AllowedTimeRange.lessThan),
42
42
  S.brand("Millis"),
@@ -48,7 +48,7 @@ export const initialMillis = S.decodeSync(Millis)(
48
48
  AllowedTimeRange.greaterThan + 1,
49
49
  );
50
50
 
51
- export const Counter = S.number.pipe(S.between(0, 65535), S.brand("Counter"));
51
+ export const Counter = S.Number.pipe(S.between(0, 65535), S.brand("Counter"));
52
52
  export type Counter = S.Schema.Type<typeof Counter>;
53
53
 
54
54
  const initialCounter = S.decodeSync(Counter)(0);
@@ -87,7 +87,7 @@ export const makeSyncTimestamp = (
87
87
  });
88
88
 
89
89
  export const makeInitialTimestamp = NanoIdGenerator.pipe(
90
- Effect.flatMap(({ nanoidAsNodeId }) => nanoidAsNodeId),
90
+ Effect.flatMap(({ nodeId }) => nodeId),
91
91
  Effect.map(
92
92
  (node): Timestamp => ({
93
93
  millis: initialMillis,
@@ -97,15 +97,13 @@ export const makeInitialTimestamp = NanoIdGenerator.pipe(
97
97
  ),
98
98
  );
99
99
 
100
- export interface Time {
101
- readonly now: Effect.Effect<Millis, TimestampTimeOutOfRangeError>;
102
- }
103
-
104
- export const Time = Context.GenericTag<Time>("@services/Time");
105
-
106
- export const TimeLive = Layer.succeed(
100
+ export class Time extends Context.Tag("Time")<
107
101
  Time,
108
- Time.of({
102
+ {
103
+ readonly now: Effect.Effect<Millis, TimestampTimeOutOfRangeError>;
104
+ }
105
+ >() {
106
+ static Live = Layer.succeed(Time, {
109
107
  now: Effect.suspend(() => S.decode(Millis)(Date.now())).pipe(
110
108
  Effect.catchTag("ParseError", () =>
111
109
  Effect.fail<TimestampTimeOutOfRangeError>({
@@ -113,8 +111,8 @@ export const TimeLive = Layer.succeed(
113
111
  }),
114
112
  ),
115
113
  ),
116
- }),
117
- );
114
+ });
115
+ }
118
116
 
119
117
  /**
120
118
  * The TimestampError type represents all Timestamp-related errors. If such an
@@ -146,39 +144,33 @@ export interface TimestampTimeOutOfRangeError {
146
144
  readonly _tag: "TimestampTimeOutOfRangeError";
147
145
  }
148
146
 
149
- const getNextMillis = (
150
- millis: ReadonlyArray<Millis>,
151
- ): Effect.Effect<
152
- Millis,
153
- TimestampDriftError | TimestampTimeOutOfRangeError,
154
- Time | Config
155
- > =>
156
- Effect.gen(function* (_) {
157
- const time = yield* _(Time);
158
- const config = yield* _(Config);
147
+ const getNextMillis = (millis: ReadonlyArray<Millis>) =>
148
+ Effect.gen(function* () {
149
+ const time = yield* Time;
150
+ const config = yield* Config;
159
151
 
160
- const now = yield* _(time.now);
152
+ const now = yield* time.now;
161
153
  const next = Math.max(now, ...millis) as Millis;
162
154
 
163
155
  if (next - now > config.maxDrift)
164
- yield* _(
165
- Effect.fail<TimestampDriftError>({
166
- _tag: "TimestampDriftError",
167
- now,
168
- next,
169
- }),
170
- );
156
+ yield* Effect.fail<TimestampDriftError>({
157
+ _tag: "TimestampDriftError",
158
+ now,
159
+ next,
160
+ });
171
161
 
172
162
  return next;
173
163
  });
174
164
 
175
- const incrementCounter = (
176
- counter: Counter,
177
- ): Either.Either<Counter, TimestampCounterOverflowError> =>
165
+ const incrementCounter = (counter: Counter) =>
178
166
  pipe(
179
167
  Number.increment(counter),
180
168
  S.decodeEither(Counter),
181
- Either.mapLeft(() => ({ _tag: "TimestampCounterOverflowError" })),
169
+ Either.mapLeft(
170
+ (): TimestampCounterOverflowError => ({
171
+ _tag: "TimestampCounterOverflowError",
172
+ }),
173
+ ),
182
174
  );
183
175
 
184
176
  const counterMin = S.decodeSync(Counter)(0);
@@ -192,11 +184,11 @@ export const sendTimestamp = (
192
184
  | TimestampTimeOutOfRangeError,
193
185
  Time | Config
194
186
  > =>
195
- Effect.gen(function* (_) {
196
- const millis = yield* _(getNextMillis([timestamp.millis]));
187
+ Effect.gen(function* () {
188
+ const millis = yield* getNextMillis([timestamp.millis]);
197
189
  const counter =
198
190
  millis === timestamp.millis
199
- ? yield* _(incrementCounter(timestamp.counter))
191
+ ? yield* incrementCounter(timestamp.counter)
200
192
  : counterMin;
201
193
  return { ...timestamp, millis, counter };
202
194
  });
@@ -217,23 +209,19 @@ export const receiveTimestamp = ({
217
209
  > =>
218
210
  Effect.gen(function* (_) {
219
211
  if (local.node === remote.node)
220
- yield* _(
221
- Effect.fail<TimestampDuplicateNodeError>({
222
- _tag: "TimestampDuplicateNodeError",
223
- node: local.node,
224
- }),
225
- );
226
-
227
- const millis = yield* _(getNextMillis([local.millis, remote.millis]));
228
- const counter = yield* _(
229
- millis === local.millis && millis === remote.millis
230
- ? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
231
- : millis === local.millis
232
- ? incrementCounter(local.counter)
233
- : millis === remote.millis
234
- ? incrementCounter(remote.counter)
235
- : Either.right(counterMin),
236
- );
212
+ yield* Effect.fail<TimestampDuplicateNodeError>({
213
+ _tag: "TimestampDuplicateNodeError",
214
+ node: local.node,
215
+ });
216
+
217
+ const millis = yield* getNextMillis([local.millis, remote.millis]);
218
+ const counter = yield* millis === local.millis && millis === remote.millis
219
+ ? incrementCounter(Math.max(local.counter, remote.counter) as Counter)
220
+ : millis === local.millis
221
+ ? incrementCounter(local.counter)
222
+ : millis === remote.millis
223
+ ? incrementCounter(remote.counter)
224
+ : Either.right(counterMin);
237
225
 
238
226
  return { ...local, millis, counter };
239
227
  });
@@ -262,17 +250,15 @@ export const millisToMerkleTreePath = (millis: Millis): MerkleTreePath =>
262
250
  .toString(3)
263
251
  .split("") as MerkleTreePath;
264
252
 
265
- const merkleTreePathToMillis = (path: MerkleTreePath): Millis =>
253
+ const merkleTreePathToMillis = (path: MerkleTreePath) =>
266
254
  path.length === 0
267
255
  ? initialMillis
268
256
  : // 16 is the length of the base 3 value of the current time in minutes.
269
257
  // Ensure it's padded to create the full value.
270
258
  ((parseInt(path.join("").padEnd(16, "0"), 3) * 1000 * 60) as Millis);
271
259
 
272
- const xorTimestampHashes = (
273
- a: TimestampHash | undefined,
274
- b: TimestampHash,
275
- ): TimestampHash => ((a || 0) ^ b) as TimestampHash;
260
+ const xorTimestampHashes = (a: TimestampHash | undefined, b: TimestampHash) =>
261
+ ((a || 0) ^ b) as TimestampHash;
276
262
 
277
263
  const insertKey = (
278
264
  tree: MerkleTree,
@@ -292,23 +278,22 @@ const insertKey = (
292
278
  };
293
279
  };
294
280
 
295
- export const insertIntoMerkleTree =
296
- (timestamp: Timestamp) =>
297
- (tree: MerkleTree): MerkleTree => {
298
- const path = millisToMerkleTreePath(timestamp.millis);
299
- const hash = timestampToHash(timestamp);
300
- return insertKey(
301
- { ...tree, hash: xorTimestampHashes(tree.hash, hash) },
302
- path,
303
- hash,
304
- );
305
- };
281
+ export const insertIntoMerkleTree = (
282
+ tree: MerkleTree,
283
+ timestamp: Timestamp,
284
+ ): MerkleTree => {
285
+ const path = millisToMerkleTreePath(timestamp.millis);
286
+ const hash = timestampToHash(timestamp);
287
+ return insertKey(
288
+ { ...tree, hash: xorTimestampHashes(tree.hash, hash) },
289
+ path,
290
+ hash,
291
+ );
292
+ };
306
293
 
307
294
  const sortedMerkleTreeKeys: ReadonlyArray<MerkleTreeKey> = ["0", "1", "2"];
308
295
 
309
- const getSortedMerkleTreeKeys = (
310
- tree: MerkleTree,
311
- ): ReadonlyArray<MerkleTreeKey> =>
296
+ const getSortedMerkleTreeKeys = (tree: MerkleTree) =>
312
297
  sortedMerkleTreeKeys.filter((key) => key in tree);
313
298
 
314
299
  export const diffMerkleTrees = (
@@ -326,7 +311,7 @@ export const diffMerkleTrees = (
326
311
  // the top of this function should pass)
327
312
  // eslint-disable-next-line no-constant-condition
328
313
  while (1) {
329
- const keys = ReadonlyArray.dedupeWith(
314
+ const keys = Arr.dedupeWith(
330
315
  getSortedMerkleTreeKeys(node1).concat(getSortedMerkleTreeKeys(node2)),
331
316
  String.Equivalence,
332
317
  );
package/src/Crypto.ts CHANGED
@@ -8,25 +8,40 @@ import * as Brand from "effect/Brand";
8
8
  import * as Context from "effect/Context";
9
9
  import * as Effect from "effect/Effect";
10
10
  import * as Layer from "effect/Layer";
11
- import { customAlphabet, nanoid } from "nanoid";
12
11
 
13
- // TODO: Add dynamic import error.
14
- export interface Bip39 {
15
- readonly make: Effect.Effect<Mnemonic>;
12
+ import { Id } from "./Model.js";
16
13
 
17
- readonly toSeed: (mnemonic: Mnemonic) => Effect.Effect<Uint8Array>;
14
+ export class Bip39 extends Context.Tag("Bip39")<
15
+ Bip39,
16
+ {
17
+ readonly make: Effect.Effect<Mnemonic>;
18
18
 
19
- readonly parse: (
20
- mnemonic: string,
21
- ) => Effect.Effect<Mnemonic, InvalidMnemonicError>;
22
- }
19
+ readonly toSeed: (mnemonic: Mnemonic) => Effect.Effect<Uint8Array>;
23
20
 
24
- export const Bip39 = Context.GenericTag<Bip39>("@services/Bip39");
21
+ readonly parse: (
22
+ mnemonic: string,
23
+ ) => Effect.Effect<Mnemonic, InvalidMnemonicError>;
24
+ }
25
+ >() {}
25
26
 
26
27
  export interface InvalidMnemonicError {
27
28
  readonly _tag: "InvalidMnemonicError";
28
29
  }
29
30
 
31
+ export const validateMnemonicToEffect =
32
+ (validateMnemonic: (mnemonic: string, wordlist: string[]) => boolean) =>
33
+ (
34
+ mnemonic: string,
35
+ wordlist: string[],
36
+ ): Effect.Effect<Mnemonic, InvalidMnemonicError, never> => {
37
+ const mnemonicTrimmed = mnemonic.trim();
38
+ return validateMnemonic(mnemonicTrimmed, wordlist)
39
+ ? Effect.succeed(mnemonicTrimmed as Mnemonic)
40
+ : Effect.fail<InvalidMnemonicError>({
41
+ _tag: "InvalidMnemonicError",
42
+ });
43
+ };
44
+
30
45
  /**
31
46
  * Mnemonic is a password generated by Evolu in BIP39 format.
32
47
  *
@@ -36,33 +51,41 @@ export interface InvalidMnemonicError {
36
51
  */
37
52
  export type Mnemonic = string & Brand.Brand<"Mnemonic">;
38
53
 
39
- export interface NanoIdGenerator {
40
- readonly nanoid: Effect.Effect<NanoId>;
41
- readonly nanoidAsNodeId: Effect.Effect<NodeId>;
42
- }
43
-
44
- export const NanoIdGenerator = Context.GenericTag<NanoIdGenerator>(
45
- "@services/NanoIdGenerator",
46
- );
54
+ export class NanoIdGenerator extends Context.Tag("NanoIdGenerator")<
55
+ NanoIdGenerator,
56
+ {
57
+ readonly nanoid: Effect.Effect<NanoId>;
58
+ readonly nodeId: Effect.Effect<NodeId>;
59
+ readonly rowId: Effect.Effect<Id>;
60
+ }
61
+ >() {}
62
+
63
+ export const createNanoIdGeneratorLive = (
64
+ customAlphabet: (
65
+ alphabet: string,
66
+ defaultSize?: number | undefined,
67
+ ) => (size?: number | undefined) => string,
68
+ nanoid: (size?: number) => string,
69
+ ): Layer.Layer<NanoIdGenerator, never, never> => {
70
+ const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
71
+ return Layer.succeed(
72
+ NanoIdGenerator,
73
+ NanoIdGenerator.of({
74
+ nanoid: Effect.sync(() => nanoid() as NanoId),
75
+ nodeId: Effect.sync(() => nanoidForNodeId() as NodeId),
76
+ rowId: Effect.sync(() => nanoid() as Id),
77
+ }),
78
+ );
79
+ };
47
80
 
48
81
  export type NanoId = string & Brand.Brand<"NanoId">;
49
82
 
50
- export const NodeId = S.string.pipe(
83
+ export const NodeId = S.String.pipe(
51
84
  S.pattern(/^[\w-]{16}$/),
52
85
  S.brand("NodeId"),
53
86
  );
54
87
  export type NodeId = S.Schema.Type<typeof NodeId>;
55
88
 
56
- const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
57
-
58
- export const NanoIdGeneratorLive = Layer.succeed(
59
- NanoIdGenerator,
60
- NanoIdGenerator.of({
61
- nanoid: Effect.sync(() => nanoid() as NanoId),
62
- nanoidAsNodeId: Effect.sync(() => nanoidForNodeId() as NodeId),
63
- }),
64
- );
65
-
66
89
  // SLIP-21 implementation
67
90
  // https://github.com/satoshilabs/slips/blob/master/slip-0021.md
68
91
  export const slip21Derive = (
@@ -81,35 +104,35 @@ export const slip21Derive = (
81
104
  return m.slice(32, 64);
82
105
  });
83
106
 
84
- /** Alias to xsalsa20poly1305, for compatibility with libsodium/nacl */
85
- export interface SecretBox {
86
- readonly seal: (
87
- key: Uint8Array,
88
- plaintext: Uint8Array,
89
- ) => Effect.Effect<Uint8Array>;
90
-
91
- readonly open: (
92
- key: Uint8Array,
93
- ciphertext: Uint8Array,
94
- ) => Effect.Effect<Uint8Array>;
95
- }
96
-
97
- export const SecretBox = Context.GenericTag<SecretBox>("@services/SecretBox");
98
-
99
- export const SecretBoxLive = Layer.succeed(
107
+ export class SecretBox extends Context.Tag("SecretBox")<
100
108
  SecretBox,
101
- SecretBox.of({
102
- seal: (key, plaintext) =>
103
- Effect.sync(() => {
104
- const nonce = randomBytes(24);
105
- const ciphertext = secretbox(key, nonce).seal(plaintext);
106
- return concatBytes(nonce, ciphertext);
107
- }),
108
- open: (key, ciphertext) =>
109
- Effect.sync(() => {
110
- const nonce = ciphertext.subarray(0, 24);
111
- const ciphertextWithoutNonce = ciphertext.subarray(24);
112
- return secretbox(key, nonce).open(ciphertextWithoutNonce);
113
- }),
114
- }),
115
- );
109
+ {
110
+ readonly seal: (
111
+ key: Uint8Array,
112
+ plaintext: Uint8Array,
113
+ ) => Effect.Effect<Uint8Array>;
114
+
115
+ readonly open: (
116
+ key: Uint8Array,
117
+ ciphertext: Uint8Array,
118
+ ) => Effect.Effect<Uint8Array>;
119
+ }
120
+ >() {
121
+ static Live = Layer.succeed(
122
+ SecretBox,
123
+ SecretBox.of({
124
+ seal: (key, plaintext) =>
125
+ Effect.sync(() => {
126
+ const nonce = randomBytes(24);
127
+ const ciphertext = secretbox(key, nonce).seal(plaintext);
128
+ return concatBytes(nonce, ciphertext);
129
+ }),
130
+ open: (key, ciphertext) =>
131
+ Effect.sync(() => {
132
+ const nonce = ciphertext.subarray(0, 24);
133
+ const ciphertextWithoutNonce = ciphertext.subarray(24);
134
+ return secretbox(key, nonce).open(ciphertextWithoutNonce);
135
+ }),
136
+ }),
137
+ );
138
+ }