@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,683 @@
1
+ import { hexToBytes } from "@noble/ciphers/utils";
2
+ import {
3
+ Brand,
4
+ Context,
5
+ Effect,
6
+ Function,
7
+ Layer,
8
+ Match,
9
+ Option,
10
+ ReadonlyArray,
11
+ ReadonlyRecord,
12
+ Ref,
13
+ pipe,
14
+ } from "effect";
15
+ import { Config, ConfigLive } from "./Config.js";
16
+ import { Bip39, Mnemonic, NanoId } from "./Crypto.js";
17
+ import {
18
+ Owner,
19
+ OwnerId,
20
+ Table,
21
+ Tables,
22
+ ensureSchema,
23
+ lazyInit,
24
+ someDefectToNoSuchTableOrColumnError,
25
+ transaction,
26
+ } from "./Db.js";
27
+ import { QueryPatches, makePatches } from "./Diff.js";
28
+ import { EvoluError, makeUnexpectedError } from "./Errors.js";
29
+ import {
30
+ MerkleTree,
31
+ diffMerkleTrees,
32
+ insertIntoMerkleTree,
33
+ merkleTreeToString,
34
+ } from "./MerkleTree.js";
35
+ import { Id, SqliteDate, cast } from "./Model.js";
36
+ import {
37
+ insertIntoMessagesIfNew,
38
+ insertValueIntoTableRowColumn,
39
+ selectLastTimestampForTableRowColumn,
40
+ selectMessagesToSync,
41
+ selectOwner,
42
+ selectOwnerTimestampAndMerkleTree,
43
+ updateOwnerTimestampAndMerkleTree,
44
+ } from "./Sql.js";
45
+ import { Query, Row, Sqlite, Value, queryObjectFromQuery } from "./Sqlite.js";
46
+ import {
47
+ Message,
48
+ NewMessage,
49
+ SyncState,
50
+ SyncWorker,
51
+ SyncWorkerOutputSyncResponse,
52
+ SyncWorkerPostMessage,
53
+ } from "./SyncWorker.js";
54
+ import {
55
+ Time,
56
+ TimeLive,
57
+ Timestamp,
58
+ TimestampCounterOverflowError,
59
+ TimestampDriftError,
60
+ TimestampError,
61
+ TimestampString,
62
+ makeSyncTimestamp,
63
+ receiveTimestamp,
64
+ sendTimestamp,
65
+ timestampToString,
66
+ unsafeTimestampFromString,
67
+ } from "./Timestamp.js";
68
+
69
+ // TODO: Refactor to Effect.
70
+ export interface DbWorker {
71
+ readonly postMessage: (input: DbWorkerInput) => void;
72
+ onMessage: (output: DbWorkerOutput) => void;
73
+ }
74
+
75
+ export const DbWorker = Context.Tag<DbWorker>("evolu/DbWorker");
76
+
77
+ export type DbWorkerInput =
78
+ | DbWorkerInputInit
79
+ | DbWorkerInputQuery
80
+ | DbWorkerInputMutate
81
+ | DbWorkerInputSync
82
+ | DbWorkerInputReset
83
+ | DbWorkerInputEnsureSchema
84
+ | SyncWorkerOutputSyncResponse;
85
+
86
+ interface DbWorkerInputInit {
87
+ readonly _tag: "init";
88
+ readonly config: Config;
89
+ readonly tables: Tables;
90
+ }
91
+
92
+ interface DbWorkerInputQuery {
93
+ readonly _tag: "query";
94
+ readonly queries: ReadonlyArray.NonEmptyReadonlyArray<Query>;
95
+ }
96
+
97
+ interface DbWorkerInputMutate {
98
+ readonly _tag: "mutate";
99
+ readonly items: ReadonlyArray.NonEmptyReadonlyArray<MutateItem>;
100
+ readonly queries: ReadonlyArray<Query>;
101
+ }
102
+
103
+ interface DbWorkerInputSync {
104
+ readonly _tag: "sync";
105
+ readonly queries: ReadonlyArray<Query>;
106
+ }
107
+
108
+ interface DbWorkerInputReset {
109
+ readonly _tag: "reset";
110
+ readonly mnemonic?: Mnemonic;
111
+ }
112
+
113
+ interface DbWorkerInputEnsureSchema {
114
+ readonly _tag: "ensureSchema";
115
+ readonly tables: Tables;
116
+ }
117
+
118
+ type DbWorkerOnMessage = DbWorker["onMessage"];
119
+
120
+ const DbWorkerOnMessage = Context.Tag<DbWorkerOnMessage>(
121
+ "evolu/DbWorkerOnMessage",
122
+ );
123
+
124
+ export type DbWorkerOutput =
125
+ | DbWorkerOutputOnError
126
+ | DbWorkerOutputOnOwner
127
+ | DbWorkerOutputOnQuery
128
+ | DbWorkerOutputOnReceive
129
+ | DbWorkerOutputOnResetOrRestore
130
+ | DbWorkerOutputOnSyncState;
131
+
132
+ interface DbWorkerOutputOnError {
133
+ readonly _tag: "onError";
134
+ readonly error: EvoluError;
135
+ }
136
+
137
+ interface DbWorkerOutputOnOwner {
138
+ readonly _tag: "onOwner";
139
+ readonly owner: Owner;
140
+ }
141
+
142
+ export interface DbWorkerOutputOnQuery {
143
+ readonly _tag: "onQuery";
144
+ readonly queriesPatches: ReadonlyArray<QueryPatches>;
145
+ readonly onCompleteIds: ReadonlyArray<OnCompleteId>;
146
+ }
147
+
148
+ export type OnCompleteId = string &
149
+ Brand.Brand<"Id"> &
150
+ Brand.Brand<"OnComplete">;
151
+
152
+ interface DbWorkerOutputOnReceive {
153
+ readonly _tag: "onReceive";
154
+ }
155
+
156
+ interface DbWorkerOutputOnResetOrRestore {
157
+ readonly _tag: "onResetOrRestore";
158
+ }
159
+
160
+ interface DbWorkerOutputOnSyncState {
161
+ readonly _tag: "onSyncState";
162
+ readonly state: SyncState;
163
+ }
164
+
165
+ export interface MutateItem {
166
+ readonly table: string;
167
+ readonly id: Id;
168
+ readonly values: ReadonlyRecord.ReadonlyRecord<
169
+ Value | Date | boolean | undefined
170
+ >;
171
+ readonly isInsert: boolean;
172
+ readonly now: SqliteDate;
173
+ readonly onCompleteId: OnCompleteId | null;
174
+ }
175
+
176
+ const init = (
177
+ input: DbWorkerInputInit,
178
+ ): Effect.Effect<Sqlite | Bip39 | NanoId, never, Owner> =>
179
+ Effect.gen(function* (_) {
180
+ const sqlite = yield* _(Sqlite);
181
+
182
+ return yield* _(
183
+ sqlite.exec(selectOwner),
184
+ Effect.map(
185
+ ({ rows: [row] }): Owner => ({
186
+ id: row.id as OwnerId,
187
+ mnemonic: row.mnemonic as Mnemonic,
188
+ // expo-sqlite 11.3.2 doesn't support Uint8Array
189
+ encryptionKey: hexToBytes(row.encryptionKey as string),
190
+ }),
191
+ ),
192
+ someDefectToNoSuchTableOrColumnError,
193
+ Effect.catchTag("NoSuchTableOrColumnError", () => lazyInit()),
194
+ Effect.tap(() => ensureSchema(input.tables)),
195
+ );
196
+ });
197
+
198
+ export type RowsCacheMap = ReadonlyMap<Query, ReadonlyArray<Row>>;
199
+
200
+ type RowsCacheRef = Ref.Ref<RowsCacheMap>;
201
+ const RowsCacheRef = Context.Tag<RowsCacheRef>("evolu/RowsCacheRef");
202
+
203
+ const query = ({
204
+ queries,
205
+ onCompleteIds = [],
206
+ }: {
207
+ readonly queries: ReadonlyArray<Query>;
208
+ readonly onCompleteIds?: ReadonlyArray<OnCompleteId>;
209
+ }): Effect.Effect<Sqlite | RowsCacheRef | DbWorkerOnMessage, never, void> =>
210
+ Effect.gen(function* (_) {
211
+ const sqlite = yield* _(Sqlite);
212
+ const rowsCache = yield* _(RowsCacheRef);
213
+ const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
214
+
215
+ const queriesRows = yield* _(
216
+ Effect.forEach(queries, (query) =>
217
+ sqlite
218
+ .exec(queryObjectFromQuery(query))
219
+ .pipe(Effect.map((result) => [query, result.rows] as const)),
220
+ ),
221
+ );
222
+ const previous = yield* _(Ref.get(rowsCache));
223
+ yield* _(Ref.set(rowsCache, new Map([...previous, ...queriesRows])));
224
+
225
+ const queriesPatches = queriesRows.map(
226
+ ([query, rows]): QueryPatches => ({
227
+ query,
228
+ patches: makePatches(previous.get(query), rows),
229
+ }),
230
+ );
231
+
232
+ dbWorkerOnMessage({ _tag: "onQuery", queriesPatches, onCompleteIds });
233
+ });
234
+
235
+ interface TimestampAndMerkleTree {
236
+ readonly timestamp: Timestamp;
237
+ readonly merkleTree: MerkleTree;
238
+ }
239
+
240
+ const readTimestampAndMerkleTree = Sqlite.pipe(
241
+ Effect.flatMap((sqlite) => sqlite.exec(selectOwnerTimestampAndMerkleTree)),
242
+ Effect.map((result) => result.rows),
243
+ Effect.map(
244
+ ([{ timestamp, merkleTree }]): TimestampAndMerkleTree => ({
245
+ timestamp: unsafeTimestampFromString(timestamp as TimestampString),
246
+ merkleTree: merkleTree as MerkleTree,
247
+ }),
248
+ ),
249
+ );
250
+
251
+ const mutateItemsToNewMessages = (
252
+ items: ReadonlyArray.NonEmptyReadonlyArray<MutateItem>,
253
+ ): ReadonlyArray.NonEmptyReadonlyArray<NewMessage> =>
254
+ pipe(
255
+ items,
256
+ ReadonlyArray.mapNonEmpty(({ id, isInsert, now, table, values }) =>
257
+ pipe(
258
+ Object.entries(values),
259
+ ReadonlyArray.filterMap(([key, value]) =>
260
+ // The value can be undefined if exactOptionalPropertyTypes isn't true.
261
+ // Don't insert nulls because null is the default value.
262
+ value === undefined || (isInsert && value == null)
263
+ ? Option.none()
264
+ : Option.some([key, value] as const),
265
+ ),
266
+ ReadonlyArray.map(
267
+ ([key, value]) =>
268
+ [
269
+ key,
270
+ typeof value === "boolean"
271
+ ? cast(value)
272
+ : value instanceof Date
273
+ ? cast(value)
274
+ : value,
275
+ ] as const,
276
+ ),
277
+ ReadonlyArray.append([isInsert ? "createdAt" : "updatedAt", now]),
278
+ ReadonlyArray.mapNonEmpty(
279
+ ([key, value]): NewMessage => ({
280
+ table,
281
+ row: id,
282
+ column: key,
283
+ value,
284
+ }),
285
+ ),
286
+ ),
287
+ ),
288
+ ReadonlyArray.flattenNonEmpty,
289
+ );
290
+
291
+ const ensureSchemaByMessages = (
292
+ messages: ReadonlyArray.NonEmptyReadonlyArray<Message>,
293
+ ): Effect.Effect<Sqlite, never, void> =>
294
+ Effect.gen(function* (_) {
295
+ const tablesMap = new Map<string, Table>();
296
+ messages.forEach((message) => {
297
+ const table = tablesMap.get(message.table);
298
+ if (table == null) {
299
+ tablesMap.set(message.table, {
300
+ name: message.table,
301
+ columns: [message.column],
302
+ });
303
+ return;
304
+ }
305
+ if (table.columns.includes(message.column)) return;
306
+ tablesMap.set(message.table, {
307
+ name: message.table,
308
+ columns: table.columns.concat(message.column),
309
+ });
310
+ });
311
+ yield* _(ensureSchema(Array.from(tablesMap.values())));
312
+ });
313
+
314
+ const applyMessages = ({
315
+ merkleTree,
316
+ messages,
317
+ }: {
318
+ merkleTree: MerkleTree;
319
+ messages: ReadonlyArray.NonEmptyReadonlyArray<Message>;
320
+ }): Effect.Effect<Sqlite, never, MerkleTree> =>
321
+ Effect.gen(function* (_) {
322
+ const sqlite = yield* _(Sqlite);
323
+
324
+ for (const message of messages) {
325
+ const timestamp: TimestampString | null = yield* _(
326
+ sqlite.exec({
327
+ sql: selectLastTimestampForTableRowColumn,
328
+ parameters: [message.table, message.row, message.column],
329
+ }),
330
+ Effect.map((result) => result.rows),
331
+ Effect.flatMap(ReadonlyArray.head),
332
+ Effect.map((row) => row.timestamp as TimestampString),
333
+ Effect.catchTag("NoSuchElementException", () => Effect.succeed(null)),
334
+ );
335
+
336
+ if (timestamp == null || timestamp < message.timestamp) {
337
+ const insert = sqlite.exec({
338
+ sql: insertValueIntoTableRowColumn(message.table, message.column),
339
+ parameters: [message.row, message.value, message.value],
340
+ });
341
+ yield* _(
342
+ insert,
343
+ someDefectToNoSuchTableOrColumnError,
344
+ Effect.catchTag("NoSuchTableOrColumnError", () =>
345
+ ensureSchemaByMessages(messages).pipe(Effect.flatMap(() => insert)),
346
+ ),
347
+ );
348
+ }
349
+
350
+ if (timestamp == null || timestamp !== message.timestamp) {
351
+ const { changes } = yield* _(
352
+ sqlite.exec({
353
+ sql: insertIntoMessagesIfNew,
354
+ parameters: [
355
+ message.timestamp,
356
+ message.table,
357
+ message.row,
358
+ message.column,
359
+ message.value,
360
+ ],
361
+ }),
362
+ );
363
+
364
+ if (changes > 0)
365
+ merkleTree = insertIntoMerkleTree(
366
+ unsafeTimestampFromString(message.timestamp),
367
+ )(merkleTree);
368
+ }
369
+ }
370
+
371
+ return merkleTree;
372
+ });
373
+
374
+ const writeTimestampAndMerkleTree = ({
375
+ timestamp,
376
+ merkleTree,
377
+ }: TimestampAndMerkleTree): Effect.Effect<Sqlite, never, void> =>
378
+ Effect.flatMap(Sqlite, (sqlite) =>
379
+ sqlite.exec({
380
+ sql: updateOwnerTimestampAndMerkleTree,
381
+ parameters: [
382
+ timestampToString(timestamp),
383
+ merkleTreeToString(merkleTree),
384
+ ],
385
+ }),
386
+ );
387
+
388
+ const mutate = ({
389
+ items,
390
+ queries,
391
+ }: DbWorkerInputMutate): Effect.Effect<
392
+ | Sqlite
393
+ | Owner
394
+ | Time
395
+ | Config
396
+ | RowsCacheRef
397
+ | DbWorkerOnMessage
398
+ | SyncWorkerPostMessage,
399
+ TimestampDriftError | TimestampCounterOverflowError,
400
+ void
401
+ > =>
402
+ Effect.gen(function* (_) {
403
+ const owner = yield* _(Owner);
404
+ let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
405
+
406
+ const messages = yield* _(
407
+ mutateItemsToNewMessages(items),
408
+ Effect.forEach((newMessage) =>
409
+ Effect.map(sendTimestamp(timestamp), (nextTimestamp): Message => {
410
+ timestamp = nextTimestamp;
411
+ return {
412
+ ...newMessage,
413
+ timestamp: timestampToString(timestamp),
414
+ };
415
+ }),
416
+ ),
417
+ );
418
+
419
+ if (ReadonlyArray.isNonEmptyReadonlyArray(messages)) {
420
+ merkleTree = yield* _(applyMessages({ merkleTree, messages }));
421
+ yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
422
+ }
423
+
424
+ const onCompleteIds = ReadonlyArray.filterMap(items, (item) =>
425
+ Option.fromNullable(item.onCompleteId),
426
+ );
427
+
428
+ if (queries.length > 0 || onCompleteIds.length > 0)
429
+ yield* _(query({ queries, onCompleteIds }));
430
+
431
+ const [config, syncWorkerPostMessage] = yield* _(
432
+ Effect.all([Config, SyncWorkerPostMessage]),
433
+ );
434
+
435
+ syncWorkerPostMessage({
436
+ _tag: "sync",
437
+ syncUrl: config.syncUrl,
438
+ messages,
439
+ timestamp,
440
+ merkleTree,
441
+ owner,
442
+ syncLoopCount: 0,
443
+ });
444
+ });
445
+
446
+ const handleSyncResponse = ({
447
+ messages,
448
+ ...response
449
+ }: SyncWorkerOutputSyncResponse): Effect.Effect<
450
+ Sqlite | Time | Config | DbWorkerOnMessage | SyncWorkerPostMessage | Owner,
451
+ TimestampError,
452
+ void
453
+ > =>
454
+ Effect.gen(function* (_) {
455
+ let { timestamp, merkleTree } = yield* _(readTimestampAndMerkleTree);
456
+
457
+ if (ReadonlyArray.isNonEmptyReadonlyArray(messages)) {
458
+ for (const message of messages)
459
+ timestamp = yield* _(
460
+ unsafeTimestampFromString(message.timestamp),
461
+ (remote) => receiveTimestamp({ local: timestamp, remote }),
462
+ );
463
+ merkleTree = yield* _(applyMessages({ merkleTree, messages }));
464
+ yield* _(writeTimestampAndMerkleTree({ timestamp, merkleTree }));
465
+ }
466
+
467
+ const dbWorkerOnMessage = yield* _(DbWorkerOnMessage);
468
+ dbWorkerOnMessage({ _tag: "onReceive" });
469
+
470
+ const diff = diffMerkleTrees(response.merkleTree, merkleTree);
471
+
472
+ const syncWorkerPostMessage = yield* _(SyncWorkerPostMessage);
473
+
474
+ if (Option.isNone(diff)) {
475
+ syncWorkerPostMessage({ _tag: "syncCompleted" });
476
+ dbWorkerOnMessage({
477
+ _tag: "onSyncState",
478
+ state: {
479
+ _tag: "SyncStateIsSynced",
480
+ time: yield* _(Time.pipe(Effect.flatMap((time) => time.now))),
481
+ },
482
+ });
483
+ return;
484
+ }
485
+
486
+ const [sqlite, config, owner] = yield* _(
487
+ Effect.all([Sqlite, Config, Owner]),
488
+ );
489
+ const messagesToSync = yield* _(
490
+ sqlite.exec({
491
+ sql: selectMessagesToSync,
492
+ parameters: [timestampToString(makeSyncTimestamp(diff.value))],
493
+ }),
494
+ Effect.map(({ rows }) => rows as unknown as ReadonlyArray<Message>),
495
+ );
496
+ syncWorkerPostMessage({
497
+ _tag: "sync",
498
+ syncUrl: config.syncUrl,
499
+ messages: messagesToSync,
500
+ timestamp,
501
+ merkleTree,
502
+ owner,
503
+ syncLoopCount: response.syncLoopCount + 1,
504
+ });
505
+ });
506
+
507
+ const sync = ({
508
+ queries,
509
+ }: DbWorkerInputSync): Effect.Effect<
510
+ | Sqlite
511
+ | Config
512
+ | DbWorkerOnMessage
513
+ | SyncWorkerPostMessage
514
+ | Owner
515
+ | RowsCacheRef,
516
+ never,
517
+ void
518
+ > =>
519
+ Effect.gen(function* (_) {
520
+ if (queries.length > 0) yield* _(query({ queries }));
521
+
522
+ const [syncWorkerPostMessage, config, owner, timestampAndMerkleTree] =
523
+ yield* _(
524
+ Effect.all([
525
+ SyncWorkerPostMessage,
526
+ Config,
527
+ Owner,
528
+ readTimestampAndMerkleTree,
529
+ ]),
530
+ );
531
+
532
+ syncWorkerPostMessage({
533
+ ...timestampAndMerkleTree,
534
+ _tag: "sync",
535
+ syncUrl: config.syncUrl,
536
+ owner,
537
+ messages: [],
538
+ syncLoopCount: 0,
539
+ });
540
+ });
541
+
542
+ const reset = (
543
+ input: DbWorkerInputReset,
544
+ ): Effect.Effect<Sqlite | Bip39 | NanoId | DbWorkerOnMessage, never, void> =>
545
+ Effect.gen(function* (_) {
546
+ const sqlite = yield* _(Sqlite);
547
+
548
+ yield* _(
549
+ sqlite.exec(`SELECT "name" FROM "sqlite_master" WHERE "type" = 'table'`),
550
+ Effect.map((result) => result.rows),
551
+ Effect.flatMap(
552
+ // The dropped table is completely removed from the database schema and
553
+ // the disk file. The table can not be recovered.
554
+ // All indices and triggers associated with the table are also deleted.
555
+ // https://sqlite.org/lang_droptable.html
556
+ Effect.forEach(
557
+ ({ name }) => sqlite.exec(`DROP TABLE "${name as string}"`),
558
+ { discard: true },
559
+ ),
560
+ ),
561
+ );
562
+
563
+ if (input.mnemonic) yield* _(lazyInit(input.mnemonic));
564
+
565
+ const onMessage = yield* _(DbWorkerOnMessage);
566
+ onMessage({ _tag: "onResetOrRestore" });
567
+ });
568
+
569
+ export const DbWorkerLive = Layer.effect(
570
+ DbWorker,
571
+ Effect.gen(function* (_) {
572
+ const syncWorker = yield* _(SyncWorker);
573
+ const rowsCacheRef = yield* _(Ref.make<RowsCacheMap>(new Map()));
574
+
575
+ const onError = (error: EvoluError): Effect.Effect<never, never, void> =>
576
+ Effect.sync(() => {
577
+ dbWorker.onMessage({ _tag: "onError", error });
578
+ });
579
+
580
+ syncWorker.onMessage = (output): void => {
581
+ switch (output._tag) {
582
+ case "UnexpectedError":
583
+ onError(output).pipe(Effect.runSync);
584
+ break;
585
+ case "SyncWorkerOutputSyncResponse":
586
+ postMessage(output);
587
+ break;
588
+ default:
589
+ dbWorker.onMessage({ _tag: "onSyncState", state: output });
590
+ }
591
+ };
592
+
593
+ const context = Context.empty().pipe(
594
+ Context.add(Sqlite, yield* _(Sqlite)),
595
+ Context.add(Bip39, yield* _(Bip39)),
596
+ Context.add(NanoId, yield* _(NanoId)),
597
+ );
598
+
599
+ const run = (
600
+ effect: Effect.Effect<Sqlite | Bip39 | NanoId, EvoluError, void>,
601
+ ): Promise<void> =>
602
+ effect.pipe(
603
+ Effect.catchAllDefect(makeUnexpectedError),
604
+ transaction,
605
+ Effect.catchAll(onError),
606
+ Effect.provide(context),
607
+ Effect.runPromise,
608
+ );
609
+
610
+ type Write = (input: DbWorkerInput) => Promise<void>;
611
+
612
+ // Write for reset only in case init fails.
613
+ const writeForInitFail: Write = (input): Promise<void> => {
614
+ if (input._tag !== "reset") return Promise.resolve(undefined);
615
+ return reset(input).pipe(
616
+ Effect.provideService(DbWorkerOnMessage, dbWorker.onMessage),
617
+ Effect.provide(context),
618
+ run,
619
+ );
620
+ };
621
+
622
+ const makeWriteForInitSuccess = (owner: Owner, config: Config): Write => {
623
+ let skipAllBecauseOfReset = false;
624
+
625
+ const layer = Layer.mergeAll(
626
+ ConfigLive(config),
627
+ Layer.succeed(DbWorkerOnMessage, dbWorker.onMessage),
628
+ Layer.succeed(Owner, owner),
629
+ Layer.succeed(SyncWorkerPostMessage, syncWorker.postMessage),
630
+ Layer.succeed(RowsCacheRef, rowsCacheRef),
631
+ TimeLive,
632
+ );
633
+
634
+ return (input) => {
635
+ if (skipAllBecauseOfReset) return Promise.resolve(undefined);
636
+
637
+ return Match.value(input).pipe(
638
+ Match.tagsExhaustive({
639
+ init: () =>
640
+ makeUnexpectedError(new Error("init must be called once")),
641
+ query,
642
+ mutate,
643
+ sync,
644
+ reset: (input) => {
645
+ skipAllBecauseOfReset = true;
646
+ return reset(input);
647
+ },
648
+ ensureSchema: (input) => ensureSchema(input.tables),
649
+ SyncWorkerOutputSyncResponse: handleSyncResponse,
650
+ }),
651
+ Effect.provide(layer),
652
+ run,
653
+ );
654
+ };
655
+ };
656
+
657
+ let write: Write = (input) => {
658
+ if (input._tag !== "init")
659
+ return run(makeUnexpectedError(new Error("init must be called first")));
660
+ write = writeForInitFail;
661
+ return init(input).pipe(
662
+ Effect.map((owner) => {
663
+ dbWorker.onMessage({ _tag: "onOwner", owner });
664
+ write = makeWriteForInitSuccess(owner, input.config);
665
+ }),
666
+ run,
667
+ );
668
+ };
669
+
670
+ let messageQueue: Promise<void> = Promise.resolve(undefined);
671
+
672
+ const postMessage: DbWorker["postMessage"] = (input) => {
673
+ messageQueue = messageQueue.then(() => write(input));
674
+ };
675
+
676
+ const dbWorker: DbWorker = {
677
+ postMessage,
678
+ onMessage: Function.constVoid,
679
+ };
680
+
681
+ return dbWorker;
682
+ }),
683
+ );