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