@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20

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 (140) hide show
  1. package/dist/src/Assert.d.ts.map +1 -1
  2. package/dist/src/Assert.js +1 -1
  3. package/dist/src/Buffer.d.ts +1 -1
  4. package/dist/src/Buffer.d.ts.map +1 -1
  5. package/dist/src/Buffer.js +1 -1
  6. package/dist/src/CallbackRegistry.d.ts +53 -0
  7. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  8. package/dist/src/CallbackRegistry.js +25 -0
  9. package/dist/src/Console.d.ts +31 -6
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +72 -9
  12. package/dist/src/Crypto.d.ts +48 -37
  13. package/dist/src/Crypto.d.ts.map +1 -1
  14. package/dist/src/Crypto.js +27 -50
  15. package/dist/src/Evolu/Db.d.ts +138 -66
  16. package/dist/src/Evolu/Db.d.ts.map +1 -1
  17. package/dist/src/Evolu/Db.js +248 -645
  18. package/dist/src/Evolu/Diff.d.ts +3 -3
  19. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  20. package/dist/src/Evolu/Diff.js +7 -5
  21. package/dist/src/Evolu/Evolu.d.ts +79 -116
  22. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  23. package/dist/src/Evolu/Evolu.js +275 -132
  24. package/dist/src/Evolu/Internal.d.ts +0 -2
  25. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  26. package/dist/src/Evolu/Internal.js +0 -2
  27. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  28. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  29. package/dist/src/Evolu/LocalAuth.js +171 -0
  30. package/dist/src/Evolu/Owner.d.ts +129 -83
  31. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  32. package/dist/src/Evolu/Owner.js +80 -89
  33. package/dist/src/Evolu/Platform.d.ts +9 -7
  34. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  35. package/dist/src/Evolu/Protocol.d.ts +114 -191
  36. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  37. package/dist/src/Evolu/Protocol.js +409 -416
  38. package/dist/src/Evolu/Public.d.ts +6 -8
  39. package/dist/src/Evolu/Public.d.ts.map +1 -1
  40. package/dist/src/Evolu/Public.js +2 -3
  41. package/dist/src/Evolu/PublicKysely.js +3 -3
  42. package/dist/src/Evolu/Relay.d.ts +1 -2
  43. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  44. package/dist/src/Evolu/Relay.js +11 -9
  45. package/dist/src/Evolu/Schema.d.ts +88 -27
  46. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  47. package/dist/src/Evolu/Schema.js +141 -24
  48. package/dist/src/Evolu/Storage.d.ts +158 -14
  49. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  50. package/dist/src/Evolu/Storage.js +32 -32
  51. package/dist/src/Evolu/Sync.d.ts +77 -13
  52. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  53. package/dist/src/Evolu/Sync.js +453 -20
  54. package/dist/src/Evolu/Timestamp.d.ts +29 -27
  55. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  56. package/dist/src/Evolu/Timestamp.js +20 -18
  57. package/dist/src/ManyToManyMap.d.ts +74 -10
  58. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  59. package/dist/src/ManyToManyMap.js +41 -6
  60. package/dist/src/Random.d.ts +3 -2
  61. package/dist/src/Random.d.ts.map +1 -1
  62. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  63. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  64. package/dist/src/RefCountedResourceManager.js +197 -0
  65. package/dist/src/Result.d.ts +144 -22
  66. package/dist/src/Result.d.ts.map +1 -1
  67. package/dist/src/Result.js +5 -2
  68. package/dist/src/Sqlite.d.ts +20 -4
  69. package/dist/src/Sqlite.d.ts.map +1 -1
  70. package/dist/src/Sqlite.js +50 -8
  71. package/dist/src/Task.d.ts +511 -0
  72. package/dist/src/Task.d.ts.map +1 -0
  73. package/dist/src/Task.js +410 -0
  74. package/dist/src/Time.d.ts +59 -0
  75. package/dist/src/Time.d.ts.map +1 -1
  76. package/dist/src/Time.js +87 -4
  77. package/dist/src/Type.d.ts +431 -341
  78. package/dist/src/Type.d.ts.map +1 -1
  79. package/dist/src/Type.js +458 -466
  80. package/dist/src/WebSocket.d.ts +5 -2
  81. package/dist/src/WebSocket.d.ts.map +1 -1
  82. package/dist/src/WebSocket.js +12 -13
  83. package/dist/src/Worker.d.ts +39 -11
  84. package/dist/src/Worker.d.ts.map +1 -1
  85. package/dist/src/Worker.js +22 -4
  86. package/dist/src/index.d.ts +2 -3
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +2 -3
  89. package/package.json +7 -7
  90. package/src/Assert.ts +2 -4
  91. package/src/Buffer.ts +1 -1
  92. package/src/CallbackRegistry.ts +84 -0
  93. package/src/Console.ts +91 -11
  94. package/src/Crypto.ts +78 -91
  95. package/src/Evolu/Db.ts +455 -947
  96. package/src/Evolu/Diff.ts +7 -5
  97. package/src/Evolu/Evolu.ts +545 -307
  98. package/src/Evolu/Internal.ts +0 -2
  99. package/src/Evolu/LocalAuth.ts +422 -0
  100. package/src/Evolu/Owner.ts +191 -131
  101. package/src/Evolu/Platform.ts +9 -9
  102. package/src/Evolu/Protocol.ts +536 -653
  103. package/src/Evolu/Public.ts +7 -9
  104. package/src/Evolu/PublicKysely.ts +3 -3
  105. package/src/Evolu/Relay.ts +17 -12
  106. package/src/Evolu/Schema.ts +271 -66
  107. package/src/Evolu/Storage.ts +263 -55
  108. package/src/Evolu/Sync.ts +758 -37
  109. package/src/Evolu/Timestamp.ts +30 -35
  110. package/src/ManyToManyMap.ts +127 -24
  111. package/src/Random.ts +3 -2
  112. package/src/RefCountedResourceManager.ts +368 -0
  113. package/src/Result.ts +149 -23
  114. package/src/Sqlite.ts +59 -24
  115. package/src/Task.ts +779 -0
  116. package/src/Time.ts +168 -4
  117. package/src/Type.ts +657 -695
  118. package/src/WebSocket.ts +23 -17
  119. package/src/Worker.ts +72 -23
  120. package/src/index.ts +2 -3
  121. package/dist/src/Callbacks.d.ts +0 -20
  122. package/dist/src/Callbacks.d.ts.map +0 -1
  123. package/dist/src/Callbacks.js +0 -18
  124. package/dist/src/Evolu/Config.d.ts +0 -82
  125. package/dist/src/Evolu/Config.d.ts.map +0 -1
  126. package/dist/src/Evolu/Config.js +0 -9
  127. package/dist/src/Evolu/Kysely.d.ts +0 -6
  128. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  129. package/dist/src/Evolu/Kysely.js +0 -21
  130. package/dist/src/NanoId.d.ts +0 -27
  131. package/dist/src/NanoId.d.ts.map +0 -1
  132. package/dist/src/NanoId.js +0 -6
  133. package/dist/src/Promise.d.ts +0 -180
  134. package/dist/src/Promise.d.ts.map +0 -1
  135. package/dist/src/Promise.js +0 -176
  136. package/src/Callbacks.ts +0 -43
  137. package/src/Evolu/Config.ts +0 -97
  138. package/src/Evolu/Kysely.ts +0 -38
  139. package/src/NanoId.ts +0 -39
  140. package/src/Promise.ts +0 -295
@@ -1,11 +1,12 @@
1
+ import { pack } from "msgpackr";
1
2
  import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
2
3
  import { assert, assertNonEmptyArray } from "../Assert.js";
3
- import { createCallbacks } from "../Callbacks.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,
@@ -48,20 +46,95 @@ import {
48
46
  DefaultColumns,
49
47
  EvoluSchema,
50
48
  evoluSchemaToDbSchema,
49
+ IndexesConfig,
51
50
  insertable,
51
+ kysely,
52
52
  Mutation,
53
+ MutationChange,
53
54
  MutationKind,
54
55
  MutationMapping,
55
56
  MutationOptions,
56
57
  updateable,
57
58
  upsertable,
58
59
  ValidateSchema,
59
- ValidMutationSize,
60
- ValidMutationSizeError,
61
60
  } from "./Schema.js";
62
- import { initialSyncState, SyncState } from "./Sync.js";
61
+ import { DbChange } from "./Storage.js";
62
+ import { initialSyncState, SyncOwner, SyncState } from "./Sync.js";
63
63
  import { TimestampError } from "./Timestamp.js";
64
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
+
65
138
  export interface Evolu<S extends EvoluSchema = EvoluSchema> {
66
139
  /**
67
140
  * Subscribe to {@link EvoluError} changes.
@@ -89,7 +162,8 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
89
162
  * All this function does is compile the Kysely query and serialize it into a
90
163
  * unique string. Both operations are fast and cheap.
91
164
  *
92
- * 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}.
93
167
  *
94
168
  * ### Example
95
169
  *
@@ -109,43 +183,20 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
109
183
  /**
110
184
  * Load {@link Query} and return a promise with {@link QueryRows}.
111
185
  *
112
- * A returned promise always resolves successfully because there is no reason
113
- * why loading should fail. All data are local, and the query is typed. A
114
- * serious unexpected Evolu error shall be handled with
115
- * {@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}.
116
189
  *
117
190
  * Loading is batched, and returned promises are cached, so there is no need
118
- * for an additional cache. Evolu's internal cache is invalidated on
119
- * mutation.
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.
120
197
  *
121
- * The returned promise is enriched with special status and value properties
122
- * for the upcoming React `use` Hook, but other UI libraries can also leverage
123
- * them. Speaking of React, there are two essential React Suspense-related
124
- * patterns that every developer should be aware of—passing promises to
125
- * children and caching over mutations.
126
- *
127
- * With promises passed to children, we can load a query as soon as possible,
128
- * but we don't have to use the returned promise immediately. That's useful
129
- * for prefetching, which is generally not necessary for local-first apps but
130
- * can be if a query takes a long time to load.
131
- *
132
- * Caching over mutation is a pattern that every developer should know. As we
133
- * said, Evolu caches promise until a mutation happens. A query loaded after
134
- * that will return a new pending promise. That's okay for general usage but
135
- * not for UI with React Suspense because a mutation would suspend rerendered
136
- * queries on a page, and that's not a good UX.
137
- *
138
- * We call this pattern "caching over mutation" because it has no globally
139
- * accepted name yet. React RFC for React Cache does not exist yet.
140
- *
141
- * For better UX, a query must be subscribed for updates. This way, instead of
142
- * Suspense flashes, the user sees new data immediately because Evolu replaces
143
- * cached promises with fresh, already resolved new ones.
144
- *
145
- * If you are curious why Evolu does not do that for all queries by default,
146
- * the answer is simple: performance. Tracking changes is costly and
147
- * meaningful only for visible (hence subscribed) queries anyway. To subscribe
148
- * to a query, use {@link Evolu#subscribeQuery}.
198
+ * To subscribe a query for automatic updates, use
199
+ * {@link Evolu#subscribeQuery}.
149
200
  *
150
201
  * ### Example
151
202
  *
@@ -153,7 +204,7 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
153
204
  * const allTodos = evolu.createQuery((db) =>
154
205
  * db.selectFrom("todo").selectAll(),
155
206
  * );
156
- * evolu.loadQuery(allTodos).then(({ rows }) => {
207
+ * evolu.loadQuery(allTodos).then((rows) => {
157
208
  * console.log(rows);
158
209
  * });
159
210
  * ```
@@ -202,56 +253,42 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
202
253
  readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
203
254
 
204
255
  /**
205
- * Subscribe to {@link AppOwner} changes.
256
+ * Promise that resolves to {@link AppOwner} when available.
206
257
  *
207
258
  * ### Example
208
259
  *
209
260
  * ```ts
210
- * const unsubscribe = evolu.subscribeAppOwner(() => {
211
- * const owner = evolu.getAppOwner();
212
- * });
261
+ * const owner = await evolu.appOwner;
213
262
  * ```
214
263
  */
215
- readonly subscribeAppOwner: StoreSubscribe;
216
-
217
- /**
218
- * Get {@link AppOwner}.
219
- *
220
- * ### Example
221
- *
222
- * ```ts
223
- * const unsubscribe = evolu.subscribeAppOwner(() => {
224
- * const owner = evolu.getAppOwner();
225
- * });
226
- * ```
227
- */
228
- readonly getAppOwner: () => AppOwner | null;
229
-
230
- /**
231
- * Subscribe to {@link SyncState} changes.
232
- *
233
- * ### Example
234
- *
235
- * ```ts
236
- * const unsubscribe = evolu.subscribeSyncState(() => {
237
- * const syncState = evolu.getSyncState();
238
- * });
239
- * ```
240
- */
241
- readonly subscribeSyncState: StoreSubscribe;
242
-
243
- /**
244
- * Get {@link SyncState}.
245
- *
246
- * ### Example
247
- *
248
- * ```ts
249
- * const unsubscribe = evolu.subscribeSyncState(() => {
250
- * const syncState = evolu.getSyncState();
251
- * });
252
- * ```
253
- */
254
- readonly getSyncState: () => SyncState;
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;
255
292
 
256
293
  /**
257
294
  * Inserts a row into the database and returns a {@link Result} with the new
@@ -437,17 +474,60 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
437
474
  readonly reloadApp: () => void;
438
475
 
439
476
  /** Export SQLite database file as Uint8Array. */
440
- 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;
441
504
  }
442
505
 
443
- /** Represent errors that can occur in Evolu. */
506
+ /** Represents errors that can occur in Evolu. */
444
507
  export type EvoluError =
445
- | TimestampError
446
508
  | ProtocolError
447
- | TransferableError
448
- | SymmetricCryptoDecryptError
449
509
  | ProtocolUnsupportedVersionError
450
- | 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
+ // }
451
531
 
452
532
  interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
453
533
  extends Evolu<S> {
@@ -458,44 +538,13 @@ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
458
538
  readonly ensureSchema: (schema: EvoluSchema) => void;
459
539
  }
460
540
 
461
- export type EvoluDeps = CreateDbWorkerDep &
462
- TimeDep &
463
- NanoIdLibDep &
541
+ export type EvoluDeps = ConsoleDep &
542
+ CreateDbWorkerDep &
543
+ LocalAuthDep &
464
544
  Partial<FlushSyncDep> &
465
- ConsoleDep &
466
- CreateAppStateDep;
467
-
468
- export interface EvoluConfigWithFunctions extends Config {
469
- /**
470
- * Callback invoked when the database is initialized. Use `isFirst` to perform
471
- * one-time setup like initial data seeding.
472
- *
473
- * ### Example
474
- *
475
- * ```ts
476
- * const evolu = createEvolu(evoluReactWebDeps)(Schema, {
477
- * onInit: ({ appOwner, isFirst }) => {
478
- * if (isFirst) {
479
- * const todoCategoryId = getOrThrow(
480
- * evolu.insert("todoCategory", {
481
- * name: "Not Urgent",
482
- * }),
483
- * );
484
- *
485
- * evolu.insert("todo", {
486
- * title: "Try React Suspense",
487
- * categoryId: todoCategoryId.id,
488
- * });
489
- * }
490
- * },
491
- * });
492
- * ```
493
- */
494
- readonly onInit?: (params: {
495
- readonly appOwner: AppOwner;
496
- readonly isFirst: boolean;
497
- }) => void;
498
- }
545
+ RandomBytesDep &
546
+ ReloadAppDep &
547
+ TimeDep;
499
548
 
500
549
  // For hot reloading and Evolu multitenancy.
501
550
  const evoluInstances = new Map<string, InternalEvoluInstance>();
@@ -519,7 +568,8 @@ let tabId: Id | null = null;
519
568
  * and merging.
520
569
  * - Automatic schema evolution that updates the underlying database with new
521
570
  * columns or tables.
522
- * - Managing owner data with resetAppOwner and restoreAppOwner.
571
+ * - Managing owner data with {@link Evolu#resetAppOwner} and
572
+ * {@link Evolu#restoreAppOwner}.
523
573
  *
524
574
  * ### Example
525
575
  *
@@ -553,78 +603,116 @@ export const createEvolu =
553
603
  (deps: EvoluDeps) =>
554
604
  <S extends EvoluSchema>(
555
605
  schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
556
- partialConfig: Partial<EvoluConfigWithFunctions> = {},
606
+ config?: EvoluConfig,
557
607
  ): Evolu<S> => {
558
- const config = { ...defaultConfig, ...partialConfig };
559
-
560
- let evolu = evoluInstances.get(config.name);
608
+ const name = config?.name ?? defaultDbConfig.name;
609
+ let evolu = evoluInstances.get(name);
561
610
 
562
611
  if (evolu == null) {
563
- evolu = createEvoluInstance(deps)(
564
- schema as EvoluSchema,
565
- config as IntentionalNever,
566
- );
567
- evoluInstances.set(config.name, evolu);
612
+ evolu = createEvoluInstance(deps)(schema as EvoluSchema, config);
613
+ evoluInstances.set(name, evolu);
568
614
  } else {
569
615
  // Hot reloading. Note that indexes are intentionally omitted.
570
616
  evolu.ensureSchema(schema as EvoluSchema);
571
617
  }
572
618
 
573
- return evolu as IntentionalNever;
619
+ return evolu as Evolu<S>;
574
620
  };
575
621
 
576
622
  const createEvoluInstance =
577
623
  (deps: EvoluDeps) =>
578
- (
579
- schema: EvoluSchema,
580
- evoluConfig: EvoluConfigWithFunctions,
581
- ): InternalEvoluInstance => {
582
- deps.console.enabled = evoluConfig.enableLogging ?? false;
624
+ (schema: EvoluSchema, config?: EvoluConfig): InternalEvoluInstance => {
625
+ deps.console.enabled = config?.enableLogging ?? false;
583
626
 
584
- deps.console.log("[evolu]", "createEvoluInstance");
627
+ const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
585
628
 
586
- const { onInit, indexes, ...config } = evoluConfig;
629
+ const dbConfig: DbConfig = { ...defaultDbConfig, ...partialDbConfig };
630
+
631
+ deps.console.log("[evolu]", "createEvoluInstance", {
632
+ name: dbConfig.name,
633
+ });
587
634
 
588
635
  const errorStore = createStore<EvoluError | null>(null);
589
636
  const rowsStore = createStore<QueryRowsMap>(new Map());
590
- const appOwnerStore = createStore<AppOwner | null>(null);
591
- 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);
592
647
 
593
648
  const subscribedQueries = createSubscribedQueries(rowsStore);
594
649
  const loadingPromises = createLoadingPromises(subscribedQueries);
595
- const callbacks = createCallbacks(deps);
650
+ const onCompleteRegistry = createCallbackRegistry(deps);
651
+ const exportRegistry =
652
+ createCallbackRegistry<Uint8Array<ArrayBuffer>>(deps);
596
653
 
597
- const appState = deps.createAppState(config);
598
- const dbWorker = deps.createDbWorker(config.name);
654
+ const dbWorker = deps.createDbWorker(dbConfig.name);
599
655
 
600
656
  const getTabId = () => {
601
657
  tabId ??= createId(deps);
602
658
  return tabId;
603
659
  };
604
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).
605
697
  dbWorker.onMessage((message) => {
606
698
  switch (message.type) {
607
- case "onInit": {
608
- appOwnerStore.set(message.appOwner);
609
- onInit?.({
610
- appOwner: message.appOwner,
611
- isFirst: message.isFirst,
612
- });
699
+ case "onError": {
700
+ errorStore.set(message.error);
613
701
  break;
614
702
  }
615
703
 
616
- case "onError": {
617
- errorStore.set(message.error);
704
+ case "onGetAppOwner": {
705
+ resolveAppOwner(message.appOwner);
618
706
  break;
619
707
  }
620
708
 
621
- case "onChange": {
709
+ case "onQueryPatches": {
622
710
  if (message.tabId !== getTabId()) return;
623
711
 
624
712
  const state = rowsStore.get();
625
713
  const nextState = new Map([
626
714
  ...state,
627
- ...message.patches.map(
715
+ ...message.queryPatches.map(
628
716
  ({ query, patches }): [Query, ReadonlyArray<Row>] => [
629
717
  query,
630
718
  applyPatches(patches, state.get(query) ?? emptyRows),
@@ -632,7 +720,7 @@ const createEvoluInstance =
632
720
  ),
633
721
  ]);
634
722
 
635
- for (const { query } of message.patches) {
723
+ for (const { query } of message.queryPatches) {
636
724
  loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
637
725
  }
638
726
 
@@ -645,33 +733,111 @@ const createEvoluInstance =
645
733
  }
646
734
 
647
735
  for (const id of message.onCompleteIds) {
648
- callbacks.execute(id);
736
+ onCompleteRegistry.execute(id);
649
737
  }
650
738
  break;
651
739
  }
652
740
 
653
- case "onReceive": {
741
+ case "refreshQueries": {
654
742
  if (message.tabId && message.tabId === getTabId()) return;
655
743
 
656
- loadingPromises.releaseUnsubscribed();
657
- 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
+
658
752
  if (isNonEmptyReadonlyArray(queries)) {
659
753
  dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
660
754
  }
755
+
661
756
  break;
662
757
  }
663
758
 
664
759
  case "onReset": {
665
760
  if (message.reload) {
666
- appState.reset();
761
+ deps.reloadApp(reloadUrl);
667
762
  } else {
668
- callbacks.execute(message.onCompleteId);
763
+ onCompleteRegistry.execute(message.onCompleteId);
669
764
  }
670
765
  break;
671
766
  }
672
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
+
673
836
  case "onExport": {
674
- callbacks.execute(message.onCompleteId, message.file);
837
+ exportRegistry.execute(
838
+ message.onCompleteId,
839
+ message.file as Uint8Array<ArrayBuffer>,
840
+ );
675
841
  break;
676
842
  }
677
843
 
@@ -704,18 +870,19 @@ const createEvoluInstance =
704
870
  return type;
705
871
  };
706
872
 
707
- dbWorker.postMessage({
708
- type: "init",
709
- config,
710
- dbSchema,
711
- });
873
+ dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
874
+
875
+ // We can't use `init` to get AppOwner because `init` runs only once per n tabs.
876
+ dbWorker.postMessage({ type: "getAppOwner" });
712
877
 
713
878
  const loadQueryMicrotaskQueue: Array<Query> = [];
714
879
 
715
880
  const mutateMicrotaskQueue: Array<
716
- [DbChange | undefined, MutationOptions["onComplete"] | undefined]
881
+ [MutationChange | null, MutationOptions["onComplete"] | undefined]
717
882
  > = [];
718
883
 
884
+ const useOwnerMicrotaskQueue: Array<[SyncOwner, boolean, Uint8Array]> = [];
885
+
719
886
  const createMutation =
720
887
  <Kind extends MutationKind>(kind: Kind) =>
721
888
  <TableName extends keyof typeof schema>(
@@ -731,8 +898,7 @@ const createEvoluInstance =
731
898
  ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
732
899
  >
733
900
  > => {
734
- const Type = getMutationType(table, kind);
735
- const result = Type.fromUnknown(props);
901
+ const result = getMutationType(table, kind).fromUnknown(props);
736
902
 
737
903
  const id =
738
904
  kind === "insert"
@@ -741,13 +907,12 @@ const createEvoluInstance =
741
907
 
742
908
  if (options?.onlyValidate !== true) {
743
909
  if (!result.ok) {
744
- // One error must invalidate the whole queue.
745
- // We insert `undefined` to detect such a situation.
746
- mutateMicrotaskQueue.push([undefined, undefined]);
910
+ // Mark the transaction as invalid by pushing null
911
+ mutateMicrotaskQueue.push([null, undefined]);
747
912
  } else {
748
913
  const values = { ...result.value };
749
-
750
914
  delete values.id;
915
+
751
916
  if (kind === "insert" || kind === "upsert") {
752
917
  // Only set createdAt if not provided by user
753
918
  if (!("createdAt" in values)) {
@@ -755,50 +920,19 @@ const createEvoluInstance =
755
920
  }
756
921
  }
757
922
 
758
- const dbChange = {
759
- table,
760
- id,
761
- values,
762
- };
763
- assertValidDbChange(dbChange);
923
+ const dbChange = { table, id, values };
924
+ assert(
925
+ DbChange.is(dbChange),
926
+ `Failed to create DbChange for table "${dbChange.table}"`,
927
+ );
764
928
 
765
- mutateMicrotaskQueue.push([dbChange, options?.onComplete]);
929
+ const mutationChange = { ...dbChange, ownerId: options?.ownerId };
930
+ mutateMicrotaskQueue.push([mutationChange, options?.onComplete]);
766
931
  }
767
932
 
768
- if (mutateMicrotaskQueue.length === 1)
769
- queueMicrotask(() => {
770
- const changes: Array<DbChange> = [];
771
- const onCompletes = [];
772
-
773
- for (const [change, onComplete] of mutateMicrotaskQueue) {
774
- if (change) changes.push(change);
775
- if (onComplete) onCompletes.push(onComplete);
776
- }
777
-
778
- const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
779
- mutateMicrotaskQueue.length = 0;
780
-
781
- // Don't mutate anything if there was a validation error.
782
- // All mutations within a queue are considered to be a transaction.
783
- if (changes.length !== mutateMicrotaskQueueLength) {
784
- return;
785
- }
786
-
787
- const onCompleteIds = onCompletes.map((onComplete) =>
788
- callbacks.register(onComplete),
789
- );
790
-
791
- loadingPromises.releaseUnsubscribed();
792
-
793
- if (isNonEmptyArray(changes))
794
- dbWorker.postMessage({
795
- type: "mutate",
796
- tabId: getTabId(),
797
- changes,
798
- onCompleteIds,
799
- subscribedQueries: subscribedQueries.get(),
800
- });
801
- });
933
+ if (mutateMicrotaskQueue.length === 1) {
934
+ queueMicrotask(processMutationQueue);
935
+ }
802
936
  }
803
937
 
804
938
  if (result.ok) return ok({ id });
@@ -812,28 +946,74 @@ const createEvoluInstance =
812
946
  );
813
947
  };
814
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
+
815
1012
  const evolu: InternalEvoluInstance = {
816
1013
  subscribeError: errorStore.subscribe,
817
1014
  getError: errorStore.get,
818
1015
 
819
- createQuery: (queryCallback, options) => {
820
- const compiledQuery = queryCallback(
821
- kysely as IntentionalNever,
822
- ).compile();
823
-
824
- if (isSqlMutation(compiledQuery.sql))
825
- throw new Error(
826
- "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.",
827
- );
828
-
829
- return serializeQuery({
830
- sql: compiledQuery.sql as SafeSql,
831
- parameters: compiledQuery.parameters as NonNullable<
832
- SqliteQuery["parameters"]
833
- >,
834
- ...(options && { options }),
835
- });
836
- },
1016
+ createQuery,
837
1017
 
838
1018
  loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
839
1019
  const { promise, isNew } = loadingPromises.get(query);
@@ -846,6 +1026,7 @@ const createEvoluInstance =
846
1026
  const queries = [...new Set(loadQueryMicrotaskQueue)];
847
1027
  loadQueryMicrotaskQueue.length = 0;
848
1028
  assertNonEmptyArray(queries);
1029
+ deps.console.log("[evolu]", "loadQuery", { queries });
849
1030
  dbWorker.postMessage({
850
1031
  type: "query",
851
1032
  tabId: getTabId(),
@@ -881,23 +1062,19 @@ const createEvoluInstance =
881
1062
  getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
882
1063
  (rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
883
1064
 
884
- subscribeAppOwner: appOwnerStore.subscribe,
885
- getAppOwner: appOwnerStore.get,
1065
+ appOwner,
886
1066
 
887
- subscribeSyncState: syncStore.subscribe,
888
- getSyncState: syncStore.get,
1067
+ // TODO: Update it for the owner-api
1068
+ // subscribeSyncState: syncStore.subscribe,
1069
+ // getSyncState: syncStore.get,
889
1070
 
890
1071
  insert: createMutation("insert"),
891
1072
  update: createMutation("update"),
892
1073
  upsert: createMutation("upsert"),
893
1074
 
894
1075
  resetAppOwner: (options) => {
895
- // Eslint bug, Promise<void> is correct by docs.
896
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
897
- const { promise, resolve } = Promise.withResolvers<void>();
898
- const onCompleteId = callbacks.register(() => {
899
- resolve();
900
- });
1076
+ const { promise, resolve } = Promise.withResolvers<undefined>();
1077
+ const onCompleteId = onCompleteRegistry.register(resolve);
901
1078
  dbWorker.postMessage({
902
1079
  type: "reset",
903
1080
  onCompleteId,
@@ -907,13 +1084,8 @@ const createEvoluInstance =
907
1084
  },
908
1085
 
909
1086
  restoreAppOwner: (mnemonic, options) => {
910
- // Eslint bug, Promise<void> is correct by docs.
911
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
912
- const { promise, resolve } = Promise.withResolvers<void>();
913
- const onCompleteId = callbacks.register(() => {
914
- resolve();
915
- });
916
-
1087
+ const { promise, resolve } = Promise.withResolvers<undefined>();
1088
+ const onCompleteId = onCompleteRegistry.register(resolve);
917
1089
  dbWorker.postMessage({
918
1090
  type: "reset",
919
1091
  onCompleteId,
@@ -924,7 +1096,7 @@ const createEvoluInstance =
924
1096
  },
925
1097
 
926
1098
  reloadApp: () => {
927
- appState.reset();
1099
+ deps.reloadApp(reloadUrl);
928
1100
  },
929
1101
 
930
1102
  ensureSchema: (schema) => {
@@ -934,22 +1106,90 @@ const createEvoluInstance =
934
1106
  },
935
1107
 
936
1108
  exportDatabase: () => {
937
- const { promise, resolve } = Promise.withResolvers<Uint8Array>();
938
- const onCompleteId = callbacks.register((arg) => {
939
- if (arg instanceof Uint8Array) resolve(arg);
940
- });
1109
+ const { promise, resolve } =
1110
+ Promise.withResolvers<Uint8Array<ArrayBuffer>>();
1111
+ const onCompleteId = exportRegistry.register(resolve);
941
1112
  dbWorker.postMessage({ type: "export", onCompleteId });
942
1113
  return promise;
943
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
+ },
944
1169
  };
945
1170
 
946
1171
  return evolu;
947
1172
  };
948
1173
 
949
- export const createNamespaceName =
950
- (config: Config) =>
951
- (name: string): string =>
952
- `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
+ };
953
1193
 
954
1194
  interface LoadingPromises {
955
1195
  get: <R extends Row>(
@@ -959,13 +1199,32 @@ interface LoadingPromises {
959
1199
  readonly isNew: boolean;
960
1200
  };
961
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
+ */
962
1211
  resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
963
1212
 
964
- 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>;
965
1224
  }
966
1225
 
967
1226
  interface LoadingPromise {
968
- /** Promise with props for the upcoming React use hook. */
1227
+ /** Promise with props for the React use hook. */
969
1228
  promise: Promise<QueryRows> & {
970
1229
  status?: "pending" | "fulfilled" | "rejected";
971
1230
  value?: QueryRows;
@@ -980,7 +1239,7 @@ const createLoadingPromises = (
980
1239
  ): LoadingPromises => {
981
1240
  const loadingPromiseMap = new Map<Query, LoadingPromise>();
982
1241
 
983
- const loadingPromises: LoadingPromises = {
1242
+ return {
984
1243
  get: <R extends Row>(
985
1244
  query: Query<R>,
986
1245
  ): {
@@ -1007,16 +1266,13 @@ const createLoadingPromises = (
1007
1266
  if (loadingPromise.promise.status !== "fulfilled") {
1008
1267
  loadingPromise.resolve(rows);
1009
1268
  } else {
1010
- // A promise can't be fulfilled 2x, so we need a new one.
1011
1269
  loadingPromise.promise = Promise.resolve(rows);
1012
1270
  }
1013
1271
 
1014
- /**
1015
- * "For example, a data framework can set the status and value fields on a
1016
- * promise preemptively, before passing to React, so that React can unwrap
1017
- * it without waiting a microtask."
1018
- * https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
1019
- */
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
1020
1276
  void Object.assign(loadingPromise.promise, {
1021
1277
  status: "fulfilled",
1022
1278
  value: rows,
@@ -1027,14 +1283,7 @@ const createLoadingPromises = (
1027
1283
  }
1028
1284
  },
1029
1285
 
1030
- /**
1031
- * We can't delete loading promises in `resolveLoadingPromises` because they
1032
- * must be cached, so repeated calls to `loadQuery` will always return the
1033
- * same promise until the data changes, and we also can't cache them forever
1034
- * because only subscribed queries are automatically updated (reactivity is
1035
- * expensive) hence this function must be called manually on any mutation.
1036
- */
1037
- releaseUnsubscribed: () => {
1286
+ releaseUnsubscribedOnMutation: () => {
1038
1287
  [...loadingPromiseMap.entries()]
1039
1288
  .filter(([query]) => !subscribedQueries.has(query))
1040
1289
  .forEach(([query, loadingPromise]) => {
@@ -1045,18 +1294,7 @@ const createLoadingPromises = (
1045
1294
  }
1046
1295
  });
1047
1296
  },
1048
- };
1049
1297
 
1050
- return loadingPromises;
1051
- };
1052
-
1053
- const assertValidDbChange: (dbChange: {
1054
- table: string;
1055
- id: Id;
1056
- values: unknown;
1057
- }) => asserts dbChange is DbChange = (dbChange) => {
1058
- assert(
1059
- DbChange.is(dbChange),
1060
- `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.`,
1061
- );
1298
+ getQueries: () => Array.from(loadingPromiseMap.keys()),
1299
+ };
1062
1300
  };