@evolu/common 6.0.1-preview.9 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +256 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +215 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +44 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +52 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +31 -6
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +72 -9
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +40 -53
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +213 -133
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +0 -2
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +0 -2
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +595 -447
  49. package/dist/src/Evolu/Public.d.ts +6 -8
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +2 -3
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Platform.d.ts +20 -0
  85. package/dist/src/Platform.d.ts.map +1 -0
  86. package/dist/src/Platform.js +22 -0
  87. package/dist/src/Random.d.ts +3 -2
  88. package/dist/src/Random.d.ts.map +1 -1
  89. package/dist/src/Relation.d.ts +79 -0
  90. package/dist/src/Relation.d.ts.map +1 -0
  91. package/dist/src/Relation.js +127 -0
  92. package/dist/src/Resources.d.ts +118 -0
  93. package/dist/src/Resources.d.ts.map +1 -0
  94. package/dist/src/Resources.js +197 -0
  95. package/dist/src/Result.d.ts +184 -52
  96. package/dist/src/Result.d.ts.map +1 -1
  97. package/dist/src/Result.js +30 -241
  98. package/dist/src/Skiplist.js +2 -1
  99. package/dist/src/Sqlite.d.ts +89 -5
  100. package/dist/src/Sqlite.d.ts.map +1 -1
  101. package/dist/src/Sqlite.js +136 -9
  102. package/dist/src/Task.d.ts +586 -0
  103. package/dist/src/Task.d.ts.map +1 -0
  104. package/dist/src/Task.js +469 -0
  105. package/dist/src/Time.d.ts +66 -1
  106. package/dist/src/Time.d.ts.map +1 -1
  107. package/dist/src/Time.js +99 -5
  108. package/dist/src/Type.d.ts +676 -343
  109. package/dist/src/Type.d.ts.map +1 -1
  110. package/dist/src/Type.js +718 -467
  111. package/dist/src/Types.d.ts +1 -75
  112. package/dist/src/Types.d.ts.map +1 -1
  113. package/dist/src/WebSocket.d.ts +5 -2
  114. package/dist/src/WebSocket.d.ts.map +1 -1
  115. package/dist/src/WebSocket.js +12 -18
  116. package/dist/src/Worker.d.ts +39 -11
  117. package/dist/src/Worker.d.ts.map +1 -1
  118. package/dist/src/Worker.js +22 -4
  119. package/dist/src/index.d.ts +8 -3
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +8 -3
  122. package/package.json +14 -13
  123. package/src/Array.ts +301 -19
  124. package/src/Assert.ts +0 -21
  125. package/src/Brand.ts +75 -0
  126. package/src/Buffer.ts +7 -7
  127. package/src/Cache.ts +85 -0
  128. package/src/Callbacks.ts +62 -22
  129. package/src/Console.ts +91 -11
  130. package/src/Crypto.ts +94 -90
  131. package/src/Evolu/Db.ts +519 -1026
  132. package/src/Evolu/Diff.ts +7 -5
  133. package/src/Evolu/Evolu.ts +476 -360
  134. package/src/Evolu/Internal.ts +0 -2
  135. package/src/Evolu/LocalAuth.ts +463 -0
  136. package/src/Evolu/Owner.ts +355 -228
  137. package/src/Evolu/Platform.ts +9 -9
  138. package/src/Evolu/Protocol.ts +846 -742
  139. package/src/Evolu/Public.ts +7 -14
  140. package/src/Evolu/PublicKysely.ts +4 -5
  141. package/src/Evolu/Query.ts +3 -2
  142. package/src/Evolu/Relay.ts +406 -103
  143. package/src/Evolu/Schema.ts +323 -91
  144. package/src/Evolu/Storage.ts +559 -137
  145. package/src/Evolu/Sync.ts +819 -36
  146. package/src/Evolu/Timestamp.ts +90 -58
  147. package/src/Identicon.ts +197 -0
  148. package/src/Instances.ts +90 -0
  149. package/src/Number.ts +6 -10
  150. package/src/Object.ts +13 -5
  151. package/src/Platform.ts +26 -0
  152. package/src/Random.ts +3 -2
  153. package/src/Relation.ts +234 -0
  154. package/src/Resources.ts +367 -0
  155. package/src/Result.ts +191 -54
  156. package/src/Skiplist.ts +1 -1
  157. package/src/Sqlite.ts +152 -17
  158. package/src/Task.ts +901 -0
  159. package/src/Time.ts +180 -5
  160. package/src/Type.ts +1135 -730
  161. package/src/Types.ts +1 -77
  162. package/src/WebSocket.ts +27 -25
  163. package/src/Worker.ts +72 -23
  164. package/src/index.ts +8 -3
  165. package/dist/src/Evolu/Config.d.ts +0 -69
  166. package/dist/src/Evolu/Config.d.ts.map +0 -1
  167. package/dist/src/Evolu/Config.js +0 -9
  168. package/dist/src/Evolu/Kysely.d.ts +0 -6
  169. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  170. package/dist/src/Evolu/Kysely.js +0 -21
  171. package/dist/src/ManyToManyMap.d.ts +0 -26
  172. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  173. package/dist/src/ManyToManyMap.js +0 -92
  174. package/dist/src/NanoId.d.ts +0 -27
  175. package/dist/src/NanoId.d.ts.map +0 -1
  176. package/dist/src/NanoId.js +0 -6
  177. package/dist/src/Promise.d.ts +0 -180
  178. package/dist/src/Promise.d.ts.map +0 -1
  179. package/dist/src/Promise.js +0 -176
  180. package/src/Evolu/Config.ts +0 -83
  181. package/src/Evolu/Kysely.ts +0 -38
  182. package/src/ManyToManyMap.ts +0 -140
  183. package/src/NanoId.ts +0 -39
  184. package/src/Promise.ts +0 -295
@@ -1,39 +1,30 @@
1
- import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
2
- import { assert, assertNonEmptyArray } from "../Assert.js";
1
+ import { pack } from "msgpackr";
2
+ import { dedupeArray, isNonEmptyArray, isNonEmptyReadonlyArray, } from "../Array.js";
3
+ import { assertNonEmptyReadonlyArray } from "../Assert.js";
3
4
  import { createCallbacks } from "../Callbacks.js";
5
+ import { eqArrayNumber } from "../Eq.js";
4
6
  import { exhaustiveCheck } from "../Function.js";
7
+ import { createInstances } from "../Instances.js";
5
8
  import { err, ok } from "../Result.js";
6
- import { isSqlMutation } from "../Sqlite.js";
9
+ import { isSqlMutation, SqliteBoolean, } from "../Sqlite.js";
7
10
  import { createStore } from "../Store.js";
8
11
  import { createId, } from "../Type.js";
9
- import { defaultConfig } from "./Config.js";
12
+ import { defaultDbConfig } from "./Db.js";
10
13
  import { applyPatches } from "./Diff.js";
11
- import { kysely } from "./Kysely.js";
12
- import { DbChange, } from "./Protocol.js";
13
14
  import { createSubscribedQueries, emptyRows, serializeQuery, } from "./Query.js";
14
- import { evoluSchemaToDbSchema, insertable, updateable, upsertable, } from "./Schema.js";
15
+ import { evoluSchemaToDbSchema, insertable, kysely, updateable, upsertable, } from "./Schema.js";
16
+ import { DbChange } from "./Storage.js";
15
17
  import { initialSyncState } from "./Sync.js";
16
- // For hot reloading and Evolu multitenancy.
17
- const evoluInstances = new Map();
18
+ const evoluInstances = createInstances();
19
+ /**
20
+ * Unique identifier for the current browser tab or app instance, lazily
21
+ * initialized on first use to distinguish between multiple tabs.
22
+ */
18
23
  let tabId = null;
19
24
  /**
20
- * Creates an {@link Evolu} instance configured with the specified
21
- * {@link EvoluSchema} and optional configuration.
22
- *
23
- * This function returns a configured Evolu instance, providing a typed
24
- * interface for querying, mutating, and syncing your application's data. The
25
- * returned instance includes:
26
- *
27
- * - Subscription methods for receiving updates on queries, the owner, errors, and
28
- * sync state.
29
- * - Methods for creating, updating, or deleting rows in a type-safe manner.
30
- * - Methods for querying data using Evolu's typed SQL queries, leveraging Kysely
31
- * under the hood.
32
- * - Built-in support for local-first and offline-first data with automatic sync
33
- * and merging.
34
- * - Automatic schema evolution that updates the underlying database with new
35
- * columns or tables.
36
- * - Managing owner data with resetAppOwner and restoreAppOwner.
25
+ * Creates an {@link Evolu} instance for a platform configured with the specified
26
+ * {@link EvoluSchema} and optional {@link EvoluConfig} providing a typed
27
+ * interface for querying, mutating, and syncing your application's data.
37
28
  *
38
29
  * ### Example
39
30
  *
@@ -62,59 +53,66 @@ let tabId = null;
62
53
  *
63
54
  * const evolu = createEvolu(evoluReactDeps)(Schema);
64
55
  * ```
56
+ *
57
+ * ### Instance Caching
58
+ *
59
+ * `createEvolu` caches instances using {@link Instances} by {@link EvoluConfig}
60
+ * name to enable hot reloading and prevent database corruption from multiple
61
+ * connections. For testing, use unique instance names to ensure proper
62
+ * isolation.
65
63
  */
66
- export const createEvolu = (deps) => (schema, partialConfig = {}) => {
67
- const config = { ...defaultConfig, ...partialConfig };
68
- let evolu = evoluInstances.get(config.name);
69
- if (evolu == null) {
70
- evolu = createEvoluInstance(deps)(schema, config);
71
- evoluInstances.set(config.name, evolu);
72
- }
73
- else {
74
- // Hot reloading. Note that indexes are intentionally omitted.
75
- evolu.ensureSchema(schema);
76
- }
77
- return evolu;
78
- };
79
- const createEvoluInstance = (deps) => (schema, evoluConfig) => {
80
- deps.console.enabled = evoluConfig.enableLogging ?? false;
81
- deps.console.log("[evolu]", "createEvoluInstance");
82
- const { initialData, indexes, ...config } = evoluConfig;
64
+ export const createEvolu = (deps) => (schema, config) => evoluInstances.ensure(config?.name ?? defaultDbConfig.name, () => createEvoluInstance(deps)(schema, config), (evolu) => {
65
+ // Hot reloading. Note that indexes are intentionally omitted.
66
+ evolu.ensureSchema(schema);
67
+ });
68
+ const createEvoluInstance = (deps) => (schema, config) => {
69
+ deps.console.enabled = config?.enableLogging ?? false;
70
+ const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
71
+ const dbConfig = { ...defaultDbConfig, ...partialDbConfig };
72
+ deps.console.log("[evolu]", "createEvoluInstance", {
73
+ name: dbConfig.name,
74
+ });
83
75
  const errorStore = createStore(null);
84
76
  const rowsStore = createStore(new Map());
85
- const ownerStore = createStore(null);
86
- const syncStore = createStore(initialSyncState);
77
+ const { promise: appOwner, resolve: resolveAppOwner } = Promise.withResolvers();
78
+ if (config?.externalAppOwner) {
79
+ resolveAppOwner(config.externalAppOwner);
80
+ }
81
+ // TODO: Update it for the owner-api
82
+ const _syncStore = createStore(initialSyncState);
87
83
  const subscribedQueries = createSubscribedQueries(rowsStore);
88
84
  const loadingPromises = createLoadingPromises(subscribedQueries);
89
- const callbacks = createCallbacks(deps);
90
- const appState = deps.createAppState(config);
91
- const dbWorker = deps.createDbWorker(config.name);
85
+ const onCompleteCallbacks = createCallbacks(deps);
86
+ const exportCallbacks = createCallbacks(deps);
87
+ const dbWorker = deps.createDbWorker(dbConfig.name);
92
88
  const getTabId = () => {
93
89
  tabId ??= createId(deps);
94
90
  return tabId;
95
91
  };
92
+ // Worker responses are delivered to all tabs. Each case must handle this
93
+ // properly (e.g., AppOwner promise resolves only once, tabId filtering).
96
94
  dbWorker.onMessage((message) => {
97
95
  switch (message.type) {
98
- case "onInit": {
99
- ownerStore.set(message.owner);
100
- break;
101
- }
102
96
  case "onError": {
103
97
  errorStore.set(message.error);
104
98
  break;
105
99
  }
106
- case "onChange": {
100
+ case "onGetAppOwner": {
101
+ resolveAppOwner(message.appOwner);
102
+ break;
103
+ }
104
+ case "onQueryPatches": {
107
105
  if (message.tabId !== getTabId())
108
106
  return;
109
107
  const state = rowsStore.get();
110
108
  const nextState = new Map([
111
109
  ...state,
112
- ...message.patches.map(({ query, patches }) => [
110
+ ...message.queryPatches.map(({ query, patches }) => [
113
111
  query,
114
112
  applyPatches(patches, state.get(query) ?? emptyRows),
115
113
  ]),
116
114
  ]);
117
- for (const { query } of message.patches) {
115
+ for (const { query } of message.queryPatches) {
118
116
  loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
119
117
  }
120
118
  if (deps.flushSync && message.onCompleteIds.length > 0) {
@@ -126,15 +124,19 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
126
124
  rowsStore.set(nextState);
127
125
  }
128
126
  for (const id of message.onCompleteIds) {
129
- callbacks.execute(id);
127
+ onCompleteCallbacks.execute(id);
130
128
  }
131
129
  break;
132
130
  }
133
- case "onReceive": {
131
+ case "refreshQueries": {
134
132
  if (message.tabId && message.tabId === getTabId())
135
133
  return;
136
- loadingPromises.releaseUnsubscribed();
137
- const queries = subscribedQueries.get();
134
+ const loadingPromisesQueries = loadingPromises.getQueries();
135
+ loadingPromises.releaseUnsubscribedOnMutation();
136
+ const queries = dedupeArray([
137
+ ...loadingPromisesQueries,
138
+ ...subscribedQueries.get(),
139
+ ]);
138
140
  if (isNonEmptyReadonlyArray(queries)) {
139
141
  dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
140
142
  }
@@ -142,15 +144,15 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
142
144
  }
143
145
  case "onReset": {
144
146
  if (message.reload) {
145
- appState.reset();
147
+ deps.reloadApp(reloadUrl);
146
148
  }
147
149
  else {
148
- callbacks.execute(message.onCompleteId);
150
+ onCompleteCallbacks.execute(message.onCompleteId);
149
151
  }
150
152
  break;
151
153
  }
152
154
  case "onExport": {
153
- callbacks.execute(message.onCompleteId, message.file);
155
+ exportCallbacks.execute(message.onCompleteId, message.file);
154
156
  break;
155
157
  }
156
158
  default:
@@ -173,110 +175,86 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
173
175
  }
174
176
  return type;
175
177
  };
176
- const initialDataDbChanges = [];
177
- /**
178
- * Note that the initial data function is called even if it is unnecessary
179
- * (initial data are already in the DB) because we don't want to wait for
180
- * SQLite's response. Initial data should be small (because they are inlined
181
- * in the code), so it's ok.
182
- */
183
- if (initialData)
184
- initialData({
185
- insert: (table, props) => {
186
- const id = createId(deps);
187
- const values = getMutationType(table, "insert").fromUnknown(props);
188
- if (values.ok) {
189
- const dbChange = { table, id, values: values.value };
190
- assertValidDbChange(dbChange);
191
- initialDataDbChanges.push(dbChange);
192
- return ok({ id });
193
- }
194
- return values;
195
- },
196
- });
197
- dbWorker.postMessage({
198
- type: "init",
199
- config,
200
- dbSchema,
201
- initialData: initialDataDbChanges,
202
- });
178
+ dbWorker.postMessage({ type: "init", config: dbConfig, dbSchema });
179
+ // We can't use `init` to get AppOwner because `init` runs only once per n tabs.
180
+ dbWorker.postMessage({ type: "getAppOwner" });
203
181
  const loadQueryMicrotaskQueue = [];
204
182
  const mutateMicrotaskQueue = [];
183
+ const useOwnerMicrotaskQueue = [];
205
184
  const createMutation = (kind) => (table, props, options) => {
206
- const Type = getMutationType(table, kind);
207
- const result = Type.fromUnknown(props);
185
+ const result = getMutationType(table, kind).fromUnknown(props);
208
186
  const id = kind === "insert"
209
187
  ? createId(deps)
210
188
  : props.id;
211
189
  if (options?.onlyValidate !== true) {
212
190
  if (!result.ok) {
213
- // One error must invalidate the whole queue.
214
- // We insert `undefined` to detect such a situation.
215
- mutateMicrotaskQueue.push([undefined, undefined]);
191
+ // Mark the transaction as invalid by pushing null
192
+ mutateMicrotaskQueue.push([null, undefined]);
216
193
  }
217
194
  else {
218
- // Remove `id` from values.
219
- const { id: _id, ...values } = result.value;
220
- const dbChange = { table, id, values };
221
- assertValidDbChange(dbChange);
222
- mutateMicrotaskQueue.push([dbChange, options?.onComplete]);
223
- }
224
- if (mutateMicrotaskQueue.length === 1)
225
- queueMicrotask(() => {
226
- const changes = [];
227
- const onCompletes = [];
228
- for (const [change, onComplete] of mutateMicrotaskQueue) {
229
- if (change)
230
- changes.push(change);
231
- if (onComplete)
232
- onCompletes.push(onComplete);
233
- }
234
- const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
235
- mutateMicrotaskQueue.length = 0;
236
- // Don't mutate anything if there was a validation error.
237
- // All mutations within a queue are considered to be a transaction.
238
- if (changes.length !== mutateMicrotaskQueueLength) {
239
- return;
240
- }
241
- const onCompleteIds = onCompletes.map((onComplete) => callbacks.register(onComplete));
242
- loadingPromises.releaseUnsubscribed();
243
- if (isNonEmptyArray(changes))
244
- dbWorker.postMessage({
245
- type: "mutate",
246
- tabId: getTabId(),
247
- changes,
248
- onCompleteIds,
249
- subscribedQueries: subscribedQueries.get(),
250
- });
195
+ const { id: _, isDeleted, ...values } = result.value;
196
+ const dbChange = DbChange.orThrow({
197
+ table,
198
+ id,
199
+ values,
200
+ isInsert: kind === "insert" || kind === "upsert",
201
+ isDelete: SqliteBoolean.is(isDeleted) ? Boolean(isDeleted) : null,
251
202
  });
203
+ mutateMicrotaskQueue.push([
204
+ { ...dbChange, ownerId: options?.ownerId },
205
+ options?.onComplete,
206
+ ]);
207
+ }
208
+ if (mutateMicrotaskQueue.length === 1) {
209
+ queueMicrotask(processMutationQueue);
210
+ }
252
211
  }
253
212
  if (result.ok)
254
213
  return ok({ id });
255
214
  return err(result.error);
256
215
  };
216
+ const processMutationQueue = () => {
217
+ const changes = [];
218
+ const onCompletes = [];
219
+ for (const [change, onComplete] of mutateMicrotaskQueue) {
220
+ if (change !== null)
221
+ changes.push(change);
222
+ if (onComplete)
223
+ onCompletes.push(onComplete);
224
+ }
225
+ const queueLength = mutateMicrotaskQueue.length;
226
+ mutateMicrotaskQueue.length = 0;
227
+ // Don't process any mutations if there was a validation error.
228
+ // All mutations within a queue run as a single transaction.
229
+ if (changes.length !== queueLength) {
230
+ return;
231
+ }
232
+ const onCompleteIds = onCompletes.map(onCompleteCallbacks.register);
233
+ loadingPromises.releaseUnsubscribedOnMutation();
234
+ if (!isNonEmptyArray(changes))
235
+ return;
236
+ dbWorker.postMessage({
237
+ type: "mutate",
238
+ tabId: getTabId(),
239
+ changes,
240
+ onCompleteIds,
241
+ subscribedQueries: subscribedQueries.get(),
242
+ });
243
+ };
257
244
  const evolu = {
258
245
  subscribeError: errorStore.subscribe,
259
246
  getError: errorStore.get,
260
- createQuery: (queryCallback, options) => {
261
- const compiledQuery = queryCallback(kysely).compile();
262
- if (isSqlMutation(compiledQuery.sql))
263
- throw new Error("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.");
264
- return serializeQuery({
265
- sql: compiledQuery.sql,
266
- parameters: compiledQuery.parameters,
267
- ...(options && { options }),
268
- });
269
- },
247
+ createQuery,
270
248
  loadQuery: (query) => {
271
249
  const { promise, isNew } = loadingPromises.get(query);
272
250
  if (isNew) {
273
251
  loadQueryMicrotaskQueue.push(query);
274
252
  if (loadQueryMicrotaskQueue.length === 1) {
275
253
  queueMicrotask(() => {
276
- // Dedupe
277
- const queries = [...new Set(loadQueryMicrotaskQueue)];
254
+ const queries = dedupeArray(loadQueryMicrotaskQueue);
278
255
  loadQueryMicrotaskQueue.length = 0;
279
- assertNonEmptyArray(queries);
256
+ assertNonEmptyReadonlyArray(queries);
257
+ deps.console.log("[evolu]", "loadQuery", { queries });
280
258
  dbWorker.postMessage({
281
259
  type: "query",
282
260
  tabId: getTabId(),
@@ -304,20 +282,16 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
304
282
  };
305
283
  },
306
284
  getQueryRows: (query) => (rowsStore.get().get(query) ?? emptyRows),
307
- subscribeAppOwner: ownerStore.subscribe,
308
- getAppOwner: ownerStore.get,
309
- subscribeSyncState: syncStore.subscribe,
310
- getSyncState: syncStore.get,
285
+ appOwner,
286
+ // TODO: Update it for the owner-api
287
+ // subscribeSyncState: syncStore.subscribe,
288
+ // getSyncState: syncStore.get,
311
289
  insert: createMutation("insert"),
312
290
  update: createMutation("update"),
313
291
  upsert: createMutation("upsert"),
314
292
  resetAppOwner: (options) => {
315
- // Eslint bug, Promise<void> is correct by docs.
316
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
317
293
  const { promise, resolve } = Promise.withResolvers();
318
- const onCompleteId = callbacks.register(() => {
319
- resolve();
320
- });
294
+ const onCompleteId = onCompleteCallbacks.register(resolve);
321
295
  dbWorker.postMessage({
322
296
  type: "reset",
323
297
  onCompleteId,
@@ -326,12 +300,8 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
326
300
  return promise;
327
301
  },
328
302
  restoreAppOwner: (mnemonic, options) => {
329
- // Eslint bug, Promise<void> is correct by docs.
330
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
331
303
  const { promise, resolve } = Promise.withResolvers();
332
- const onCompleteId = callbacks.register(() => {
333
- resolve();
334
- });
304
+ const onCompleteId = onCompleteCallbacks.register(resolve);
335
305
  dbWorker.postMessage({
336
306
  type: "reset",
337
307
  onCompleteId,
@@ -341,7 +311,7 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
341
311
  return promise;
342
312
  },
343
313
  reloadApp: () => {
344
- appState.reset();
314
+ deps.reloadApp(reloadUrl);
345
315
  },
346
316
  ensureSchema: (schema) => {
347
317
  mutationTypesCache.clear();
@@ -350,20 +320,72 @@ const createEvoluInstance = (deps) => (schema, evoluConfig) => {
350
320
  },
351
321
  exportDatabase: () => {
352
322
  const { promise, resolve } = Promise.withResolvers();
353
- const onCompleteId = callbacks.register((arg) => {
354
- if (arg instanceof Uint8Array)
355
- resolve(arg);
356
- });
323
+ const onCompleteId = exportCallbacks.register(resolve);
357
324
  dbWorker.postMessage({ type: "export", onCompleteId });
358
325
  return promise;
359
326
  },
327
+ useOwner: (owner) => {
328
+ const scheduleOwnerQueueProcessing = () => {
329
+ if (useOwnerMicrotaskQueue.length !== 1)
330
+ return;
331
+ queueMicrotask(() => {
332
+ const queue = [...useOwnerMicrotaskQueue];
333
+ useOwnerMicrotaskQueue.length = 0;
334
+ const result = [];
335
+ const skipIndices = new Set();
336
+ for (let i = 0; i < queue.length; i++) {
337
+ if (skipIndices.has(i))
338
+ continue;
339
+ const [currentOwner, currentUse, currentOwnerSerialized] = queue[i];
340
+ // Look for opposite action with same owner
341
+ for (let j = i + 1; j < queue.length; j++) {
342
+ if (skipIndices.has(j))
343
+ continue;
344
+ const [, otherUse, otherOwnerSerialized] = queue[j];
345
+ if (currentUse !== otherUse &&
346
+ eqArrayNumber(currentOwnerSerialized, otherOwnerSerialized)) {
347
+ // Found cancel-out pair, skip both
348
+ skipIndices.add(i).add(j);
349
+ break;
350
+ }
351
+ }
352
+ if (!skipIndices.has(i)) {
353
+ result.push([currentOwner, currentUse, currentOwnerSerialized]);
354
+ }
355
+ }
356
+ for (const [owner, use] of result) {
357
+ dbWorker.postMessage({ type: "useOwner", owner, use });
358
+ }
359
+ });
360
+ };
361
+ useOwnerMicrotaskQueue.push([owner, true, pack(owner)]);
362
+ scheduleOwnerQueueProcessing();
363
+ const unuse = () => {
364
+ useOwnerMicrotaskQueue.push([owner, false, pack(owner)]);
365
+ scheduleOwnerQueueProcessing();
366
+ };
367
+ return unuse;
368
+ },
369
+ /** Disposal is not implemented yet. */
370
+ [Symbol.dispose]: () => {
371
+ throw new Error("Evolu instance disposal is not yet implemented");
372
+ },
360
373
  };
361
374
  return evolu;
362
375
  };
363
- export const createNamespaceName = (config) => (name) => `evolu:${config.name}:${name}`;
376
+ export const createQuery = (queryCallback, options) => {
377
+ const compiledQuery = queryCallback(kysely).compile();
378
+ if (isSqlMutation(compiledQuery.sql))
379
+ throw new Error("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.");
380
+ return serializeQuery({
381
+ sql: compiledQuery.sql,
382
+ parameters: compiledQuery.parameters,
383
+ ...(options && { options }),
384
+ });
385
+ };
364
386
  const createLoadingPromises = (subscribedQueries) => {
365
387
  const loadingPromiseMap = new Map();
366
- const loadingPromises = {
388
+ return {
367
389
  get: (query) => {
368
390
  let loadingPromise = loadingPromiseMap.get(query);
369
391
  const isNew = !loadingPromise;
@@ -385,15 +407,12 @@ const createLoadingPromises = (subscribedQueries) => {
385
407
  loadingPromise.resolve(rows);
386
408
  }
387
409
  else {
388
- // A promise can't be fulfilled 2x, so we need a new one.
389
410
  loadingPromise.promise = Promise.resolve(rows);
390
411
  }
391
- /**
392
- * "For example, a data framework can set the status and value fields on a
393
- * promise preemptively, before passing to React, so that React can unwrap
394
- * it without waiting a microtask."
395
- * https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
396
- */
412
+ // Set status and value fields for React's `use` Hook to unwrap synchronously.
413
+ // While undocumented in React docs, React still uses these properties internally,
414
+ // and Evolu's own promise caching logic depends on checking `promise.status`.
415
+ // https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
397
416
  void Object.assign(loadingPromise.promise, {
398
417
  status: "fulfilled",
399
418
  value: rows,
@@ -402,14 +421,7 @@ const createLoadingPromises = (subscribedQueries) => {
402
421
  loadingPromiseMap.delete(query);
403
422
  }
404
423
  },
405
- /**
406
- * We can't delete loading promises in `resolveLoadingPromises` because they
407
- * must be cached, so repeated calls to `loadQuery` will always return the
408
- * same promise until the data changes, and we also can't cache them forever
409
- * because only subscribed queries are automatically updated (reactivity is
410
- * expensive) hence this function must be called manually on any mutation.
411
- */
412
- releaseUnsubscribed: () => {
424
+ releaseUnsubscribedOnMutation: () => {
413
425
  [...loadingPromiseMap.entries()]
414
426
  .filter(([query]) => !subscribedQueries.has(query))
415
427
  .forEach(([query, loadingPromise]) => {
@@ -421,9 +433,6 @@ const createLoadingPromises = (subscribedQueries) => {
421
433
  }
422
434
  });
423
435
  },
436
+ getQueries: () => Array.from(loadingPromiseMap.keys()),
424
437
  };
425
- return loadingPromises;
426
- };
427
- const assertValidDbChange = (dbChange) => {
428
- assert(DbChange.is(dbChange), `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.`);
429
438
  };
@@ -9,11 +9,9 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- export * from "./Config.js";
13
12
  export * from "./Db.js";
14
13
  export * from "./Diff.js";
15
14
  export * from "./Evolu.js";
16
- export * from "./Kysely.js";
17
15
  export * from "./Owner.js";
18
16
  export * from "./Platform.js";
19
17
  export * from "./Protocol.js";
@@ -1 +1 @@
1
- {"version":3,"file":"Internal.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Internal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"Internal.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Internal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
@@ -9,11 +9,9 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- export * from "./Config.js";
13
12
  export * from "./Db.js";
14
13
  export * from "./Diff.js";
15
14
  export * from "./Evolu.js";
16
- export * from "./Kysely.js";
17
15
  export * from "./Owner.js";
18
16
  export * from "./Platform.js";
19
17
  export * from "./Protocol.js";