@evolu/common 4.1.1 → 5.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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Config.d.ts +27 -30
  3. package/dist/src/Config.d.ts.map +1 -1
  4. package/dist/src/Config.js +32 -5
  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 +368 -170
  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 -16
  35. package/dist/src/Sqlite.d.ts.map +1 -1
  36. package/dist/src/Sqlite.js +28 -20
  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 +77 -36
  46. package/src/Crdt.ts +61 -76
  47. package/src/Crypto.ts +83 -60
  48. package/src/Db.ts +782 -352
  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 -44
  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,158 +162,227 @@ 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;
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
+ );
221
186
 
222
- const _emptyRows: ReadonlyArray<Row> = [];
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
+ );
223
209
 
224
- export const emptyRows = <R extends Row>(): ReadonlyArray<R> =>
225
- _emptyRows as ReadonlyArray<R>;
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
+ }),
226
242
 
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>>>;
243
+ loadQueries: (queries) =>
244
+ Effect.logDebug(["Db loadQueries", { queries }]).pipe(
245
+ Effect.zipRight(loadQueries(queries, queryRowsRef)),
246
+ afterInit({ transaction: "shared" }),
247
+ ),
231
248
 
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
- }
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;
238
254
 
239
- export type QueryResultsFromQueries<Q extends Queries> = {
240
- [P in keyof Q]: Q[P] extends Query<infer R> ? QueryResult<R> : never;
241
- };
255
+ const [toSyncMutations, localOnlyMutations] = Arr.partition(
256
+ mutations,
257
+ /** Table name starting with '_' is local only (not synced). */
258
+ (mutation) => mutation.table.startsWith("_"),
259
+ );
242
260
 
243
- export type QueryResultsPromisesFromQueries<Q extends Queries> = {
244
- [P in keyof Q]: Q[P] extends Query<infer R> ? Promise<QueryResult<R>> : never;
245
- };
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: `
@@ -369,9 +392,9 @@ select
369
392
  from
370
393
  sqlite_master
371
394
  join pragma_table_info(sqlite_master.name) as table_info
372
- `.trim(),
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,14 +404,14 @@ from
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
417
  select
@@ -401,8 +424,8 @@ where
401
424
  name not like 'index_evolu_%'
402
425
  `.trim(),
403
426
  }),
404
- Effect.map((result) =>
405
- ReadonlyArray.map(
427
+ (result) =>
428
+ Arr.map(
406
429
  result.rows,
407
430
  (row): Index => ({
408
431
  name: row.name as string,
@@ -414,42 +437,37 @@ where
414
437
  sql: (row.sql as string).replace("CREATE INDEX", "create index"),
415
438
  }),
416
439
  ),
417
- ),
418
440
  );
419
441
 
420
442
  return { tables, indexes };
421
443
  },
422
444
  );
423
445
 
424
- export const ensureSchema = (
425
- schema: SqliteSchema,
426
- ): Effect.Effect<void, never, Sqlite> =>
427
- Effect.gen(function* (_) {
428
- const sqlite = yield* _(Sqlite);
429
- const currentSchema = yield* _(getSchema);
430
-
446
+ const ensureSchema = (newSchema: DbSchema) => (currentSchema: DbSchema) =>
447
+ Effect.gen(function* () {
448
+ yield* Effect.logTrace("Db ensureSchema");
431
449
  const sql: string[] = [];
432
450
 
433
- schema.tables.forEach((table) => {
451
+ newSchema.tables.forEach((table) => {
434
452
  const currentTable = currentSchema.tables.find(
435
453
  (t) => t.name === table.name,
436
454
  );
437
455
  if (!currentTable) {
438
456
  sql.push(
439
457
  `
440
- create table ${table.name} (
441
- "id" text primary key,
442
- ${table.columns
443
- .filter((c) => c !== "id")
444
- // "A column with affinity BLOB does not prefer one storage class over another
445
- // and no attempt is made to coerce data from one storage class into another."
446
- // https://www.sqlite.org/datatype3.html
447
- .map((name) => `"${name}" blob`)
448
- .join(", ")}
449
- );`.trim(),
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(),
450
468
  );
451
469
  } else {
452
- ReadonlyArray.differenceWith(String.Equivalence)(
470
+ Arr.differenceWith(String.Equivalence)(
453
471
  table.columns,
454
472
  currentTable.columns,
455
473
  ).forEach((newColumn) => {
@@ -461,53 +479,465 @@ create table ${table.name} (
461
479
  });
462
480
 
463
481
  // Remove old indexes.
464
- ReadonlyArray.differenceWith(indexEquivalence)(
482
+ Arr.differenceWith(indexEquivalence)(
465
483
  currentSchema.indexes,
466
- ReadonlyArray.intersectionWith(indexEquivalence)(
484
+ Arr.intersectionWith(indexEquivalence)(
467
485
  currentSchema.indexes,
468
- schema.indexes,
486
+ newSchema.indexes,
469
487
  ),
470
488
  ).forEach((indexToDrop) => {
471
489
  sql.push(`drop index "${indexToDrop.name}";`);
472
490
  });
473
491
 
474
492
  // Add new indexes.
475
- ReadonlyArray.differenceWith(indexEquivalence)(
476
- schema.indexes,
493
+ Arr.differenceWith(indexEquivalence)(
494
+ newSchema.indexes,
477
495
  currentSchema.indexes,
478
496
  ).forEach((newIndex) => {
479
497
  sql.push(`${newIndex.sql};`);
480
498
  });
499
+ if (sql.length > 0) {
500
+ const sqlite = yield* Sqlite;
501
+ yield* sqlite.exec({ sql: sql.join("\n") });
502
+ }
503
+ });
481
504
 
482
- if (sql.length > 0)
483
- yield* _(
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),
484
530
  sqlite.exec({
485
- sql: sql.join("\n"),
531
+ ...Sql.insertOwner,
532
+ parameters: [
533
+ owner.id,
534
+ owner.mnemonic,
535
+ owner.encryptionKey,
536
+ timestampToString(initialTimestampString),
537
+ merkleTreeToString(initialMerkleTree),
538
+ ],
486
539
  }),
487
- );
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;
488
563
  });
489
564
 
490
- export const dropAllTables: Effect.Effect<void, never, Sqlite> = Effect.gen(
491
- function* (_) {
492
- const sqlite = yield* _(Sqlite);
493
- const schema = yield* _(getSchema);
494
- const sql = schema.tables
495
- // The dropped table is completely removed from the database schema and
496
- // the disk file. The table can not be recovered.
497
- // All indices and triggers associated with the table are also deleted.
498
- // https://sqlite.org/lang_droptable.html
499
- .map((table) => `drop table "${table.name}";`)
500
- .join("");
501
- yield* _(sqlite.exec({ sql }));
502
- },
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
+ })),
503
573
  );
504
574
 
505
- export type RowsStore = Store<RowsStoreValue>;
506
- export const RowsStore = Context.GenericTag<RowsStore>("@services/RowsStore");
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(
609
+ sqlite.exec({
610
+ ...Sql.selectLastTimestampForTableRowColumn,
611
+ parameters: [message.table, message.row, message.column, 1],
612
+ }),
613
+ ({ rows }) =>
614
+ rows.length > 0 ? (rows[0].timestamp as TimestampString) : null,
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;
639
+ });
640
+
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
+ );
507
673
 
508
- type RowsStoreValue = ReadonlyMap<Query, ReadonlyArray<Row>>;
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
+ });
509
681
 
510
- export const RowsStoreLive = Layer.effect(
511
- RowsStore,
512
- makeStore<RowsStoreValue>(new Map()),
513
- );
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
+ });
703
+
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
+ };