@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
package/src/Db.ts CHANGED
@@ -1,206 +1,160 @@
1
- import * as AST from "@effect/schema/AST";
2
1
  import * as S from "@effect/schema/Schema";
3
- import { make } from "@effect/schema/Schema";
2
+ import * as Arr from "effect/Array";
4
3
  import * as Brand from "effect/Brand";
4
+ import * as Console from "effect/Console";
5
5
  import * as Context from "effect/Context";
6
+ import * as Deferred from "effect/Deferred";
6
7
  import * as Effect from "effect/Effect";
7
- import * as Exit from "effect/Exit";
8
- import { pipe } from "effect/Function";
9
- import * as Layer from "effect/Layer";
8
+ import { Equivalence } from "effect/Equivalence";
9
+ import { constVoid, pipe } from "effect/Function";
10
10
  import * as Option from "effect/Option";
11
11
  import * as Predicate from "effect/Predicate";
12
- import * as ReadonlyArray from "effect/ReadonlyArray";
13
- import * as ReadonlyRecord from "effect/ReadonlyRecord";
12
+ import * as Record from "effect/Record";
13
+ import * as Ref from "effect/Ref";
14
+ import * as Schedule from "effect/Schedule";
14
15
  import * as String from "effect/String";
15
- import * as Types from "effect/Types";
16
+ import * as SynchronizedRef from "effect/SynchronizedRef";
16
17
  import * as Kysely from "kysely";
18
+ import { Config } from "./Config.js";
17
19
  import {
20
+ MerkleTree,
21
+ Millis,
22
+ Time,
23
+ Timestamp,
24
+ TimestampCounterOverflowError,
25
+ TimestampDriftError,
26
+ TimestampString,
27
+ TimestampTimeOutOfRangeError,
28
+ diffMerkleTrees,
18
29
  initialMerkleTree,
30
+ insertIntoMerkleTree,
19
31
  makeInitialTimestamp,
32
+ makeSyncTimestamp,
20
33
  merkleTreeToString,
34
+ receiveTimestamp,
35
+ sendTimestamp,
21
36
  timestampToString,
37
+ unsafeTimestampFromString,
22
38
  } from "./Crdt.js";
23
39
  import { Bip39, Mnemonic, NanoIdGenerator } from "./Crypto.js";
24
- import { EvoluTypeError } from "./ErrorStore.js";
25
- import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
26
- import { Owner, makeOwner } from "./Owner.js";
40
+ import { QueryPatches, makePatches } from "./Diff.js";
27
41
  import {
28
- createMessageTable,
29
- createMessageTableIndex,
30
- createOwnerTable,
31
- insertOwner,
32
- } from "./Sql.js";
42
+ EvoluError,
43
+ ensureTransferableError,
44
+ makeUnexpectedError,
45
+ } from "./Error.js";
46
+ import { Id, cast } from "./Model.js";
47
+ import { Owner, OwnerId, makeOwner } from "./Owner.js";
48
+ import { SyncLock } from "./Platform.js";
49
+ import * as Sql from "./Sql.js";
33
50
  import {
34
- Index,
35
- indexEquivalence,
36
51
  JsonObjectOrArray,
37
52
  Sqlite,
53
+ SqliteExecResult,
54
+ SqliteFactory,
38
55
  SqliteQuery,
39
56
  SqliteQueryOptions,
40
- SqliteSchema,
41
- Table,
57
+ SqliteQueryPlanRow,
58
+ SqliteTransactionMode,
42
59
  Value,
60
+ drawSqliteQueryPlan,
43
61
  isJsonObjectOrArray,
44
62
  } from "./Sqlite.js";
45
- import { Store, makeStore } from "./Store.js";
63
+ import {
64
+ Message,
65
+ NewMessage,
66
+ Sync,
67
+ SyncData,
68
+ SyncFactory,
69
+ SyncResult,
70
+ SyncState,
71
+ } from "./Sync.js";
72
+
73
+ export interface Db {
74
+ readonly init: (
75
+ schema: DbSchema,
76
+ initialData: ReadonlyArray<Mutation>,
77
+ onError: Callbacks["onError"],
78
+ onSyncStateChange: Callbacks["onSyncStateChange"],
79
+ onReceive: Callbacks["onReceive"],
80
+ ) => Effect.Effect<
81
+ Owner,
82
+ | NotSupportedPlatformError
83
+ | TimestampTimeOutOfRangeError
84
+ | TimestampDriftError
85
+ | TimestampCounterOverflowError,
86
+ Config
87
+ >;
88
+
89
+ readonly loadQueries: (
90
+ queries: ReadonlyArray<Query>,
91
+ ) => Effect.Effect<ReadonlyArray<QueryPatches>>;
92
+
93
+ readonly mutate: (
94
+ mutations: ReadonlyArray<Mutation>,
95
+ queriesToRefresh: ReadonlyArray<Query>,
96
+ ) => Effect.Effect<
97
+ ReadonlyArray<QueryPatches>,
98
+ | TimestampTimeOutOfRangeError
99
+ | TimestampDriftError
100
+ | TimestampCounterOverflowError,
101
+ Config
102
+ >;
103
+
104
+ readonly resetOwner: () => Effect.Effect<void>;
105
+
106
+ readonly restoreOwner: (
107
+ schema: DbSchema,
108
+ mnemonic: Mnemonic,
109
+ ) => Effect.Effect<void>;
110
+
111
+ readonly ensureSchema: (schema: DbSchema) => Effect.Effect<void>;
112
+
113
+ readonly sync: (
114
+ queriesToRefresh: ReadonlyArray<Query>,
115
+ ) => Effect.Effect<ReadonlyArray<QueryPatches>, never, Config>;
116
+ }
46
117
 
47
- export type DatabaseSchema = ReadonlyRecord.ReadonlyRecord<string, TableSchema>;
118
+ export interface DbSchema {
119
+ readonly tables: ReadonlyArray<Table>;
120
+ readonly indexes: ReadonlyArray<Index>;
121
+ }
48
122
 
49
- type TableSchema = ReadonlyRecord.ReadonlyRecord<string, Value> & {
50
- readonly id: Id;
51
- };
123
+ export interface Table {
124
+ readonly name: string;
125
+ readonly columns: ReadonlyArray<string>;
126
+ }
52
127
 
53
- /**
54
- * Create table schema.
55
- *
56
- * Supported types are null, string, number, Uint8Array, JSON Object, and JSON
57
- * Array. Use SqliteDate for dates and SqliteBoolean for booleans.
58
- *
59
- * Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added
60
- * by default.
61
- *
62
- * @example
63
- * const TodoId = id("Todo");
64
- * type TodoId = S.Schema.Type<typeof TodoId>;
65
- *
66
- * const TodoTable = table({
67
- * id: TodoId,
68
- * title: NonEmptyString1000,
69
- * isCompleted: S.nullable(SqliteBoolean),
70
- * });
71
- * type TodoTable = S.Schema.Type<typeof TodoTable>;
72
- */
73
- export const table = <Fields extends TableFields>(
74
- fields: Fields,
75
- // Because Schema is invariant, we have to do validation like this.
76
- ): ValidateFieldsTypes<Fields> extends true
77
- ? ValidateFieldsNames<Fields> extends true
78
- ? ValidateFieldsHasId<Fields> extends true
79
- ? S.Schema<
80
- Types.Simplify<
81
- S.Struct.Type<Fields> & S.Schema.Type<typeof ReservedColumns>
82
- >,
83
- Types.Simplify<
84
- S.Struct.Encoded<Fields> & S.Schema.Encoded<typeof ReservedColumns>
85
- >
86
- >
87
- : EvoluTypeError<"table() called without id column.">
88
- : EvoluTypeError<"table() called with a reserved column. Reserved columns are createdAt, updatedAt, isDeleted. Those columns are added by default.">
89
- : EvoluTypeError<"table() called with unsupported type. Supported types are null, string, number, Uint8Array, JSON Object, and JSON Array. Use SqliteDate for dates and SqliteBoolean for booleans."> =>
90
- S.struct(fields).pipe(S.extend(ReservedColumns)) as never;
91
-
92
- const ReservedColumns = S.struct({
93
- createdAt: SqliteDate,
94
- updatedAt: SqliteDate,
95
- isDeleted: SqliteBoolean,
128
+ export const Index = S.Struct({
129
+ name: S.String,
130
+ sql: S.String,
96
131
  });
132
+ export type Index = S.Schema.Type<typeof Index>;
97
133
 
98
- type TableFields = Record<string, S.Schema<any>>;
99
-
100
- type ValidateFieldsTypes<Fields extends TableFields> =
101
- keyof Fields extends infer K
102
- ? K extends keyof Fields
103
- ? Fields[K] extends TableFields
104
- ? ValidateFieldsTypes<Fields[K]>
105
- : // eslint-disable-next-line @typescript-eslint/no-unused-vars
106
- Fields[K] extends S.Schema<infer A, infer _I>
107
- ? A extends Value
108
- ? true
109
- : false
110
- : never
111
- : never
112
- : never;
113
-
114
- type ValidateFieldsNames<Fields extends TableFields> =
115
- keyof Fields extends infer K
116
- ? K extends keyof Fields
117
- ? K extends "createdAt" | "updatedAt" | "isDeleted"
118
- ? false
119
- : true
120
- : never
121
- : never;
122
-
123
- type ValidateFieldsHasId<Fields extends TableFields> = "id" extends keyof Fields
124
- ? true
125
- : false;
134
+ export interface Mutation {
135
+ readonly table: string;
136
+ readonly id: Id;
137
+ readonly values: Record.ReadonlyRecord<
138
+ string,
139
+ Value | Date | boolean | undefined
140
+ >;
141
+ readonly isInsert: boolean;
142
+ }
126
143
 
127
- /**
128
- * Create database schema.
129
- *
130
- * Tables with a name prefixed with _ are local-only, which means they are not
131
- * synced. Local-only tables are useful for device-specific or temporal data.
132
- *
133
- * @example
134
- * const Database = database({
135
- * // A local-only table.
136
- * _todo: TodoTable,
137
- * todo: TodoTable,
138
- * todoCategory: TodoCategoryTable,
139
- * });
140
- * type Database = S.Schema.Type<typeof Database>;
141
- */
142
- export const database = S.struct;
144
+ export interface NotSupportedPlatformError {
145
+ readonly _tag: "NotSupportedPlatformError";
146
+ }
143
147
 
144
148
  // https://blog.beraliv.dev/2021-05-07-opaque-type-in-typescript
145
149
  declare const __queryBrand: unique symbol;
146
150
 
147
151
  /**
148
- * The query is an SQL query serialized as a string with a branded type
149
- * representing a row it returns.
152
+ * Query is SQL query serialized as a string with a branded type representing a
153
+ * row it returns.
150
154
  */
151
155
  export type Query<R extends Row = Row> = string &
152
156
  Brand.Brand<"Query"> & { readonly [__queryBrand]: R };
153
157
 
154
- export type Queries<R extends Row = Row> = ReadonlyArray<Query<R>>;
155
-
156
- interface SerializedSqliteQuery {
157
- readonly sql: string;
158
- readonly parameters: (
159
- | null
160
- | string
161
- | number
162
- | Array<number>
163
- | { json: JsonObjectOrArray }
164
- )[];
165
- readonly options?: SqliteQueryOptions;
166
- }
167
-
168
- // We use queries as keys, hence JSON.stringify.
169
- export const serializeQuery = <R extends Row>({
170
- sql,
171
- parameters = [],
172
- options,
173
- }: SqliteQuery): Query<R> => {
174
- const query: SerializedSqliteQuery = {
175
- sql,
176
- parameters: parameters.map((p) =>
177
- Predicate.isUint8Array(p)
178
- ? Array.from(p)
179
- : isJsonObjectOrArray(p)
180
- ? { json: p }
181
- : p,
182
- ),
183
- ...(options && { options }),
184
- };
185
- return JSON.stringify(query) as Query<R>;
186
- };
187
-
188
- export const deserializeQuery = <R extends Row>(
189
- query: Query<R>,
190
- ): SqliteQuery => {
191
- const serializedSqliteQuery = JSON.parse(query) as SerializedSqliteQuery;
192
- return {
193
- ...serializedSqliteQuery,
194
- parameters: serializedSqliteQuery.parameters.map((p) =>
195
- Array.isArray(p)
196
- ? new Uint8Array(p)
197
- : typeof p === "object" && p != null
198
- ? p.json
199
- : p,
200
- ),
201
- };
202
- };
203
-
204
158
  export type Row = {
205
159
  [key: string]:
206
160
  | Value
@@ -208,170 +162,239 @@ export type Row = {
208
162
  | ReadonlyArray<Row>; // for jsonArrayFrom from kysely/helpers/sqlite
209
163
  };
210
164
 
211
- /**
212
- * Extract {@link Row} from {@link Query} instance.
213
- *
214
- * @example
215
- * const allTodos = evolu.createQuery((db) =>
216
- * db.selectFrom("todo").selectAll(),
217
- * );
218
- * type AllTodosRow = ExtractRow<typeof allTodos>;
219
- */
220
- export type ExtractRow<T extends Query> = T extends Query<infer R> ? R : never;
221
-
222
- const _emptyRows: ReadonlyArray<Row> = [];
223
-
224
- export const emptyRows = <R extends Row>(): ReadonlyArray<R> =>
225
- _emptyRows as ReadonlyArray<R>;
165
+ export class DbFactory extends Context.Tag("DbFactory")<
166
+ DbFactory,
167
+ {
168
+ readonly createDb: Effect.Effect<Db>;
169
+ }
170
+ >() {}
171
+
172
+ export const createDb: Effect.Effect<
173
+ Db,
174
+ never,
175
+ SqliteFactory | Bip39 | NanoIdGenerator | Time | SyncFactory | SyncLock
176
+ > = Effect.gen(function* () {
177
+ const { createSqlite } = yield* SqliteFactory;
178
+ const { createSync } = yield* SyncFactory;
179
+
180
+ const initContext = Context.empty().pipe(
181
+ Context.add(Bip39, yield* Bip39),
182
+ Context.add(NanoIdGenerator, yield* NanoIdGenerator),
183
+ Context.add(Time, yield* Time),
184
+ Context.add(SyncLock, yield* SyncLock),
185
+ );
226
186
 
227
- /** An object with rows and row properties. */
228
- export interface QueryResult<R extends Row = Row> {
229
- /** An array containing all the rows returned by the query. */
230
- readonly rows: ReadonlyArray<Readonly<Kysely.Simplify<R>>>;
187
+ const afterInitContext =
188
+ yield* Deferred.make<
189
+ Context.Context<
190
+ | Bip39
191
+ | NanoIdGenerator
192
+ | Time
193
+ | SyncLock
194
+ | Sqlite
195
+ | Owner
196
+ | Sync
197
+ | Callbacks
198
+ >
199
+ >();
200
+
201
+ const afterInit =
202
+ (options: { readonly transaction: SqliteTransactionMode }) =>
203
+ <A, E, R>(effect: Effect.Effect<A, E, R>) =>
204
+ Effect.flatMap(Deferred.await(afterInitContext), (context) =>
205
+ Effect.flatMap(Sqlite, (sqlite) =>
206
+ sqlite.transaction(options.transaction)(effect),
207
+ ).pipe(Effect.provide(context)),
208
+ );
231
209
 
232
- /**
233
- * The first row returned by the query, or null if no rows were returned. This
234
- * property is useful for queries that are expected to return a single row.
235
- */
236
- readonly row: Readonly<Kysely.Simplify<R>> | null;
237
- }
210
+ const queryRowsRef = yield* SynchronizedRef.make<QueryRowsMap>(new Map());
211
+
212
+ const db: Db = {
213
+ init: (schema, initialData, onError, onSyncStateChange, onReceive) =>
214
+ Effect.gen(function* () {
215
+ yield* Effect.logDebug(["Db init", { schema }]);
216
+ const sqlite = yield* createSqlite;
217
+ const contextWithSqlite = Context.add(initContext, Sqlite, sqlite);
218
+ const owner = yield* getSchema.pipe(
219
+ Effect.tap(ensureSchema(schema)),
220
+ Effect.flatMap((currentSchema) => {
221
+ if (currentSchema.tables.map((t) => t.name).includes("evolu_owner"))
222
+ return readOwner;
223
+ return createOwner().pipe(Effect.tap(applyMutations(initialData)));
224
+ }),
225
+ sqlite.transaction("exclusive"),
226
+ Effect.provide(contextWithSqlite),
227
+ );
228
+ const sync = yield* createSync.pipe(Effect.provide(initContext));
229
+ yield* sync.init(owner);
230
+ Deferred.unsafeDone(
231
+ afterInitContext,
232
+ Effect.succeed(
233
+ contextWithSqlite.pipe(
234
+ Context.add(Owner, owner),
235
+ Context.add(Sync, sync),
236
+ Context.add(Callbacks, { onError, onSyncStateChange, onReceive }),
237
+ ),
238
+ ),
239
+ );
240
+ return owner;
241
+ }),
238
242
 
239
- export type QueryResultsFromQueries<Q extends Queries> = {
240
- [P in keyof Q]: Q[P] extends Query<infer R> ? QueryResult<R> : never;
241
- };
243
+ loadQueries: (queries) =>
244
+ Effect.logDebug(["Db loadQueries", { queries }]).pipe(
245
+ Effect.zipRight(loadQueries(queries, queryRowsRef)),
246
+ afterInit({ transaction: "shared" }),
247
+ ),
242
248
 
243
- export type QueryResultsPromisesFromQueries<Q extends Queries> = {
244
- [P in keyof Q]: Q[P] extends Query<infer R> ? Promise<QueryResult<R>> : never;
245
- };
249
+ mutate: (mutations, queriesToRefresh) =>
250
+ Effect.gen(function* () {
251
+ yield* Effect.logDebug(["Db mutate", { mutations, queriesToRefresh }]);
252
+ const time = yield* Time;
253
+ const sqlite = yield* Sqlite;
254
+
255
+ const [toSyncMutations, localOnlyMutations] = Arr.partition(
256
+ mutations,
257
+ /** Table name starting with '_' is local only (not synced). */
258
+ (mutation) => mutation.table.startsWith("_"),
259
+ );
260
+
261
+ for (const mutation of localOnlyMutations) {
262
+ const isDeleteMutation = mutationToNewMessages(mutation).some(
263
+ ({ column, value }) => column === "isDeleted" && value === 1,
264
+ );
265
+ if (isDeleteMutation) {
266
+ yield* sqlite.exec({
267
+ sql: `delete from "${mutation.table}" where "id" = ?;`,
268
+ parameters: [mutation.id],
269
+ });
270
+ } else {
271
+ const messages = mutationToNewMessages(mutation);
272
+ for (const message of messages) {
273
+ const now = yield* time.now;
274
+ yield* upsertValueIntoTableRowColumn(message, messages, now);
275
+ }
276
+ }
277
+ }
278
+ if (toSyncMutations.length > 0) {
279
+ yield* Effect.tap(applyMutations(toSyncMutations), forkSync);
280
+ }
281
+ return yield* loadQueries(queriesToRefresh, queryRowsRef);
282
+ }).pipe(afterInit({ transaction: "exclusive" })),
283
+
284
+ resetOwner: () =>
285
+ Effect.logTrace("Db resetOwner").pipe(
286
+ Effect.tap(dropAllTables),
287
+ afterInit({ transaction: "last" }),
288
+ ),
246
289
 
247
- const queryResultCache = new WeakMap<ReadonlyArray<Row>, QueryResult<Row>>();
290
+ restoreOwner: (schema, mnemonic) =>
291
+ Effect.logTrace("Db restoreOwner").pipe(
292
+ Effect.tap(dropAllTables),
293
+ Effect.tap(Effect.flatMap(getSchema, ensureSchema(schema))),
294
+ Effect.tap(createOwner(mnemonic)),
295
+ afterInit({ transaction: "last" }),
296
+ ),
248
297
 
249
- export const queryResultFromRows = <R extends Row>(
250
- rows: ReadonlyArray<R>,
251
- ): QueryResult<R> => {
252
- let queryResult = queryResultCache.get(rows);
253
- if (queryResult == null) {
254
- queryResult = { rows, row: rows[0] };
255
- queryResultCache.set(rows, queryResult);
256
- }
257
- return queryResult as QueryResult<R>;
258
- };
298
+ ensureSchema: (schema) =>
299
+ getSchema.pipe(
300
+ Effect.flatMap(ensureSchema(schema)),
301
+ afterInit({ transaction: "exclusive" }),
302
+ ),
259
303
 
260
- // TODO: https://discord.com/channels/795981131316985866/1218626687546294386/1218796529725476935
261
- // https://github.com/Effect-TS/schema/releases/tag/v0.18.0
262
- const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
263
- schema: S.Schema<A, I>,
264
- ): { [K in keyof A]: S.Schema<A[K], I[K]> } => {
265
- const out: Record<PropertyKey, S.Schema<any>> = {};
266
- const propertySignatures = AST.getPropertySignatures(schema.ast);
267
- for (let i = 0; i < propertySignatures.length; i++) {
268
- const propertySignature = propertySignatures[i];
269
- out[propertySignature.name] = make(propertySignature.type);
270
- }
271
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
272
- return out as any;
273
- };
304
+ sync: (queriesToRefresh) =>
305
+ Effect.logDebug(["Db sync", { queriesToRefresh }]).pipe(
306
+ Effect.zipRight(forkSync()),
307
+ Effect.zipRight(loadQueries(queriesToRefresh, queryRowsRef)),
308
+ afterInit({ transaction: "shared" }),
309
+ ),
274
310
 
275
- export const schemaToTables = (schema: S.Schema<any>): ReadonlyArray<Table> =>
276
- pipe(
277
- getPropertySignatures(schema),
278
- ReadonlyRecord.toEntries,
279
- ReadonlyArray.map(
280
- ([name, schema]): Table => ({
281
- name,
282
- columns: Object.keys(getPropertySignatures(schema)),
283
- }),
284
- ),
285
- );
311
+ // TODO:
312
+ // dispose: () =>
313
+ // Effect.logTrace("Db dispose").pipe(
314
+ // Effect.tap(Scope.close(scope, Exit.succeed("Db disposed"))),
315
+ // afterInit({ transaction: "last" }),
316
+ // ),
317
+ };
286
318
 
287
- export const transaction = <R, E, A>(
288
- effect: Effect.Effect<A, E, R>,
289
- ): Effect.Effect<A, E, Sqlite | R> =>
290
- Effect.flatMap(Sqlite, (sqlite) =>
291
- Effect.acquireUseRelease(
292
- sqlite.exec({ sql: "begin" }),
293
- () => effect,
294
- (_, exit) =>
295
- Exit.isFailure(exit)
296
- ? sqlite.exec({ sql: "rollback" })
297
- : sqlite.exec({ sql: "end" }),
298
- ),
299
- );
319
+ return db;
320
+ });
300
321
 
301
- export interface NoSuchTableOrColumnError {
302
- readonly _tag: "NoSuchTableOrColumnError";
322
+ export interface Callbacks {
323
+ readonly onError: (error: EvoluError) => void;
324
+ readonly onSyncStateChange: (state: SyncState) => void;
325
+ readonly onReceive: () => void;
303
326
  }
304
327
 
305
- export const SqliteNoSuchTableOrColumnError = S.struct({
306
- message: S.union(
307
- S.string.pipe(S.includes("no such table")),
308
- S.string.pipe(S.includes("no such column")),
309
- S.string.pipe(S.includes("has no column")),
310
- ),
311
- });
312
- export type SqliteNoSuchTableOrColumnError = S.Schema.Type<
313
- typeof SqliteNoSuchTableOrColumnError
314
- >;
315
-
316
- export const someDefectToNoSuchTableOrColumnError = Effect.catchSomeDefect(
317
- (error) =>
318
- S.is(SqliteNoSuchTableOrColumnError)(error)
319
- ? Option.some(
320
- Effect.fail<NoSuchTableOrColumnError>({
321
- _tag: "NoSuchTableOrColumnError",
328
+ export const Callbacks = Context.GenericTag<Callbacks>("Callbacks");
329
+
330
+ export type QueryRowsMap = ReadonlyMap<Query, ReadonlyArray<Row>>;
331
+
332
+ const loadQueries = (
333
+ queries: ReadonlyArray<Query>,
334
+ queryRowsRef: SynchronizedRef.SynchronizedRef<QueryRowsMap>,
335
+ ) =>
336
+ Effect.gen(function* () {
337
+ const sqlite = yield* Sqlite;
338
+ const previousState = yield* SynchronizedRef.getAndUpdateEffect(
339
+ queryRowsRef,
340
+ (previousState) =>
341
+ Effect.map(
342
+ Effect.forEach(queries, (query) => {
343
+ const sqliteQuery = deserializeQuery(query);
344
+ return sqlite.exec(sqliteQuery).pipe(
345
+ Effect.tap(maybeExplainQueryPlan(sqliteQuery)),
346
+ Effect.map(({ rows }) => [query, rows] as const),
347
+ );
322
348
  }),
323
- )
324
- : Option.none(),
325
- );
326
-
327
- export const lazyInit = (
328
- mnemonic?: Mnemonic,
329
- ): Effect.Effect<Owner, never, Sqlite | Bip39 | NanoIdGenerator> =>
330
- Effect.gen(function* (_) {
331
- const [owner, sqlite, initialTimestampString] = yield* _(
332
- Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp], {
333
- concurrency: "unbounded",
334
- }),
349
+ (queriesRows) => new Map([...previousState, ...queriesRows]),
350
+ ),
335
351
  );
336
-
337
- yield* _(
338
- Effect.all([
339
- sqlite.exec(createMessageTable),
340
- sqlite.exec(createMessageTableIndex),
341
- sqlite.exec(createOwnerTable),
342
- sqlite.exec({
343
- ...insertOwner,
344
- parameters: [
345
- owner.id,
346
- owner.mnemonic,
347
- owner.encryptionKey,
348
- timestampToString(initialTimestampString),
349
- merkleTreeToString(initialMerkleTree),
350
- ],
351
- }),
352
- ]),
352
+ const currentState = yield* SynchronizedRef.get(queryRowsRef);
353
+ return queries.map(
354
+ (query): QueryPatches => ({
355
+ query,
356
+ patches: makePatches(
357
+ previousState.get(query),
358
+ currentState.get(query) || [],
359
+ ),
360
+ }),
353
361
  );
354
-
355
- return owner;
356
362
  });
357
363
 
358
- const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
359
- function* (_) {
360
- const sqlite = yield* _(Sqlite);
364
+ const maybeExplainQueryPlan = (sqliteQuery: SqliteQuery) => {
365
+ if (!sqliteQuery.options?.logExplainQueryPlan) return Effect.void;
366
+ return Sqlite.pipe(
367
+ Effect.flatMap((sqlite) =>
368
+ sqlite.exec({
369
+ ...sqliteQuery,
370
+ sql: `EXPLAIN QUERY PLAN ${sqliteQuery.sql}`,
371
+ }),
372
+ ),
373
+ Effect.tap(Console.log("ExplainQueryPlan", sqliteQuery)),
374
+ Effect.tap(({ rows }) =>
375
+ Console.log(drawSqliteQueryPlan(rows as SqliteQueryPlanRow[])),
376
+ ),
377
+ Effect.map(constVoid),
378
+ );
379
+ };
361
380
 
362
- const tables = yield* _(
381
+ const getSchema: Effect.Effect<DbSchema, never, Sqlite> = Effect.gen(
382
+ function* (_) {
383
+ yield* Effect.logTrace("Db getSchema");
384
+ const sqlite = yield* Sqlite;
385
+ const tables = yield* Effect.map(
363
386
  sqlite.exec({
364
387
  // https://til.simonwillison.net/sqlite/list-all-columns-in-a-database
365
388
  sql: `
366
- select
367
- sqlite_master.name as tableName,
368
- table_info.name as columnName
369
- from
370
- sqlite_master
371
- join pragma_table_info(sqlite_master.name) as table_info
372
- `.trim(),
389
+ select
390
+ sqlite_master.name as tableName,
391
+ table_info.name as columnName
392
+ from
393
+ sqlite_master
394
+ join pragma_table_info(sqlite_master.name) as table_info
395
+ `.trim(),
373
396
  }),
374
- Effect.map(({ rows }) => {
397
+ ({ rows }) => {
375
398
  const map = new Map<string, string[]>();
376
399
  rows.forEach((row) => {
377
400
  const { tableName, columnName } = row as {
@@ -381,27 +404,28 @@ const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
381
404
  if (!map.has(tableName)) map.set(tableName, []);
382
405
  map.get(tableName)?.push(columnName);
383
406
  });
384
- return Array.from(map, ([name, columns]) => ({
407
+ return globalThis.Array.from(map, ([name, columns]) => ({
385
408
  name,
386
409
  columns,
387
410
  }));
388
- }),
411
+ },
389
412
  );
390
413
 
391
- const indexes = yield* _(
414
+ const indexes = yield* Effect.map(
392
415
  sqlite.exec({
393
416
  sql: `
394
- select
395
- name, sql
396
- from
397
- sqlite_master
398
- where
399
- type='index' and
400
- name not like 'sqlite_%' and
401
- name not like 'index_evolu_%'`,
417
+ select
418
+ name, sql
419
+ from
420
+ sqlite_master
421
+ where
422
+ type='index' and
423
+ name not like 'sqlite_%' and
424
+ name not like 'index_evolu_%'
425
+ `.trim(),
402
426
  }),
403
- Effect.map((result) =>
404
- ReadonlyArray.map(
427
+ (result) =>
428
+ Arr.map(
405
429
  result.rows,
406
430
  (row): Index => ({
407
431
  name: row.name as string,
@@ -413,41 +437,37 @@ const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
413
437
  sql: (row.sql as string).replace("CREATE INDEX", "create index"),
414
438
  }),
415
439
  ),
416
- ),
417
440
  );
418
441
 
419
442
  return { tables, indexes };
420
443
  },
421
444
  );
422
445
 
423
- export const ensureSchema = (
424
- schema: SqliteSchema,
425
- ): Effect.Effect<void, never, Sqlite> =>
426
- Effect.gen(function* (_) {
427
- const sqlite = yield* _(Sqlite);
428
- const currentSchema = yield* _(getSchema);
429
-
446
+ const ensureSchema = (newSchema: DbSchema) => (currentSchema: DbSchema) =>
447
+ Effect.gen(function* () {
448
+ yield* Effect.logTrace("Db ensureSchema");
430
449
  const sql: string[] = [];
431
450
 
432
- schema.tables.forEach((table) => {
451
+ newSchema.tables.forEach((table) => {
433
452
  const currentTable = currentSchema.tables.find(
434
453
  (t) => t.name === table.name,
435
454
  );
436
455
  if (!currentTable) {
437
- sql.push(`
438
- create table ${table.name} (
439
- "id" text primary key,
440
- ${table.columns
441
- .filter((c) => c !== "id")
442
- // "A column with affinity BLOB does not prefer one storage class over another
443
- // and no attempt is made to coerce data from one storage class into another."
444
- // https://www.sqlite.org/datatype3.html
445
- .map((name) => `"${name}" blob`)
446
- .join(", ")}
447
- );
448
- `);
456
+ sql.push(
457
+ `
458
+ create table ${table.name} (
459
+ "id" text primary key,
460
+ ${table.columns
461
+ .filter((c) => c !== "id")
462
+ // "A column with affinity BLOB does not prefer one storage class over another
463
+ // and no attempt is made to coerce data from one storage class into another."
464
+ // https://www.sqlite.org/datatype3.html
465
+ .map((name) => `"${name}" blob`)
466
+ .join(", ")}
467
+ );`.trim(),
468
+ );
449
469
  } else {
450
- ReadonlyArray.differenceWith(String.Equivalence)(
470
+ Arr.differenceWith(String.Equivalence)(
451
471
  table.columns,
452
472
  currentTable.columns,
453
473
  ).forEach((newColumn) => {
@@ -459,53 +479,465 @@ export const ensureSchema = (
459
479
  });
460
480
 
461
481
  // Remove old indexes.
462
- ReadonlyArray.differenceWith(indexEquivalence)(
482
+ Arr.differenceWith(indexEquivalence)(
463
483
  currentSchema.indexes,
464
- ReadonlyArray.intersectionWith(indexEquivalence)(
484
+ Arr.intersectionWith(indexEquivalence)(
465
485
  currentSchema.indexes,
466
- schema.indexes,
486
+ newSchema.indexes,
467
487
  ),
468
488
  ).forEach((indexToDrop) => {
469
489
  sql.push(`drop index "${indexToDrop.name}";`);
470
490
  });
471
491
 
472
492
  // Add new indexes.
473
- ReadonlyArray.differenceWith(indexEquivalence)(
474
- schema.indexes,
493
+ Arr.differenceWith(indexEquivalence)(
494
+ newSchema.indexes,
475
495
  currentSchema.indexes,
476
496
  ).forEach((newIndex) => {
477
497
  sql.push(`${newIndex.sql};`);
478
498
  });
499
+ if (sql.length > 0) {
500
+ const sqlite = yield* Sqlite;
501
+ yield* sqlite.exec({ sql: sql.join("\n") });
502
+ }
503
+ });
504
+
505
+ const indexEquivalence: Equivalence<Index> = (self, that) =>
506
+ self.name === that.name && self.sql === that.sql;
507
+
508
+ const readOwner = Effect.logTrace("Db readOwner").pipe(
509
+ Effect.zipRight(Sqlite),
510
+ Effect.flatMap((sqlite) => sqlite.exec(Sql.selectOwner)),
511
+ Effect.map(
512
+ ({ rows: [row] }): Owner => ({
513
+ id: row.id as OwnerId,
514
+ mnemonic: row.mnemonic as Mnemonic,
515
+ encryptionKey: row.encryptionKey as Uint8Array,
516
+ }),
517
+ ),
518
+ );
519
+
520
+ const createOwner = (mnemonic?: Mnemonic) =>
521
+ Effect.logTrace("Db createOwner").pipe(
522
+ Effect.zipRight(
523
+ Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp]),
524
+ ),
525
+ Effect.tap(([owner, sqlite, initialTimestampString]) =>
526
+ Effect.all([
527
+ sqlite.exec(Sql.createMessageTable),
528
+ sqlite.exec(Sql.createMessageTableIndex),
529
+ sqlite.exec(Sql.createOwnerTable),
530
+ sqlite.exec({
531
+ ...Sql.insertOwner,
532
+ parameters: [
533
+ owner.id,
534
+ owner.mnemonic,
535
+ owner.encryptionKey,
536
+ timestampToString(initialTimestampString),
537
+ merkleTreeToString(initialMerkleTree),
538
+ ],
539
+ }),
540
+ ]),
541
+ ),
542
+ Effect.map(([owner]) => owner),
543
+ );
544
+
545
+ const applyMutations = (mutations: ReadonlyArray<Mutation>) =>
546
+ Effect.gen(function* (_) {
547
+ const { timestamp, merkleTree } = yield* readTimestampAndMerkleTree;
548
+ const [nextTimestamp, messages] = yield* Effect.mapAccum(
549
+ mutations.flatMap(mutationToNewMessages),
550
+ timestamp,
551
+ (currentTimestamp, newMessage) =>
552
+ Effect.map(sendTimestamp(currentTimestamp), (nextTimestamp) => {
553
+ const message: Message = {
554
+ ...newMessage,
555
+ timestamp: timestampToString(nextTimestamp),
556
+ };
557
+ return [nextTimestamp, message];
558
+ }),
559
+ );
560
+ const nextMerkleTree = yield* applyMessages(merkleTree, messages);
561
+ yield* writeTimestampAndMerkleTree(nextTimestamp, nextMerkleTree);
562
+ return messages;
563
+ });
564
+
565
+ const readTimestampAndMerkleTree = Sqlite.pipe(
566
+ Effect.flatMap((sqlite) =>
567
+ sqlite.exec(Sql.selectOwnerTimestampAndMerkleTree),
568
+ ),
569
+ Effect.map(({ rows: [{ timestamp, merkleTree }] }) => ({
570
+ timestamp: unsafeTimestampFromString(timestamp as TimestampString),
571
+ merkleTree: merkleTree as MerkleTree,
572
+ })),
573
+ );
479
574
 
480
- if (sql.length > 0)
481
- yield* _(
575
+ const mutationToNewMessages = (mutation: Mutation) =>
576
+ pipe(
577
+ Object.entries(mutation.values),
578
+ Arr.filterMap(([column, value]) =>
579
+ // The value can be undefined if exactOptionalPropertyTypes isn't true.
580
+ // Don't insert nulls because null is the default value.
581
+ value === undefined || (mutation.isInsert && value == null)
582
+ ? Option.none()
583
+ : Option.some([column, value] as const),
584
+ ),
585
+ Arr.map(
586
+ ([column, value]): NewMessage => ({
587
+ table: mutation.table,
588
+ row: mutation.id,
589
+ column,
590
+ value:
591
+ typeof value === "boolean"
592
+ ? cast(value)
593
+ : value instanceof Date
594
+ ? cast(value)
595
+ : value,
596
+ }),
597
+ ),
598
+ );
599
+
600
+ const applyMessages = (
601
+ merkleTree: MerkleTree,
602
+ messages: ReadonlyArray<Message>,
603
+ ): Effect.Effect<MerkleTree, never, Sqlite> =>
604
+ Effect.gen(function* () {
605
+ const sqlite = yield* Sqlite;
606
+ for (const message of messages) {
607
+ const messageTimestamp = unsafeTimestampFromString(message.timestamp);
608
+ const lastTimestamp = yield* Effect.map(
482
609
  sqlite.exec({
483
- sql: sql.join(""),
610
+ ...Sql.selectLastTimestampForTableRowColumn,
611
+ parameters: [message.table, message.row, message.column, 1],
484
612
  }),
613
+ ({ rows }) =>
614
+ rows.length > 0 ? (rows[0].timestamp as TimestampString) : null,
485
615
  );
616
+ if (lastTimestamp == null || lastTimestamp < message.timestamp) {
617
+ yield* upsertValueIntoTableRowColumn(
618
+ message,
619
+ messages,
620
+ messageTimestamp.millis,
621
+ );
622
+ }
623
+ if (lastTimestamp == null || lastTimestamp !== message.timestamp) {
624
+ const { changes } = yield* sqlite.exec({
625
+ ...Sql.insertIntoMessagesIfNew,
626
+ parameters: [
627
+ message.timestamp,
628
+ message.table,
629
+ message.row,
630
+ message.column,
631
+ message.value,
632
+ ],
633
+ });
634
+ if (changes === 1)
635
+ merkleTree = insertIntoMerkleTree(merkleTree, messageTimestamp);
636
+ }
637
+ }
638
+ return merkleTree;
486
639
  });
487
640
 
488
- export const dropAllTables: Effect.Effect<void, never, Sqlite> = Effect.gen(
489
- function* (_) {
490
- const sqlite = yield* _(Sqlite);
491
- const schema = yield* _(getSchema);
492
- const sql = schema.tables
493
- // The dropped table is completely removed from the database schema and
494
- // the disk file. The table can not be recovered.
495
- // All indices and triggers associated with the table are also deleted.
496
- // https://sqlite.org/lang_droptable.html
497
- .map((table) => `drop table "${table.name}";`)
498
- .join("");
499
- yield* _(sqlite.exec({ sql }));
500
- },
501
- );
641
+ export const upsertValueIntoTableRowColumn = (
642
+ message: NewMessage,
643
+ messages: ReadonlyArray<NewMessage>,
644
+ millis: Millis,
645
+ ): Effect.Effect<SqliteExecResult, never, Sqlite> =>
646
+ Sqlite.pipe(
647
+ Effect.map((sqlite) => {
648
+ const now = cast(new Date(millis));
649
+ return sqlite.exec({
650
+ sql: `
651
+ insert into
652
+ "${message.table}" ("id", "${message.column}", "createdAt", "updatedAt")
653
+ values
654
+ (?, ?, ?, ?)
655
+ on conflict do update set
656
+ "${message.column}" = ?,
657
+ "updatedAt" = ?
658
+ `.trim(),
659
+ parameters: [message.row, message.value, now, now, message.value, now],
660
+ });
661
+ }),
662
+ Effect.flatMap((insert) =>
663
+ Effect.catchSomeDefect(insert, (error) =>
664
+ S.is(SqliteNoSuchTableOrColumnError)(error)
665
+ ? Option.some(
666
+ // If one message fails, we ensure schema for all messages.
667
+ ensureSchemaByNewMessages(messages).pipe(Effect.zipRight(insert)),
668
+ )
669
+ : Option.none(),
670
+ ),
671
+ ),
672
+ );
502
673
 
503
- export type RowsStore = Store<RowsStoreValue>;
504
- export const RowsStore = Context.GenericTag<RowsStore>("@services/RowsStore");
674
+ const SqliteNoSuchTableOrColumnError = S.Struct({
675
+ message: S.Union(
676
+ S.String.pipe(S.includes("no such table")),
677
+ S.String.pipe(S.includes("no such column")),
678
+ S.String.pipe(S.includes("has no column")),
679
+ ),
680
+ });
505
681
 
506
- type RowsStoreValue = ReadonlyMap<Query, ReadonlyArray<Row>>;
682
+ const ensureSchemaByNewMessages = (messages: ReadonlyArray<NewMessage>) =>
683
+ Effect.gen(function* () {
684
+ const tablesMap = new Map<string, Table>();
685
+ messages.forEach((message) => {
686
+ const table = tablesMap.get(message.table);
687
+ if (table == null) {
688
+ tablesMap.set(message.table, {
689
+ name: message.table,
690
+ columns: [message.column, "createdAt", "updatedAt"],
691
+ });
692
+ return;
693
+ }
694
+ if (table.columns.includes(message.column)) return;
695
+ tablesMap.set(message.table, {
696
+ name: message.table,
697
+ columns: table.columns.concat(message.column),
698
+ });
699
+ });
700
+ const tables = Arr.fromIterable(tablesMap.values());
701
+ yield* Effect.flatMap(getSchema, ensureSchema({ tables, indexes: [] }));
702
+ });
507
703
 
508
- export const RowsStoreLive = Layer.effect(
509
- RowsStore,
510
- makeStore<RowsStoreValue>(new Map()),
511
- );
704
+ const writeTimestampAndMerkleTree = (
705
+ timestamp: Timestamp,
706
+ merkleTree: MerkleTree,
707
+ ) =>
708
+ Effect.flatMap(Sqlite, (sqlite) =>
709
+ sqlite.exec({
710
+ ...Sql.updateOwnerTimestampAndMerkleTree,
711
+ parameters: [
712
+ merkleTreeToString(merkleTree),
713
+ timestampToString(timestamp),
714
+ ],
715
+ }),
716
+ );
717
+
718
+ const dropAllTables = Effect.gen(function* () {
719
+ yield* Effect.logTrace("Db dropAllTables");
720
+ const sqlite = yield* Sqlite;
721
+ const schema = yield* getSchema;
722
+ yield* Effect.forEach(schema.tables, (table) =>
723
+ // The dropped table is completely removed from the database schema and
724
+ // the disk file. The table can not be recovered.
725
+ // All indices and triggers associated with the table are also deleted.
726
+ // https://sqlite.org/lang_droptable.html
727
+ sqlite.exec({ sql: `drop table "${table.name}"` }),
728
+ );
729
+ });
730
+
731
+ const forkSync = (messages: ReadonlyArray<Message> = []) =>
732
+ SyncLock.pipe(
733
+ Effect.flatMap((syncLock) => syncLock.tryAcquire),
734
+ Effect.matchEffect({
735
+ onFailure: () => Effect.void,
736
+ onSuccess: () => syncLoop(messages),
737
+ }),
738
+ Effect.scoped,
739
+ Effect.forkDaemon,
740
+ );
741
+
742
+ const syncLoop = (
743
+ messages: ReadonlyArray<Message> = [],
744
+ ): Effect.Effect<void, never, Sync | Callbacks | Time | Sqlite | Config> =>
745
+ Effect.gen(function* (_) {
746
+ const sqlite = yield* Sqlite;
747
+ const sync = yield* Sync;
748
+ const callbacks = yield* Callbacks;
749
+ const time = yield* Time;
750
+
751
+ const syncDataRef = yield* readTimestampAndMerkleTree.pipe(
752
+ sqlite.transaction("shared"),
753
+ Effect.map((a): SyncData => ({ ...a, messages })),
754
+ Effect.flatMap(Ref.make),
755
+ );
756
+
757
+ callbacks.onSyncStateChange({ _tag: "SyncStateIsSyncing" });
758
+
759
+ yield* Ref.get(syncDataRef).pipe(
760
+ Effect.flatMap(sync.sync),
761
+ Effect.flatMap(handleSyncResult),
762
+ Effect.repeat({
763
+ // TODO: Schedule.jittered(Schedule.exponential("10 millis"))
764
+ schedule: Schedule.recurs(50),
765
+ until: Option.match({
766
+ onNone: () =>
767
+ Effect.gen(function* () {
768
+ callbacks.onSyncStateChange({
769
+ _tag: "SyncStateIsSynced",
770
+ time: yield* time.now,
771
+ });
772
+ return true;
773
+ }),
774
+ onSome: (syncData) =>
775
+ Effect.gen(function* () {
776
+ yield* Ref.set(syncDataRef, syncData);
777
+ return false;
778
+ }),
779
+ }),
780
+ }),
781
+ Effect.catchAllDefect((error) =>
782
+ // Db can run in a Web Worker, so we must ensure transferable error.
783
+ Effect.fail(makeUnexpectedError(ensureTransferableError(error))),
784
+ ),
785
+ Effect.catchTag("SyncStateIsNotSynced", (state) =>
786
+ Effect.succeed(callbacks.onSyncStateChange(state)),
787
+ ),
788
+ Effect.catchAll((error) => Effect.succeed(callbacks.onError(error))),
789
+ );
790
+ });
791
+
792
+ const handleSyncResult = (result: SyncResult) =>
793
+ Effect.flatMap(Sqlite, (sqlite) =>
794
+ Effect.gen(function* (_) {
795
+ const { onReceive } = yield* Callbacks;
796
+ const current = yield* readTimestampAndMerkleTree;
797
+ const nextTimestamp = yield* Effect.reduce(
798
+ result.messages,
799
+ current.timestamp,
800
+ (local, message) =>
801
+ receiveTimestamp({
802
+ local,
803
+ remote: unsafeTimestampFromString(message.timestamp),
804
+ }),
805
+ );
806
+ const nextMerkleTree = yield* applyMessages(
807
+ current.merkleTree,
808
+ result.messages,
809
+ );
810
+ if (result.messages.length > 0) {
811
+ yield* writeTimestampAndMerkleTree(nextTimestamp, nextMerkleTree);
812
+ onReceive();
813
+ }
814
+ return yield* diffMerkleTrees(result.merkleTree, nextMerkleTree).pipe(
815
+ Effect.flatMap((diff) =>
816
+ sqlite.exec({
817
+ ...Sql.selectMessagesToSync,
818
+ parameters: [timestampToString(makeSyncTimestamp(diff))],
819
+ }),
820
+ ),
821
+ Effect.map(
822
+ ({ rows }): SyncData => ({
823
+ messages: rows as unknown as ReadonlyArray<Message>,
824
+ merkleTree: nextMerkleTree,
825
+ timestamp: nextTimestamp,
826
+ }),
827
+ ),
828
+ Effect.option,
829
+ );
830
+ }).pipe(sqlite.transaction("exclusive")),
831
+ );
832
+
833
+ interface SerializedSqliteQuery {
834
+ readonly sql: string;
835
+ readonly parameters: (
836
+ | null
837
+ | string
838
+ | number
839
+ | Array<number>
840
+ | { json: JsonObjectOrArray }
841
+ )[];
842
+ readonly options?: SqliteQueryOptions;
843
+ }
844
+
845
+ // We use queries as keys, hence JSON.stringify.
846
+ export const serializeQuery = <R extends Row>({
847
+ sql,
848
+ parameters = [],
849
+ options,
850
+ }: SqliteQuery): Query<R> => {
851
+ const query: SerializedSqliteQuery = {
852
+ sql,
853
+ parameters: parameters.map((p) =>
854
+ Predicate.isUint8Array(p)
855
+ ? Arr.fromIterable(p)
856
+ : isJsonObjectOrArray(p)
857
+ ? { json: p }
858
+ : p,
859
+ ),
860
+ ...(options && { options }),
861
+ };
862
+ return JSON.stringify(query) as Query<R>;
863
+ };
864
+
865
+ export const deserializeQuery = <R extends Row>(
866
+ query: Query<R>,
867
+ ): SqliteQuery => {
868
+ const serializedSqliteQuery = JSON.parse(query) as SerializedSqliteQuery;
869
+ return {
870
+ ...serializedSqliteQuery,
871
+ parameters: serializedSqliteQuery.parameters.map((p) =>
872
+ Arr.isArray(p)
873
+ ? new Uint8Array(p)
874
+ : typeof p === "object" && p != null
875
+ ? p.json
876
+ : p,
877
+ ),
878
+ };
879
+ };
880
+
881
+ /**
882
+ * Extract {@link Row} from {@link Query} instance.
883
+ *
884
+ * @example
885
+ * const allTodos = evolu.createQuery((db) =>
886
+ * db.selectFrom("todo").selectAll(),
887
+ * );
888
+ * type AllTodosRow = ExtractRow<typeof allTodos>;
889
+ */
890
+ export type ExtractRow<T extends Query> = T extends Query<infer R> ? R : never;
891
+
892
+ // To preserve identity.
893
+ const _emptyRows: ReadonlyArray<Row> = [];
894
+ export const emptyRows = <R extends Row>(): ReadonlyArray<R> =>
895
+ _emptyRows as ReadonlyArray<R>;
896
+
897
+ /** An object with rows and row properties. */
898
+ export interface QueryResult<R extends Row = Row> {
899
+ /** An array containing all the rows returned by the query. */
900
+ readonly rows: ReadonlyArray<Readonly<Kysely.Simplify<R>>>;
901
+
902
+ /**
903
+ * The first row returned by the query, or null if no rows were returned. This
904
+ * property is useful for queries that are expected to return a single row.
905
+ */
906
+ readonly row: Readonly<Kysely.Simplify<R>> | null;
907
+ }
908
+
909
+ export type Queries<R extends Row = Row> = ReadonlyArray<Query<R>>;
910
+
911
+ export type QueryResultsFromQueries<Q extends Queries> = {
912
+ [P in keyof Q]: Q[P] extends Query<infer R> ? QueryResult<R> : never;
913
+ };
914
+
915
+ export type QueryResultsPromisesFromQueries<Q extends Queries> = {
916
+ [P in keyof Q]: Q[P] extends Query<infer R> ? Promise<QueryResult<R>> : never;
917
+ };
918
+
919
+ // To preserve identity.
920
+ const queryResultCache = new WeakMap<ReadonlyArray<Row>, QueryResult<Row>>();
921
+ export const queryResultFromRows = <R extends Row>(
922
+ rows: ReadonlyArray<R>,
923
+ ): QueryResult<R> => {
924
+ let queryResult = queryResultCache.get(rows);
925
+ if (queryResult == null) {
926
+ queryResult = { rows, row: rows[0] };
927
+ queryResultCache.set(rows, queryResult);
928
+ }
929
+ return queryResult as QueryResult<R>;
930
+ };
931
+
932
+ export const notSupportedPlatformWorker: Db = {
933
+ init: () =>
934
+ Effect.fail<NotSupportedPlatformError>({
935
+ _tag: "NotSupportedPlatformError",
936
+ }),
937
+ loadQueries: () => Effect.succeed([]),
938
+ mutate: () => Effect.succeed([]),
939
+ resetOwner: () => Effect.void,
940
+ restoreOwner: () => Effect.void,
941
+ ensureSchema: () => Effect.void,
942
+ sync: () => Effect.succeed([]),
943
+ };