@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20

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 (140) hide show
  1. package/dist/src/Assert.d.ts.map +1 -1
  2. package/dist/src/Assert.js +1 -1
  3. package/dist/src/Buffer.d.ts +1 -1
  4. package/dist/src/Buffer.d.ts.map +1 -1
  5. package/dist/src/Buffer.js +1 -1
  6. package/dist/src/CallbackRegistry.d.ts +53 -0
  7. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  8. package/dist/src/CallbackRegistry.js +25 -0
  9. package/dist/src/Console.d.ts +31 -6
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +72 -9
  12. package/dist/src/Crypto.d.ts +48 -37
  13. package/dist/src/Crypto.d.ts.map +1 -1
  14. package/dist/src/Crypto.js +27 -50
  15. package/dist/src/Evolu/Db.d.ts +138 -66
  16. package/dist/src/Evolu/Db.d.ts.map +1 -1
  17. package/dist/src/Evolu/Db.js +248 -645
  18. package/dist/src/Evolu/Diff.d.ts +3 -3
  19. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  20. package/dist/src/Evolu/Diff.js +7 -5
  21. package/dist/src/Evolu/Evolu.d.ts +79 -116
  22. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  23. package/dist/src/Evolu/Evolu.js +275 -132
  24. package/dist/src/Evolu/Internal.d.ts +0 -2
  25. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  26. package/dist/src/Evolu/Internal.js +0 -2
  27. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  28. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  29. package/dist/src/Evolu/LocalAuth.js +171 -0
  30. package/dist/src/Evolu/Owner.d.ts +129 -83
  31. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  32. package/dist/src/Evolu/Owner.js +80 -89
  33. package/dist/src/Evolu/Platform.d.ts +9 -7
  34. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  35. package/dist/src/Evolu/Protocol.d.ts +114 -191
  36. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  37. package/dist/src/Evolu/Protocol.js +409 -416
  38. package/dist/src/Evolu/Public.d.ts +6 -8
  39. package/dist/src/Evolu/Public.d.ts.map +1 -1
  40. package/dist/src/Evolu/Public.js +2 -3
  41. package/dist/src/Evolu/PublicKysely.js +3 -3
  42. package/dist/src/Evolu/Relay.d.ts +1 -2
  43. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  44. package/dist/src/Evolu/Relay.js +11 -9
  45. package/dist/src/Evolu/Schema.d.ts +88 -27
  46. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  47. package/dist/src/Evolu/Schema.js +141 -24
  48. package/dist/src/Evolu/Storage.d.ts +158 -14
  49. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  50. package/dist/src/Evolu/Storage.js +32 -32
  51. package/dist/src/Evolu/Sync.d.ts +77 -13
  52. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  53. package/dist/src/Evolu/Sync.js +453 -20
  54. package/dist/src/Evolu/Timestamp.d.ts +29 -27
  55. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  56. package/dist/src/Evolu/Timestamp.js +20 -18
  57. package/dist/src/ManyToManyMap.d.ts +74 -10
  58. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  59. package/dist/src/ManyToManyMap.js +41 -6
  60. package/dist/src/Random.d.ts +3 -2
  61. package/dist/src/Random.d.ts.map +1 -1
  62. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  63. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  64. package/dist/src/RefCountedResourceManager.js +197 -0
  65. package/dist/src/Result.d.ts +144 -22
  66. package/dist/src/Result.d.ts.map +1 -1
  67. package/dist/src/Result.js +5 -2
  68. package/dist/src/Sqlite.d.ts +20 -4
  69. package/dist/src/Sqlite.d.ts.map +1 -1
  70. package/dist/src/Sqlite.js +50 -8
  71. package/dist/src/Task.d.ts +511 -0
  72. package/dist/src/Task.d.ts.map +1 -0
  73. package/dist/src/Task.js +410 -0
  74. package/dist/src/Time.d.ts +59 -0
  75. package/dist/src/Time.d.ts.map +1 -1
  76. package/dist/src/Time.js +87 -4
  77. package/dist/src/Type.d.ts +431 -341
  78. package/dist/src/Type.d.ts.map +1 -1
  79. package/dist/src/Type.js +458 -466
  80. package/dist/src/WebSocket.d.ts +5 -2
  81. package/dist/src/WebSocket.d.ts.map +1 -1
  82. package/dist/src/WebSocket.js +12 -13
  83. package/dist/src/Worker.d.ts +39 -11
  84. package/dist/src/Worker.d.ts.map +1 -1
  85. package/dist/src/Worker.js +22 -4
  86. package/dist/src/index.d.ts +2 -3
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +2 -3
  89. package/package.json +7 -7
  90. package/src/Assert.ts +2 -4
  91. package/src/Buffer.ts +1 -1
  92. package/src/CallbackRegistry.ts +84 -0
  93. package/src/Console.ts +91 -11
  94. package/src/Crypto.ts +78 -91
  95. package/src/Evolu/Db.ts +455 -947
  96. package/src/Evolu/Diff.ts +7 -5
  97. package/src/Evolu/Evolu.ts +545 -307
  98. package/src/Evolu/Internal.ts +0 -2
  99. package/src/Evolu/LocalAuth.ts +422 -0
  100. package/src/Evolu/Owner.ts +191 -131
  101. package/src/Evolu/Platform.ts +9 -9
  102. package/src/Evolu/Protocol.ts +536 -653
  103. package/src/Evolu/Public.ts +7 -9
  104. package/src/Evolu/PublicKysely.ts +3 -3
  105. package/src/Evolu/Relay.ts +17 -12
  106. package/src/Evolu/Schema.ts +271 -66
  107. package/src/Evolu/Storage.ts +263 -55
  108. package/src/Evolu/Sync.ts +758 -37
  109. package/src/Evolu/Timestamp.ts +30 -35
  110. package/src/ManyToManyMap.ts +127 -24
  111. package/src/Random.ts +3 -2
  112. package/src/RefCountedResourceManager.ts +368 -0
  113. package/src/Result.ts +149 -23
  114. package/src/Sqlite.ts +59 -24
  115. package/src/Task.ts +779 -0
  116. package/src/Time.ts +168 -4
  117. package/src/Type.ts +657 -695
  118. package/src/WebSocket.ts +23 -17
  119. package/src/Worker.ts +72 -23
  120. package/src/index.ts +2 -3
  121. package/dist/src/Callbacks.d.ts +0 -20
  122. package/dist/src/Callbacks.d.ts.map +0 -1
  123. package/dist/src/Callbacks.js +0 -18
  124. package/dist/src/Evolu/Config.d.ts +0 -82
  125. package/dist/src/Evolu/Config.d.ts.map +0 -1
  126. package/dist/src/Evolu/Config.js +0 -9
  127. package/dist/src/Evolu/Kysely.d.ts +0 -6
  128. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  129. package/dist/src/Evolu/Kysely.js +0 -21
  130. package/dist/src/NanoId.d.ts +0 -27
  131. package/dist/src/NanoId.d.ts.map +0 -1
  132. package/dist/src/NanoId.js +0 -6
  133. package/dist/src/Promise.d.ts +0 -180
  134. package/dist/src/Promise.d.ts.map +0 -1
  135. package/dist/src/Promise.js +0 -176
  136. package/src/Callbacks.ts +0 -43
  137. package/src/Evolu/Config.ts +0 -97
  138. package/src/Evolu/Kysely.ts +0 -38
  139. package/src/NanoId.ts +0 -39
  140. package/src/Promise.ts +0 -295
package/src/Evolu/Db.ts CHANGED
@@ -1,64 +1,43 @@
1
1
  import { isNonEmptyArray, NonEmptyReadonlyArray } from "../Array.js";
2
- import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
3
- import { CallbackId } from "../Callbacks.js";
4
- import { ConsoleDep } from "../Console.js";
2
+ import { CallbackId } from "../CallbackRegistry.js";
3
+ import { ConsoleConfig, ConsoleDep } from "../Console.js";
5
4
  import {
6
- CreateMnemonicDep,
7
- CreateRandomBytesDep,
8
5
  createSymmetricCrypto,
9
6
  EncryptionKey,
7
+ RandomBytesDep,
10
8
  SymmetricCryptoDecryptError,
11
- SymmetricCryptoDep,
12
9
  } from "../Crypto.js";
13
- import { eqArrayNumber } from "../Eq.js";
14
10
  import { TransferableError } from "../Error.js";
15
- import { constFalse, exhaustiveCheck } from "../Function.js";
16
- import { NanoIdLibDep } from "../NanoId.js";
17
- import { objectToEntries } from "../Object.js";
18
11
  import { RandomDep } from "../Random.js";
19
12
  import { ok, Result } from "../Result.js";
20
13
  import {
21
14
  createSqlite,
22
15
  CreateSqliteDriverDep,
23
16
  explainSqliteQueryPlan,
24
- SafeSql,
25
17
  sql,
26
18
  SqliteDep,
27
19
  SqliteError,
28
- SqliteQuery,
29
- SqliteRow,
30
- SqliteValue,
31
20
  } from "../Sqlite.js";
32
21
  import { TimeDep } from "../Time.js";
33
- import { array, Id, Mnemonic, object, SimpleName, String } from "../Type.js";
22
+ import { Id, Mnemonic, SimpleName } from "../Type.js";
23
+ import { CreateWebSocketDep } from "../WebSocket.js";
34
24
  import {
35
- createInitializedWorker,
25
+ createInitializedWorkerWithHandlers,
26
+ MessageHandlers,
36
27
  Worker,
37
- WorkerPostMessageDep,
38
28
  } from "../Worker.js";
39
- import { Config } from "./Config.js";
40
29
  import { makePatches, QueryPatches } from "./Diff.js";
41
- import { AppOwner, createAppOwner, OwnerId, WriteKey } from "./Owner.js";
42
30
  import {
43
- applyProtocolMessageAsClient,
44
- Base64Url256,
45
- BinaryId,
46
- binaryIdToId,
47
- BinaryOwnerId,
48
- binaryOwnerIdToOwnerId,
49
- CrdtMessage,
50
- createProtocolMessageForSync,
51
- createProtocolMessageFromCrdtMessages,
52
- DbChange,
53
- decryptAndDecodeDbChange,
54
- encodeAndEncryptDbChange,
55
- idToBinaryId,
56
- ownerIdToBinaryOwnerId,
57
- ProtocolError,
58
- protocolVersion,
59
- Storage,
60
- StorageDep,
61
- } from "./Protocol.js";
31
+ AppOwner,
32
+ createAppOwner,
33
+ createOwnerSecret,
34
+ mnemonicToOwnerSecret,
35
+ OwnerEncryptionKey,
36
+ OwnerId,
37
+ OwnerWriteKey,
38
+ TransportConfig,
39
+ } from "./Owner.js";
40
+ import { ProtocolError, protocolVersion } from "./Protocol.js";
62
41
  import {
63
42
  createQueryRowsCache,
64
43
  deserializeQuery,
@@ -67,42 +46,145 @@ import {
67
46
  QueryRowsCache,
68
47
  } from "./Query.js";
69
48
  import {
70
- createSqliteStorageBase,
71
- CreateSqliteStorageBaseOptions,
72
- SqliteStorageBase,
73
- } from "./Storage.js";
74
- import { CreateSyncDep, SyncConfig, SyncDep } from "./Sync.js";
49
+ DbSchema,
50
+ ensureDbSchema,
51
+ getDbSchema,
52
+ MutationChange,
53
+ } from "./Schema.js";
54
+ import {
55
+ applyLocalOnlyChange,
56
+ Clock,
57
+ createClock,
58
+ createSync,
59
+ SyncDep,
60
+ SyncOwner,
61
+ } from "./Sync.js";
75
62
  import {
76
- binaryTimestampToTimestamp,
77
- createInitialTimestamp,
78
- receiveTimestamp,
79
- sendTimestamp,
80
63
  Timestamp,
81
- TimestampConfigDep,
82
- TimestampCounterOverflowError,
83
- TimestampDriftError,
64
+ TimestampConfig,
84
65
  TimestampError,
85
66
  TimestampString,
86
67
  timestampStringToTimestamp,
87
- TimestampTimeOutOfRangeError,
88
- timestampToBinaryTimestamp,
89
68
  timestampToTimestampString,
90
69
  } from "./Timestamp.js";
91
70
 
92
- export const DbTable = object({
93
- name: Base64Url256,
94
- columns: array(Base64Url256),
95
- });
96
- export type DbTable = typeof DbTable.Type;
97
-
98
- export const DbIndex = object({ name: String, sql: String });
99
- export type DbIndex = typeof DbIndex.Type;
71
+ export interface DbConfig extends ConsoleConfig, TimestampConfig {
72
+ /**
73
+ * The name of the Evolu instance. Evolu is multitenant - it can run multiple
74
+ * instances concurrently. Each instance must have a unique name.
75
+ *
76
+ * The instance name is used as the SQLite database filename for persistent
77
+ * storage, ensuring that database files are separated and invisible to each
78
+ * other.
79
+ *
80
+ * The default value is: `Evolu`.
81
+ *
82
+ * ### Example
83
+ *
84
+ * ```ts
85
+ * // name: SimpleName.orThrow("MyApp")
86
+ * ```
87
+ */
88
+ readonly name: SimpleName;
89
+
90
+ /**
91
+ * Transport configuration for data sync and backup. Supports single transport
92
+ * or multiple transports simultaneously for redundancy.
93
+ *
94
+ * Currently supports:
95
+ *
96
+ * - WebSocket: Real-time bidirectional communication with relay servers
97
+ *
98
+ * Empty transports create local-only instances. Transports can be dynamically
99
+ * added and removed for any owner (including {@link AppOwner}) via
100
+ * {@link Evolu#useOwner}.
101
+ *
102
+ * The default value is:
103
+ *
104
+ * `{ type: "WebSocket", url: "wss://free.evoluhq.com" }`.
105
+ *
106
+ * ### Example
107
+ *
108
+ * ```ts
109
+ * // Single WebSocket relay
110
+ * transports: [{ type: "WebSocket", url: "wss://relay1.example.com" }];
111
+ *
112
+ * // Multiple WebSocket relays for redundancy
113
+ * transports: [
114
+ * { type: "WebSocket", url: "wss://relay1.example.com" },
115
+ * { type: "WebSocket", url: "wss://relay2.example.com" },
116
+ * { type: "WebSocket", url: "wss://relay3.example.com" },
117
+ * ];
118
+ *
119
+ * // Local-only instance (no sync) - useful for device settings
120
+ * transports: [];
121
+ * ```
122
+ */
123
+ readonly transports: ReadonlyArray<TransportConfig>;
124
+
125
+ /**
126
+ * External AppOwner to use when creating Evolu instance. Use this when you
127
+ * want to manage AppOwner creation and persistence externally (e.g., with
128
+ * your own authentication system). If omitted, Evolu will automatically
129
+ * create and persist an AppOwner locally.
130
+ *
131
+ * For device-specific settings and account management state, we can use a
132
+ * separate local-only Evolu instance via `transports: []`.
133
+ *
134
+ * ### Example
135
+ *
136
+ * ```ts
137
+ * const ConfigId = id("Config");
138
+ * type ConfigId = typeof ConfigId.Type;
139
+ *
140
+ * const DeviceSchema = {
141
+ * config: {
142
+ * id: ConfigId,
143
+ * key: NonEmptyString50,
144
+ * value: NonEmptyString50,
145
+ * },
146
+ * };
147
+ *
148
+ * // Local-only instance for device settings (no sync)
149
+ * const deviceEvolu = createEvolu(evoluReactWebDeps)(DeviceSchema, {
150
+ * name: SimpleName.orThrow("MyApp-Device"),
151
+ * transports: [], // No sync - stays local to device
152
+ * });
153
+ *
154
+ * // Main synced instance for user data
155
+ * const evolu = createEvolu(evoluReactWebDeps)(MainSchema, {
156
+ * name: SimpleName.orThrow("MyApp"),
157
+ * // Default transports for sync
158
+ * });
159
+ * ```
160
+ */
161
+ readonly externalAppOwner?: AppOwner;
162
+
163
+ /**
164
+ * Use in-memory SQLite database instead of persistent storage. Useful for
165
+ * testing or temporary data that doesn't need persistence.
166
+ *
167
+ * In-memory databases exist only in RAM and are completely destroyed when the
168
+ * process ends, making them forensically safe for sensitive data.
169
+ *
170
+ * The default value is: `false`.
171
+ */
172
+ readonly inMemory?: boolean;
173
+
174
+ /**
175
+ * Encryption key for the SQLite database.
176
+ *
177
+ * @experimental
178
+ */
179
+ readonly encryptionKey?: EncryptionKey;
180
+ }
100
181
 
101
- export const DbSchema = object({
102
- tables: array(DbTable),
103
- indexes: array(DbIndex),
104
- });
105
- export type DbSchema = typeof DbSchema.Type;
182
+ export const defaultDbConfig: DbConfig = {
183
+ name: SimpleName.orThrow("Evolu"),
184
+ transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
185
+ maxDrift: 5 * 60 * 1000,
186
+ enableLogging: false,
187
+ };
106
188
 
107
189
  export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
108
190
 
@@ -115,13 +197,16 @@ export interface CreateDbWorkerDep {
115
197
  export type DbWorkerInput =
116
198
  | {
117
199
  readonly type: "init";
118
- readonly config: Config;
200
+ readonly config: DbConfig;
119
201
  readonly dbSchema: DbSchema;
120
202
  }
203
+ | {
204
+ readonly type: "getAppOwner";
205
+ }
121
206
  | {
122
207
  readonly type: "mutate";
123
208
  readonly tabId: Id;
124
- readonly changes: NonEmptyReadonlyArray<DbChange>;
209
+ readonly changes: NonEmptyReadonlyArray<MutationChange>;
125
210
  readonly onCompleteIds: ReadonlyArray<CallbackId>;
126
211
  readonly subscribedQueries: ReadonlyArray<Query>;
127
212
  }
@@ -146,31 +231,35 @@ export type DbWorkerInput =
146
231
  | {
147
232
  readonly type: "export";
148
233
  readonly onCompleteId: CallbackId;
234
+ }
235
+ | {
236
+ readonly type: "useOwner";
237
+ readonly use: boolean;
238
+ readonly owner: SyncOwner;
149
239
  };
150
240
 
151
241
  export type DbWorkerOutput =
152
- | {
153
- readonly type: "onInit";
154
- readonly appOwner: AppOwner;
155
- readonly isFirst: boolean;
156
- }
157
242
  | {
158
243
  readonly type: "onError";
159
244
  readonly error:
245
+ | ProtocolError
160
246
  | SqliteError
161
- | TransferableError
247
+ | SymmetricCryptoDecryptError
162
248
  | TimestampError
163
- | ProtocolError
164
- | SymmetricCryptoDecryptError;
249
+ | TransferableError;
165
250
  }
166
251
  | {
167
- readonly type: "onChange";
252
+ readonly type: "onGetAppOwner";
253
+ readonly appOwner: AppOwner;
254
+ }
255
+ | {
256
+ readonly type: "onQueryPatches";
168
257
  readonly tabId: Id;
169
- readonly patches: ReadonlyArray<QueryPatches>;
258
+ readonly queryPatches: ReadonlyArray<QueryPatches>;
170
259
  readonly onCompleteIds: ReadonlyArray<CallbackId>;
171
260
  }
172
261
  | {
173
- readonly type: "onReceive";
262
+ readonly type: "refreshQueries";
174
263
  readonly tabId?: Id;
175
264
  }
176
265
  | {
@@ -184,617 +273,196 @@ export type DbWorkerOutput =
184
273
  readonly file: Uint8Array;
185
274
  };
186
275
 
187
- export type DbWorkerPlatformDeps = CreateSqliteDriverDep &
188
- CreateSyncDep &
189
- ConsoleDep &
190
- TimeDep &
276
+ export type DbWorkerPlatformDeps = ConsoleDep &
277
+ CreateSqliteDriverDep &
278
+ CreateWebSocketDep &
279
+ RandomBytesDep &
191
280
  RandomDep &
192
- NanoIdLibDep &
193
- CreateMnemonicDep &
194
- CreateRandomBytesDep;
281
+ TimeDep;
195
282
 
196
283
  type DbWorkerDeps = Omit<
197
284
  DbWorkerPlatformDeps,
198
- keyof CreateSqliteDriverDep | keyof CreateSyncDep
285
+ keyof CreateSqliteDriverDep | keyof CreateWebSocketDep
199
286
  > &
287
+ GetQueryRowsCacheDep &
288
+ PostMessageDep &
200
289
  SqliteDep &
201
290
  SyncDep &
202
- TimestampConfigDep &
203
- SymmetricCryptoDep &
204
- PostMessageDep &
205
- OwnersDep &
206
- ClockDep &
207
- GetQueryRowsCacheDep &
208
- ClientStorageDep;
209
-
210
- type PostMessageDep = WorkerPostMessageDep<DbWorkerOutput>;
211
-
212
- interface OwnersDep {
213
- readonly owners: Owners;
214
- }
215
-
216
- type Owners = Map<OwnerId, AppOwner>;
217
-
218
- interface ClockDep {
219
- readonly clock: Clock;
220
- }
221
-
222
- interface Clock {
223
- readonly get: () => Timestamp;
224
- readonly save: (timestamp: Timestamp) => Result<void, SqliteError>;
225
- }
291
+ AppOwnerDep;
226
292
 
227
293
  interface GetQueryRowsCacheDep {
228
294
  readonly getQueryRowsCache: (tabId: Id) => QueryRowsCache;
229
295
  }
230
296
 
231
- const createClock =
232
- (deps: NanoIdLibDep & SqliteDep) =>
233
- (initialTimestamp = createInitialTimestamp(deps)): Clock => {
234
- let currentTimestamp = initialTimestamp;
235
-
236
- return {
237
- get: () => currentTimestamp,
238
- save: (timestamp) => {
239
- currentTimestamp = timestamp;
240
-
241
- const timestampString = timestampToTimestampString(timestamp);
242
- const saveTimestamp = deps.sqlite.exec(sql.prepared`
243
- update evolu_config set "clock" = ${timestampString};
244
- `);
245
- if (!saveTimestamp.ok) return saveTimestamp;
297
+ export interface PostMessageDep {
298
+ readonly postMessage: (message: DbWorkerOutput) => void;
299
+ }
246
300
 
247
- return ok();
248
- },
249
- };
250
- };
301
+ export interface AppOwnerDep {
302
+ readonly appOwner: AppOwner;
303
+ }
251
304
 
252
305
  export const createDbWorkerForPlatform = (
253
306
  platformDeps: DbWorkerPlatformDeps,
254
- ): DbWorker => {
255
- const tabQueryRowsCacheMap = new Map<Id, QueryRowsCache>();
256
- const getQueryRowsCache = (tabId: Id) => {
257
- let cache = tabQueryRowsCacheMap.get(tabId);
258
- if (!cache) {
259
- cache = createQueryRowsCache();
260
- tabQueryRowsCacheMap.set(tabId, cache);
307
+ ): DbWorker =>
308
+ createInitializedWorkerWithHandlers<
309
+ DbWorkerInput,
310
+ DbWorkerOutput,
311
+ DbWorkerDeps
312
+ >({ init: createDbWorkerDeps(platformDeps), handlers });
313
+
314
+ const createDbWorkerDeps =
315
+ (platformDeps: DbWorkerPlatformDeps) =>
316
+ async (
317
+ initMessage: Extract<DbWorkerInput, { type: "init" }>,
318
+ postMessage: (msg: DbWorkerOutput) => void,
319
+ ): Promise<DbWorkerDeps | null> => {
320
+ platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
321
+
322
+ const sqliteResult = await createSqlite(platformDeps)(
323
+ initMessage.config.name,
324
+ {
325
+ memory: initMessage.config.inMemory ?? false,
326
+ encryptionKey: initMessage.config.encryptionKey ?? undefined,
327
+ },
328
+ );
329
+ if (!sqliteResult.ok) {
330
+ postMessage({ type: "onError", error: sqliteResult.error });
331
+ return null;
261
332
  }
262
- return cache;
263
- };
264
-
265
- return createInitializedWorker<DbWorkerInput, DbWorkerOutput, DbWorkerDeps>({
266
- init: async (initMessage, postMessage, safeHandler) => {
267
- platformDeps.console.enabled = initMessage.config.enableLogging ?? false;
268
-
269
- const sqliteResult = await createSqlite(platformDeps)(
270
- initMessage.config.name,
271
- { memory: initMessage.config.inMemory ?? false },
272
- );
273
- if (!sqliteResult.ok) {
274
- postMessage({ type: "onError", error: sqliteResult.error });
275
- return null;
276
- }
277
-
278
- const sqlite = sqliteResult.value;
279
- const platformDepsWithSqlite = { ...platformDeps, sqlite };
333
+ const sqlite = sqliteResult.value;
280
334
 
281
- const deps = sqlite.transaction(() => {
282
- const currentDbSchema = getDbSchema({ sqlite })();
283
- if (!currentDbSchema.ok) return currentDbSchema;
335
+ const deps = sqlite.transaction(() => {
336
+ const currentDbSchema = getDbSchema({ sqlite })();
337
+ if (!currentDbSchema.ok) return currentDbSchema;
284
338
 
285
- let appOwner: AppOwner;
286
- let clock: Clock;
339
+ let appOwner: AppOwner;
340
+ let clock: Clock;
287
341
 
288
- const dbIsInitialized = currentDbSchema.value.tables.some(
289
- (table) => table.name === "evolu_version",
290
- );
342
+ const dbIsInitialized = currentDbSchema.value.tables.some(
343
+ (table) => table.name === "evolu_version",
344
+ );
291
345
 
292
- if (dbIsInitialized) {
293
- const versionResult = sqlite.exec<{
294
- protocolVersion: number;
295
- }>(sql`select protocolVersion from evolu_version limit 1;`);
296
- if (!versionResult.ok) return versionResult;
297
-
298
- // TODO: Handle version migrations here if needed
299
- // const [{ protocolVersion }] = protocolVersionResult.value.rows;
300
- // if (protocolVersion < currentProtocolVersion) {
301
- // const migrateResult = migrateDatabase({ sqlite })(
302
- // protocolVersion,
303
- // currentProtocolVersion
304
- // );
305
- // if (!migrateResult.ok) return migrateResult;
306
- // }
307
-
308
- const configResult = sqlite.exec<{
309
- clock: TimestampString;
310
- appOwnerId: OwnerId;
311
- appOwnerEncryptionKey: EncryptionKey;
312
- appOwnerWriteKey: WriteKey;
313
- appOwnerMnemonic: Mnemonic | null;
314
- }>(sql`
315
- select
316
- clock,
317
- appOwnerId,
318
- appOwnerEncryptionKey,
319
- appOwnerWriteKey,
320
- appOwnerMnemonic
321
- from evolu_config
322
- limit 1;
323
- `);
324
- if (!configResult.ok) return configResult;
325
-
326
- const [config] = configResult.value.rows;
327
-
328
- appOwner = {
329
- type: "AppOwner",
330
- id: config.appOwnerId,
331
- encryptionKey: config.appOwnerEncryptionKey,
332
- writeKey: config.appOwnerWriteKey,
333
- mnemonic: config.appOwnerMnemonic,
334
- };
335
- clock = createClock(platformDepsWithSqlite)(
336
- timestampStringToTimestamp(config.clock),
337
- );
338
- } else {
339
- appOwner =
340
- initMessage.config.initialAppOwner ??
341
- createAppOwner(platformDeps.createMnemonic());
342
- clock = createClock(platformDepsWithSqlite)();
343
- const initializeDbResult = initializeDb(platformDepsWithSqlite)(
344
- appOwner,
346
+ if (dbIsInitialized) {
347
+ const currentVersion = sqlite.exec<{
348
+ protocolVersion: number;
349
+ }>(sql`select protocolVersion from evolu_version limit 1;`);
350
+ if (!currentVersion.ok) return currentVersion;
351
+
352
+ // TODO: Handle version migrations here if needed
353
+ // const [{ protocolVersion }] = protocolVersionResult.value.rows;
354
+ // if (protocolVersion < currentProtocolVersion) {
355
+ // const migrateResult = migrateDatabase({ sqlite })(
356
+ // protocolVersion,
357
+ // currentProtocolVersion
358
+ // );
359
+ // if (!migrateResult.ok) return migrateResult;
360
+ // }
361
+
362
+ const configResult = sqlite.exec<{
363
+ clock: TimestampString;
364
+ appOwnerId: OwnerId;
365
+ appOwnerEncryptionKey: OwnerEncryptionKey;
366
+ appOwnerWriteKey: OwnerWriteKey;
367
+ appOwnerMnemonic: Mnemonic | null;
368
+ }>(sql`
369
+ select
345
370
  clock,
346
- );
347
- if (!initializeDbResult.ok) return initializeDbResult;
348
- }
349
-
350
- const ensureDbSchemaResult = ensureDbSchema({ sqlite })(
351
- initMessage.dbSchema,
352
- currentDbSchema.value,
353
- );
354
- if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
355
-
356
- const owners: Owners = new Map();
357
- owners.set(appOwner.id, appOwner);
358
-
359
- const depsWithoutSyncAndStorage = {
360
- ...platformDeps,
361
- postMessage,
362
- sqlite,
363
- timestampConfig: initMessage.config,
364
- symmetricCrypto: createSymmetricCrypto(platformDeps),
365
- getQueryRowsCache,
366
- clock,
367
- owners,
368
- };
371
+ appOwnerId,
372
+ appOwnerEncryptionKey,
373
+ appOwnerWriteKey,
374
+ appOwnerMnemonic
375
+ from evolu_config
376
+ limit 1;
377
+ `);
378
+ if (!configResult.ok) return configResult;
369
379
 
370
- const storage = createClientStorage(depsWithoutSyncAndStorage)({
371
- onStorageError: (error) => {
372
- postMessage({ type: "onError", error });
373
- },
374
- });
375
- if (!storage.ok) return storage;
380
+ const [config] = configResult.value.rows;
376
381
 
377
- const depsWithoutSync = {
378
- ...depsWithoutSyncAndStorage,
379
- storage: storage.value,
382
+ appOwner = {
383
+ type: "AppOwner",
384
+ id: config.appOwnerId,
385
+ encryptionKey: config.appOwnerEncryptionKey,
386
+ writeKey: config.appOwnerWriteKey,
387
+ mnemonic: config.appOwnerMnemonic,
380
388
  };
381
389
 
382
- postMessage({
383
- type: "onInit",
384
- appOwner,
385
- isFirst: !dbIsInitialized,
386
- });
387
-
388
- const sync = platformDeps.createSync(platformDeps)({
389
- ...initMessage.config,
390
- onOpen: safeHandler(handleSyncOpen(depsWithoutSync)),
391
- onMessage: safeHandler(createHandleSyncMessage(depsWithoutSync)),
392
- });
390
+ clock = createClock({ ...platformDeps, sqlite })(
391
+ timestampStringToTimestamp(config.clock),
392
+ );
393
+ } else {
394
+ appOwner =
395
+ initMessage.config.externalAppOwner ??
396
+ createAppOwner(createOwnerSecret(platformDeps));
393
397
 
394
- return ok({ ...depsWithoutSync, sync });
395
- });
398
+ clock = createClock({ ...platformDeps, sqlite })();
396
399
 
397
- if (!deps.ok) {
398
- postMessage({ type: "onError", error: deps.error });
399
- return null;
400
+ const result = initializeDb({ sqlite })(appOwner, clock.get());
401
+ if (!result.ok) return result;
400
402
  }
401
403
 
402
- return deps.value;
403
- },
404
-
405
- onMessage: (deps) => (message) => {
406
- switch (message.type) {
407
- case "mutate": {
408
- const mutate = deps.sqlite.transaction(() => {
409
- const toSyncChanges = [];
410
- const localOnlyChanges = [];
411
-
412
- for (const change of message.changes) {
413
- // Table name starting with '_' is local-only (not synced).
414
- if (change.table.startsWith("_")) localOnlyChanges.push(change);
415
- else toSyncChanges.push(change);
416
- }
417
-
418
- for (const change of localOnlyChanges) {
419
- if (
420
- "isDeleted" in change.values &&
421
- change.values.isDeleted === 1
422
- ) {
423
- const result = deps.sqlite.exec(sql`
424
- delete from ${sql.identifier(change.table)}
425
- where id = ${change.id};
426
- `);
427
- if (!result.ok) return result;
428
- } else {
429
- const date = new Date(deps.time.now()).toISOString();
430
- for (const [column, value] of objectToEntries(change.values)) {
431
- const result = deps.sqlite.exec(sql.prepared`
432
- insert into ${sql.identifier(change.table)}
433
- ("id", ${sql.identifier(column)}, createdAt, updatedAt)
434
- values (${change.id}, ${value}, ${date}, ${date})
435
- on conflict ("id") do update
436
- set
437
- ${sql.identifier(column)} = ${value},
438
- updatedAt = ${date};
439
- `);
440
- if (!result.ok) return result;
441
- }
442
- }
443
- }
444
-
445
- /**
446
- * We don't have to wait for the transaction to end because changes
447
- * are idempotent, so there is no reason why they should fail. We
448
- * want to call onChange ASAP.
449
- */
450
- const onChange = () => {
451
- deps.console.log("[db]", "onChange", {
452
- subscribedQueries: message.subscribedQueries,
453
- });
454
- const patches = loadQueries(deps)(
455
- message.tabId,
456
- message.subscribedQueries,
457
- );
458
- if (!patches.ok) {
459
- deps.postMessage({ type: "onError", error: patches.error });
460
- return;
461
- }
462
- // Notify the tab that performed the mutation.
463
- deps.postMessage({
464
- type: "onChange",
465
- tabId: message.tabId,
466
- patches: patches.value,
467
- onCompleteIds: message.onCompleteIds,
468
- });
469
- // Notify other tabs to refresh their queries.
470
- deps.postMessage({ type: "onReceive", tabId: message.tabId });
471
- };
472
-
473
- if (!isNonEmptyArray(toSyncChanges)) {
474
- onChange();
475
- return ok();
476
- }
477
-
478
- const messages = applyChanges(deps)(toSyncChanges, onChange);
479
- if (!messages.ok) return messages;
480
-
481
- // TODO: Use owner from db change or AppOwner
482
- const owner = Array.from(deps.owners.values())[0];
483
-
484
- const protocolMessage = createProtocolMessageFromCrdtMessages(deps)(
485
- owner,
486
- messages.value,
487
- );
488
-
489
- deps.console.log(
490
- "[db]",
491
- "send data message",
492
- messages.value,
493
- protocolMessage,
494
- );
495
- deps.sync.send(protocolMessage);
496
-
497
- return ok();
498
- });
499
-
500
- if (!mutate.ok) {
501
- deps.postMessage({ type: "onError", error: mutate.error });
502
- return;
503
- }
504
-
505
- break;
506
- }
507
-
508
- case "query": {
509
- const patches = loadQueries(deps)(message.tabId, message.queries);
510
-
511
- if (!patches.ok) {
512
- deps.postMessage({ type: "onError", error: patches.error });
513
- return;
514
- }
515
-
516
- deps.postMessage({
517
- type: "onChange",
518
- tabId: message.tabId,
519
- patches: patches.value,
520
- onCompleteIds: [],
521
- });
522
- break;
523
- }
524
-
525
- case "reset": {
526
- const resetResult = deps.sqlite.transaction(() => {
527
- const dropAllTablesResult = dropAllTables(deps);
528
- if (!dropAllTablesResult.ok) return dropAllTablesResult;
529
-
530
- if (message.restore) {
531
- const dbSchema = getDbSchema(deps)();
532
- if (!dbSchema.ok) return dbSchema;
533
-
534
- const ensureDbSchemaResult = ensureDbSchema(deps)(
535
- message.restore.dbSchema,
536
- dbSchema.value,
537
- );
538
- if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
539
-
540
- const appOwner = createAppOwner(message.restore.mnemonic);
541
- const clock = createClock(deps)();
542
-
543
- const initializeDbResult = initializeDb(deps)(appOwner, clock);
544
- if (!initializeDbResult.ok) return initializeDbResult;
545
- }
546
- return ok();
547
- });
548
-
549
- if (!resetResult.ok) {
550
- deps.postMessage({ type: "onError", error: resetResult.error });
551
- return;
552
- }
553
-
554
- deps.postMessage({
555
- type: "onReset",
556
- onCompleteId: message.onCompleteId,
557
- reload: message.reload,
558
- });
559
-
560
- break;
561
- }
562
-
563
- case "ensureDbSchema": {
564
- const ensureSchema = deps.sqlite.transaction(() => {
565
- const dbSchema = getDbSchema(deps)();
566
- if (!dbSchema.ok) return dbSchema;
567
-
568
- const ensureDbSchemaResult = ensureDbSchema(deps)(
569
- message.dbSchema,
570
- dbSchema.value,
571
- );
572
- if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
573
-
574
- return ok();
575
- });
576
-
577
- if (!ensureSchema.ok) {
578
- deps.postMessage({ type: "onError", error: ensureSchema.error });
579
- return;
580
- }
581
- break;
582
- }
404
+ const result = ensureDbSchema({ sqlite })(
405
+ initMessage.dbSchema,
406
+ currentDbSchema.value,
407
+ );
408
+ if (!result.ok) return result;
583
409
 
584
- case "export": {
585
- const file = deps.sqlite.export();
410
+ const sync = createSync({
411
+ ...platformDeps,
412
+ clock,
413
+ sqlite,
414
+ symmetricCrypto: createSymmetricCrypto(platformDeps),
415
+ timestampConfig: initMessage.config,
416
+ postMessage,
417
+ })({
418
+ appOwner,
419
+ transports: initMessage.config.transports,
420
+ onError: (error) => {
421
+ postMessage({ type: "onError", error });
422
+ },
423
+ onReceive: () => {
424
+ postMessage({ type: "refreshQueries" });
425
+ },
426
+ });
427
+ if (!sync.ok) return sync;
586
428
 
587
- if (!file.ok) {
588
- deps.postMessage({ type: "onError", error: file.error });
589
- return;
590
- }
429
+ sync.value.useOwner(true, appOwner);
591
430
 
592
- deps.postMessage({
593
- type: "onExport",
594
- onCompleteId: message.onCompleteId,
595
- file: file.value,
596
- });
597
- break;
431
+ const tabQueryRowsCacheMap = new Map<Id, QueryRowsCache>();
432
+ const getQueryRowsCache = (tabId: Id) => {
433
+ let cache = tabQueryRowsCacheMap.get(tabId);
434
+ if (!cache) {
435
+ cache = createQueryRowsCache();
436
+ tabQueryRowsCacheMap.set(tabId, cache);
598
437
  }
599
-
600
- default:
601
- exhaustiveCheck(message);
602
- }
603
- },
604
- });
605
- };
606
-
607
- /**
608
- * Get the current database schema by reading SQLite metadata.
609
- *
610
- * TODO: Refactor out Evolu stuff and move it to Sqlite.
611
- */
612
- export const getDbSchema =
613
- (deps: SqliteDep) =>
614
- ({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
615
- DbSchema,
616
- SqliteError
617
- > => {
618
- const map = new Map<Base64Url256, Array<Base64Url256>>();
619
-
620
- const tableAndColumnInfoRows = deps.sqlite.exec(sql`
621
- select
622
- sqlite_master.name as tableName,
623
- table_info.name as columnName
624
- from
625
- sqlite_master
626
- join pragma_table_info(sqlite_master.name) as table_info;
627
- `);
628
-
629
- if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
630
-
631
- tableAndColumnInfoRows.value.rows.forEach((row) => {
632
- const { tableName, columnName } = row as unknown as {
633
- tableName: Base64Url256;
634
- columnName: Base64Url256;
438
+ return cache;
635
439
  };
636
- if (!map.has(tableName)) map.set(tableName, []);
637
- map.get(tableName)?.push(columnName);
638
- });
639
-
640
- const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
641
-
642
- const indexesRows = deps.sqlite.exec(
643
- allIndexes
644
- ? sql`
645
- select name, sql
646
- from sqlite_master
647
- where type = 'index' and name not like 'sqlite_%';
648
- `
649
- : sql`
650
- select name, sql
651
- from sqlite_master
652
- where
653
- type = 'index'
654
- and name not like 'sqlite_%'
655
- and name not like 'evolu_%';
656
- `,
657
- );
658
-
659
- if (!indexesRows.ok) return indexesRows;
660
-
661
- const indexes = indexesRows.value.rows.map(
662
- (row): DbIndex => ({
663
- name: row.name as string,
664
- /**
665
- * SQLite returns "CREATE INDEX" for "create index" for some reason.
666
- * Other keywords remain unchanged. We have to normalize the casing for
667
- * {@link indexesAreEqual} manually.
668
- */
669
- sql: (row.sql as string)
670
- .replace("CREATE INDEX", "create index")
671
- .replace("CREATE UNIQUE INDEX", "create unique index"),
672
- }),
673
- );
674
-
675
- return ok({ tables, indexes });
676
- };
677
440
 
678
- const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
679
- self.name === that.name && self.sql === that.sql;
680
-
681
- export interface DbSnapshot {
682
- readonly schema: DbSchema;
683
- readonly tables: Array<{
684
- name: string;
685
- rows: ReadonlyArray<SqliteRow>;
686
- }>;
687
- }
688
-
689
- // TODO: Move to test helpers.
690
- export const getDbSnapshot = (deps: SqliteDep): DbSnapshot => {
691
- const schema = getDbSchema(deps)({ allIndexes: true });
692
- assert(schema.ok, "bug");
693
-
694
- const tables = [];
695
-
696
- for (const table of schema.value.tables) {
697
- const result = deps.sqlite.exec(sql`
698
- select * from ${sql.identifier(table.name)};
699
- `);
700
- assert(result.ok, "bug");
441
+ const deps: DbWorkerDeps = {
442
+ ...platformDeps,
443
+ getQueryRowsCache,
444
+ postMessage,
445
+ sqlite,
446
+ sync: sync.value,
447
+ appOwner,
448
+ };
701
449
 
702
- tables.push({
703
- name: table.name,
704
- rows: result.value.rows,
450
+ return ok(deps);
705
451
  });
706
- }
707
-
708
- return { schema: schema.value, tables };
709
- };
710
452
 
711
- const ensureDbSchema =
712
- (deps: SqliteDep) =>
713
- (
714
- newSchema: DbSchema,
715
- currentSchema: DbSchema,
716
- options?: { ignoreIndexes: boolean },
717
- ): Result<void, SqliteError> => {
718
- const queries: Array<SqliteQuery> = [];
719
-
720
- newSchema.tables.forEach((newTable) => {
721
- const currentTable = currentSchema.tables.find(
722
- (t) => t.name === newTable.name,
723
- );
724
- if (!currentTable) {
725
- queries.push({
726
- sql: createAppTable(newTable.name, newTable.columns),
727
- parameters: [],
728
- });
729
- } else {
730
- newTable.columns
731
- .filter((newColumn) => !currentTable.columns.includes(newColumn))
732
- .forEach((newColumn) => {
733
- queries.push(sql`
734
- alter table ${sql.identifier(newTable.name)}
735
- add column ${sql.identifier(newColumn)} blob;
736
- `);
737
- });
738
- }
739
- });
740
-
741
- if (options?.ignoreIndexes !== true) {
742
- // Remove current indexes that are not in the newSchema.
743
- currentSchema.indexes
744
- .filter(
745
- (currentIndex) =>
746
- !newSchema.indexes.some((newIndex) =>
747
- indexesAreEqual(newIndex, currentIndex),
748
- ),
749
- )
750
- .forEach((index) => {
751
- queries.push(sql`drop index ${sql.identifier(index.name)};`);
752
- });
753
-
754
- // Add new indexes that are not in the currentSchema.
755
- newSchema.indexes
756
- .filter(
757
- (newIndex) =>
758
- !currentSchema.indexes.some((currentIndex) =>
759
- indexesAreEqual(newIndex, currentIndex),
760
- ),
761
- )
762
- .forEach((newIndex) => {
763
- queries.push({ sql: `${newIndex.sql};` as SafeSql, parameters: [] });
764
- });
453
+ if (!deps.ok) {
454
+ postMessage({ type: "onError", error: deps.error });
455
+ return null;
765
456
  }
766
457
 
767
- for (const query of queries) {
768
- const result = deps.sqlite.exec(query);
769
- if (!result.ok) return result;
770
- }
771
- return ok();
458
+ return deps.value;
772
459
  };
773
460
 
774
- export const createAppTable = (
775
- tableName: string,
776
- columns: ReadonlyArray<string>,
777
- ): SafeSql =>
778
- `
779
- create table ${sql.identifier(tableName).sql} (
780
- "id" text primary key,
781
- ${columns
782
- // Add default columns.
783
- .concat(["createdAt", "updatedAt", "isDeleted"])
784
- .filter((c) => c !== "id")
785
- // "A column with affinity BLOB does not prefer one storage class over another
786
- // and no attempt is made to coerce data from one storage class into another."
787
- // https://www.sqlite.org/datatype3.html
788
- .map((name) => `${sql.identifier(name).sql} blob`)
789
- .join(", ")}
790
- );
791
- ` as SafeSql;
792
-
793
461
  const initializeDb =
794
- (deps: SqliteDep & CreateMnemonicDep & TimeDep & CreateRandomBytesDep) =>
462
+ (deps: SqliteDep) =>
795
463
  (
796
464
  initialAppOwner: AppOwner,
797
- initialClock: Clock,
465
+ initialClock: Timestamp,
798
466
  ): Result<void, SqliteError> => {
799
467
  for (const query of [
800
468
  // Never change structure to ensure all versions can read it.
@@ -832,7 +500,7 @@ const initializeDb =
832
500
  )
833
501
  values
834
502
  (
835
- ${timestampToTimestampString(initialClock.get())},
503
+ ${timestampToTimestampString(initialClock)},
836
504
  ${initialAppOwner.id},
837
505
  ${initialAppOwner.encryptionKey},
838
506
  ${initialAppOwner.writeKey},
@@ -885,140 +553,166 @@ const initializeDb =
885
553
  return ok();
886
554
  };
887
555
 
888
- const applyChanges =
889
- (
890
- deps: SqliteDep &
891
- TimeDep &
892
- TimestampConfigDep &
893
- RandomDep &
894
- ClientStorageDep &
895
- OwnersDep &
896
- ClockDep,
897
- ) =>
898
- (
899
- changes: NonEmptyReadonlyArray<DbChange>,
900
- onChange?: () => void,
901
- ): Result<
902
- NonEmptyReadonlyArray<CrdtMessage>,
903
- | TimestampTimeOutOfRangeError
904
- | TimestampDriftError
905
- | TimestampCounterOverflowError
906
- | SqliteError
907
- > => {
908
- let clockTimestamp = deps.clock.get();
909
-
910
- const messages: Array<CrdtMessage> = [];
911
-
912
- for (const change of changes) {
913
- const nextTimestamp = sendTimestamp(deps)(clockTimestamp);
914
- if (!nextTimestamp.ok) return nextTimestamp;
915
- clockTimestamp = nextTimestamp.value;
916
- messages.push({ timestamp: clockTimestamp, change });
917
- }
556
+ const handlers: Omit<MessageHandlers<DbWorkerInput, DbWorkerDeps>, "init"> = {
557
+ getAppOwner: (deps) => () => {
558
+ deps.postMessage({
559
+ type: "onGetAppOwner",
560
+ appOwner: deps.appOwner,
561
+ });
562
+ },
918
563
 
919
- const apply = applyMessages(deps)(messages, clockTimestamp);
920
- if (!apply.ok) return apply;
564
+ mutate: (deps) => (message) => {
565
+ const mutate = deps.sqlite.transaction(() => {
566
+ const syncChanges: Array<MutationChange> = [];
921
567
 
922
- if (onChange) onChange();
568
+ for (const change of message.changes) {
569
+ const isLocalOnlyChange = change.table.startsWith("_");
570
+ if (isLocalOnlyChange) {
571
+ const result = applyLocalOnlyChange(deps)(change);
572
+ if (!result.ok) return result;
573
+ } else {
574
+ syncChanges.push(change);
575
+ }
576
+ }
923
577
 
924
- assertNonEmptyReadonlyArray(messages);
925
- return ok(messages);
926
- };
578
+ if (isNonEmptyArray(syncChanges)) {
579
+ const result = deps.sync.applyChanges(syncChanges);
580
+ if (!result.ok) return result;
581
+ }
927
582
 
928
- const applyMessages =
929
- (deps: SqliteDep & RandomDep & ClientStorageDep & ClockDep & OwnersDep) =>
930
- (
931
- messages: ReadonlyArray<CrdtMessage>,
932
- clockTimestamp: Timestamp,
933
- ): Result<void, SqliteError> => {
934
- const ownerId = ownerIdToBinaryOwnerId(
935
- Array.from(deps.owners.values())[0].id,
936
- );
583
+ // Read writes before commit to update UI ASAP
584
+ const queryPatches = loadQueries(deps)(
585
+ message.tabId,
586
+ message.subscribedQueries,
587
+ );
588
+ if (!queryPatches.ok) return queryPatches;
589
+
590
+ // Update the tab that performed the mutation.
591
+ deps.postMessage({
592
+ type: "onQueryPatches",
593
+ tabId: message.tabId,
594
+ queryPatches: queryPatches.value,
595
+ onCompleteIds: message.onCompleteIds,
596
+ });
937
597
 
938
- for (const message of messages) {
939
- const result1 = applyMessageToAppTable(deps)(ownerId, message);
940
- if (!result1.ok) return result1;
598
+ // Notify other tabs to refresh their queries.
599
+ deps.postMessage({ type: "refreshQueries", tabId: message.tabId });
941
600
 
942
- const result2 = applyMessageToTimestampAndHistoryTables(deps)(
943
- ownerId,
944
- message,
945
- );
946
- if (!result2.ok) return result2;
601
+ return ok();
602
+ });
603
+
604
+ if (!mutate.ok) {
605
+ deps.postMessage({ type: "onError", error: mutate.error });
606
+ return;
947
607
  }
608
+ },
948
609
 
949
- return deps.clock.save(clockTimestamp);
950
- };
610
+ query: (deps) => (message) => {
611
+ const queryPatches = loadQueries(deps)(message.tabId, message.queries);
951
612
 
952
- const applyMessageToAppTable =
953
- (deps: SqliteDep) =>
954
- (ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
955
- const timestamp = timestampToBinaryTimestamp(message.timestamp);
956
- const updatedAt = new Date(message.timestamp.millis).toISOString();
957
-
958
- for (const [column, value] of objectToEntries(message.change.values)) {
959
- const result = deps.sqlite.exec(sql.prepared`
960
- with
961
- lastTimestamp as (
962
- select "timestamp"
963
- from evolu_history
964
- where
965
- "ownerId" = ${ownerId}
966
- and "table" = ${message.change.table}
967
- and "id" = ${message.change.id}
968
- and "column" = ${column}
969
- order by "timestamp" desc
970
- limit 1
971
- )
972
- insert into ${sql.identifier(message.change.table)}
973
- ("id", ${sql.identifier(column)}, updatedAt)
974
- select ${message.change.id}, ${value}, ${updatedAt}
975
- where
976
- (select "timestamp" from lastTimestamp) is null
977
- or (select "timestamp" from lastTimestamp) < ${timestamp}
978
- on conflict ("id") do update
979
- set
980
- ${sql.identifier(column)} = ${value},
981
- updatedAt = ${updatedAt}
982
- where
983
- (select "timestamp" from lastTimestamp) is null
984
- or (select "timestamp" from lastTimestamp) < ${timestamp};
985
- `);
613
+ if (!queryPatches.ok) {
614
+ deps.postMessage({ type: "onError", error: queryPatches.error });
615
+ return;
616
+ }
986
617
 
987
- if (!result.ok) return result;
618
+ deps.postMessage({
619
+ type: "onQueryPatches",
620
+ tabId: message.tabId,
621
+ queryPatches: queryPatches.value,
622
+ onCompleteIds: [],
623
+ });
624
+ },
625
+
626
+ reset: (deps) => (message) => {
627
+ const resetResult = deps.sqlite.transaction(() => {
628
+ const dbSchema = getDbSchema(deps)();
629
+ if (!dbSchema.ok) return dbSchema;
630
+
631
+ for (const table of dbSchema.value.tables) {
632
+ /**
633
+ * The dropped table is completely removed from the database schema and
634
+ * the disk file. The table can not be recovered. All indices and
635
+ * triggers associated with the table are also deleted.
636
+ * https://sqlite.org/lang_droptable.html
637
+ */
638
+ const result = deps.sqlite.exec(sql`
639
+ drop table ${sql.identifier(table.name)};
640
+ `);
641
+ if (!result.ok) return result;
642
+ }
643
+
644
+ if (message.restore) {
645
+ const dbSchema = getDbSchema(deps)();
646
+ if (!dbSchema.ok) return dbSchema;
647
+
648
+ const ensureDbSchemaResult = ensureDbSchema(deps)(
649
+ message.restore.dbSchema,
650
+ dbSchema.value,
651
+ );
652
+ if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
653
+
654
+ const secret = mnemonicToOwnerSecret(message.restore.mnemonic);
655
+ const appOwner = createAppOwner(secret);
656
+ const clock = createClock(deps)();
657
+
658
+ const initializeDbResult = initializeDb(deps)(appOwner, clock.get());
659
+ if (!initializeDbResult.ok) return initializeDbResult;
660
+ }
661
+
662
+ return ok();
663
+ });
664
+
665
+ if (!resetResult.ok) {
666
+ deps.postMessage({ type: "onError", error: resetResult.error });
667
+ return;
988
668
  }
989
669
 
990
- return ok();
991
- };
670
+ deps.postMessage({
671
+ type: "onReset",
672
+ onCompleteId: message.onCompleteId,
673
+ reload: message.reload,
674
+ });
675
+ },
992
676
 
993
- export const applyMessageToTimestampAndHistoryTables =
994
- (deps: SqliteDep & ClientStorageDep) =>
995
- (ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
996
- const timestamp = timestampToBinaryTimestamp(message.timestamp);
997
- const id = idToBinaryId(message.change.id);
677
+ ensureDbSchema: (deps) => (message) => {
678
+ const ensureSchema = deps.sqlite.transaction(() => {
679
+ const dbSchema = getDbSchema(deps)();
680
+ if (!dbSchema.ok) return dbSchema;
998
681
 
999
- const result = deps.storage.insertTimestamp(ownerId, timestamp);
1000
- if (!result.ok) return result;
682
+ const ensureDbSchemaResult = ensureDbSchema(deps)(
683
+ message.dbSchema,
684
+ dbSchema.value,
685
+ );
686
+ if (!ensureDbSchemaResult.ok) return ensureDbSchemaResult;
1001
687
 
1002
- for (const [column, value] of Object.entries(message.change.values)) {
1003
- const result = deps.sqlite.exec(sql.prepared`
1004
- insert into evolu_history
1005
- ("ownerId", "table", "id", "column", "value", "timestamp")
1006
- values
1007
- (
1008
- ${ownerId},
1009
- ${message.change.table},
1010
- ${id},
1011
- ${column},
1012
- ${value},
1013
- ${timestamp}
1014
- )
1015
- on conflict do nothing;
1016
- `);
1017
- if (!result.ok) return result;
688
+ return ok();
689
+ });
690
+
691
+ if (!ensureSchema.ok) {
692
+ deps.postMessage({ type: "onError", error: ensureSchema.error });
693
+ return;
1018
694
  }
695
+ },
1019
696
 
1020
- return ok();
1021
- };
697
+ export: (deps) => (message) => {
698
+ const file = deps.sqlite.export();
699
+
700
+ if (!file.ok) {
701
+ deps.postMessage({ type: "onError", error: file.error });
702
+ return;
703
+ }
704
+
705
+ deps.postMessage({
706
+ type: "onExport",
707
+ onCompleteId: message.onCompleteId,
708
+ file: file.value,
709
+ });
710
+ },
711
+
712
+ useOwner: (deps) => (message) => {
713
+ deps.sync.useOwner(message.use, message.owner);
714
+ },
715
+ };
1022
716
 
1023
717
  const loadQueries =
1024
718
  (deps: GetQueryRowsCacheDep & SqliteDep) =>
@@ -1057,189 +751,3 @@ const loadQueries =
1057
751
  );
1058
752
  return ok(queryPatchesArray);
1059
753
  };
1060
-
1061
- const dropAllTables = (deps: SqliteDep): Result<void, SqliteError> => {
1062
- const schema = getDbSchema(deps)();
1063
- if (!schema.ok) return schema;
1064
- for (const table of schema.value.tables) {
1065
- /**
1066
- * The dropped table is completely removed from the database schema and the
1067
- * disk file. The table can not be recovered. All indices and triggers
1068
- * associated with the table are also deleted.
1069
- * https://sqlite.org/lang_droptable.html
1070
- */
1071
- const result = deps.sqlite.exec(sql`
1072
- drop table ${sql.identifier(table.name)};
1073
- `);
1074
- if (!result.ok) return result;
1075
- }
1076
- return ok();
1077
- };
1078
-
1079
- const handleSyncOpen =
1080
- (deps: StorageDep & ConsoleDep & OwnersDep): SyncConfig["onOpen"] =>
1081
- (send) => {
1082
- for (const [id] of deps.owners) {
1083
- const message = createProtocolMessageForSync(deps)(id);
1084
- if (!message) return;
1085
- deps.console.log("[db]", "send initial sync message", message);
1086
- send(message);
1087
- }
1088
- };
1089
-
1090
- const createHandleSyncMessage =
1091
- (
1092
- deps: PostMessageDep & StorageDep & SqliteDep & ConsoleDep & OwnersDep,
1093
- ): SyncConfig["onMessage"] =>
1094
- (input, send) => {
1095
- deps.console.log("[db]", "receive sync message", input);
1096
-
1097
- const output = deps.sqlite.transaction(() =>
1098
- applyProtocolMessageAsClient(deps)(input, {
1099
- getWriteKey: (ownerId) => deps.owners.get(ownerId)?.writeKey ?? null,
1100
- }),
1101
- );
1102
- if (!output.ok) {
1103
- deps.postMessage({ type: "onError", error: output.error });
1104
- return;
1105
- }
1106
-
1107
- if (output.value) {
1108
- deps.console.log("[db]", "respond sync message", output.value);
1109
- send(output.value);
1110
- }
1111
- };
1112
-
1113
- export interface ClientStorageDep {
1114
- readonly storage: ClientStorage;
1115
- }
1116
-
1117
- export interface ClientStorage extends SqliteStorageBase, Storage {}
1118
-
1119
- const createClientStorage =
1120
- (
1121
- deps: SqliteDep &
1122
- PostMessageDep &
1123
- SymmetricCryptoDep &
1124
- RandomDep &
1125
- TimeDep &
1126
- OwnersDep &
1127
- ClockDep &
1128
- TimestampConfigDep,
1129
- ) =>
1130
- (
1131
- options: CreateSqliteStorageBaseOptions,
1132
- ): Result<ClientStorage, SqliteError> => {
1133
- const sqliteStorageBase = createSqliteStorageBase(deps)(options);
1134
- if (!sqliteStorageBase.ok) return sqliteStorageBase;
1135
-
1136
- const storage: ClientStorage = {
1137
- ...sqliteStorageBase.value,
1138
-
1139
- validateWriteKey: constFalse,
1140
- setWriteKey: constFalse,
1141
-
1142
- writeMessages: (ownerId, messages) => {
1143
- const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
1144
- assert(owner, "Missing owner");
1145
-
1146
- const decodedAndDecryptedMessages: Array<CrdtMessage> = [];
1147
-
1148
- for (const message of messages) {
1149
- const dbChange = decryptAndDecodeDbChange(deps)(
1150
- message,
1151
- owner.encryptionKey,
1152
- );
1153
-
1154
- if (!dbChange.ok) {
1155
- deps.postMessage({
1156
- type: "onError",
1157
- error: dbChange.error,
1158
- });
1159
- return false;
1160
- }
1161
-
1162
- decodedAndDecryptedMessages.push({
1163
- timestamp: message.timestamp,
1164
- change: dbChange.value,
1165
- });
1166
- }
1167
-
1168
- let clockTimestamp = deps.clock.get();
1169
-
1170
- for (const message of messages) {
1171
- const receive = receiveTimestamp(deps)(
1172
- clockTimestamp,
1173
- message.timestamp,
1174
- );
1175
- if (!receive.ok) {
1176
- deps.postMessage({ type: "onError", error: receive.error });
1177
- return false;
1178
- }
1179
- clockTimestamp = receive.value;
1180
- }
1181
-
1182
- const applyMessagesResult = applyMessages({ ...deps, storage })(
1183
- decodedAndDecryptedMessages,
1184
- clockTimestamp,
1185
- );
1186
-
1187
- if (!applyMessagesResult.ok) {
1188
- deps.postMessage({
1189
- type: "onError",
1190
- error: applyMessagesResult.error,
1191
- });
1192
- return false;
1193
- }
1194
-
1195
- deps.postMessage({ type: "onReceive" });
1196
-
1197
- return true;
1198
- },
1199
-
1200
- readDbChange: (ownerId, timestamp) => {
1201
- const owner = deps.owners.get(binaryOwnerIdToOwnerId(ownerId));
1202
- assert(owner, "Missing owner");
1203
-
1204
- const result = deps.sqlite.exec<{
1205
- table: Base64Url256;
1206
- id: BinaryId;
1207
- column: Base64Url256;
1208
- value: SqliteValue;
1209
- }>(sql`
1210
- select "table", "id", "column", "value"
1211
- from evolu_history
1212
- where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
1213
- `);
1214
- if (!result.ok) {
1215
- deps.postMessage({ type: "onError", error: result.error });
1216
- return null;
1217
- }
1218
-
1219
- const { rows } = result.value;
1220
- assert(rows.length > 0, "Rows must not be empty");
1221
-
1222
- const { table, id } = rows[0];
1223
- const values: Record<string, SqliteValue> = {};
1224
-
1225
- for (const r of rows) {
1226
- assert(r.table === table, "All rows must have the same table");
1227
- assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
1228
- values[r.column] = r.value;
1229
- }
1230
-
1231
- const message: CrdtMessage = {
1232
- timestamp: binaryTimestampToTimestamp(timestamp),
1233
- change: {
1234
- table: rows[0].table,
1235
- id: binaryIdToId(rows[0].id),
1236
- values,
1237
- },
1238
- };
1239
-
1240
- return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
1241
- },
1242
- };
1243
-
1244
- return ok(storage);
1245
- };