@evolu/common 3.1.8 → 4.0.1

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 (49) hide show
  1. package/dist/src/Config.d.ts +6 -0
  2. package/dist/src/Config.d.ts.map +1 -1
  3. package/dist/src/Config.js +1 -0
  4. package/dist/src/Crdt.d.ts +2 -2
  5. package/dist/src/Crdt.d.ts.map +1 -1
  6. package/dist/src/Crdt.js +2 -2
  7. package/dist/src/Crypto.d.ts +5 -4
  8. package/dist/src/Crypto.d.ts.map +1 -1
  9. package/dist/src/Crypto.js +2 -2
  10. package/dist/src/Db.d.ts +2 -2
  11. package/dist/src/Db.d.ts.map +1 -1
  12. package/dist/src/Db.js +5 -7
  13. package/dist/src/DbWorker.d.ts +8 -8
  14. package/dist/src/DbWorker.d.ts.map +1 -1
  15. package/dist/src/DbWorker.js +44 -28
  16. package/dist/src/Evolu.d.ts +4 -5
  17. package/dist/src/Evolu.d.ts.map +1 -1
  18. package/dist/src/Evolu.js +10 -13
  19. package/dist/src/OnCompletes.d.ts +3 -3
  20. package/dist/src/OnCompletes.d.ts.map +1 -1
  21. package/dist/src/OnCompletes.js +3 -3
  22. package/dist/src/Platform.d.ts +3 -1
  23. package/dist/src/Platform.d.ts.map +1 -1
  24. package/dist/src/Platform.js +1 -0
  25. package/dist/src/Sql.d.ts +10 -10
  26. package/dist/src/Sql.d.ts.map +1 -1
  27. package/dist/src/Sql.js +48 -47
  28. package/dist/src/Sqlite.d.ts +3 -2
  29. package/dist/src/Sqlite.d.ts.map +1 -1
  30. package/dist/src/Sqlite.js +11 -0
  31. package/dist/src/SyncWorker.d.ts +3 -4
  32. package/dist/src/SyncWorker.d.ts.map +1 -1
  33. package/dist/src/SyncWorker.js +1 -1
  34. package/dist/src/Types.d.ts +13 -0
  35. package/dist/src/Types.d.ts.map +1 -0
  36. package/dist/src/Types.js +1 -0
  37. package/package.json +5 -5
  38. package/src/Config.ts +8 -0
  39. package/src/Crdt.ts +2 -2
  40. package/src/Crypto.ts +11 -7
  41. package/src/Db.ts +9 -11
  42. package/src/DbWorker.ts +54 -44
  43. package/src/Evolu.ts +23 -18
  44. package/src/OnCompletes.ts +4 -4
  45. package/src/Platform.ts +6 -5
  46. package/src/Sql.ts +52 -49
  47. package/src/Sqlite.ts +18 -2
  48. package/src/SyncWorker.ts +4 -6
  49. package/src/Types.ts +11 -0
package/src/DbWorker.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { hexToBytes } from "@noble/ciphers/utils";
2
1
  import * as Context from "effect/Context";
3
2
  import * as Effect from "effect/Effect";
4
3
  import * as Function from "effect/Function";
@@ -11,6 +10,7 @@ import * as ReadonlyRecord from "effect/ReadonlyRecord";
11
10
  import { Config, ConfigLive } from "./Config.js";
12
11
  import {
13
12
  MerkleTree,
13
+ Millis,
14
14
  Time,
15
15
  TimeLive,
16
16
  Timestamp,
@@ -28,7 +28,7 @@ import {
28
28
  timestampToString,
29
29
  unsafeTimestampFromString,
30
30
  } from "./Crdt.js";
31
- import { Bip39, Mnemonic, NanoId } from "./Crypto.js";
31
+ import { Bip39, Mnemonic, NanoIdGenerator } from "./Crypto.js";
32
32
  import {
33
33
  Queries,
34
34
  Query,
@@ -44,9 +44,10 @@ import {
44
44
  } from "./Db.js";
45
45
  import { QueryPatches, makePatches } from "./Diff.js";
46
46
  import { EvoluError, makeUnexpectedError } from "./ErrorStore.js";
47
- import { Id, SqliteDate, cast } from "./Model.js";
47
+ import { Id, cast } from "./Model.js";
48
48
  import { OnCompleteId } from "./OnCompletes.js";
49
49
  import { Owner, OwnerId } from "./Owner.js";
50
+ import { DbWorkerLock } from "./Platform.js";
50
51
  import * as Sql from "./Sql.js";
51
52
  import { Sqlite, Value } from "./Sqlite.js";
52
53
  import {
@@ -58,12 +59,9 @@ import {
58
59
  SyncWorkerOutputSyncResponse,
59
60
  SyncWorkerPostMessage,
60
61
  } from "./SyncWorker.js";
62
+ import { Messaging } from "./Types.js";
61
63
 
62
- export interface DbWorker {
63
- readonly postMessage: (input: DbWorkerInput) => void;
64
- onMessage: (output: DbWorkerOutput) => void;
65
- }
66
-
64
+ export interface DbWorker extends Messaging<DbWorkerInput, DbWorkerOutput> {}
67
65
  export const DbWorker = Context.GenericTag<DbWorker>("@services/DbWorker");
68
66
 
69
67
  export type DbWorkerInput =
@@ -157,7 +155,6 @@ export interface Mutation {
157
155
  Value | Date | boolean | undefined
158
156
  >;
159
157
  readonly isInsert: boolean;
160
- readonly now: SqliteDate;
161
158
  readonly onCompleteId: OnCompleteId | null;
162
159
  }
163
160
 
@@ -170,8 +167,7 @@ const init = Effect.gen(function* (_) {
170
167
  ({ rows: [row] }): Owner => ({
171
168
  id: row.id as OwnerId,
172
169
  mnemonic: row.mnemonic as Mnemonic,
173
- // expo-sqlite 11.3.2 doesn't support Uint8Array
174
- encryptionKey: hexToBytes(row.encryptionKey as string),
170
+ encryptionKey: row.encryptionKey as Uint8Array,
175
171
  }),
176
172
  ),
177
173
  someDefectToNoSuchTableOrColumnError,
@@ -222,9 +218,8 @@ const readTimestampAndMerkleTree = Sqlite.pipe(
222
218
  Effect.flatMap((sqlite) =>
223
219
  sqlite.exec(Sql.selectOwnerTimestampAndMerkleTree),
224
220
  ),
225
- Effect.map((result) => result.rows),
226
221
  Effect.map(
227
- ([{ timestamp, merkleTree }]): TimestampAndMerkleTree => ({
222
+ ({ rows: [{ timestamp, merkleTree }] }): TimestampAndMerkleTree => ({
228
223
  timestamp: unsafeTimestampFromString(timestamp as TimestampString),
229
224
  merkleTree: merkleTree as MerkleTree,
230
225
  }),
@@ -236,9 +231,10 @@ export const mutationsToNewMessages = (
236
231
  ): ReadonlyArray<NewMessage> =>
237
232
  pipe(
238
233
  mutations,
239
- ReadonlyArray.map(({ id, isInsert, now, table, values }) =>
234
+ ReadonlyArray.map(({ id, isInsert, table, values }) =>
240
235
  pipe(
241
236
  Object.entries(values),
237
+ // Filter values.
242
238
  ReadonlyArray.filterMap(([key, value]) =>
243
239
  // The value can be undefined if exactOptionalPropertyTypes isn't true.
244
240
  // Don't insert nulls because null is the default value.
@@ -246,6 +242,7 @@ export const mutationsToNewMessages = (
246
242
  ? Option.none()
247
243
  : Option.some([key, value] as const),
248
244
  ),
245
+ // Cast values.
249
246
  ReadonlyArray.map(
250
247
  ([key, value]) =>
251
248
  [
@@ -257,7 +254,6 @@ export const mutationsToNewMessages = (
257
254
  : value,
258
255
  ] as const,
259
256
  ),
260
- ReadonlyArray.append([isInsert ? "createdAt" : "updatedAt", now]),
261
257
  ReadonlyArray.map(
262
258
  ([key, value]): NewMessage => ({
263
259
  table,
@@ -282,7 +278,7 @@ const ensureSchemaByNewMessages = (
282
278
  if (table == null) {
283
279
  tablesMap.set(message.table, {
284
280
  name: message.table,
285
- columns: [message.column],
281
+ columns: [message.column, "createdAt", "updatedAt"],
286
282
  });
287
283
  return;
288
284
  }
@@ -298,13 +294,21 @@ const ensureSchemaByNewMessages = (
298
294
  export const upsertValueIntoTableRowColumn = (
299
295
  message: NewMessage,
300
296
  messages: ReadonlyArray<NewMessage>,
297
+ millis: Millis,
301
298
  ): Effect.Effect<void, never, Sqlite> =>
302
299
  Effect.gen(function* (_) {
303
300
  const sqlite = yield* _(Sqlite);
304
-
301
+ const createdAtOrUpdatedAt = cast(new Date(millis));
305
302
  const insert = sqlite.exec({
306
303
  sql: Sql.upsertValueIntoTableRowColumn(message.table, message.column),
307
- parameters: [message.row, message.value, message.value],
304
+ parameters: [
305
+ message.row,
306
+ message.value,
307
+ createdAtOrUpdatedAt,
308
+ createdAtOrUpdatedAt,
309
+ message.value,
310
+ createdAtOrUpdatedAt,
311
+ ],
308
312
  });
309
313
 
310
314
  yield* _(
@@ -340,7 +344,8 @@ const applyMessages = ({
340
344
  );
341
345
 
342
346
  if (timestamp == null || timestamp < message.timestamp) {
343
- yield* _(upsertValueIntoTableRowColumn(message, messages));
347
+ const { millis } = unsafeTimestampFromString(message.timestamp);
348
+ yield* _(upsertValueIntoTableRowColumn(message, messages, millis));
344
349
  }
345
350
 
346
351
  if (timestamp == null || timestamp !== message.timestamp) {
@@ -406,11 +411,11 @@ const mutate = ({
406
411
  ),
407
412
  ).map(mutationsToNewMessages);
408
413
 
409
- yield* _(
410
- Effect.forEach(toUpsert, (message) =>
411
- upsertValueIntoTableRowColumn(message, toUpsert),
412
- ),
413
- );
414
+ const time = yield* _(Time);
415
+ for (const messageToUpsert of toUpsert) {
416
+ const now = yield* _(time.now);
417
+ yield* _(upsertValueIntoTableRowColumn(messageToUpsert, toUpsert, now));
418
+ }
414
419
 
415
420
  const { exec } = yield* _(Sqlite);
416
421
  yield* _(
@@ -537,8 +542,8 @@ const sync = ({
537
542
  > =>
538
543
  Effect.gen(function* (_) {
539
544
  if (queries.length > 0) yield* _(query({ queries }));
540
-
541
- (yield* _(SyncWorkerPostMessage))({
545
+ const syncWorkerPostMessage = yield* _(SyncWorkerPostMessage);
546
+ syncWorkerPostMessage({
542
547
  _tag: "sync",
543
548
  ...(yield* _(readTimestampAndMerkleTree)),
544
549
  syncUrl: (yield* _(Config)).syncUrl,
@@ -550,7 +555,11 @@ const sync = ({
550
555
 
551
556
  const reset = (
552
557
  input: DbWorkerInputReset,
553
- ): Effect.Effect<void, never, Sqlite | Bip39 | NanoId | DbWorkerOnMessage> =>
558
+ ): Effect.Effect<
559
+ void,
560
+ never,
561
+ Sqlite | Bip39 | NanoIdGenerator | DbWorkerOnMessage
562
+ > =>
554
563
  Effect.gen(function* (_) {
555
564
  const sqlite = yield* _(Sqlite);
556
565
 
@@ -575,7 +584,7 @@ const reset = (
575
584
  onMessage({ _tag: "onResetOrRestore" });
576
585
  });
577
586
 
578
- export const DbWorkerLive = Layer.effect(
587
+ export const DbWorkerCommonLive = Layer.effect(
579
588
  DbWorker,
580
589
  Effect.gen(function* (_) {
581
590
  const syncWorker = yield* _(SyncWorker);
@@ -591,7 +600,7 @@ export const DbWorkerLive = Layer.effect(
591
600
  onError(output).pipe(Effect.runSync);
592
601
  break;
593
602
  case "SyncWorkerOutputSyncResponse":
594
- postMessage(output);
603
+ dbWorker.postMessage(output);
595
604
  break;
596
605
  default:
597
606
  dbWorker.onMessage({ _tag: "onSyncState", state: output });
@@ -601,7 +610,7 @@ export const DbWorkerLive = Layer.effect(
601
610
  const runContext = Context.empty().pipe(
602
611
  Context.add(Sqlite, yield* _(Sqlite)),
603
612
  Context.add(Bip39, yield* _(Bip39)),
604
- Context.add(NanoId, yield* _(NanoId)),
613
+ Context.add(NanoIdGenerator, yield* _(NanoIdGenerator)),
605
614
  Context.add(DbWorkerOnMessage, (output) => {
606
615
  dbWorker.onMessage(output);
607
616
  }),
@@ -611,7 +620,7 @@ export const DbWorkerLive = Layer.effect(
611
620
  effect: Effect.Effect<
612
621
  void,
613
622
  EvoluError,
614
- Sqlite | Bip39 | NanoId | DbWorkerOnMessage
623
+ Sqlite | Bip39 | NanoIdGenerator | DbWorkerOnMessage
615
624
  >,
616
625
  ): Promise<void> =>
617
626
  effect.pipe(
@@ -622,15 +631,18 @@ export const DbWorkerLive = Layer.effect(
622
631
  Effect.runPromise,
623
632
  );
624
633
 
625
- type Write = (input: DbWorkerInput) => Promise<void>;
634
+ type HandleInput = (input: DbWorkerInput) => Promise<void>;
626
635
 
627
- // Write for reset only in case init fails.
628
- const writeForInitFail: Write = (input): Promise<void> => {
636
+ /** If init fails, we have to allow reset at least. */
637
+ const handleInputForInitFail: HandleInput = (input): Promise<void> => {
629
638
  if (input._tag !== "reset") return Promise.resolve(undefined);
630
639
  return reset(input).pipe(run);
631
640
  };
632
641
 
633
- const makeWriteForInitSuccess = (config: Config, owner: Owner): Write => {
642
+ const makeHandleInputForInitSuccess = (
643
+ config: Config,
644
+ owner: Owner,
645
+ ): HandleInput => {
634
646
  let skipAllBecauseOfReset = false;
635
647
 
636
648
  const layer = Layer.mergeAll(
@@ -663,27 +675,25 @@ export const DbWorkerLive = Layer.effect(
663
675
  };
664
676
  };
665
677
 
666
- let write: Write = (input) => {
678
+ let handleInput: HandleInput = (input) => {
667
679
  if (input._tag !== "init")
668
680
  return run(makeUnexpectedError(new Error("init must be called first")));
669
- write = writeForInitFail;
681
+ handleInput = handleInputForInitFail;
670
682
  return init.pipe(
671
683
  Effect.map((owner) => {
672
684
  dbWorker.onMessage({ _tag: "onOwner", owner });
673
- write = makeWriteForInitSuccess(input.config, owner);
685
+ handleInput = makeHandleInputForInitSuccess(input.config, owner);
674
686
  }),
675
687
  run,
676
688
  );
677
689
  };
678
690
 
679
- let messageQueue: Promise<void> = Promise.resolve(undefined);
680
-
681
- const postMessage: DbWorker["postMessage"] = (input) => {
682
- messageQueue = messageQueue.then(() => write(input));
683
- };
691
+ const dbWorkerLock = yield* _(DbWorkerLock);
684
692
 
685
693
  const dbWorker: DbWorker = {
686
- postMessage,
694
+ postMessage: (input) => {
695
+ dbWorkerLock(() => handleInput(input));
696
+ },
687
697
  onMessage: Function.constVoid,
688
698
  };
689
699
 
package/src/Evolu.ts CHANGED
@@ -10,8 +10,7 @@ import * as Number from "effect/Number";
10
10
  import * as ReadonlyArray from "effect/ReadonlyArray";
11
11
  import * as Kysely from "kysely";
12
12
  import { Config, ConfigLive } from "./Config.js";
13
- import { Time, TimeLive } from "./Crdt.js";
14
- import { Mnemonic, NanoId, NanoIdLive } from "./Crypto.js";
13
+ import { Mnemonic, NanoIdGenerator, NanoIdGeneratorLive } from "./Crypto.js";
15
14
  import {
16
15
  DatabaseSchema,
17
16
  Queries,
@@ -29,11 +28,11 @@ import {
29
28
  import { DbWorker, DbWorkerOutputOnQuery, Mutation } from "./DbWorker.js";
30
29
  import { applyPatches } from "./Diff.js";
31
30
  import { EvoluError, makeErrorStore } from "./ErrorStore.js";
32
- import { SqliteBoolean, SqliteDate, cast } from "./Model.js";
31
+ import { SqliteBoolean, SqliteDate } from "./Model.js";
33
32
  import { OnCompletes, OnCompletesLive } from "./OnCompletes.js";
34
33
  import { Owner } from "./Owner.js";
35
34
  import { AppState, FlushSync, PlatformName } from "./Platform.js";
36
- import { SqliteQuery } from "./Sqlite.js";
35
+ import { SqliteQuery, isSqlMutation } from "./Sqlite.js";
37
36
  import { Store, Unsubscribe, makeStore } from "./Store.js";
38
37
  import { SyncState } from "./SyncWorker.js";
39
38
 
@@ -319,17 +318,22 @@ type CreateQuery<S extends DatabaseSchema> = <R extends Row>(
319
318
  ) => Query<R>;
320
319
 
321
320
  type QueryCallback<S extends DatabaseSchema, R extends Row> = (
322
- db: Pick<Kysely.Kysely<QuerySchema<S>>, "selectFrom" | "fn">,
321
+ db: Pick<
322
+ Kysely.Kysely<QuerySchema<S>>,
323
+ "selectFrom" | "fn" | "with" | "withRecursive"
324
+ >,
323
325
  ) => Kysely.SelectQueryBuilder<any, any, R>;
324
326
 
325
327
  type QuerySchema<S extends DatabaseSchema> = {
326
- readonly [Table in keyof S]: NullableExceptId<{
328
+ readonly [Table in keyof S]: NullableExceptForIdAndAutomaticColumns<{
327
329
  readonly [Column in keyof S[Table]]: S[Table][Column];
328
330
  }>;
329
331
  };
330
332
 
331
- type NullableExceptId<T> = {
332
- readonly [K in keyof T]: K extends "id" ? T[K] : T[K] | null;
333
+ type NullableExceptForIdAndAutomaticColumns<T> = {
334
+ readonly [K in keyof T]: K extends "id" | "createdAt" | "updatedAt"
335
+ ? T[K]
336
+ : T[K] | null;
333
337
  };
334
338
 
335
339
  const kysely = new Kysely.Kysely<QuerySchema<DatabaseSchema>>({
@@ -349,10 +353,14 @@ export const makeCreateQuery =
349
353
  <R extends Row>(queryCallback: QueryCallback<S, R>) =>
350
354
  pipe(
351
355
  queryCallback(kysely as Kysely.Kysely<QuerySchema<S>>).compile(),
352
- ({ sql, parameters }): SqliteQuery => ({
353
- sql,
354
- parameters: parameters as SqliteQuery["parameters"],
355
- }),
356
+ ({ sql, parameters }): SqliteQuery => {
357
+ if (isSqlMutation(sql))
358
+ throw new Error(
359
+ "SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu mutation API.",
360
+ );
361
+
362
+ return { sql, parameters: parameters as SqliteQuery["parameters"] };
363
+ },
356
364
  (query) => serializeQuery<R>(query),
357
365
  );
358
366
 
@@ -643,9 +651,8 @@ type Castable<T> = {
643
651
  const MutateLive = Layer.effect(
644
652
  Mutate,
645
653
  Effect.gen(function* (_) {
646
- const nanoid = yield* _(NanoId);
654
+ const { nanoid } = yield* _(NanoIdGenerator);
647
655
  const onCompletes = yield* _(OnCompletes);
648
- const time = yield* _(Time);
649
656
  const subscribedQueries = yield* _(SubscribedQueries);
650
657
  const loadingPromises = yield* _(LoadingPromises);
651
658
  const dbWorker = yield* _(DbWorker);
@@ -653,7 +660,7 @@ const MutateLive = Layer.effect(
653
660
 
654
661
  return Mutate.of((table, { id, ...values }, onComplete) => {
655
662
  const isInsert = id == null;
656
- if (isInsert) id = Effect.runSync(nanoid.nanoid) as never;
663
+ if (isInsert) id = Effect.runSync(nanoid) as never;
657
664
 
658
665
  const onCompleteId = onComplete
659
666
  ? onCompletes.add(onComplete).pipe(Effect.runSync)
@@ -666,7 +673,6 @@ const MutateLive = Layer.effect(
666
673
  id,
667
674
  values,
668
675
  isInsert,
669
- now: cast(new Date(Effect.runSync(time.now))),
670
676
  onCompleteId,
671
677
  },
672
678
  ];
@@ -785,9 +791,8 @@ const EvoluCommon = Layer.effect(
785
791
  Layer.provide(Layer.merge(RowsStoreLive, OnCompletesLive)),
786
792
  );
787
793
 
788
- /** EvoluCommonLive has only platform independent side-effects. */
789
794
  export const EvoluCommonLive = EvoluCommon.pipe(
790
- Layer.provide(Layer.merge(TimeLive, NanoIdLive)),
795
+ Layer.provide(NanoIdGeneratorLive),
791
796
  );
792
797
 
793
798
  /**
@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4
4
  import * as Layer from "effect/Layer";
5
5
  import * as Option from "effect/Option";
6
6
  import * as ReadonlyArray from "effect/ReadonlyArray";
7
- import { NanoId } from "./Crypto.js";
7
+ import { NanoId, NanoIdGenerator } from "./Crypto.js";
8
8
 
9
9
  export interface OnCompletes {
10
10
  readonly add: (onComplete: OnComplete) => Effect.Effect<OnCompleteId>;
@@ -16,7 +16,7 @@ export interface OnCompletes {
16
16
 
17
17
  export type OnComplete = () => void;
18
18
 
19
- export type OnCompleteId = string & Brand.Brand<"OnCompleteId">;
19
+ export type OnCompleteId = NanoId & Brand.Brand<"OnCompleteId">;
20
20
 
21
21
  export const OnCompletes = Context.GenericTag<OnCompletes>(
22
22
  "@services/OnCompletes",
@@ -25,12 +25,12 @@ export const OnCompletes = Context.GenericTag<OnCompletes>(
25
25
  export const OnCompletesLive = Layer.effect(
26
26
  OnCompletes,
27
27
  Effect.gen(function* (_) {
28
- const nanoid = yield* _(NanoId);
28
+ const { nanoid } = yield* _(NanoIdGenerator);
29
29
  const map = new Map<OnCompleteId, OnComplete>();
30
30
 
31
31
  return OnCompletes.of({
32
32
  add: (onComplete) =>
33
- nanoid.nanoid.pipe(
33
+ nanoid.pipe(
34
34
  Effect.map((nanoid) => {
35
35
  const id = nanoid as OnCompleteId;
36
36
  map.set(id, onComplete);
package/src/Platform.ts CHANGED
@@ -2,11 +2,7 @@ import * as Context from "effect/Context";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Layer from "effect/Layer";
4
4
 
5
- export type PlatformName =
6
- | "server"
7
- | "web-with-opfs"
8
- | "web-without-opfs"
9
- | "react-native";
5
+ export type PlatformName = "server" | "web" | "react-native";
10
6
 
11
7
  export const PlatformName = Context.GenericTag<PlatformName>(
12
8
  "@services/PlatformName",
@@ -40,6 +36,11 @@ export interface SyncLock {
40
36
 
41
37
  export const SyncLock = Context.GenericTag<SyncLock>("@services/SyncLock");
42
38
 
39
+ export type DbWorkerLock = (callback: () => Promise<void>) => void;
40
+ export const DbWorkerLock = Context.GenericTag<DbWorkerLock>(
41
+ "@services/DbWorkerLock",
42
+ );
43
+
43
44
  export type Fetch = (
44
45
  url: string,
45
46
  body: Uint8Array,
package/src/Sql.ts CHANGED
@@ -3,33 +3,33 @@
3
3
  // [Playground Link](https://kyse.link/?p=b&i=haFkpnNxbGl0ZaF2pjAuMjYuMKFz2gFoaW1wb3J0IHsgR2VuZXJhdGVkIH0gZnJvbSAna3lzZWx5JwoKZGVjbGFyZSBnbG9iYWwgewogIGludGVyZmFjZSBEQiB7CiAgICBldm9sdV9tZXNzYWdlOiB7CiAgICAgIHRpbWVzdGFtcDogc3RyaW5nLAogICAgICB0YWJsZTogc3RyaW5nLAogICAgICByb3c6IHN0cmluZywKICAgICAgY29sdW1uOiBzdHJpbmcsCiAgICAgIHZhbHVlOiB1bmtub3duCiAgICB9LAoKICAgIGV2b2x1X293bmVyOiB7CiAgICAgIGlkOiBzdHJpbmcKICAgICAgbW5lbW9uaWM6IHN0cmluZwogICAgICBlbmNyeXB0aW9uS2V5OiBVaW50OEFycmF5LAogICAgICB0aW1lc3RhbXA6IHN0cmluZywKICAgICAgbWVya2xlVHJlZTogc3RyaW5nCiAgICB9CiAgfQp9oXHaBnhhd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJpZCIsICJtbmVtb25pYyIsICJlbmNyeXB0aW9uS2V5Il0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNjaGVtYQogIC5jcmVhdGVUYWJsZSgnZXZvbHVfbWVzc2FnZScpCiAgLmFkZENvbHVtbigndGltZXN0YW1wJywgJ2Jsb2InLCBjb2wgPT4gY29sLnByaW1hcnlLZXkoKSkKICAuYWRkQ29sdW1uKCd0YWJsZScsICdibG9iJykKICAuYWRkQ29sdW1uKCdyb3cnLCAnYmxvYicpCiAgLmFkZENvbHVtbignY29sdW1uJywgJ2Jsb2InKQogIC5hZGRDb2x1bW4oJ3ZhbHVlJywgJ2Jsb2InKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlSW5kZXgoImluZGV4X2V2b2x1X21lc3NhZ2UiKQogIC5vbigiZXZvbHVfbWVzc2FnZSIpCiAgLmNvbHVtbnMoWyJ0YWJsZSIsICJyb3ciLCAiY29sdW1uIiwgInRpbWVzdGFtcCJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlVGFibGUoJ2V2b2x1X19vd25lcicpCiAgLmFkZENvbHVtbignaWQnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbW5lbW9uaWMnLCAnYmxvYicpCiAgLmFkZENvbHVtbignZW5jcnlwdGlvbktleScsICdibG9iJykKICAuYWRkQ29sdW1uKCd0aW1lc3RhbXAnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbWVya2xlVHJlZScsICdibG9iJykKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfb3duZXIiKQogIC52YWx1ZXMoewogICAgImlkIjogImIiLAogICAgIm1uZW1vbmljIjogImEiLAogICAgImVuY3J5cHRpb25LZXkiOiBuZXcgVWludDhBcnJheSgpLAogICAgInRpbWVzdGFtcCI6ICJhIiwKICAgICJtZXJrbGVUcmVlIjogImIiCiAgfSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJ0aW1lc3RhbXAiLCAibWVya2xlVHJlZSJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zZWxlY3RGcm9tKCJldm9sdV9tZXNzYWdlIikKICAuc2VsZWN0KCJ0aW1lc3RhbXAiKQogIC53aGVyZSgndGFibGUnLCAnPScsICcxJykKICAud2hlcmUoJ3JvdycsICc9JywgJzInKQogIC53aGVyZSgnY29sdW1uJywgJz0nLCAnMycpCiAgLm9yZGVyQnkoInRpbWVzdGFtcCIsICJkZXNjIikKICAubGltaXQoMSkKICAuZXhlY3V0ZVRha2VGaXJzdCgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfbWVzc2FnZSIpCiAgLnZhbHVlcyh7CiAgICAidGltZXN0YW1wIjogJzEnLAogICAgInRhYmxlIjogJzInLAogICAgInJvdyI6ICczJywKICAgICJjb2x1bW4iOiAnNCcsCiAgICAidmFsdWUiOiAnNScsCiAgfSkKICAub25Db25mbGljdChvYyA9PiBvYy5kb05vdGhpbmcoKSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkudXBkYXRlVGFibGUoImV2b2x1X293bmVyIikKICAuc2V0KHsKICAgICJtZXJrbGVUcmVlIjogJzEnLAogICAgInRpbWVzdGFtcCI6ICIyIgogIH0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNlbGVjdEZyb20oImV2b2x1X21lc3NhZ2UiKQogIC5zZWxlY3RBbGwoKQogIC53aGVyZSgidGltZXN0YW1wIiwgIj49IiwgJzEnKQogIC5vcmRlckJ5KCJ0aW1lc3RhbXAiKQogIC5leGVjdXRlKCmhY8M=)
4
4
 
5
5
  export const selectOwner = `
6
- SELECT
6
+ select
7
7
  "id",
8
8
  "mnemonic",
9
9
  "encryptionKey"
10
- FROM
10
+ from
11
11
  "evolu_owner"
12
- `;
12
+ `.trim();
13
13
 
14
14
  export const createMessageTable = `
15
- CREATE TABLE
15
+ create table
16
16
  "evolu_message" (
17
- "timestamp" blob PRIMARY KEY,
17
+ "timestamp" blob primary key,
18
18
  "table" blob,
19
19
  "row" blob,
20
20
  "column" blob,
21
21
  "value" blob
22
22
  );
23
- `;
23
+ `.trim();
24
24
 
25
25
  export const createMessageTableIndex = `
26
- CREATE INDEX "index_evolu_message" ON "evolu_message" (
26
+ create index "index_evolu_message" on "evolu_message" (
27
27
  "table", "row", "column", "timestamp"
28
28
  );
29
- `;
29
+ `.trim();
30
30
 
31
31
  export const createOwnerTable = `
32
- CREATE TABLE
32
+ create table
33
33
  "evolu_owner" (
34
34
  "id" blob,
35
35
  "mnemonic" blob,
@@ -37,10 +37,10 @@ CREATE TABLE
37
37
  "timestamp" blob,
38
38
  "merkleTree" blob
39
39
  );
40
- `;
40
+ `.trim();
41
41
 
42
42
  export const insertOwner = `
43
- INSERT INTO
43
+ insert into
44
44
  "evolu_owner" (
45
45
  "id",
46
46
  "mnemonic",
@@ -48,73 +48,76 @@ INSERT INTO
48
48
  "timestamp",
49
49
  "merkleTree"
50
50
  )
51
- VALUES
51
+ values
52
52
  (?, ?, ?, ?, ?);
53
- `;
53
+ `.trim();
54
54
 
55
55
  export const selectOwnerTimestampAndMerkleTree = `
56
- SELECT
56
+ select
57
57
  "timestamp",
58
58
  "merkleTree"
59
- FROM
59
+ from
60
60
  "evolu_owner"
61
- `;
61
+ `.trim();
62
62
 
63
63
  export const selectLastTimestampForTableRowColumn = `
64
- SELECT
64
+ select
65
65
  "timestamp"
66
- FROM
66
+ from
67
67
  "evolu_message"
68
- WHERE
68
+ where
69
69
  "table" = ?
70
- AND "row" = ?
71
- AND "column" = ?
72
- ORDER BY
73
- "timestamp" DESC
74
- LIMIT
70
+ and "row" = ?
71
+ and "column" = ?
72
+ order by
73
+ "timestamp" desc
74
+ limit
75
75
  1
76
- `;
76
+ `.trim();
77
77
 
78
78
  export const upsertValueIntoTableRowColumn = (
79
79
  table: string,
80
80
  column: string,
81
- ): string => `
82
- INSERT INTO
83
- "${table}" ("id", "${column}")
84
- VALUES
85
- (?, ?)
86
- ON CONFLICT DO UPDATE SET
87
- "${column}" = ?
88
- `;
81
+ ): string =>
82
+ `
83
+ insert into
84
+ "${table}" ("id", "${column}", "createdAt", "updatedAt")
85
+ values
86
+ (?, ?, ?, ?)
87
+ on conflict do update set
88
+ "${column}" = ?,
89
+ "updatedAt" = ?
90
+ `.trim();
89
91
 
90
- export const deleteTableRow = (table: string): string => `
91
- DELETE FROM "${table}"
92
- WHERE
92
+ export const deleteTableRow = (table: string): string =>
93
+ `
94
+ delete from "${table}"
95
+ where
93
96
  "id" = ?;
94
- `;
97
+ `.trim();
95
98
 
96
99
  export const insertIntoMessagesIfNew = `
97
- INSERT INTO
100
+ insert into
98
101
  "evolu_message" ("timestamp", "table", "row", "column", "value")
99
- VALUES
102
+ values
100
103
  (?, ?, ?, ?, ?)
101
- ON CONFLICT DO NOTHING
102
- `;
104
+ on conflict do nothing
105
+ `.trim();
103
106
 
104
107
  export const updateOwnerTimestampAndMerkleTree = `
105
- UPDATE "evolu_owner"
106
- SET
108
+ update "evolu_owner"
109
+ set
107
110
  "timestamp" = ?,
108
111
  "merkleTree" = ?
109
- `;
112
+ `.trim();
110
113
 
111
114
  export const selectMessagesToSync = `
112
- SELECT
115
+ select
113
116
  *
114
- FROM
117
+ from
115
118
  "evolu_message"
116
- WHERE
119
+ where
117
120
  "timestamp" >= ?
118
- ORDER BY
121
+ order by
119
122
  "timestamp"
120
- `;
123
+ `.trim();
package/src/Sqlite.ts CHANGED
@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
3
3
  import * as Predicate from "effect/Predicate";
4
4
 
5
5
  export interface Sqlite {
6
- readonly exec: (arg: string | SqliteQuery) => Effect.Effect<ExecResult>;
6
+ readonly exec: (arg: string | SqliteQuery) => Effect.Effect<SqliteExecResult>;
7
7
  }
8
8
 
9
9
  export const Sqlite = Context.GenericTag<Sqlite>("@services/Sqlite");
@@ -21,7 +21,7 @@ type JsonArray = ReadonlyArray<Json>;
21
21
  type JsonPrimitive = string | number | boolean | null;
22
22
  type Json = JsonPrimitive | JsonObject | JsonArray;
23
23
 
24
- interface ExecResult {
24
+ export interface SqliteExecResult {
25
25
  readonly rows: SqliteRow[];
26
26
  readonly changes: number;
27
27
  }
@@ -80,3 +80,19 @@ const parseObject = (o: Record<string, unknown>): Record<string, unknown> => {
80
80
  for (const key in o) o[key] = parse(o[key]);
81
81
  return o;
82
82
  };
83
+
84
+ /** This is good enough detection because SQL strings in Evolu are predictable. */
85
+ const isSqlMutationRegEx = new RegExp(
86
+ `\\b(${[
87
+ "alter",
88
+ "create",
89
+ "delete",
90
+ "drop",
91
+ "insert",
92
+ "replace",
93
+ "update",
94
+ ].join("|")})\\b`,
95
+ );
96
+
97
+ export const isSqlMutation = (sql: string): boolean =>
98
+ isSqlMutationRegEx.test(sql);
package/src/SyncWorker.ts CHANGED
@@ -31,12 +31,10 @@ import {
31
31
  SyncResponse,
32
32
  } from "./Protobuf.js";
33
33
  import { JsonObjectOrArray, Value } from "./Sqlite.js";
34
+ import { Messaging } from "./Types.js";
34
35
 
35
- export interface SyncWorker {
36
- readonly postMessage: (input: SyncWorkerInput) => void;
37
- onMessage: (output: SyncWorkerOutput) => void;
38
- }
39
-
36
+ export interface SyncWorker
37
+ extends Messaging<SyncWorkerInput, SyncWorkerOutput> {}
40
38
  export const SyncWorker = Context.GenericTag<SyncWorker>(
41
39
  "@services/SyncWorker",
42
40
  );
@@ -325,7 +323,7 @@ const sync = (
325
323
  );
326
324
  });
327
325
 
328
- export const SyncWorkerLive = Layer.effect(
326
+ export const SyncWorkerCommonLive = Layer.effect(
329
327
  SyncWorker,
330
328
  Effect.gen(function* (_) {
331
329
  const syncLock = yield* _(SyncLock);