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

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 +69 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +64 -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 +161 -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 +273 -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 +238 -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 +240 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +189 -91
  63. package/dist/src/Evolu/Sync.d.ts +67 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +441 -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 +622 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +666 -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 +90 -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 +517 -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 +355 -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 +437 -93
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +532 -135
  139. package/src/Evolu/Sync.ts +766 -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 +1084 -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
@@ -1,11 +1,13 @@
1
+ import { pack } from "msgpackr";
1
2
  import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
2
- import { assertNonEmptyArray } from "../Assert.js";
3
+ import { assert, assertNonEmptyArray } from "../Assert.js";
3
4
  import { createCallbacks } from "../Callbacks.js";
4
5
  import { ConsoleDep } from "../Console.js";
5
- import { SymmetricCryptoDecryptError } from "../Crypto.js";
6
+ import { RandomBytesDep, SymmetricCryptoDecryptError } from "../Crypto.js";
7
+ import { eqArrayNumber } from "../Eq.js";
6
8
  import { TransferableError } from "../Error.js";
7
9
  import { exhaustiveCheck } from "../Function.js";
8
- import { NanoIdLibDep } from "../NanoId.js";
10
+ import { createInstances, Instances } from "../Instances.js";
9
11
  import { err, ok, Result } from "../Result.js";
10
12
  import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
11
13
  import { createStore, StoreSubscribe } from "../Store.js";
@@ -18,19 +20,16 @@ import {
18
20
  InferType,
19
21
  Mnemonic,
20
22
  ObjectType,
23
+ SimpleName,
24
+ ValidMutationSize,
25
+ ValidMutationSizeError,
21
26
  } from "../Type.js";
22
27
  import { IntentionalNever } from "../Types.js";
23
- import { Config, defaultConfig } from "./Config.js";
24
- import { CreateDbWorkerDep } from "./Db.js";
28
+ import { CreateDbWorkerDep, DbConfig, defaultDbConfig } from "./Db.js";
25
29
  import { applyPatches } from "./Diff.js";
26
- import { kysely } from "./Kysely.js";
27
30
  import { AppOwner } from "./Owner.js";
28
- import { CreateAppStateDep, FlushSyncDep } from "./Platform.js";
29
- import {
30
- DbChange,
31
- ProtocolError,
32
- ProtocolUnsupportedVersionError,
33
- } from "./Protocol.js";
31
+ import { FlushSyncDep, ReloadAppDep } from "./Platform.js";
32
+ import { ProtocolError } from "./Protocol.js";
34
33
  import {
35
34
  createSubscribedQueries,
36
35
  emptyRows,
@@ -44,24 +43,58 @@ import {
44
43
  SubscribedQueries,
45
44
  } from "./Query.js";
46
45
  import {
47
- assertValidEvoluSchema,
48
46
  CreateQuery,
47
+ DefaultColumns,
49
48
  EvoluSchema,
49
+ evoluSchemaToDbSchema,
50
+ IndexesConfig,
50
51
  insertable,
52
+ kysely,
51
53
  Mutation,
54
+ MutationChange,
52
55
  MutationKind,
53
56
  MutationMapping,
54
57
  MutationOptions,
55
58
  updateable,
56
59
  upsertable,
57
- validEvoluSchemaToDbSchema,
58
- ValidMutationSize,
59
- ValidMutationSizeError,
60
+ ValidateSchema,
60
61
  } from "./Schema.js";
61
- import { initialSyncState, SyncState } from "./Sync.js";
62
+ import { DbChange } from "./Storage.js";
63
+ import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
62
64
  import { TimestampError } from "./Timestamp.js";
63
65
 
64
- export interface Evolu<S extends EvoluSchema = EvoluSchema> {
66
+ export interface EvoluConfig extends Partial<DbConfig> {
67
+ /**
68
+ * Use the `indexes` option to define SQLite indexes.
69
+ *
70
+ * Table and column names are not typed because Kysely doesn't support it.
71
+ *
72
+ * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
73
+ *
74
+ * ### Example
75
+ *
76
+ * ```ts
77
+ * const evolu = createEvolu(evoluReactDeps)(Schema, {
78
+ * indexes: (create) => [
79
+ * create("todoCreatedAt").on("todo").column("createdAt"),
80
+ * create("todoCategoryCreatedAt")
81
+ * .on("todoCategory")
82
+ * .column("createdAt"),
83
+ * ],
84
+ * });
85
+ * ```
86
+ */
87
+ readonly indexes?: IndexesConfig;
88
+
89
+ /**
90
+ * URL to reload browser tabs after reset or restore.
91
+ *
92
+ * The default value is `/`.
93
+ */
94
+ readonly reloadUrl?: string;
95
+ }
96
+
97
+ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
65
98
  /**
66
99
  * Subscribe to {@link EvoluError} changes.
67
100
  *
@@ -88,7 +121,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
88
121
  * All this function does is compile the Kysely query and serialize it into a
89
122
  * unique string. Both operations are fast and cheap.
90
123
  *
91
- * For mutations, use {@link Evolu.insert} and {@link Evolu.update}.
124
+ * For mutations, use {@link Evolu#insert}, {@link Evolu#update}, or
125
+ * {@link Evolu#upsert}.
92
126
  *
93
127
  * ### Example
94
128
  *
@@ -108,43 +142,16 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
108
142
  /**
109
143
  * Load {@link Query} and return a promise with {@link QueryRows}.
110
144
  *
111
- * A returned promise always resolves successfully because there is no reason
112
- * why loading should fail. All data are local, and the query is typed. A
113
- * serious unexpected Evolu error shall be handled with
114
- * {@link Evolu.subscribeError}.
115
- *
116
- * Loading is batched, and returned promises are cached, so there is no need
117
- * for an additional cache. Evolu's internal cache is invalidated on
118
- * mutation.
119
- *
120
- * The returned promise is enriched with special status and value properties
121
- * for the upcoming React `use` Hook, but other UI libraries can also leverage
122
- * them. Speaking of React, there are two essential React Suspense-related
123
- * patterns that every developer should be aware of—passing promises to
124
- * children and caching over mutations.
125
- *
126
- * With promises passed to children, we can load a query as soon as possible,
127
- * but we don't have to use the returned promise immediately. That's useful
128
- * for prefetching, which is generally not necessary for local-first apps but
129
- * can be if a query takes a long time to load.
145
+ * The returned promise always resolves successfully because there is no
146
+ * reason why loading should fail. All data are local, and the query is typed.
147
+ * Unexpected errors are handled with {@link Evolu#subscribeError}.
130
148
  *
131
- * Caching over mutation is a pattern that every developer should know. As we
132
- * said, Evolu caches promise until a mutation happens. A query loaded after
133
- * that will return a new pending promise. That's okay for general usage but
134
- * not for UI with React Suspense because a mutation would suspend rerendered
135
- * queries on a page, and that's not a good UX.
149
+ * Loading is batched, and returned promises are cached until resolved to
150
+ * prevent redundant database queries and to support React Suspense (which
151
+ * requires stable promise references while pending).
136
152
  *
137
- * We call this pattern "caching over mutation" because it has no globally
138
- * accepted name yet. React RFC for React Cache does not exist yet.
139
- *
140
- * For better UX, a query must be subscribed for updates. This way, instead of
141
- * Suspense flashes, the user sees new data immediately because Evolu replaces
142
- * cached promises with fresh, already resolved new ones.
143
- *
144
- * If you are curious why Evolu does not do that for all queries by default,
145
- * the answer is simple: performance. Tracking changes is costly and
146
- * meaningful only for visible (hence subscribed) queries anyway. To subscribe
147
- * to a query, use {@link Evolu.subscribeQuery}.
153
+ * To subscribe a query for automatic updates, use
154
+ * {@link Evolu#subscribeQuery}.
148
155
  *
149
156
  * ### Example
150
157
  *
@@ -152,7 +159,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
152
159
  * const allTodos = evolu.createQuery((db) =>
153
160
  * db.selectFrom("todo").selectAll(),
154
161
  * );
155
- * evolu.loadQuery(allTodos).then(({ rows }) => {
162
+ * evolu.loadQuery(allTodos).then((rows) => {
156
163
  * console.log(rows);
157
164
  * });
158
165
  * ```
@@ -201,86 +208,168 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
201
208
  readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
202
209
 
203
210
  /**
204
- * Subscribe to {@link AppOwner} changes.
205
- *
206
- * ### Example
211
+ * Promise that resolves to {@link AppOwner} when available.
207
212
  *
208
- * ```ts
209
- * const unsubscribe = evolu.subscribeAppOwner(() => {
210
- * const owner = evolu.getAppOwner();
211
- * });
212
- * ```
213
- */
214
- readonly subscribeAppOwner: StoreSubscribe;
215
-
216
- /**
217
- * Get {@link AppOwner}.
213
+ * Note: With web-only deps, this promise will not resolve during SSR because
214
+ * there is no AppOwner on the server.
218
215
  *
219
216
  * ### Example
220
217
  *
221
218
  * ```ts
222
- * const unsubscribe = evolu.subscribeAppOwner(() => {
223
- * const owner = evolu.getAppOwner();
224
- * });
219
+ * const owner = await evolu.appOwner;
225
220
  * ```
226
221
  */
227
- readonly getAppOwner: () => AppOwner | null;
222
+ readonly appOwner: Promise<AppOwner>;
228
223
 
229
224
  /**
230
- * Subscribe to {@link SyncState} changes.
225
+ * Inserts a row into the database and returns a {@link Result} with the new
226
+ * {@link Id}.
231
227
  *
232
- * ### Example
228
+ * The first argument is the table name, and the second is an object
229
+ * containing the row data. An optional third argument provides mutation
230
+ * options including an `onComplete` callback and `onlyValidate` flag.
233
231
  *
234
- * ```ts
235
- * const unsubscribe = evolu.subscribeSyncState(() => {
236
- * const syncState = evolu.getSyncState();
237
- * });
238
- * ```
239
- */
240
- readonly subscribeSyncState: StoreSubscribe;
241
-
242
- /**
243
- * Get {@link SyncState}.
232
+ * Returns a Result type - use `.ok` to check if the insertion succeeded, and
233
+ * `.value.id` to access the generated ID on success, or `.error` to handle
234
+ * validation errors.
235
+ *
236
+ * Evolu does not use SQL for mutations to ensure data can be safely and
237
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
238
+ * to automatically add and update {@link DefaultColumns}.
244
239
  *
245
240
  * ### Example
246
241
  *
247
242
  * ```ts
248
- * const unsubscribe = evolu.subscribeSyncState(() => {
249
- * const syncState = evolu.getSyncState();
243
+ * const result = evolu.insert("todo", {
244
+ * title: "Learn Evolu",
245
+ * isCompleted: false,
250
246
  * });
251
- * ```
252
- */
253
- readonly getSyncState: () => SyncState;
254
-
255
- /**
256
- * Inserts a row.
257
247
  *
258
- * ### Example
248
+ * if (result.ok) {
249
+ * console.log("Todo created with ID:", result.value.id);
250
+ * } else {
251
+ * console.error("Validation error:", result.error);
252
+ * }
259
253
  *
260
- * ```ts
261
- * // TODO:
254
+ * // With onComplete callback
255
+ * evolu.insert(
256
+ * "todo",
257
+ * { title: "Another todo" },
258
+ * {
259
+ * onComplete: () => {
260
+ * console.log("Insert completed");
261
+ * },
262
+ * },
263
+ * );
262
264
  * ```
263
265
  */
264
266
  insert: Mutation<S, "insert">;
265
267
 
266
268
  /**
267
- * Updates a row.
269
+ * Updates a row in the database and returns a {@link Result} with the existing
270
+ * {@link Id}.
271
+ *
272
+ * The first argument is the table name, and the second is an object
273
+ * containing the row data including the required `id` field. An optional
274
+ * third argument provides mutation options including an `onComplete` callback
275
+ * and `onlyValidate` flag.
276
+ *
277
+ * Returns a Result type - use `.ok` to check if the update succeeded, and
278
+ * `.value.id` to access the ID on success, or `.error` to handle validation
279
+ * errors.
280
+ *
281
+ * Evolu does not use SQL for mutations to ensure data can be safely and
282
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
283
+ * to automatically add and update {@link DefaultColumns}.
268
284
  *
269
285
  * ### Example
270
286
  *
271
287
  * ```ts
272
- * // TODO:
288
+ * const result = evolu.update("todo", {
289
+ * id: todoId,
290
+ * title: "Updated title",
291
+ * isCompleted: true,
292
+ * });
293
+ *
294
+ * if (result.ok) {
295
+ * console.log("Todo updated with ID:", result.value.id);
296
+ * } else {
297
+ * console.error("Validation error:", result.error);
298
+ * }
299
+ *
300
+ * // To delete a row, set isDeleted to true
301
+ * evolu.update("todo", { id: todoId, isDeleted: true });
302
+ *
303
+ * // With onComplete callback
304
+ * evolu.update(
305
+ * "todo",
306
+ * { id: todoId, title: "New title" },
307
+ * {
308
+ * onComplete: () => {
309
+ * console.log("Update completed");
310
+ * },
311
+ * },
312
+ * );
273
313
  * ```
274
314
  */
275
315
  update: Mutation<S, "update">;
276
316
 
277
317
  /**
278
- * Upserts a row.
318
+ * Upserts a row in the database and returns a {@link Result} with the existing
319
+ * {@link Id}.
320
+ *
321
+ * The first argument is the table name, and the second is an object
322
+ * containing the row data including the required `id` field. An optional
323
+ * third argument provides mutation options including an `onComplete` callback
324
+ * and `onlyValidate` flag.
325
+ *
326
+ * This function allows you to use custom IDs and optionally set `createdAt`,
327
+ * which is useful for external systems, data migrations, or when the same row
328
+ * may already be created on a different device.
329
+ *
330
+ * Returns a Result type - use `.ok` to check if the upsert succeeded, and
331
+ * `.value.id` to access the ID on success, or `.error` to handle validation
332
+ * errors.
333
+ *
334
+ * Evolu does not use SQL for mutations to ensure data can be safely and
335
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
336
+ * to automatically add and update {@link DefaultColumns}.
279
337
  *
280
338
  * ### Example
281
339
  *
282
340
  * ```ts
283
- * // TODO:
341
+ * // Use deterministic ID for stable upserts across devices
342
+ * const stableId = createIdFromString("my-todo-1");
343
+ *
344
+ * const result = evolu.upsert("todo", {
345
+ * id: stableId,
346
+ * title: "Learn Evolu",
347
+ * isCompleted: false,
348
+ * });
349
+ *
350
+ * if (result.ok) {
351
+ * console.log("Todo upserted with ID:", result.value.id);
352
+ * } else {
353
+ * console.error("Validation error:", result.error);
354
+ * }
355
+ *
356
+ * // Data migration with custom createdAt
357
+ * evolu.upsert("todo", {
358
+ * id: externalId,
359
+ * title: "Migrated todo",
360
+ * createdAt: new Date("2023-01-01"), // Preserve original timestamp
361
+ * });
362
+ *
363
+ * // With onComplete callback
364
+ * evolu.upsert(
365
+ * "todo",
366
+ * { id: stableId, title: "Updated title" },
367
+ * {
368
+ * onComplete: () => {
369
+ * console.log("Upsert completed");
370
+ * },
371
+ * },
372
+ * );
284
373
  * ```
285
374
  */
286
375
  upsert: Mutation<S, "upsert">;
@@ -300,7 +389,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
300
389
 
301
390
  /**
302
391
  * Restore {@link AppOwner} with all their synced data. It uses
303
- * {@link Evolu.resetAppOwner}, so be careful.
392
+ * {@link Evolu#resetAppOwner}, so be careful.
304
393
  */
305
394
  readonly restoreAppOwner: (
306
395
  mnemonic: Mnemonic,
@@ -316,17 +405,42 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
316
405
  readonly reloadApp: () => void;
317
406
 
318
407
  /** Export SQLite database file as Uint8Array. */
319
- readonly exportDatabase: () => Promise<Uint8Array>;
408
+ readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
409
+
410
+ /**
411
+ * Use an owner. Using an owner means syncing it and subscribing to
412
+ * broadcasted changes. Returns a function to stop using the owner.
413
+ *
414
+ * Transport connections are automatically deduplicated and reference-counted,
415
+ * so multiple owners using the same transport will share a single
416
+ * connection.
417
+ *
418
+ * ### Example
419
+ *
420
+ * ```ts
421
+ * // Use an owner (starts syncing and subscribing to changes).
422
+ * const unuse = evolu.useOwner(shardOwner);
423
+ *
424
+ * // Later, stop using the owner.
425
+ * unuse();
426
+ *
427
+ * // Bulk operations.
428
+ * const unuses = owners.map((owner) => evolu.useOwner(owner));
429
+ * // Later: unuses.forEach(unuse => unuse());
430
+ * ```
431
+ *
432
+ * @experimental
433
+ */
434
+ readonly useOwner: (owner: SyncOwner) => () => void;
320
435
  }
321
436
 
322
- /** Represent errors that can occur in Evolu. */
437
+ /** Represents errors that can occur in Evolu. */
323
438
  export type EvoluError =
324
- | TimestampError
325
439
  | ProtocolError
326
- | TransferableError
440
+ | SqliteError
327
441
  | SymmetricCryptoDecryptError
328
- | ProtocolUnsupportedVersionError
329
- | SqliteError;
442
+ | TimestampError
443
+ | TransferableError;
330
444
 
331
445
  interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
332
446
  extends Evolu<S> {
@@ -337,68 +451,25 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
337
451
  readonly ensureSchema: (schema: EvoluSchema) => void;
338
452
  }
339
453
 
340
- export type EvoluDeps = CreateDbWorkerDep &
341
- TimeDep &
342
- NanoIdLibDep &
454
+ export type EvoluDeps = ConsoleDep &
455
+ CreateDbWorkerDep &
343
456
  Partial<FlushSyncDep> &
344
- ConsoleDep &
345
- CreateAppStateDep;
346
-
347
- export interface EvoluConfigWithInitialData<S extends EvoluSchema = EvoluSchema>
348
- extends Config {
349
- /**
350
- * Use this option to create initial data (fixtures).
351
- *
352
- * ### Example
353
- *
354
- * ```ts
355
- * const evolu = createEvolu(evoluReactWebDeps)(Schema, {
356
- * initialData: (evolu) => {
357
- * const todoCategory = evolu.insert("todoCategory", {
358
- * name: "Not Urgent",
359
- * });
360
- *
361
- * // This is a developer error, which should be fixed immediately.
362
- * assert(todoCategory.ok, "invalid initial data");
363
- *
364
- * evolu.insert("todo", {
365
- * title: "Try React Suspense",
366
- * categoryId: todoCategory.value.id,
367
- * });
368
- * },
369
- * });
370
- * ```
371
- */
372
- initialData?: (evolu: EvoluForInitialData<S>) => void;
373
- }
374
-
375
- export interface EvoluForInitialData<S extends EvoluSchema = EvoluSchema> {
376
- insert: Mutation<S, "insert">;
377
- }
457
+ RandomBytesDep &
458
+ ReloadAppDep &
459
+ TimeDep;
378
460
 
379
- // For hot reloading and Evolu multitenancy.
380
- const evoluInstances = new Map<string, InternalEvoluInstance>();
461
+ const evoluInstances = createInstances<SimpleName, InternalEvoluInstance>();
381
462
 
463
+ /**
464
+ * Unique identifier for the current browser tab or app instance, lazily
465
+ * initialized on first use to distinguish between multiple tabs.
466
+ */
382
467
  let tabId: Id | null = null;
383
468
 
384
469
  /**
385
- * Creates an {@link Evolu} instance configured with the specified
386
- * {@link EvoluSchema} and optional configuration.
387
- *
388
- * This function returns a configured Evolu instance, providing a typed
389
- * interface for querying, mutating, and syncing your application's data. The
390
- * returned instance includes:
391
- *
392
- * - Subscription methods for receiving updates on queries, the owner, errors, and
393
- * sync state.
394
- * - Methods for creating, updating, or deleting rows in a type-safe manner.
395
- * - Methods for querying data using Evolu's typed SQL queries, leveraging Kysely
396
- * under the hood.
397
- * - Built-in support for local-first and offline-first data with automatic sync
398
- * and merging.
399
- * - Automatic schema evolution that updates the underlying database with new
400
- * columns or tables.
401
- * - Managing owner data with resetAppOwner and restoreAppOwner.
470
+ * Creates an {@link Evolu} instance for a platform configured with the specified
471
+ * {@link EvoluSchema} and optional {@link EvoluConfig} providing a typed
472
+ * interface for querying, mutating, and syncing your application's data.
402
473
  *
403
474
  * ### Example
404
475
  *
@@ -427,78 +498,88 @@ let tabId: Id | null = null;
427
498
  *
428
499
  * const evolu = createEvolu(evoluReactDeps)(Schema);
429
500
  * ```
501
+ *
502
+ * ### Instance Caching
503
+ *
504
+ * `createEvolu` caches instances using {@link Instances} by {@link EvoluConfig}
505
+ * name to enable hot reloading and prevent database corruption from multiple
506
+ * connections. For testing, use unique instance names to ensure proper
507
+ * isolation.
430
508
  */
431
509
  export const createEvolu =
432
510
  (deps: EvoluDeps) =>
433
511
  <S extends EvoluSchema>(
434
- // TODO: Validate missing Id, unsupported types, used default types via TS types
435
- // with type errors messages as we had it in the old Evolu.
436
- schema: S,
437
- partialConfig: Partial<EvoluConfigWithInitialData<S>> = {},
438
- ): Evolu<S> => {
439
- const config = { ...defaultConfig, ...partialConfig };
440
-
441
- let evolu = evoluInstances.get(config.name);
442
-
443
- if (evolu == null) {
444
- evolu = createEvoluInstance(deps)(schema, config as IntentionalNever);
445
- evoluInstances.set(config.name, evolu);
446
- } else {
447
- // Hot reloading. Note that indexes are intentionally omitted.
448
- evolu.ensureSchema(schema);
449
- }
450
-
451
- return evolu as IntentionalNever;
452
- };
512
+ schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
513
+ config?: EvoluConfig,
514
+ ): Evolu<S> =>
515
+ evoluInstances.ensure(
516
+ config?.name ?? defaultDbConfig.name,
517
+ () => createEvoluInstance(deps)(schema as EvoluSchema, config),
518
+ (evolu) => {
519
+ // Hot reloading. Note that indexes are intentionally omitted.
520
+ evolu.ensureSchema(schema as EvoluSchema);
521
+ },
522
+ ) as Evolu<S>;
453
523
 
454
524
  const createEvoluInstance =
455
525
  (deps: EvoluDeps) =>
456
- (
457
- schema: EvoluSchema,
458
- evoluConfig: EvoluConfigWithInitialData,
459
- ): InternalEvoluInstance => {
460
- deps.console.enabled = evoluConfig.enableLogging ?? false;
526
+ (schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
527
+ deps.console.enabled = config?.enableLogging ?? false;
528
+
529
+ const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
461
530
 
462
- deps.console.log("[evolu]", "createEvoluInstance");
531
+ const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
463
532
 
464
- const { initialData, indexes, ...config } = evoluConfig;
533
+ deps.console.log("[evolu]", "createEvoluInstance", {
534
+ name: dbConfig.name,
535
+ });
465
536
 
466
537
  const errorStore = createStore<EvoluError | null>(null);
467
538
  const rowsStore = createStore<QueryRowsMap>(new Map());
468
- const ownerStore = createStore<AppOwner | null>(null);
469
- const syncStore = createStore<SyncState>(initialSyncState);
539
+
540
+ const { promise: appOwner, resolve: resolveAppOwner } =
541
+ Promise.withResolvers<AppOwner>();
542
+
543
+ if (config?.externalAppOwner) {
544
+ resolveAppOwner(config.externalAppOwner);
545
+ }
546
+
547
+ // TODO: Update it for the owner-api
548
+ const _syncStore = createStore<SyncState>(initialSyncState);
470
549
 
471
550
  const subscribedQueries = createSubscribedQueries(rowsStore);
472
551
  const loadingPromises = createLoadingPromises(subscribedQueries);
473
- const callbacks = createCallbacks(deps);
552
+ const onCompleteCallbacks = createCallbacks(deps);
553
+ const exportCallbacks = createCallbacks<Uint8Array<ArrayBuffer>>(deps);
474
554
 
475
- const appState = deps.createAppState(config);
476
- const dbWorker = deps.createDbWorker(config.name);
555
+ const dbWorker = deps.createDbWorker(dbConfig.name);
477
556
 
478
557
  const getTabId = () => {
479
558
  tabId ??= createId(deps);
480
559
  return tabId;
481
560
  };
482
561
 
562
+ // Worker responses are delivered to all tabs. Each case must handle this
563
+ // properly (e.g., AppOwner promise resolves only once, tabId filtering).
483
564
  dbWorker.onMessage((message) => {
484
565
  switch (message.type) {
485
- case "onInit": {
486
- ownerStore.set(message.owner);
566
+ case "onError": {
567
+ errorStore.set(message.error);
487
568
  break;
488
569
  }
489
570
 
490
- case "onError": {
491
- errorStore.set(message.error);
571
+ case "onGetAppOwner": {
572
+ resolveAppOwner(message.appOwner);
492
573
  break;
493
574
  }
494
575
 
495
- case "onChange": {
576
+ case "onQueryPatches": {
496
577
  if (message.tabId !== getTabId()) return;
497
578
 
498
579
  const state = rowsStore.get();
499
580
  const nextState = new Map([
500
581
  ...state,
501
- ...message.patches.map(
582
+ ...message.queryPatches.map(
502
583
  ({ query, patches }): [Query, ReadonlyArray<Row>] => [
503
584
  query,
504
585
  applyPatches(patches, state.get(query) ?? emptyRows),
@@ -506,7 +587,7 @@ const createEvoluInstance =
506
587
  ),
507
588
  ]);
508
589
 
509
- for (const { query } of message.patches) {
590
+ for (const { query } of message.queryPatches) {
510
591
  loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
511
592
  }
512
593
 
@@ -519,33 +600,43 @@ const createEvoluInstance =
519
600
  }
520
601
 
521
602
  for (const id of message.onCompleteIds) {
522
- callbacks.execute(id);
603
+ onCompleteCallbacks.execute(id);
523
604
  }
524
605
  break;
525
606
  }
526
607
 
527
- case "onReceive": {
608
+ case "refreshQueries": {
528
609
  if (message.tabId && message.tabId === getTabId()) return;
529
610
 
530
- loadingPromises.releaseUnsubscribed();
531
- const queries = subscribedQueries.get();
611
+ const loadingPromisesQueries = loadingPromises.getQueries();
612
+ loadingPromises.releaseUnsubscribedOnMutation();
613
+
614
+ const queries = [
615
+ // Dedupe
616
+ ...new Set([...loadingPromisesQueries, ...subscribedQueries.get()]),
617
+ ];
618
+
532
619
  if (isNonEmptyReadonlyArray(queries)) {
533
620
  dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
534
621
  }
622
+
535
623
  break;
536
624
  }
537
625
 
538
626
  case "onReset": {
539
627
  if (message.reload) {
540
- appState.reset();
628
+ deps.reloadApp(reloadUrl);
541
629
  } else {
542
- callbacks.execute(message.onCompleteId);
630
+ onCompleteCallbacks.execute(message.onCompleteId);
543
631
  }
544
632
  break;
545
633
  }
546
634
 
547
635
  case "onExport": {
548
- callbacks.execute(message.onCompleteId, message.file);
636
+ exportCallbacks.execute(
637
+ message.onCompleteId,
638
+ message.file as Uint8Array<ArrayBuffer>,
639
+ );
549
640
  break;
550
641
  }
551
642
 
@@ -554,10 +645,7 @@ const createEvoluInstance =
554
645
  }
555
646
  });
556
647
 
557
- const dbSchema = validEvoluSchemaToDbSchema(
558
- assertValidEvoluSchema(schema),
559
- indexes,
560
- );
648
+ const dbSchema = evoluSchemaToDbSchema(schema, indexes);
561
649
 
562
650
  const mutationTypesCache = new Map<
563
651
  MutationKind,
@@ -581,48 +669,19 @@ const createEvoluInstance =
581
669
  return type;
582
670
  };
583
671
 
584
- const initialDataDbChanges: Array<DbChange> = [];
585
-
586
- /**
587
- * Note that the initial data function is called even if it is unnecessary
588
- * (initial data are already in the DB) because we don't want to wait for
589
- * SQLite's response. Initial data should be small (because they are inlined
590
- * in the code), so it's ok.
591
- */
592
- if (initialData)
593
- initialData({
594
- insert: (table, props) => {
595
- const Type = getMutationType(table, "insert");
596
- const id = createId(deps);
597
-
598
- const result = Type.fromUnknown(props);
599
-
600
- if (result.ok) {
601
- initialDataDbChanges.push({
602
- id,
603
- table,
604
- values: result.value,
605
- } as unknown as DbChange);
606
- return ok({ id });
607
- }
608
-
609
- return result;
610
- },
611
- });
672
+ dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
612
673
 
613
- dbWorker.postMessage({
614
- type: "init",
615
- config,
616
- dbSchema,
617
- initialData: initialDataDbChanges,
618
- });
674
+ // We can't use `init` to get AppOwner because `init` runs only once per n tabs.
675
+ dbWorker.postMessage({ type: "getAppOwner" });
619
676
 
620
677
  const loadQueryMicrotaskQueue: Array<Query> = [];
621
678
 
622
679
  const mutateMicrotaskQueue: Array<
623
- [DbChange | undefined, MutationOptions["onComplete"] | undefined]
680
+ [MutationChange | null, MutationOptions["onComplete"] | undefined]
624
681
  > = [];
625
682
 
683
+ const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
684
+
626
685
  const createMutation =
627
686
  <Kind extends MutationKind>(kind: Kind) =>
628
687
  <TableName extends keyof typeof schema>(
@@ -638,8 +697,7 @@ const createEvoluInstance =
638
697
  ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
639
698
  >
640
699
  > => {
641
- const Type = getMutationType(table, kind);
642
- const result = Type.fromUnknown(props);
700
+ const result = getMutationType(table, kind).fromUnknown(props);
643
701
 
644
702
  const id =
645
703
  kind === "insert"
@@ -648,51 +706,32 @@ const createEvoluInstance =
648
706
 
649
707
  if (options?.onlyValidate !== true) {
650
708
  if (!result.ok) {
651
- // One error must invalidate the whole queue.
652
- // We insert `undefined` to detect such a situation.
653
- mutateMicrotaskQueue.push([undefined, undefined]);
709
+ // Mark the transaction as invalid by pushing null
710
+ mutateMicrotaskQueue.push([null, undefined]);
654
711
  } else {
655
- // Remove `id` from values.
656
- const { id: _id, ...values } = result.value;
657
- // EvoluSchema Types ensure valid types.
658
- const change = { table, id, values } as unknown as DbChange;
659
- mutateMicrotaskQueue.push([change, options?.onComplete]);
660
- }
661
-
662
- if (mutateMicrotaskQueue.length === 1)
663
- queueMicrotask(() => {
664
- const changes = [];
665
- const onCompletes = [];
712
+ const values = { ...result.value };
713
+ delete values.id;
666
714
 
667
- for (const [change, onComplete] of mutateMicrotaskQueue) {
668
- if (change) changes.push(change);
669
- if (onComplete) onCompletes.push(onComplete);
715
+ if (kind === "insert" || kind === "upsert") {
716
+ // Only set createdAt if not provided by user
717
+ if (!("createdAt" in values)) {
718
+ values.createdAt = new Date(deps.time.now()).toISOString();
670
719
  }
720
+ }
671
721
 
672
- const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
673
- mutateMicrotaskQueue.length = 0;
674
-
675
- // Don't mutate anything if there was a validation error.
676
- // All mutations within a queue are considered to be a transaction.
677
- if (changes.length !== mutateMicrotaskQueueLength) {
678
- return;
679
- }
722
+ const dbChange = { table, id, values };
723
+ assert(
724
+ DbChange.is(dbChange),
725
+ `Failed to create DbChange for table "${dbChange.table}"`,
726
+ );
680
727
 
681
- const onCompleteIds = onCompletes.map((onComplete) =>
682
- callbacks.register(onComplete),
683
- );
684
-
685
- loadingPromises.releaseUnsubscribed();
728
+ const mutationChange = { ...dbChange, ownerId: options?.ownerId };
729
+ mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
730
+ }
686
731
 
687
- if (isNonEmptyArray(changes))
688
- dbWorker.postMessage({
689
- type: "mutate",
690
- tabId: getTabId(),
691
- changes,
692
- onCompleteIds,
693
- subscribedQueries: subscribedQueries.get(),
694
- });
695
- });
732
+ if (mutateMicrotaskQueue.length === 1) {
733
+ queueMicrotask(processMutationQueue);
734
+ }
696
735
  }
697
736
 
698
737
  if (result.ok) return ok({ id });
@@ -706,28 +745,43 @@ const createEvoluInstance =
706
745
  );
707
746
  };
708
747
 
748
+ const processMutationQueue = () => {
749
+ const changes: Array<MutationChange> = [];
750
+ const onCompletes = [];
751
+
752
+ for (const [change, onComplete] of mutateMicrotaskQueue) {
753
+ if (change !== null) changes.push(change);
754
+ if (onComplete) onCompletes.push(onComplete);
755
+ }
756
+
757
+ const queueLength = mutateMicrotaskQueue.length;
758
+ mutateMicrotaskQueue.length = 0;
759
+
760
+ // Don't process any mutations if there was a validation error.
761
+ // All mutations within a queue run as a single transaction.
762
+ if (changes.length !== queueLength) {
763
+ return;
764
+ }
765
+
766
+ const onCompleteIds = onCompletes.map(onCompleteCallbacks.register);
767
+ loadingPromises.releaseUnsubscribedOnMutation();
768
+
769
+ if (!isNonEmptyArray(changes)) return;
770
+
771
+ dbWorker.postMessage({
772
+ type: "mutate",
773
+ tabId: getTabId(),
774
+ changes,
775
+ onCompleteIds,
776
+ subscribedQueries: subscribedQueries.get(),
777
+ });
778
+ };
779
+
709
780
  const evolu: InternalEvoluInstance = {
710
781
  subscribeError: errorStore.subscribe,
711
782
  getError: errorStore.get,
712
783
 
713
- createQuery: (queryCallback, options) => {
714
- const compiledQuery = queryCallback(
715
- kysely as IntentionalNever,
716
- ).compile();
717
-
718
- if (isSqlMutation(compiledQuery.sql))
719
- throw new Error(
720
- "SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu Mutation API.",
721
- );
722
-
723
- return serializeQuery({
724
- sql: compiledQuery.sql as SafeSql,
725
- parameters: compiledQuery.parameters as NonNullable<
726
- SqliteQuery["parameters"]
727
- >,
728
- ...(options && { options }),
729
- });
730
- },
784
+ createQuery,
731
785
 
732
786
  loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
733
787
  const { promise, isNew } = loadingPromises.get(query);
@@ -740,6 +794,7 @@ const createEvoluInstance =
740
794
  const queries = [...new Set(loadQueryMicrotaskQueue)];
741
795
  loadQueryMicrotaskQueue.length = 0;
742
796
  assertNonEmptyArray(queries);
797
+ deps.console.log("[evolu]", "loadQuery", { queries });
743
798
  dbWorker.postMessage({
744
799
  type: "query",
745
800
  tabId: getTabId(),
@@ -775,23 +830,19 @@ const createEvoluInstance =
775
830
  getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
776
831
  (rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
777
832
 
778
- subscribeAppOwner: ownerStore.subscribe,
779
- getAppOwner: ownerStore.get,
833
+ appOwner,
780
834
 
781
- subscribeSyncState: syncStore.subscribe,
782
- getSyncState: syncStore.get,
835
+ // TODO: Update it for the owner-api
836
+ // subscribeSyncState: syncStore.subscribe,
837
+ // getSyncState: syncStore.get,
783
838
 
784
839
  insert: createMutation("insert"),
785
840
  update: createMutation("update"),
786
841
  upsert: createMutation("upsert"),
787
842
 
788
843
  resetAppOwner: (options) => {
789
- // Eslint bug, Promise<void> is correct by docs.
790
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
791
- const { promise, resolve } = Promise.withResolvers<void>();
792
- const onCompleteId = callbacks.register(() => {
793
- resolve();
794
- });
844
+ const { promise, resolve } = Promise.withResolvers<undefined>();
845
+ const onCompleteId = onCompleteCallbacks.register(resolve);
795
846
  dbWorker.postMessage({
796
847
  type: "reset",
797
848
  onCompleteId,
@@ -801,12 +852,8 @@ const createEvoluInstance =
801
852
  },
802
853
 
803
854
  restoreAppOwner: (mnemonic, options) => {
804
- // Eslint bug, Promise<void> is correct by docs.
805
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
806
- const { promise, resolve } = Promise.withResolvers<void>();
807
- const onCompleteId = callbacks.register(() => {
808
- resolve();
809
- });
855
+ const { promise, resolve } = Promise.withResolvers<undefined>();
856
+ const onCompleteId = onCompleteCallbacks.register(resolve);
810
857
  dbWorker.postMessage({
811
858
  type: "reset",
812
859
  onCompleteId,
@@ -817,35 +864,105 @@ const createEvoluInstance =
817
864
  },
818
865
 
819
866
  reloadApp: () => {
820
- appState.reset();
867
+ deps.reloadApp(reloadUrl);
821
868
  },
822
869
 
823
870
  ensureSchema: (schema) => {
824
871
  mutationTypesCache.clear();
825
- const validSchema = assertValidEvoluSchema(schema);
826
- dbWorker.postMessage({
827
- type: "ensureDbSchema",
828
- dbSchema: validEvoluSchemaToDbSchema(validSchema),
829
- });
872
+ const dbSchema = evoluSchemaToDbSchema(schema);
873
+ dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
830
874
  },
831
875
 
832
876
  exportDatabase: () => {
833
- const { promise, resolve } = Promise.withResolvers<Uint8Array>();
834
- const onCompleteId = callbacks.register((arg) => {
835
- if (arg instanceof Uint8Array) resolve(arg);
836
- });
877
+ const { promise, resolve } =
878
+ Promise.withResolvers<Uint8Array<ArrayBuffer>>();
879
+ const onCompleteId = exportCallbacks.register(resolve);
837
880
  dbWorker.postMessage({ type: "export", onCompleteId });
838
881
  return promise;
839
882
  },
883
+
884
+ useOwner: (owner) => {
885
+ const scheduleOwnerQueueProcessing = () => {
886
+ if (useOwnerMicrotaskQueue.length !== 1) return;
887
+ queueMicrotask(() => {
888
+ const queue = [...useOwnerMicrotaskQueue];
889
+ useOwnerMicrotaskQueue.length = 0;
890
+
891
+ const result: Array<[SyncOwner, boolean, Uint8Array]> = [];
892
+ const skipIndices = new Set<number>();
893
+
894
+ for (let i = 0; i < queue.length; i++) {
895
+ if (skipIndices.has(i)) continue;
896
+
897
+ const [currentOwner, currentUse, currentOwnerSerialized] =
898
+ queue[i];
899
+
900
+ // Look for opposite action with same owner
901
+ for (let j = i + 1; j < queue.length; j++) {
902
+ if (skipIndices.has(j)) continue;
903
+
904
+ const [, otherUse, otherOwnerSerialized] = queue[j];
905
+
906
+ if (
907
+ currentUse !== otherUse &&
908
+ eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)
909
+ ) {
910
+ // Found cancel-out pair, skip both
911
+ skipIndices.add(i).add(j);
912
+ break;
913
+ }
914
+ }
915
+
916
+ if (!skipIndices.has(i)) {
917
+ result.push([currentOwner, currentUse, currentOwnerSerialized]);
918
+ }
919
+ }
920
+
921
+ for (const [owner, use] of result) {
922
+ dbWorker.postMessage({ type: "useOwner", owner, use });
923
+ }
924
+ });
925
+ };
926
+
927
+ useOwnerMicrotaskQueue.push([owner, true, pack(owner)]);
928
+ scheduleOwnerQueueProcessing();
929
+
930
+ const unuse = () => {
931
+ useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
932
+ scheduleOwnerQueueProcessing();
933
+ };
934
+
935
+ return unuse;
936
+ },
937
+
938
+ /** Disposal is not implemented yet. */
939
+ [Symbol.dispose]: () => {
940
+ throw new Error("Evolu instance disposal is not yet implemented");
941
+ },
840
942
  };
841
943
 
842
944
  return evolu;
843
945
  };
844
946
 
845
- export const createNamespaceName =
846
- (config: Config) =>
847
- (name: string): string =>
848
- `evolu:${config.name}:${name}`;
947
+ export const createQuery = <R extends Row>(
948
+ queryCallback: Parameters<CreateQuery<EvoluSchema>>[0],
949
+ options?: Parameters<CreateQuery<EvoluSchema>>[1],
950
+ ): Query<R> => {
951
+ const compiledQuery = queryCallback(kysely as IntentionalNever).compile();
952
+
953
+ if (isSqlMutation(compiledQuery.sql))
954
+ throw new Error(
955
+ "SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu Mutation API.",
956
+ );
957
+
958
+ return serializeQuery({
959
+ sql: compiledQuery.sql as SafeSql,
960
+ parameters: compiledQuery.parameters as NonNullable<
961
+ SqliteQuery["parameters"]
962
+ >,
963
+ ...(options && { options }),
964
+ });
965
+ };
849
966
 
850
967
  interface LoadingPromises {
851
968
  get: <R extends Row>(
@@ -857,11 +974,13 @@ interface LoadingPromises {
857
974
 
858
975
  resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
859
976
 
860
- releaseUnsubscribed: () => void;
977
+ releaseUnsubscribedOnMutation: () => void;
978
+
979
+ getQueries: () => ReadonlyArray<Query>;
861
980
  }
862
981
 
863
982
  interface LoadingPromise {
864
- /** Promise with props for the upcoming React use hook. */
983
+ /** Promise with props for the React use hook. */
865
984
  promise: Promise<QueryRows> & {
866
985
  status?: "pending" | "fulfilled" | "rejected";
867
986
  value?: QueryRows;
@@ -876,7 +995,7 @@ const createLoadingPromises = (
876
995
  ): LoadingPromises => {
877
996
  const loadingPromiseMap = new Map<Query, LoadingPromise>();
878
997
 
879
- const loadingPromises: LoadingPromises = {
998
+ return {
880
999
  get: <R extends Row>(
881
1000
  query: Query<R>,
882
1001
  ): {
@@ -903,16 +1022,13 @@ const createLoadingPromises = (
903
1022
  if (loadingPromise.promise.status !== "fulfilled") {
904
1023
  loadingPromise.resolve(rows);
905
1024
  } else {
906
- // A promise can't be fulfilled 2x, so we need a new one.
907
1025
  loadingPromise.promise = Promise.resolve(rows);
908
1026
  }
909
1027
 
910
- /**
911
- * "For example, a data framework can set the status and value fields on a
912
- * promise preemptively, before passing to React, so that React can unwrap
913
- * it without waiting a microtask."
914
- * https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
915
- */
1028
+ // Set status and value fields for React's `use` Hook to unwrap synchronously.
1029
+ // While undocumented in React docs, React still uses these properties internally,
1030
+ // and Evolu's own promise caching logic depends on checking `promise.status`.
1031
+ // https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
916
1032
  void Object.assign(loadingPromise.promise, {
917
1033
  status: "fulfilled",
918
1034
  value: rows,
@@ -923,14 +1039,7 @@ const createLoadingPromises = (
923
1039
  }
924
1040
  },
925
1041
 
926
- /**
927
- * We can't delete loading promises in `resolveLoadingPromises` because they
928
- * must be cached, so repeated calls to `loadQuery` will always return the
929
- * same promise until the data changes, and we also can't cache them forever
930
- * because only subscribed queries are automatically updated (reactivity is
931
- * expensive) hence this function must be called manually on any mutation.
932
- */
933
- releaseUnsubscribed: () => {
1042
+ releaseUnsubscribedOnMutation: () => {
934
1043
  [...loadingPromiseMap.entries()]
935
1044
  .filter(([query]) => !subscribedQueries.has(query))
936
1045
  .forEach(([query, loadingPromise]) => {
@@ -941,7 +1050,7 @@ const createLoadingPromises = (
941
1050
  }
942
1051
  });
943
1052
  },
944
- };
945
1053
 
946
- return loadingPromises;
1054
+ getQueries: () => Array.from(loadingPromiseMap.keys()),
1055
+ };
947
1056
  };