@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +159 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +209 -124
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +118 -104
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +256 -211
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +558 -359
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +72 -4
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +124 -13
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +68 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +414 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +176 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +30 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +474 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +502 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +528 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +282 -227
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +788 -640
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +238 -18
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +708 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +183 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +708 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
@@ -1,11 +1,12 @@
1
+ import { pack } from "msgpackr";
1
2
  import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
2
- import { assertNonEmptyArray } from "../Assert.js";
3
- import { createCallbacks } from "../Callbacks.js";
3
+ import { assert, assertNonEmptyArray } from "../Assert.js";
4
+ import { createCallbackRegistry } from "../CallbackRegistry.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";
9
10
  import { err, ok, Result } from "../Result.js";
10
11
  import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
11
12
  import { createStore, StoreSubscribe } from "../Store.js";
@@ -18,19 +19,16 @@ import {
18
19
  InferType,
19
20
  Mnemonic,
20
21
  ObjectType,
22
+ ValidMutationSize,
23
+ ValidMutationSizeError,
21
24
  } from "../Type.js";
22
25
  import { IntentionalNever } from "../Types.js";
23
- import { Config, defaultConfig } from "./Config.js";
24
- import { CreateDbWorkerDep } from "./Db.js";
26
+ import { CreateDbWorkerDep, DbConfig, defaultDbConfig } from "./Db.js";
25
27
  import { applyPatches } from "./Diff.js";
26
- import { kysely } from "./Kysely.js";
28
+ import { LocalAuthDep } from "./LocalAuth.js";
27
29
  import { AppOwner } from "./Owner.js";
28
- import { CreateAppStateDep, FlushSyncDep } from "./Platform.js";
29
- import {
30
- DbChange,
31
- ProtocolError,
32
- ProtocolUnsupportedVersionError,
33
- } from "./Protocol.js";
30
+ import { FlushSyncDep, ReloadAppDep } from "./Platform.js";
31
+ import { ProtocolError, ProtocolUnsupportedVersionError } from "./Protocol.js";
34
32
  import {
35
33
  createSubscribedQueries,
36
34
  emptyRows,
@@ -44,23 +42,99 @@ import {
44
42
  SubscribedQueries,
45
43
  } from "./Query.js";
46
44
  import {
47
- assertValidEvoluSchema,
48
45
  CreateQuery,
46
+ DefaultColumns,
49
47
  EvoluSchema,
48
+ evoluSchemaToDbSchema,
49
+ IndexesConfig,
50
50
  insertable,
51
+ kysely,
51
52
  Mutation,
53
+ MutationChange,
52
54
  MutationKind,
53
55
  MutationMapping,
54
56
  MutationOptions,
55
57
  updateable,
56
58
  upsertable,
57
- validEvoluSchemaToDbSchema,
58
- ValidMutationSize,
59
- ValidMutationSizeError,
59
+ ValidateSchema,
60
60
  } from "./Schema.js";
61
- import { initialSyncState, SyncState } from "./Sync.js";
61
+ import { DbChange } from "./Storage.js";
62
+ import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
62
63
  import { TimestampError } from "./Timestamp.js";
63
64
 
65
+ export interface EvoluConfig extends Partial<DbConfig> {
66
+ /**
67
+ * Use the `indexes` option to define SQLite indexes.
68
+ *
69
+ * Table and column names are not typed because Kysely doesn't support it.
70
+ *
71
+ * https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
72
+ *
73
+ * ### Example
74
+ *
75
+ * ```ts
76
+ * const evolu = createEvolu(evoluReactDeps)(Schema, {
77
+ * indexes: (create) => [
78
+ * create("todoCreatedAt").on("todo").column("createdAt"),
79
+ * create("todoCategoryCreatedAt")
80
+ * .on("todoCategory")
81
+ * .column("createdAt"),
82
+ * ],
83
+ * });
84
+ * ```
85
+ */
86
+ readonly indexes?: IndexesConfig;
87
+
88
+ /**
89
+ * URL to reload browser tabs after reset or restore.
90
+ *
91
+ * The default value is `/`.
92
+ */
93
+ readonly reloadUrl?: string;
94
+ }
95
+
96
+ // /**
97
+ // * Validated database change with schema-typed values.
98
+ // *
99
+ // * This is a tagged union where the tag is the table name and the values are
100
+ // * updateable (validated against the schema). This represents the content of a
101
+ // * {@link CrdtMessage} without the timestamp, which is sufficient for business
102
+ // * logic validation in {@link EvoluConfig.onMessage}.
103
+ // */
104
+ // export type ValidatedDbChange<S extends EvoluSchema> = {
105
+ // [Table in keyof S]: {
106
+ // readonly table: Table;
107
+ // readonly id: Id;
108
+ // readonly values: Updateable<S[Table]> & { readonly createdAt?: DateIso };
109
+ // };
110
+ // }[keyof S];
111
+
112
+ // /**
113
+ // * Local-only mutation interface for use within {@link EvoluConfig.onMessage}
114
+ // * callback.
115
+ // *
116
+ // * Provides type-safe mutation methods that only accept tables with names
117
+ // * starting with underscore (local-only tables). All methods require fully
118
+ // * validated branded values. No validation is performed as TypeScript ensures
119
+ // * type correctness.
120
+ // */
121
+ // export interface LocalOnly<S extends EvoluSchema> {
122
+ // readonly insert: <T extends keyof S & `_${string}`>(
123
+ // table: T,
124
+ // values: InferType<ObjectType<InsertableProps<S[T]>>>,
125
+ // ) => InferType<S[T]["id"]>;
126
+
127
+ // readonly update: <T extends keyof S & `_${string}`>(
128
+ // table: T,
129
+ // values: InferType<ObjectType<UpdateableProps<S[T]>>>,
130
+ // ) => void;
131
+
132
+ // readonly upsert: <T extends keyof S & `_${string}`>(
133
+ // table: T,
134
+ // values: InferType<ObjectType<UpsertableProps<S[T]>>>,
135
+ // ) => void;
136
+ // }
137
+
64
138
  export interface Evolu<S extends EvoluSchema = EvoluSchema> {
65
139
  /**
66
140
  * Subscribe to {@link EvoluError} changes.
@@ -88,7 +162,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
88
162
  * All this function does is compile the Kysely query and serialize it into a
89
163
  * unique string. Both operations are fast and cheap.
90
164
  *
91
- * For mutations, use {@link Evolu.insert} and {@link Evolu.update}.
165
+ * For mutations, use {@link Evolu#insert}, {@link Evolu#update}, or
166
+ * {@link Evolu#upsert}.
92
167
  *
93
168
  * ### Example
94
169
  *
@@ -108,43 +183,20 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
108
183
  /**
109
184
  * Load {@link Query} and return a promise with {@link QueryRows}.
110
185
  *
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}.
186
+ * The returned promise always resolves successfully because there is no
187
+ * reason why loading should fail. All data are local, and the query is typed.
188
+ * Unexpected errors are handled with {@link Evolu#subscribeError}.
115
189
  *
116
190
  * 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.
130
- *
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}.
191
+ * for an additional cache. Evolu's internal cache is invalidated on mutation.
192
+ * Unsubscribed queries are removed from the cache, so loading them again will
193
+ * return a new pending promise. Subscribed queries remain in the cache to
194
+ * prevent unnecessary Suspense boundaries from activating. Their promises are
195
+ * replaced with `Promise.resolve(rows)`, allowing React to synchronously
196
+ * unwrap the updated data without suspending.
197
+ *
198
+ * To subscribe a query for automatic updates, use
199
+ * {@link Evolu#subscribeQuery}.
148
200
  *
149
201
  * ### Example
150
202
  *
@@ -152,7 +204,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
152
204
  * const allTodos = evolu.createQuery((db) =>
153
205
  * db.selectFrom("todo").selectAll(),
154
206
  * );
155
- * evolu.loadQuery(allTodos).then(({ rows }) => {
207
+ * evolu.loadQuery(allTodos).then((rows) => {
156
208
  * console.log(rows);
157
209
  * });
158
210
  * ```
@@ -201,86 +253,192 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
201
253
  readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
202
254
 
203
255
  /**
204
- * Subscribe to {@link AppOwner} changes.
256
+ * Promise that resolves to {@link AppOwner} when available.
205
257
  *
206
258
  * ### Example
207
259
  *
208
260
  * ```ts
209
- * const unsubscribe = evolu.subscribeAppOwner(() => {
210
- * const owner = evolu.getAppOwner();
211
- * });
261
+ * const owner = await evolu.appOwner;
212
262
  * ```
213
263
  */
214
- readonly subscribeAppOwner: StoreSubscribe;
264
+ readonly appOwner: Promise<AppOwner>;
265
+
266
+ // TODO: Update it for the owners
267
+ // /**
268
+ // * Subscribe to {@link SyncState} changes.
269
+ // *
270
+ // * ### Example
271
+ // *
272
+ // * ```ts
273
+ // * const unsubscribe = evolu.subscribeSyncState(() => {
274
+ // * const syncState = evolu.getSyncState();
275
+ // * });
276
+ // * ```
277
+ // */
278
+ // readonly subscribeSyncState: StoreSubscribe;
279
+
280
+ // /**
281
+ // * Get {@link SyncState}.
282
+ // *
283
+ // * ### Example
284
+ // *
285
+ // * ```ts
286
+ // * const unsubscribe = evolu.subscribeSyncState(() => {
287
+ // * const syncState = evolu.getSyncState();
288
+ // * });
289
+ // * ```
290
+ // */
291
+ // readonly getSyncState: () => SyncState;
215
292
 
216
293
  /**
217
- * Get {@link AppOwner}.
294
+ * Inserts a row into the database and returns a {@link Result} with the new
295
+ * {@link Id}.
218
296
  *
219
- * ### Example
297
+ * The first argument is the table name, and the second is an object
298
+ * containing the row data. An optional third argument provides mutation
299
+ * options including an `onComplete` callback and `onlyValidate` flag.
220
300
  *
221
- * ```ts
222
- * const unsubscribe = evolu.subscribeAppOwner(() => {
223
- * const owner = evolu.getAppOwner();
224
- * });
225
- * ```
226
- */
227
- readonly getAppOwner: () => AppOwner | null;
228
-
229
- /**
230
- * Subscribe to {@link SyncState} changes.
301
+ * Returns a Result type - use `.ok` to check if the insertion succeeded, and
302
+ * `.value.id` to access the generated ID on success, or `.error` to handle
303
+ * validation errors.
231
304
  *
232
- * ### Example
233
- *
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}.
305
+ * Evolu does not use SQL for mutations to ensure data can be safely and
306
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
307
+ * to automatically add and update {@link DefaultColumns}.
244
308
  *
245
309
  * ### Example
246
310
  *
247
311
  * ```ts
248
- * const unsubscribe = evolu.subscribeSyncState(() => {
249
- * const syncState = evolu.getSyncState();
312
+ * const result = evolu.insert("todo", {
313
+ * title: "Learn Evolu",
314
+ * isCompleted: false,
250
315
  * });
251
- * ```
252
- */
253
- readonly getSyncState: () => SyncState;
254
-
255
- /**
256
- * Inserts a row.
257
316
  *
258
- * ### Example
317
+ * if (result.ok) {
318
+ * console.log("Todo created with ID:", result.value.id);
319
+ * } else {
320
+ * console.error("Validation error:", result.error);
321
+ * }
259
322
  *
260
- * ```ts
261
- * // TODO:
323
+ * // With onComplete callback
324
+ * evolu.insert(
325
+ * "todo",
326
+ * { title: "Another todo" },
327
+ * {
328
+ * onComplete: () => {
329
+ * console.log("Insert completed");
330
+ * },
331
+ * },
332
+ * );
262
333
  * ```
263
334
  */
264
335
  insert: Mutation<S, "insert">;
265
336
 
266
337
  /**
267
- * Updates a row.
338
+ * Updates a row in the database and returns a {@link Result} with the existing
339
+ * {@link Id}.
340
+ *
341
+ * The first argument is the table name, and the second is an object
342
+ * containing the row data including the required `id` field. An optional
343
+ * third argument provides mutation options including an `onComplete` callback
344
+ * and `onlyValidate` flag.
345
+ *
346
+ * Returns a Result type - use `.ok` to check if the update succeeded, and
347
+ * `.value.id` to access the ID on success, or `.error` to handle validation
348
+ * errors.
349
+ *
350
+ * Evolu does not use SQL for mutations to ensure data can be safely and
351
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
352
+ * to automatically add and update {@link DefaultColumns}.
268
353
  *
269
354
  * ### Example
270
355
  *
271
356
  * ```ts
272
- * // TODO:
357
+ * const result = evolu.update("todo", {
358
+ * id: todoId,
359
+ * title: "Updated title",
360
+ * isCompleted: true,
361
+ * });
362
+ *
363
+ * if (result.ok) {
364
+ * console.log("Todo updated with ID:", result.value.id);
365
+ * } else {
366
+ * console.error("Validation error:", result.error);
367
+ * }
368
+ *
369
+ * // To delete a row, set isDeleted to true
370
+ * evolu.update("todo", { id: todoId, isDeleted: true });
371
+ *
372
+ * // With onComplete callback
373
+ * evolu.update(
374
+ * "todo",
375
+ * { id: todoId, title: "New title" },
376
+ * {
377
+ * onComplete: () => {
378
+ * console.log("Update completed");
379
+ * },
380
+ * },
381
+ * );
273
382
  * ```
274
383
  */
275
384
  update: Mutation<S, "update">;
276
385
 
277
386
  /**
278
- * Upserts a row.
387
+ * Upserts a row in the database and returns a {@link Result} with the existing
388
+ * {@link Id}.
389
+ *
390
+ * The first argument is the table name, and the second is an object
391
+ * containing the row data including the required `id` field. An optional
392
+ * third argument provides mutation options including an `onComplete` callback
393
+ * and `onlyValidate` flag.
394
+ *
395
+ * This function allows you to use custom IDs and optionally set `createdAt`,
396
+ * which is useful for external systems, data migrations, or when the same row
397
+ * may already be created on a different device.
398
+ *
399
+ * Returns a Result type - use `.ok` to check if the upsert succeeded, and
400
+ * `.value.id` to access the ID on success, or `.error` to handle validation
401
+ * errors.
402
+ *
403
+ * Evolu does not use SQL for mutations to ensure data can be safely and
404
+ * predictably merged without conflicts. Explicit mutations also allow Evolu
405
+ * to automatically add and update {@link DefaultColumns}.
279
406
  *
280
407
  * ### Example
281
408
  *
282
409
  * ```ts
283
- * // TODO:
410
+ * // Use deterministic ID for stable upserts across devices
411
+ * const stableId = createIdFromString("my-todo-1");
412
+ *
413
+ * const result = evolu.upsert("todo", {
414
+ * id: stableId,
415
+ * title: "Learn Evolu",
416
+ * isCompleted: false,
417
+ * });
418
+ *
419
+ * if (result.ok) {
420
+ * console.log("Todo upserted with ID:", result.value.id);
421
+ * } else {
422
+ * console.error("Validation error:", result.error);
423
+ * }
424
+ *
425
+ * // Data migration with custom createdAt
426
+ * evolu.upsert("todo", {
427
+ * id: externalId,
428
+ * title: "Migrated todo",
429
+ * createdAt: new Date("2023-01-01"), // Preserve original timestamp
430
+ * });
431
+ *
432
+ * // With onComplete callback
433
+ * evolu.upsert(
434
+ * "todo",
435
+ * { id: stableId, title: "Updated title" },
436
+ * {
437
+ * onComplete: () => {
438
+ * console.log("Upsert completed");
439
+ * },
440
+ * },
441
+ * );
284
442
  * ```
285
443
  */
286
444
  upsert: Mutation<S, "upsert">;
@@ -300,7 +458,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
300
458
 
301
459
  /**
302
460
  * Restore {@link AppOwner} with all their synced data. It uses
303
- * {@link Evolu.resetAppOwner}, so be careful.
461
+ * {@link Evolu#resetAppOwner}, so be careful.
304
462
  */
305
463
  readonly restoreAppOwner: (
306
464
  mnemonic: Mnemonic,
@@ -316,17 +474,60 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
316
474
  readonly reloadApp: () => void;
317
475
 
318
476
  /** Export SQLite database file as Uint8Array. */
319
- readonly exportDatabase: () => Promise<Uint8Array>;
477
+ readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
478
+
479
+ /**
480
+ * Use an owner. Using an owner means syncing it and subscribing to
481
+ * broadcasted changes. Returns a function to stop using the owner.
482
+ *
483
+ * Transport connections are automatically deduplicated and reference-counted,
484
+ * so multiple owners using the same transport will share a single
485
+ * connection.
486
+ *
487
+ * ### Example
488
+ *
489
+ * ```ts
490
+ * // Use an owner (starts syncing and subscribing to changes).
491
+ * const unuse = evolu.useOwner(shardOwner);
492
+ *
493
+ * // Later, stop using the owner.
494
+ * unuse();
495
+ *
496
+ * // Bulk operations.
497
+ * const unuses = owners.map((owner) => evolu.useOwner(owner));
498
+ * // Later: unuses.forEach(unuse => unuse());
499
+ * ```
500
+ *
501
+ * @experimental
502
+ */
503
+ readonly useOwner: (owner: SyncOwner) => () => void;
320
504
  }
321
505
 
322
- /** Represent errors that can occur in Evolu. */
506
+ /** Represents errors that can occur in Evolu. */
323
507
  export type EvoluError =
324
- | TimestampError
325
508
  | ProtocolError
326
- | TransferableError
327
- | SymmetricCryptoDecryptError
328
509
  | ProtocolUnsupportedVersionError
329
- | SqliteError;
510
+ | SqliteError
511
+ | SymmetricCryptoDecryptError
512
+ | TimestampError
513
+ | TransferableError;
514
+
515
+ // /**
516
+ // * Error reported when a message is invalid or rejected during processing.
517
+ // *
518
+ // * This error should never happen because a properly written app should ensure
519
+ // * data correctness, but it can occur for two reasons:
520
+ // *
521
+ // * 1. An attack from someone who modified app code
522
+ // * 2. A bug by the developer
523
+ // *
524
+ // * Both cases are useful to report for debugging and security monitoring.
525
+ // */
526
+ // export interface OnMessageError {
527
+ // readonly type: "OnMessageError";
528
+ // readonly invalidChanges: ReadonlyArray<DbChange>;
529
+ // readonly rejectedChanges: ReadonlyArray<DbChange>;
530
+ // }
330
531
 
331
532
  interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
332
533
  extends Evolu<S> {
@@ -337,44 +538,13 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
337
538
  readonly ensureSchema: (schema: EvoluSchema) => void;
338
539
  }
339
540
 
340
- export type EvoluDeps = CreateDbWorkerDep &
341
- TimeDep &
342
- NanoIdLibDep &
541
+ export type EvoluDeps = ConsoleDep &
542
+ CreateDbWorkerDep &
543
+ LocalAuthDep &
343
544
  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
- }
545
+ RandomBytesDep &
546
+ ReloadAppDep &
547
+ TimeDep;
378
548
 
379
549
  // For hot reloading and Evolu multitenancy.
380
550
  const evoluInstances = new Map<string, InternalEvoluInstance>();
@@ -398,7 +568,8 @@ let tabId: Id | null = null;
398
568
  * and merging.
399
569
  * - Automatic schema evolution that updates the underlying database with new
400
570
  * columns or tables.
401
- * - Managing owner data with resetAppOwner and restoreAppOwner.
571
+ * - Managing owner data with {@link Evolu#resetAppOwner} and
572
+ * {@link Evolu#restoreAppOwner}.
402
573
  *
403
574
  * ### Example
404
575
  *
@@ -431,74 +602,117 @@ let tabId: Id | null = null;
431
602
  export const createEvolu =
432
603
  (deps: EvoluDeps) =>
433
604
  <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>> = {},
605
+ schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
606
+ config?: EvoluConfig,
438
607
  ): Evolu<S> => {
439
- const config = { ...defaultConfig, ...partialConfig };
440
-
441
- let evolu = evoluInstances.get(config.name);
608
+ const name = config?.name ?? defaultDbConfig.name;
609
+ let evolu = evoluInstances.get(name);
442
610
 
443
611
  if (evolu == null) {
444
- evolu = createEvoluInstance(deps)(schema, config as IntentionalNever);
445
- evoluInstances.set(config.name, evolu);
612
+ evolu = createEvoluInstance(deps)(schema as EvoluSchema, config);
613
+ evoluInstances.set(name, evolu);
446
614
  } else {
447
615
  // Hot reloading. Note that indexes are intentionally omitted.
448
- evolu.ensureSchema(schema);
616
+ evolu.ensureSchema(schema as EvoluSchema);
449
617
  }
450
618
 
451
- return evolu as IntentionalNever;
619
+ return evolu as Evolu<S>;
452
620
  };
453
621
 
454
622
  const createEvoluInstance =
455
623
  (deps: EvoluDeps) =>
456
- (
457
- schema: EvoluSchema,
458
- evoluConfig: EvoluConfigWithInitialData,
459
- ): InternalEvoluInstance => {
460
- deps.console.enabled = evoluConfig.enableLogging ?? false;
624
+ (schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
625
+ deps.console.enabled = config?.enableLogging ?? false;
626
+
627
+ const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
461
628
 
462
- deps.console.log("[evolu]", "createEvoluInstance");
629
+ const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
463
630
 
464
- const { initialData, indexes, ...config } = evoluConfig;
631
+ deps.console.log("[evolu]", "createEvoluInstance", {
632
+ name: dbConfig.name,
633
+ });
465
634
 
466
635
  const errorStore = createStore<EvoluError | null>(null);
467
636
  const rowsStore = createStore<QueryRowsMap>(new Map());
468
- const ownerStore = createStore<AppOwner | null>(null);
469
- const syncStore = createStore<SyncState>(initialSyncState);
637
+
638
+ const { promise: appOwner, resolve: resolveAppOwner } =
639
+ Promise.withResolvers<AppOwner>();
640
+
641
+ if (config?.externalAppOwner) {
642
+ resolveAppOwner(config.externalAppOwner);
643
+ }
644
+
645
+ // TODO: Update it for the owner-api
646
+ const _syncStore = createStore<SyncState>(initialSyncState);
470
647
 
471
648
  const subscribedQueries = createSubscribedQueries(rowsStore);
472
649
  const loadingPromises = createLoadingPromises(subscribedQueries);
473
- const callbacks = createCallbacks(deps);
650
+ const onCompleteRegistry = createCallbackRegistry(deps);
651
+ const exportRegistry =
652
+ createCallbackRegistry<Uint8Array<ArrayBuffer>>(deps);
474
653
 
475
- const appState = deps.createAppState(config);
476
- const dbWorker = deps.createDbWorker(config.name);
654
+ const dbWorker = deps.createDbWorker(dbConfig.name);
477
655
 
478
656
  const getTabId = () => {
479
657
  tabId ??= createId(deps);
480
658
  return tabId;
481
659
  };
482
660
 
661
+ // const createLocalOnly = (
662
+ // localMutations: Array<MutationChange>,
663
+ // defaultOwnerId: OwnerId | undefined,
664
+ // ): LocalOnly<EvoluSchema> => ({
665
+ // insert: (table, values) => {
666
+ // const id = createId(deps);
667
+ // localMutations.push({
668
+ // table,
669
+ // id,
670
+ // values,
671
+ // ownerId: defaultOwnerId,
672
+ // });
673
+ // return id;
674
+ // },
675
+ // update: (table, values) => {
676
+ // const { id, ...rest } = values;
677
+ // localMutations.push({
678
+ // table,
679
+ // id: id as Id,
680
+ // values: rest,
681
+ // ownerId: defaultOwnerId,
682
+ // });
683
+ // },
684
+ // upsert: (table, values) => {
685
+ // const { id, ...rest } = values as Record<string, unknown> & { id: Id };
686
+ // localMutations.push({
687
+ // table,
688
+ // id: id,
689
+ // values: rest as MutationChange["values"],
690
+ // ownerId: defaultOwnerId,
691
+ // });
692
+ // },
693
+ // });
694
+
695
+ // Worker responses are delivered to all tabs. Each case must handle this
696
+ // properly (e.g., AppOwner promise resolves only once, tabId filtering).
483
697
  dbWorker.onMessage((message) => {
484
698
  switch (message.type) {
485
- case "onInit": {
486
- ownerStore.set(message.owner);
699
+ case "onError": {
700
+ errorStore.set(message.error);
487
701
  break;
488
702
  }
489
703
 
490
- case "onError": {
491
- errorStore.set(message.error);
704
+ case "onGetAppOwner": {
705
+ resolveAppOwner(message.appOwner);
492
706
  break;
493
707
  }
494
708
 
495
- case "onChange": {
709
+ case "onQueryPatches": {
496
710
  if (message.tabId !== getTabId()) return;
497
711
 
498
712
  const state = rowsStore.get();
499
713
  const nextState = new Map([
500
714
  ...state,
501
- ...message.patches.map(
715
+ ...message.queryPatches.map(
502
716
  ({ query, patches }): [Query, ReadonlyArray<Row>] => [
503
717
  query,
504
718
  applyPatches(patches, state.get(query) ?? emptyRows),
@@ -506,7 +720,7 @@ const createEvoluInstance =
506
720
  ),
507
721
  ]);
508
722
 
509
- for (const { query } of message.patches) {
723
+ for (const { query } of message.queryPatches) {
510
724
  loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
511
725
  }
512
726
 
@@ -519,33 +733,111 @@ const createEvoluInstance =
519
733
  }
520
734
 
521
735
  for (const id of message.onCompleteIds) {
522
- callbacks.execute(id);
736
+ onCompleteRegistry.execute(id);
523
737
  }
524
738
  break;
525
739
  }
526
740
 
527
- case "onReceive": {
741
+ case "refreshQueries": {
528
742
  if (message.tabId && message.tabId === getTabId()) return;
529
743
 
530
- loadingPromises.releaseUnsubscribed();
531
- const queries = subscribedQueries.get();
744
+ const loadingPromisesQueries = loadingPromises.getQueries();
745
+ loadingPromises.releaseUnsubscribedOnMutation();
746
+
747
+ const queries = [
748
+ // Dedupe
749
+ ...new Set([...loadingPromisesQueries, ...subscribedQueries.get()]),
750
+ ];
751
+
532
752
  if (isNonEmptyReadonlyArray(queries)) {
533
753
  dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
534
754
  }
755
+
535
756
  break;
536
757
  }
537
758
 
538
759
  case "onReset": {
539
760
  if (message.reload) {
540
- appState.reset();
761
+ deps.reloadApp(reloadUrl);
541
762
  } else {
542
- callbacks.execute(message.onCompleteId);
763
+ onCompleteRegistry.execute(message.onCompleteId);
543
764
  }
544
765
  break;
545
766
  }
546
767
 
768
+ // case "processNewMessages": {
769
+ // void requestIdleTask(
770
+ // toTask(async () => {
771
+ // const approved: Array<Timestamp> = [];
772
+ // const invalidChanges: Array<DbChange> = [];
773
+ // const rejectedChanges: Array<DbChange> = [];
774
+ // const localMutations: Array<MutationChange> = [];
775
+
776
+ // for (const crdtMessage of message.messages) {
777
+ // let isApproved = true;
778
+ // let isValid = true;
779
+
780
+ // const table = crdtMessage.change.table;
781
+ // if (table in schema) {
782
+ // const { createdAt, ...values } = crdtMessage.change.values;
783
+ // isValid =
784
+ // (createdAt ? DateIso.is(createdAt) : true) &&
785
+ // getMutationType(table, "update").is({
786
+ // id: crdtMessage.change.id,
787
+ // ...values,
788
+ // });
789
+ // } else {
790
+ // isValid = false;
791
+ // }
792
+
793
+ // if (!isValid) {
794
+ // isApproved = false;
795
+ // invalidChanges.push(crdtMessage.change);
796
+ // } else if (onMessage) {
797
+ // // At this point, we've validated that the message conforms to the
798
+ // // schema, so the typed callback can safely process it.
799
+ // isApproved = await onMessage(crdtMessage.change, {
800
+ // ownerId: message.ownerId,
801
+ // localOnly: createLocalOnly(localMutations, message.ownerId),
802
+ // });
803
+ // if (!isApproved) {
804
+ // rejectedChanges.push(crdtMessage.change);
805
+ // }
806
+ // }
807
+
808
+ // if (isApproved) {
809
+ // approved.push(crdtMessage.timestamp);
810
+ // }
811
+ // }
812
+
813
+ // // Report OnMessageError if there were any invalid or rejected changes
814
+ // if (invalidChanges.length > 0 || rejectedChanges.length > 0) {
815
+ // const onMessageError: OnMessageError = {
816
+ // type: "OnMessageError",
817
+ // invalidChanges,
818
+ // rejectedChanges,
819
+ // };
820
+ // errorStore.set(onMessageError);
821
+ // }
822
+
823
+ // dbWorker.postMessage({
824
+ // type: "onProcessNewMessages",
825
+ // onCompleteId: message.onCompleteId,
826
+ // approved,
827
+ // localMutations,
828
+ // });
829
+
830
+ // return ok();
831
+ // }),
832
+ // )();
833
+ // break;
834
+ // }
835
+
547
836
  case "onExport": {
548
- callbacks.execute(message.onCompleteId, message.file);
837
+ exportRegistry.execute(
838
+ message.onCompleteId,
839
+ message.file as Uint8Array<ArrayBuffer>,
840
+ );
549
841
  break;
550
842
  }
551
843
 
@@ -554,10 +846,7 @@ const createEvoluInstance =
554
846
  }
555
847
  });
556
848
 
557
- const dbSchema = validEvoluSchemaToDbSchema(
558
- assertValidEvoluSchema(schema),
559
- indexes,
560
- );
849
+ const dbSchema = evoluSchemaToDbSchema(schema, indexes);
561
850
 
562
851
  const mutationTypesCache = new Map<
563
852
  MutationKind,
@@ -581,48 +870,19 @@ const createEvoluInstance =
581
870
  return type;
582
871
  };
583
872
 
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
- }
873
+ dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
608
874
 
609
- return result;
610
- },
611
- });
612
-
613
- dbWorker.postMessage({
614
- type: "init",
615
- config,
616
- dbSchema,
617
- initialData: initialDataDbChanges,
618
- });
875
+ // We can't use `init` to get AppOwner because `init` runs only once per n tabs.
876
+ dbWorker.postMessage({ type: "getAppOwner" });
619
877
 
620
878
  const loadQueryMicrotaskQueue: Array<Query> = [];
621
879
 
622
880
  const mutateMicrotaskQueue: Array<
623
- [DbChange | undefined, MutationOptions["onComplete"] | undefined]
881
+ [MutationChange | null, MutationOptions["onComplete"] | undefined]
624
882
  > = [];
625
883
 
884
+ const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
885
+
626
886
  const createMutation =
627
887
  <Kind extends MutationKind>(kind: Kind) =>
628
888
  <TableName extends keyof typeof schema>(
@@ -638,8 +898,7 @@ const createEvoluInstance =
638
898
  ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
639
899
  >
640
900
  > => {
641
- const Type = getMutationType(table, kind);
642
- const result = Type.fromUnknown(props);
901
+ const result = getMutationType(table, kind).fromUnknown(props);
643
902
 
644
903
  const id =
645
904
  kind === "insert"
@@ -648,51 +907,32 @@ const createEvoluInstance =
648
907
 
649
908
  if (options?.onlyValidate !== true) {
650
909
  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]);
910
+ // Mark the transaction as invalid by pushing null
911
+ mutateMicrotaskQueue.push([null, undefined]);
654
912
  } 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 = [];
913
+ const values = { ...result.value };
914
+ delete values.id;
666
915
 
667
- for (const [change, onComplete] of mutateMicrotaskQueue) {
668
- if (change) changes.push(change);
669
- if (onComplete) onCompletes.push(onComplete);
916
+ if (kind === "insert" || kind === "upsert") {
917
+ // Only set createdAt if not provided by user
918
+ if (!("createdAt" in values)) {
919
+ values.createdAt = new Date(deps.time.now()).toISOString();
670
920
  }
921
+ }
671
922
 
672
- const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
673
- mutateMicrotaskQueue.length = 0;
923
+ const dbChange = { table, id, values };
924
+ assert(
925
+ DbChange.is(dbChange),
926
+ `Failed to create DbChange for table "${dbChange.table}"`,
927
+ );
674
928
 
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
- }
680
-
681
- const onCompleteIds = onCompletes.map((onComplete) =>
682
- callbacks.register(onComplete),
683
- );
684
-
685
- loadingPromises.releaseUnsubscribed();
929
+ const mutationChange = { ...dbChange, ownerId: options?.ownerId };
930
+ mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
931
+ }
686
932
 
687
- if (isNonEmptyArray(changes))
688
- dbWorker.postMessage({
689
- type: "mutate",
690
- tabId: getTabId(),
691
- changes,
692
- onCompleteIds,
693
- subscribedQueries: subscribedQueries.get(),
694
- });
695
- });
933
+ if (mutateMicrotaskQueue.length === 1) {
934
+ queueMicrotask(processMutationQueue);
935
+ }
696
936
  }
697
937
 
698
938
  if (result.ok) return ok({ id });
@@ -706,28 +946,74 @@ const createEvoluInstance =
706
946
  );
707
947
  };
708
948
 
949
+ const processMutationQueue = () => {
950
+ const changes: Array<MutationChange> = [];
951
+ const onCompleteCallbacks = [];
952
+
953
+ for (const [change, onComplete] of mutateMicrotaskQueue) {
954
+ if (change !== null) changes.push(change);
955
+ if (onComplete) onCompleteCallbacks.push(onComplete);
956
+ }
957
+
958
+ const queueLength = mutateMicrotaskQueue.length;
959
+ mutateMicrotaskQueue.length = 0;
960
+
961
+ // Don't process any mutations if there was a validation error.
962
+ // All mutations within a queue run as a single transaction.
963
+ if (changes.length !== queueLength) {
964
+ return;
965
+ }
966
+
967
+ const onCompleteIds = onCompleteCallbacks.map(
968
+ onCompleteRegistry.register,
969
+ );
970
+
971
+ loadingPromises.releaseUnsubscribedOnMutation();
972
+
973
+ if (!isNonEmptyArray(changes)) return;
974
+
975
+ // if (onMessage) {
976
+ // const rejectedChanges: Array<DbChange> = [];
977
+ // const localMutations: Array<MutationChange> = [];
978
+
979
+ // for (const change of changes) {
980
+ // const localOnly = createLocalOnly(localMutations, change.ownerId);
981
+
982
+ // const isApproved = await onMessage(change, {
983
+ // ownerId: change.ownerId,
984
+ // localOnly,
985
+ // });
986
+ // if (!isApproved) {
987
+ // rejectedChanges.push(change);
988
+ // }
989
+ // }
990
+
991
+ // if (rejectedChanges.length > 0) {
992
+ // errorStore.set({
993
+ // type: "OnMessageError",
994
+ // invalidChanges: [],
995
+ // rejectedChanges,
996
+ // });
997
+ // return;
998
+ // }
999
+
1000
+ // changes.push(...localMutations);
1001
+ // }
1002
+
1003
+ dbWorker.postMessage({
1004
+ type: "mutate",
1005
+ tabId: getTabId(),
1006
+ changes,
1007
+ onCompleteIds,
1008
+ subscribedQueries: subscribedQueries.get(),
1009
+ });
1010
+ };
1011
+
709
1012
  const evolu: InternalEvoluInstance = {
710
1013
  subscribeError: errorStore.subscribe,
711
1014
  getError: errorStore.get,
712
1015
 
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
- },
1016
+ createQuery,
731
1017
 
732
1018
  loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
733
1019
  const { promise, isNew } = loadingPromises.get(query);
@@ -740,6 +1026,7 @@ const createEvoluInstance =
740
1026
  const queries = [...new Set(loadQueryMicrotaskQueue)];
741
1027
  loadQueryMicrotaskQueue.length = 0;
742
1028
  assertNonEmptyArray(queries);
1029
+ deps.console.log("[evolu]", "loadQuery", { queries });
743
1030
  dbWorker.postMessage({
744
1031
  type: "query",
745
1032
  tabId: getTabId(),
@@ -775,23 +1062,19 @@ const createEvoluInstance =
775
1062
  getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
776
1063
  (rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
777
1064
 
778
- subscribeAppOwner: ownerStore.subscribe,
779
- getAppOwner: ownerStore.get,
1065
+ appOwner,
780
1066
 
781
- subscribeSyncState: syncStore.subscribe,
782
- getSyncState: syncStore.get,
1067
+ // TODO: Update it for the owner-api
1068
+ // subscribeSyncState: syncStore.subscribe,
1069
+ // getSyncState: syncStore.get,
783
1070
 
784
1071
  insert: createMutation("insert"),
785
1072
  update: createMutation("update"),
786
1073
  upsert: createMutation("upsert"),
787
1074
 
788
1075
  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
- });
1076
+ const { promise, resolve } = Promise.withResolvers<undefined>();
1077
+ const onCompleteId = onCompleteRegistry.register(resolve);
795
1078
  dbWorker.postMessage({
796
1079
  type: "reset",
797
1080
  onCompleteId,
@@ -801,12 +1084,8 @@ const createEvoluInstance =
801
1084
  },
802
1085
 
803
1086
  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
- });
1087
+ const { promise, resolve } = Promise.withResolvers<undefined>();
1088
+ const onCompleteId = onCompleteRegistry.register(resolve);
810
1089
  dbWorker.postMessage({
811
1090
  type: "reset",
812
1091
  onCompleteId,
@@ -817,35 +1096,100 @@ const createEvoluInstance =
817
1096
  },
818
1097
 
819
1098
  reloadApp: () => {
820
- appState.reset();
1099
+ deps.reloadApp(reloadUrl);
821
1100
  },
822
1101
 
823
1102
  ensureSchema: (schema) => {
824
1103
  mutationTypesCache.clear();
825
- const validSchema = assertValidEvoluSchema(schema);
826
- dbWorker.postMessage({
827
- type: "ensureDbSchema",
828
- dbSchema: validEvoluSchemaToDbSchema(validSchema),
829
- });
1104
+ const dbSchema = evoluSchemaToDbSchema(schema);
1105
+ dbWorker.postMessage({ type: "ensureDbSchema", dbSchema });
830
1106
  },
831
1107
 
832
1108
  exportDatabase: () => {
833
- const { promise, resolve } = Promise.withResolvers<Uint8Array>();
834
- const onCompleteId = callbacks.register((arg) => {
835
- if (arg instanceof Uint8Array) resolve(arg);
836
- });
1109
+ const { promise, resolve } =
1110
+ Promise.withResolvers<Uint8Array<ArrayBuffer>>();
1111
+ const onCompleteId = exportRegistry.register(resolve);
837
1112
  dbWorker.postMessage({ type: "export", onCompleteId });
838
1113
  return promise;
839
1114
  },
1115
+
1116
+ useOwner: (owner) => {
1117
+ const scheduleOwnerQueueProcessing = () => {
1118
+ if (useOwnerMicrotaskQueue.length !== 1) return;
1119
+ queueMicrotask(() => {
1120
+ const queue = [...useOwnerMicrotaskQueue];
1121
+ useOwnerMicrotaskQueue.length = 0;
1122
+
1123
+ const result: Array<[SyncOwner, boolean, Uint8Array]> = [];
1124
+ const skipIndices = new Set<number>();
1125
+
1126
+ for (let i = 0; i < queue.length; i++) {
1127
+ if (skipIndices.has(i)) continue;
1128
+
1129
+ const [currentOwner, currentUse, currentOwnerSerialized] =
1130
+ queue[i];
1131
+
1132
+ // Look for opposite action with same owner
1133
+ for (let j = i + 1; j < queue.length; j++) {
1134
+ if (skipIndices.has(j)) continue;
1135
+
1136
+ const [, otherUse, otherOwnerSerialized] = queue[j];
1137
+
1138
+ if (
1139
+ currentUse !== otherUse &&
1140
+ eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)
1141
+ ) {
1142
+ // Found cancel-out pair, skip both
1143
+ skipIndices.add(i).add(j);
1144
+ break;
1145
+ }
1146
+ }
1147
+
1148
+ if (!skipIndices.has(i)) {
1149
+ result.push([currentOwner, currentUse, currentOwnerSerialized]);
1150
+ }
1151
+ }
1152
+
1153
+ for (const [owner, use] of result) {
1154
+ dbWorker.postMessage({ type: "useOwner", owner, use });
1155
+ }
1156
+ });
1157
+ };
1158
+
1159
+ useOwnerMicrotaskQueue.push([owner, true, pack(owner)]);
1160
+ scheduleOwnerQueueProcessing();
1161
+
1162
+ const unuse = () => {
1163
+ useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
1164
+ scheduleOwnerQueueProcessing();
1165
+ };
1166
+
1167
+ return unuse;
1168
+ },
840
1169
  };
841
1170
 
842
1171
  return evolu;
843
1172
  };
844
1173
 
845
- export const createNamespaceName =
846
- (config: Config) =>
847
- (name: string): string =>
848
- `evolu:${config.name}:${name}`;
1174
+ export const createQuery = <R extends Row>(
1175
+ queryCallback: Parameters<CreateQuery<EvoluSchema>>[0],
1176
+ options?: Parameters<CreateQuery<EvoluSchema>>[1],
1177
+ ): Query<R> => {
1178
+ const compiledQuery = queryCallback(kysely as IntentionalNever).compile();
1179
+
1180
+ if (isSqlMutation(compiledQuery.sql))
1181
+ throw new Error(
1182
+ "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.",
1183
+ );
1184
+
1185
+ return serializeQuery({
1186
+ sql: compiledQuery.sql as SafeSql,
1187
+ parameters: compiledQuery.parameters as NonNullable<
1188
+ SqliteQuery["parameters"]
1189
+ >,
1190
+ ...(options && { options }),
1191
+ });
1192
+ };
849
1193
 
850
1194
  interface LoadingPromises {
851
1195
  get: <R extends Row>(
@@ -855,13 +1199,32 @@ interface LoadingPromises {
855
1199
  readonly isNew: boolean;
856
1200
  };
857
1201
 
1202
+ /**
1203
+ * Resolve a cached promise with updated rows.
1204
+ *
1205
+ * If the promise is not yet fulfilled, it will be resolved normally. If
1206
+ * already fulfilled (subscribed query updated after mutation), the promise
1207
+ * property is replaced with a new `Promise.resolve(rows)` while keeping the
1208
+ * same cached object reference. The promise is not removed from the cache
1209
+ * because React Suspense requires repeated calls to return the same promise.
1210
+ */
858
1211
  resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
859
1212
 
860
- releaseUnsubscribed: () => void;
1213
+ /**
1214
+ * Release unsubscribed queries from the cache.
1215
+ *
1216
+ * Loading promises can't be released in `resolve` because they must be cached
1217
+ * for React Suspense, but they also can't be cached forever because only
1218
+ * subscribed queries are automatically updated (reactivity is expensive
1219
+ * because it's implemented via refetching subscribed queries).
1220
+ */
1221
+ releaseUnsubscribedOnMutation: () => void;
1222
+
1223
+ getQueries: () => ReadonlyArray<Query>;
861
1224
  }
862
1225
 
863
1226
  interface LoadingPromise {
864
- /** Promise with props for the upcoming React use hook. */
1227
+ /** Promise with props for the React use hook. */
865
1228
  promise: Promise<QueryRows> & {
866
1229
  status?: "pending" | "fulfilled" | "rejected";
867
1230
  value?: QueryRows;
@@ -876,7 +1239,7 @@ const createLoadingPromises = (
876
1239
  ): LoadingPromises => {
877
1240
  const loadingPromiseMap = new Map<Query, LoadingPromise>();
878
1241
 
879
- const loadingPromises: LoadingPromises = {
1242
+ return {
880
1243
  get: <R extends Row>(
881
1244
  query: Query<R>,
882
1245
  ): {
@@ -903,16 +1266,13 @@ const createLoadingPromises = (
903
1266
  if (loadingPromise.promise.status !== "fulfilled") {
904
1267
  loadingPromise.resolve(rows);
905
1268
  } else {
906
- // A promise can't be fulfilled 2x, so we need a new one.
907
1269
  loadingPromise.promise = Promise.resolve(rows);
908
1270
  }
909
1271
 
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
- */
1272
+ // Set status and value fields for React's `use` Hook to unwrap synchronously.
1273
+ // While undocumented in React docs, React still uses these properties internally,
1274
+ // and Evolu's own promise caching logic depends on checking `promise.status`.
1275
+ // https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
916
1276
  void Object.assign(loadingPromise.promise, {
917
1277
  status: "fulfilled",
918
1278
  value: rows,
@@ -923,14 +1283,7 @@ const createLoadingPromises = (
923
1283
  }
924
1284
  },
925
1285
 
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: () => {
1286
+ releaseUnsubscribedOnMutation: () => {
934
1287
  [...loadingPromiseMap.entries()]
935
1288
  .filter(([query]) => !subscribedQueries.has(query))
936
1289
  .forEach(([query, loadingPromise]) => {
@@ -941,7 +1294,7 @@ const createLoadingPromises = (
941
1294
  }
942
1295
  });
943
1296
  },
944
- };
945
1297
 
946
- return loadingPromises;
1298
+ getQueries: () => Array.from(loadingPromiseMap.keys()),
1299
+ };
947
1300
  };