@evolu/common 6.0.1-preview.8 → 7.0.0

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