@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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