@evolu/common 8.0.0-next.2 → 8.0.0-next.4

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 (73) hide show
  1. package/dist/src/Assert.d.ts +5 -5
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +4 -4
  4. package/dist/src/Identicon.js +4 -4
  5. package/dist/src/LockManager.d.ts +74 -0
  6. package/dist/src/LockManager.d.ts.map +1 -0
  7. package/dist/src/LockManager.js +103 -0
  8. package/dist/src/Microtask.d.ts.map +1 -1
  9. package/dist/src/Microtask.js +90 -26
  10. package/dist/src/Object.d.ts.map +1 -1
  11. package/dist/src/RefCount.d.ts.map +1 -1
  12. package/dist/src/RefCount.js +153 -68
  13. package/dist/src/Resource.d.ts +6 -6
  14. package/dist/src/Resource.d.ts.map +1 -1
  15. package/dist/src/Resource.js +211 -162
  16. package/dist/src/Schedule.d.ts +5 -5
  17. package/dist/src/Schedule.d.ts.map +1 -1
  18. package/dist/src/Schedule.js +53 -44
  19. package/dist/src/Sqlite.d.ts +2 -0
  20. package/dist/src/Sqlite.d.ts.map +1 -1
  21. package/dist/src/Sqlite.js +64 -40
  22. package/dist/src/Task.d.ts +6 -33
  23. package/dist/src/Task.d.ts.map +1 -1
  24. package/dist/src/Task.js +213 -204
  25. package/dist/src/Test.d.ts +4 -4
  26. package/dist/src/Time.d.ts +5 -0
  27. package/dist/src/Time.d.ts.map +1 -1
  28. package/dist/src/Time.js +5 -0
  29. package/dist/src/Type.d.ts +7 -0
  30. package/dist/src/Type.d.ts.map +1 -1
  31. package/dist/src/Type.js +7 -0
  32. package/dist/src/WebSocket.d.ts.map +1 -1
  33. package/dist/src/WebSocket.js +7 -6
  34. package/dist/src/Worker.d.ts +42 -8
  35. package/dist/src/Worker.d.ts.map +1 -1
  36. package/dist/src/Worker.js +298 -75
  37. package/dist/src/index.d.ts +1 -0
  38. package/dist/src/index.d.ts.map +1 -1
  39. package/dist/src/index.js +1 -0
  40. package/dist/src/local-first/Db.d.ts +5 -4
  41. package/dist/src/local-first/Db.d.ts.map +1 -1
  42. package/dist/src/local-first/Db.js +44 -38
  43. package/dist/src/local-first/Evolu.d.ts +3 -2
  44. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  45. package/dist/src/local-first/Evolu.js +90 -80
  46. package/dist/src/local-first/Owner.d.ts.map +1 -1
  47. package/dist/src/local-first/Shared.d.ts +65 -54
  48. package/dist/src/local-first/Shared.d.ts.map +1 -1
  49. package/dist/src/local-first/Shared.js +387 -373
  50. package/package.json +1 -1
  51. package/src/Array.ts +8 -8
  52. package/src/Assert.ts +5 -5
  53. package/src/Identicon.ts +4 -4
  54. package/src/LockManager.ts +181 -0
  55. package/src/Microtask.ts +17 -11
  56. package/src/Object.ts +3 -3
  57. package/src/RefCount.ts +25 -21
  58. package/src/Resource.ts +63 -56
  59. package/src/Schedule.ts +70 -48
  60. package/src/Set.ts +4 -4
  61. package/src/Sqlite.ts +41 -30
  62. package/src/Task.ts +77 -120
  63. package/src/Test.ts +4 -4
  64. package/src/Time.ts +7 -0
  65. package/src/Type.ts +9 -0
  66. package/src/WebSocket.ts +7 -6
  67. package/src/Worker.ts +240 -49
  68. package/src/index.ts +1 -0
  69. package/src/local-first/Db.ts +69 -46
  70. package/src/local-first/Evolu.ts +74 -68
  71. package/src/local-first/Owner.ts +1 -1
  72. package/src/local-first/Query.ts +1 -1
  73. package/src/local-first/Shared.ts +472 -464
@@ -59,64 +59,45 @@ import { emptyArray, firstInArray, isNonEmptyArray, shiftFromArray, } from "../A
59
59
  import { assert, assertNotDisposed } from "../Assert.js";
60
60
  import { createCallbacks } from "../Callbacks.js";
61
61
  import { exhaustiveCheck } from "../Function.js";
62
+ import { acquireLeaderLock } from "../LockManager.js";
62
63
  import { structuralLookup } from "../Lookup.js";
63
64
  import { createRefCountByKey } from "../RefCount.js";
64
65
  import { createSharedResourceByKey, createSharedResourceByKeyWithClaims, } from "../Resource.js";
65
66
  import { ok } from "../Result.js";
66
- import { spaced } from "../Schedule.js";
67
- import { repeat, unabortable, } from "../Task.js";
68
- import { createId } from "../Type.js";
67
+ import { createStore } from "../Store.js";
68
+ import { AbortError, createMutex } from "../Task.js";
69
+ import {} from "../Type.js";
69
70
  import { createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, parseProtocolHeader, } from "./Protocol.js";
70
71
  import { makePatches, } from "./Query.js";
72
+ export const consoleEntryOrErrorBroadcastChannelName = "evolu:console-entry-or-error";
71
73
  export const initSharedWorker = (self) => async (run) => {
72
74
  const env_1 = { stack: [], error: void 0, hasError: false };
73
75
  try {
74
- const { createMessagePort, consoleStoreOutputEntry, createWebSocket } = run.deps;
75
- const console = run.deps.console.child("SharedWorker");
76
- const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
77
- const sharedWorkerReady = Promise.withResolvers();
78
- // TODO: Use heartbeat to detect and prune dead ports.
79
- const tabPorts = new Set();
80
- // Buffer console/error outputs until the first tab connects its port.
81
- const queuedTabOutputs = [];
82
- const postTabOutput = (output) => {
83
- if (tabPorts.size === 0)
84
- queuedTabOutputs.push(output);
85
- else
86
- for (const port of tabPorts)
87
- port.postMessage(output);
76
+ const { deps } = run;
77
+ const console = deps.console.child("SharedWorker");
78
+ const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
79
+ const tabLeaderPortStore = disposer.use(createStore(null));
80
+ const consoleEntryOrErrorBroadcastChannel = disposer.use(deps.createBroadcastChannel(consoleEntryOrErrorBroadcastChannelName));
81
+ const postConsoleEntryOrError = (output) => {
82
+ consoleEntryOrErrorBroadcastChannel.postMessage(output);
88
83
  };
89
- stack.defer(consoleStoreOutputEntry.subscribe(() => {
90
- const entry = consoleStoreOutputEntry.get();
91
- if (entry)
92
- postTabOutput({ type: "OnConsoleEntry", entry });
93
- }));
84
+ const sharedWorkerReady = Promise.withResolvers();
94
85
  // Register ASAP so the worker does not miss connections.
95
86
  self.onConnect = (port) => {
96
87
  void sharedWorkerReady.promise.then(() => {
97
88
  // The underlying port buffers messages until onMessage is assigned.
98
89
  port.onMessage = (message) => {
99
90
  switch (message.type) {
100
- case "InitTab": {
101
- /**
102
- * One SharedWorker serves multiple tabs. The most recently
103
- * initialized tab's level wins.
104
- */
91
+ case "AnnounceTabLeader": {
105
92
  console.setLevel(message.consoleLevel);
106
- const tabPort = createMessagePort(message.port);
107
- tabPorts.add(tabPort);
108
- if (queuedTabOutputs.length > 0) {
109
- queuedTabOutputs.forEach(postTabOutput);
110
- queuedTabOutputs.length = 0;
111
- }
93
+ tabLeaderPortStore.set(port);
94
+ console.info("tabLeaderAnnounced");
112
95
  break;
113
96
  }
114
97
  case "CreateEvolu": {
115
- void sharedWorkerRunWithSharedEvoluDeps(async (run) => {
116
- const sharedEvoluResult = await run(sharedEvolusByName.acquire(message.name));
117
- if (!sharedEvoluResult.ok)
118
- return sharedEvoluResult;
119
- sharedEvoluResult.value.createInstance(message.evoluPort, message.dbWorkerPort, () => void sharedWorkerRunWithSharedEvoluDeps(sharedEvolusByName.release(message.name)));
98
+ void sharedWorkerRun(async (run) => {
99
+ const tenant = await run.orThrow(tenantsByName.acquire(message));
100
+ tenant.addInstance(message, () => void sharedWorkerRun(tenantsByName.release(message)));
120
101
  return ok();
121
102
  });
122
103
  break;
@@ -127,8 +108,12 @@ export const initSharedWorker = (self) => async (run) => {
127
108
  };
128
109
  });
129
110
  };
130
- const initSharedWorkerRun = run.create();
131
- const transports = stack.use(await initSharedWorkerRun.orThrow(createSharedResourceByKeyWithClaims((transport) => createWebSocket(transport.url, {
111
+ disposer.defer(deps.consoleStoreOutputEntry.subscribe(() => {
112
+ const entry = deps.consoleStoreOutputEntry.get();
113
+ if (entry)
114
+ postConsoleEntryOrError({ type: "ConsoleEntry", entry });
115
+ }));
116
+ const transports = disposer.use(await run.orThrow(createSharedResourceByKeyWithClaims((transport) => deps.createWebSocket(transport.url, {
132
117
  binaryType: "arraybuffer",
133
118
  onOpen: () => {
134
119
  const ownerIds = transports.getClaimsForResource(transport);
@@ -136,38 +121,37 @@ export const initSharedWorker = (self) => async (run) => {
136
121
  url: transport.url,
137
122
  ownerIds: [...ownerIds],
138
123
  });
139
- forEachSharedEvolu((sharedEvolu) => {
140
- sharedEvolu.requestCreateSyncMessages(ownerIds);
124
+ forEachTenant((tenant) => {
125
+ tenant.requestCreateSyncMessages(ownerIds);
141
126
  });
142
127
  },
143
128
  onMessage(data) {
144
129
  if (!(data instanceof ArrayBuffer))
145
130
  return;
146
- const inputMessage = new globalThis.Uint8Array(data);
147
- const headerResult = parseProtocolHeader(new globalThis.Uint8Array(data));
131
+ const message = new Uint8Array(data);
132
+ const headerResult = parseProtocolHeader(message);
148
133
  if (!headerResult.ok) {
149
134
  console.debug("transportInvalidProtocolMessage", {
150
135
  url: transport.url,
151
- byteLength: inputMessage.byteLength,
136
+ byteLength: message.byteLength,
152
137
  });
153
- // TODO: Propagate invalid protocol messages to sync state.
154
138
  return;
155
139
  }
156
140
  console.debug("transportProtocolMessage", {
157
141
  url: transport.url,
158
142
  ownerId: headerResult.value.ownerId,
159
- byteLength: inputMessage.byteLength,
143
+ byteLength: message.byteLength,
160
144
  });
161
- forEachSharedEvolu((sharedEvolu) => {
162
- sharedEvolu.requestApplySyncMessage(headerResult.value.ownerId, inputMessage);
145
+ forEachTenant((tenant) => {
146
+ tenant.requestApplySyncMessage(headerResult.value.ownerId, message);
163
147
  });
164
148
  },
165
149
  }), {
166
150
  onFirstClaimAdded: (ownerId, webSocket) => {
167
151
  if (!webSocket.isOpen())
168
152
  return;
169
- forEachSharedEvolu((sharedEvolu) => {
170
- sharedEvolu.requestCreateSyncMessages(new Set([ownerId]));
153
+ forEachTenant((tenant) => {
154
+ tenant.requestCreateSyncMessages(new Set([ownerId]));
171
155
  });
172
156
  },
173
157
  onLastClaimRemoved: (ownerId, webSocket) => {
@@ -177,23 +161,21 @@ export const initSharedWorker = (self) => async (run) => {
177
161
  idleDisposeAfter: "3s",
178
162
  resourceLookup: structuralLookup,
179
163
  })));
180
- const sharedWorkerRunWithSharedEvoluDeps = initSharedWorkerRun.addDeps({
164
+ const sharedWorkerRun = run.create().addDeps({
165
+ postConsoleEntryOrError,
166
+ tabLeaderPortStore,
181
167
  transports,
182
- postTabOutput,
183
168
  });
184
- const sharedEvolusByName = stack.use(await sharedWorkerRunWithSharedEvoluDeps.orThrow(createSharedResourceByKey(createSharedEvolu, {
185
- lookup: structuralLookup,
169
+ const tenantsByName = disposer.use(await sharedWorkerRun.orThrow(createSharedResourceByKey(createEvoluTenant, {
170
+ lookup: (message) => message.name,
186
171
  })));
187
- const forEachSharedEvolu = (callback) => {
188
- for (const sharedEvolu of sharedEvolusByName
189
- .snapshot()
190
- .resourcesByKey.values()) {
191
- callback(sharedEvolu);
172
+ sharedWorkerReady.resolve();
173
+ const forEachTenant = (callback) => {
174
+ for (const tenant of tenantsByName.snapshot().resourcesByKey.values()) {
175
+ callback(tenant);
192
176
  }
193
177
  };
194
- sharedWorkerReady.resolve();
195
- console.info("initSharedWorker");
196
- return ok(stack.move());
178
+ return ok(disposer.move());
197
179
  }
198
180
  catch (e_1) {
199
181
  env_1.error = e_1;
@@ -205,91 +187,151 @@ export const initSharedWorker = (self) => async (run) => {
205
187
  await result_1;
206
188
  }
207
189
  };
208
- const createSharedEvolu = (name) => (run) => {
209
- const stack = new AsyncDisposableStack();
210
- const sharedEvoluRun = run.create();
211
- const console = run.deps.console.child(name).child("SharedWorker");
212
- const { createMessagePort, postTabOutput, transports } = run.deps;
213
- const evoluInstancesByPortId = new Map();
214
- const queue = [];
215
- const callbacks = stack.use(createCallbacks(run.deps));
216
- let leaderDbWorkerPort = null;
217
- let queueProcessingFiber = null;
218
- stack.use(sharedEvoluRun);
219
- const moved = stack.move();
220
- const ensureQueueProcessing = () => {
221
- if (queueProcessingFiber ||
222
- !isNonEmptyArray(queue) ||
223
- !leaderDbWorkerPort) {
224
- return;
225
- }
226
- const first = firstInArray(queue);
227
- const callbackId = callbacks.register(({ response }) => {
228
- queueProcessingFiber?.abort();
229
- queueProcessingFiber = null;
230
- switch (response.type) {
231
- case "ForEvolu": {
232
- handleResponseForEvolu(response, first);
233
- break;
190
+ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, memoryOnly, }) => async (run) => {
191
+ const env_2 = { stack: [], error: void 0, hasError: false };
192
+ try {
193
+ const disposer = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
194
+ const tenantRun = disposer.use(run.create());
195
+ const { deps } = run;
196
+ const console = deps.console.child(name).child("SharedWorker");
197
+ const instancesById = disposer.adopt(new Map(), async (instancesById) => {
198
+ const env_3 = { stack: [], error: void 0, hasError: false };
199
+ try {
200
+ const disposer = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
201
+ for (const instance of [...instancesById.values()]) {
202
+ disposer.use(instance);
234
203
  }
235
- case "ForSharedWorker":
236
- handleResponseForSharedWorker(response);
237
- break;
238
- default:
239
- exhaustiveCheck(response);
240
204
  }
241
- // Complete the current queue item and continue with the next one.
242
- shiftFromArray(queue);
243
- ensureQueueProcessing();
205
+ catch (e_3) {
206
+ env_3.error = e_3;
207
+ env_3.hasError = true;
208
+ }
209
+ finally {
210
+ const result_3 = __disposeResources(env_3);
211
+ if (result_3)
212
+ await result_3;
213
+ }
244
214
  });
245
- /**
246
- * The leader DbWorker can disappear at any time when its tab closes, so
247
- * keep resending the current request until some leader answers. The
248
- * response handler aborts this retry loop and advances the queue.
249
- */
250
- queueProcessingFiber = sharedEvoluRun.daemon(repeat(() => {
251
- assert(leaderDbWorkerPort, "Expected a leader DbWorker");
252
- leaderDbWorkerPort.postMessage({ callbackId, request: first });
253
- return ok();
254
- }, spaced("5s")));
255
- };
256
- const handleResponseForEvolu = (response, first) => {
257
- const instance = evoluInstancesByPortId.get(response.evoluPortId);
258
- const port = instance?.evoluPort;
259
- if (!port)
260
- return;
261
- switch (response.message.type) {
262
- case "Mutate":
263
- case "Query": {
264
- const previousRowsByQuery = instance.rowsByQuery;
265
- const nextRowsByQuery = new Map(previousRowsByQuery);
266
- const patchesByQuery = new Map();
267
- for (const [query, rows] of response.message.rowsByQuery) {
268
- nextRowsByQuery.set(query, rows);
269
- patchesByQuery.set(query, makePatches(previousRowsByQuery.get(query), rows));
215
+ let dbWorkerPort = null;
216
+ const dbWorkerInited = Promise.withResolvers();
217
+ const initDbWorker = () => {
218
+ const tabLeaderPort = deps.tabLeaderPortStore.get();
219
+ assert(tabLeaderPort, "Expected tab leader port.");
220
+ const dbWorkerChannel = deps.createMessageChannel();
221
+ const currentDbWorkerPort = dbWorkerChannel.port2;
222
+ currentDbWorkerPort.onMessage = (message) => {
223
+ switch (message.type) {
224
+ case "LeaderAcquired": {
225
+ assert(dbWorkerPort !== currentDbWorkerPort, "Expected a new DbWorker port.");
226
+ dbWorkerPort?.[Symbol.dispose]();
227
+ dbWorkerPort = currentDbWorkerPort;
228
+ queueRequestInFlight = false;
229
+ console.info("leaderAcquired");
230
+ dbWorkerInited.resolve();
231
+ runQueue();
232
+ break;
233
+ }
234
+ case "OnQueuedResponse": {
235
+ callbacks.execute(message.callbackId, message);
236
+ break;
237
+ }
238
+ default:
239
+ exhaustiveCheck(message);
270
240
  }
271
- instance.rowsByQuery = nextRowsByQuery;
272
- instance.evoluPort.postMessage({
273
- type: "OnPatchesByQuery",
274
- patchesByQuery,
275
- onCompleteIds: first.message.type === "Mutate"
276
- ? first.message.onCompleteIds
277
- : emptyArray,
278
- });
279
- if (response.message.type === "Mutate") {
280
- const mutateResponse = response.message;
281
- for (const [otherEvoluPortId, otherEvoluInstance,] of evoluInstancesByPortId) {
282
- if (otherEvoluPortId === response.evoluPortId)
283
- continue;
284
- otherEvoluInstance.evoluPort.postMessage({
285
- type: "RefreshQueries",
286
- });
241
+ };
242
+ tabLeaderPort.postMessage({
243
+ type: "DbWorkerInit",
244
+ name,
245
+ consoleLevel,
246
+ sqliteSchema,
247
+ encryptionKey,
248
+ memoryOnly,
249
+ port: dbWorkerChannel.port1.native,
250
+ }, [dbWorkerChannel.port1.native]);
251
+ };
252
+ const queue = [];
253
+ const callbacks = disposer.use(createCallbacks(run.deps));
254
+ let queueRequestInFlight = false;
255
+ const runQueue = () => {
256
+ if (queueRequestInFlight || !isNonEmptyArray(queue) || !dbWorkerPort) {
257
+ return;
258
+ }
259
+ const request = firstInArray(queue);
260
+ const callbackId = callbacks.register(({ response }) => {
261
+ switch (response.type) {
262
+ case "ForEvolu": {
263
+ handleResponseForEvolu(response, request);
264
+ break;
287
265
  }
288
- sharedEvoluRun((run) => {
266
+ case "ForSharedWorker":
267
+ handleResponseForSharedWorker(response);
268
+ break;
269
+ default:
270
+ exhaustiveCheck(response);
271
+ }
272
+ // Complete the current queue item and continue with the next one.
273
+ shiftFromArray(queue);
274
+ queueRequestInFlight = false;
275
+ runQueue();
276
+ });
277
+ queueRequestInFlight = true;
278
+ dbWorkerPort.postMessage({ type: "Request", callbackId, request });
279
+ };
280
+ disposer.defer(async () => {
281
+ const env_4 = { stack: [], error: void 0, hasError: false };
282
+ try {
283
+ dbWorkerPort?.postMessage({ type: "Dispose" });
284
+ dbWorkerPort = null;
285
+ queueRequestInFlight = false;
286
+ const _ = __addDisposableResource(env_4, await tenantRun.orThrow(acquireLeaderLock(name)), true);
287
+ }
288
+ catch (e_4) {
289
+ env_4.error = e_4;
290
+ env_4.hasError = true;
291
+ }
292
+ finally {
293
+ const result_4 = __disposeResources(env_4);
294
+ if (result_4)
295
+ await result_4;
296
+ }
297
+ });
298
+ disposer.defer(deps.tabLeaderPortStore.subscribe(initDbWorker));
299
+ initDbWorker();
300
+ await dbWorkerInited.promise;
301
+ const disposables = disposer.move();
302
+ const handleResponseForEvolu = (response, first) => {
303
+ const instance = instancesById.get(response.id);
304
+ if (!instance)
305
+ return;
306
+ switch (response.message.type) {
307
+ case "Mutate":
308
+ case "Query": {
309
+ const nextRowsByQuery = new Map(instance.rowsByQuery);
310
+ const patchesByQuery = new Map();
311
+ for (const [query, rows] of response.message.rowsByQuery) {
312
+ nextRowsByQuery.set(query, rows);
313
+ patchesByQuery.set(query, makePatches(instance.rowsByQuery.get(query), rows));
314
+ }
315
+ instance.rowsByQuery = nextRowsByQuery;
316
+ instance.port.postMessage({
317
+ type: "OnPatchesByQuery",
318
+ patchesByQuery,
319
+ onCompleteIds: first.message.type === "Mutate"
320
+ ? first.message.onCompleteIds
321
+ : emptyArray,
322
+ });
323
+ if (response.message.type === "Mutate") {
324
+ for (const [instanceId, instance] of instancesById) {
325
+ if (instanceId === response.id)
326
+ continue;
327
+ instance.port.postMessage({
328
+ type: "RefreshQueries",
329
+ });
330
+ }
289
331
  const protocolMessagesByOwnerId = new Map();
290
332
  for (const syncOwner of instance.usedSyncOwners.keys()) {
291
333
  const { owner } = syncOwner;
292
- const messages = mutateResponse.messagesByOwnerId.get(owner.id);
334
+ const messages = response.message.messagesByOwnerId.get(owner.id);
293
335
  // Skip owners this instance does not currently sync for
294
336
  // writing. Read-only owners cannot produce protocol
295
337
  // messages because they do not have a write key.
@@ -298,252 +340,217 @@ const createSharedEvolu = (name) => (run) => {
298
340
  protocolMessagesByOwnerId.set(owner.id, createProtocolMessageFromCrdtMessages(run.deps)(owner, messages));
299
341
  }
300
342
  sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
301
- return ok();
302
- });
343
+ }
344
+ break;
303
345
  }
304
- break;
346
+ case "Export":
347
+ instance.port.postMessage({ type: "OnExport", file: response.message.file }, [response.message.file.buffer]);
348
+ break;
349
+ default:
350
+ exhaustiveCheck(response.message);
305
351
  }
306
- case "Export":
307
- port.postMessage({ type: "OnExport", file: response.message.file }, [
308
- response.message.file.buffer,
309
- ]);
310
- break;
311
- default:
312
- exhaustiveCheck(response.message);
313
- }
314
- };
315
- const handleResponseForSharedWorker = (response) => {
316
- switch (response.message.type) {
317
- case "CreateSyncMessages":
318
- sendProtocolMessagesByOwnerId(response.message.protocolMessagesByOwnerId);
319
- break;
320
- case "ApplySyncMessage":
321
- if (response.message.didWriteMessages) {
322
- refreshQueries();
323
- }
324
- if (!response.message.result.ok) {
325
- if (response.message.result.error.type !== "AbortError") {
326
- postTabOutput({
327
- type: "OnError",
328
- error: response.message.result.error,
329
- });
352
+ };
353
+ const handleResponseForSharedWorker = (response) => {
354
+ switch (response.message.type) {
355
+ case "CreateSyncMessages":
356
+ sendProtocolMessagesByOwnerId(response.message.protocolMessagesByOwnerId);
357
+ break;
358
+ case "ApplySyncMessage":
359
+ if (response.message.didWriteMessages) {
360
+ for (const instance of instancesById.values()) {
361
+ instance.port.postMessage({ type: "RefreshQueries" });
362
+ }
330
363
  }
331
- }
332
- else {
333
- switch (response.message.result.value.type) {
334
- case "Response":
335
- sendProtocolMessagesByOwnerId(new Map([
336
- [
337
- response.message.ownerId,
338
- response.message.result.value.message,
339
- ],
340
- ]));
341
- break;
342
- case "Broadcast":
343
- case "NoResponse":
344
- break;
345
- default:
346
- exhaustiveCheck(response.message.result.value);
364
+ if (!response.message.result.ok) {
365
+ if (response.message.result.error.type !== "AbortError") {
366
+ deps.postConsoleEntryOrError({
367
+ type: "Error",
368
+ error: response.message.result.error,
369
+ });
370
+ }
371
+ }
372
+ else {
373
+ switch (response.message.result.value.type) {
374
+ case "Response":
375
+ sendProtocolMessagesByOwnerId(new Map([
376
+ [
377
+ response.message.ownerId,
378
+ response.message.result.value.message,
379
+ ],
380
+ ]));
381
+ break;
382
+ case "Broadcast":
383
+ case "NoResponse":
384
+ break;
385
+ default:
386
+ exhaustiveCheck(response.message.result.value);
387
+ }
347
388
  }
389
+ break;
390
+ default:
391
+ exhaustiveCheck(response.message);
392
+ }
393
+ };
394
+ const sendProtocolMessagesByOwnerId = (protocolMessagesByOwnerId) => {
395
+ for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
396
+ for (const transport of deps.transports.getResourceKeysForClaim(ownerId)) {
397
+ const webSocket = deps.transports.getResource(transport);
398
+ if (!webSocket?.isOpen())
399
+ continue;
400
+ console.debug("sendProtocolMessage", {
401
+ ownerId,
402
+ url: transport.url,
403
+ byteLength: protocolMessage.byteLength,
404
+ });
405
+ webSocket.send(protocolMessage);
348
406
  }
349
- break;
350
- default:
351
- exhaustiveCheck(response.message);
352
- }
353
- };
354
- const sendProtocolMessagesByOwnerId = (protocolMessagesByOwnerId) => {
355
- for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
356
- for (const transport of transports.getResourceKeysForClaim(ownerId)) {
357
- const webSocket = transports.getResource(transport);
358
- if (!webSocket?.isOpen())
359
- continue;
360
- console.debug("sendProtocolMessage", {
361
- ownerId,
362
- url: transport.url,
363
- byteLength: protocolMessage.byteLength,
364
- });
365
- webSocket.send(protocolMessage);
366
407
  }
367
- }
368
- };
369
- const refreshQueries = () => {
370
- for (const evoluInstance of evoluInstancesByPortId.values()) {
371
- evoluInstance.evoluPort.postMessage({ type: "RefreshQueries" });
372
- }
373
- };
374
- const getUsedOwnersById = (ownerIds) => {
375
- const ownersById = new Map();
376
- for (const evoluInstance of evoluInstancesByPortId.values()) {
377
- for (const { owner } of evoluInstance.usedSyncOwners.keys()) {
378
- if (!ownerIds.has(owner.id) || !("writeKey" in owner))
379
- continue;
380
- ownersById.set(owner.id, owner);
408
+ };
409
+ const getUsedOwnersById = (ownerIds) => {
410
+ const ownersById = new Map();
411
+ for (const instance of instancesById.values()) {
412
+ for (const { owner } of instance.usedSyncOwners.keys()) {
413
+ if (!ownerIds.has(owner.id) || !("writeKey" in owner))
414
+ continue;
415
+ ownersById.set(owner.id, owner);
416
+ }
381
417
  }
382
- }
383
- return ownersById;
384
- };
385
- const removeAllUsedSyncOwners = (evoluInstance) => unabortable(async (run) => {
386
- for (const syncOwner of evoluInstance.usedSyncOwners.keys()) {
387
- while (evoluInstance.usedSyncOwners.has(syncOwner)) {
388
- await run(toggleUsedSyncOwner(evoluInstance, syncOwner, "remove"));
418
+ return ownersById;
419
+ };
420
+ const toggleSyncOwner = (instance, syncOwner, action) => async (run) => {
421
+ if (action === "add") {
422
+ instance.usedSyncOwners.increment(syncOwner);
423
+ return await run(deps.transports.addClaim(syncOwner.owner.id, syncOwner.transports));
389
424
  }
390
- }
391
- return ok();
392
- });
393
- const toggleUsedSyncOwner = (evoluInstance, syncOwner, action) => unabortable(async (run) => {
394
- if (action === "add") {
395
- evoluInstance.usedSyncOwners.increment(syncOwner);
396
- await run(transports.addClaim(syncOwner.owner.id, syncOwner.transports));
397
- }
398
- else {
399
- evoluInstance.usedSyncOwners.decrement(syncOwner);
400
- await run(transports.removeClaim(syncOwner.owner.id, syncOwner.transports));
401
- }
402
- return ok();
403
- });
404
- return ok({
405
- createInstance: (nativeEvoluPort, nativeDbWorkerPort, releaseSharedEvolu) => {
406
- assertNotDisposed(moved);
407
- const evoluPort = createMessagePort(nativeEvoluPort);
408
- const dbWorkerPort = createMessagePort(nativeDbWorkerPort);
409
- const evoluPortId = createId(run.deps);
410
- const evoluInstance = {
411
- evoluPort,
412
- dbWorkerPort,
413
- releaseSharedEvolu,
414
- rowsByQuery: new Map(),
415
- usedSyncOwners: createRefCountByKey({
416
- lookup: ({ owner: { id } }) => id,
417
- }),
418
- };
419
- evoluInstancesByPortId.set(evoluPortId, evoluInstance);
420
- dbWorkerPort.onMessage = (message) => {
421
- switch (message.type) {
422
- case "LeaderAcquired": {
423
- leaderDbWorkerPort = evoluInstance.dbWorkerPort;
424
- console.info("leaderAcquired");
425
- ensureQueueProcessing();
426
- break;
427
- }
428
- case "OnQueuedResponse": {
429
- callbacks.execute(message.callbackId, message);
430
- break;
431
- }
432
- case "OnConsoleEntry":
433
- case "OnError": {
434
- postTabOutput(message);
435
- break;
436
- }
437
- default:
438
- exhaustiveCheck(message);
439
- }
440
- };
441
- evoluPort.onMessage = (evoluMessage) => {
442
- switch (evoluMessage.type) {
443
- case "Query":
444
- case "Export": {
445
- queue.push({
446
- type: "ForEvolu",
447
- evoluPortId,
448
- message: evoluMessage,
449
- });
450
- ensureQueueProcessing();
451
- break;
452
- }
453
- case "Mutate": {
454
- // TODO: Delegate do vsech evolu instances, co to pouzivaji
455
- queue.push({
456
- type: "ForEvolu",
457
- evoluPortId,
458
- message: evoluMessage,
459
- });
460
- ensureQueueProcessing();
461
- break;
425
+ else {
426
+ instance.usedSyncOwners.decrement(syncOwner);
427
+ return await run(deps.transports.removeClaim(syncOwner.owner.id, syncOwner.transports));
428
+ }
429
+ };
430
+ return ok({
431
+ addInstance: (message, onDisposed) => {
432
+ assertNotDisposed(disposables);
433
+ const instance = {
434
+ id: message.id,
435
+ port: deps.createMessagePort(message.evoluPort),
436
+ onDisposed,
437
+ rowsByQuery: new Map(),
438
+ useOwnerMutex: createMutex(),
439
+ usedSyncOwners: createRefCountByKey({
440
+ lookup: (syncOwner) => syncOwner.owner.id,
441
+ }),
442
+ [Symbol.asyncDispose]: () => disposer.disposeAsync(),
443
+ };
444
+ instancesById.set(instance.id, instance);
445
+ const disposer = new AsyncDisposableStack();
446
+ disposer.defer(instance.onDisposed);
447
+ disposer.use(instance.usedSyncOwners);
448
+ disposer.use(instance.useOwnerMutex);
449
+ disposer.defer(async () => {
450
+ await tenantRun(instance.useOwnerMutex.withLock(async (run) => {
451
+ for (const syncOwner of instance.usedSyncOwners.keys()) {
452
+ while (instance.usedSyncOwners.has(syncOwner)) {
453
+ await run(toggleSyncOwner(instance, syncOwner, "remove"));
454
+ }
455
+ }
456
+ return ok();
457
+ }));
458
+ });
459
+ disposer.defer(() => {
460
+ instancesById.delete(instance.id);
461
+ console.info("evoluDispose", { name, id: instance.id });
462
+ });
463
+ disposer.use(instance.port);
464
+ void tenantRun(acquireLeaderLock(message.id)).then((result) => {
465
+ if (!result.ok)
466
+ return;
467
+ const lock = result.value;
468
+ if (disposer.disposed) {
469
+ return lock[Symbol.asyncDispose]();
462
470
  }
463
- case "UseOwner": {
464
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
465
- if (!evoluInstance)
471
+ disposer.use(lock);
472
+ return disposer.disposeAsync();
473
+ });
474
+ instance.port.onMessage = (message) => {
475
+ switch (message.type) {
476
+ case "Query":
477
+ case "Export": {
478
+ queue.push({ type: "ForEvolu", id: instance.id, message });
479
+ runQueue();
466
480
  break;
467
- for (const { owner, action } of evoluMessage.actions) {
468
- console.debug("useOwner", {
469
- evoluPortId,
470
- action,
471
- ownerId: owner.owner.id,
472
- transportUrls: owner.transports.map(({ url }) => url),
473
- });
474
- void sharedEvoluRun(toggleUsedSyncOwner(evoluInstance, owner, action));
475
481
  }
476
- break;
477
- }
478
- case "Dispose": {
479
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
480
- if (!evoluInstance)
482
+ case "Mutate": {
483
+ // TODO: Delegate do vsech evolu instances, co to pouzivaji
484
+ queue.push({ type: "ForEvolu", id: instance.id, message });
485
+ runQueue();
481
486
  break;
482
- evoluInstancesByPortId.delete(evoluPortId);
483
- const hadLastPort = evoluInstancesByPortId.size === 0;
484
- console.info("evoluDispose", {
485
- name,
486
- evoluPortId,
487
- hadLastPort,
488
- });
489
- // Potential plan: keep DbWorker ports in a SharedResource
490
- // abstraction instead of deleting them eagerly here. DbWorkers use
491
- // leader election because SQLite WASM needs a single active owner.
492
- // When the last Evolu instance is disposed, broadcast shutdown to
493
- // all DbWorkers so the current leader can dispose itself and the
494
- // followers can clean up consistently.
495
- sharedEvoluRun(async (run) => {
496
- await run(removeAllUsedSyncOwners(evoluInstance));
497
- evoluInstance.usedSyncOwners[Symbol.dispose]();
498
- return ok();
499
- });
500
- // if (hadLastPort) evoluInstance.releaseSharedEvolu();
501
- // TODO: Dispose SharedEvolu on the last port.
502
- // TODO: Decided what to do with DbWorker but probably dispose it, but
503
- // https://bugs.webkit.org/show_bug.cgi?id=301520
504
- break;
487
+ }
488
+ case "UseOwner": {
489
+ void tenantRun(instance.useOwnerMutex.withLock(async (run) => {
490
+ for (const { owner, action } of message.actions) {
491
+ console.debug("useOwner", {
492
+ id: instance.id,
493
+ action,
494
+ ownerId: owner.owner.id,
495
+ transportUrls: owner.transports.map(({ url }) => url),
496
+ });
497
+ await run(toggleSyncOwner(instance, owner, action));
498
+ }
499
+ return ok();
500
+ }));
501
+ break;
502
+ }
503
+ default:
504
+ exhaustiveCheck(message);
505
505
  }
506
- default:
507
- exhaustiveCheck(evoluMessage);
508
- }
509
- };
510
- },
511
- requestCreateSyncMessages: (ownerIds) => {
512
- const ownersToSync = [...getUsedOwnersById(ownerIds).values()];
513
- if (!isNonEmptyArray(ownersToSync))
514
- return;
515
- console.debug("requestCreateSyncMessages", {
516
- ownerIds: ownersToSync.map(({ id }) => id),
517
- });
518
- queue.push({
519
- type: "ForSharedWorker",
520
- message: {
521
- type: "CreateSyncMessages",
522
- owners: ownersToSync,
523
- },
524
- });
525
- ensureQueueProcessing();
526
- },
527
- requestApplySyncMessage: (ownerId, inputMessage) => {
528
- const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
529
- if (!owner)
530
- return;
531
- console.debug("requestApplySyncMessage", {
532
- ownerId,
533
- byteLength: inputMessage.byteLength,
534
- });
535
- queue.push({
536
- type: "ForSharedWorker",
537
- message: {
538
- type: "ApplySyncMessage",
539
- owner,
540
- inputMessage,
541
- },
542
- });
543
- ensureQueueProcessing();
544
- },
545
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
546
- });
506
+ };
507
+ },
508
+ requestCreateSyncMessages: (ownerIds) => {
509
+ const ownersToSync = [...getUsedOwnersById(ownerIds).values()];
510
+ if (!isNonEmptyArray(ownersToSync))
511
+ return;
512
+ console.debug("requestCreateSyncMessages", {
513
+ ownerIds: ownersToSync.map(({ id }) => id),
514
+ });
515
+ queue.push({
516
+ type: "ForSharedWorker",
517
+ message: {
518
+ type: "CreateSyncMessages",
519
+ owners: ownersToSync,
520
+ },
521
+ });
522
+ runQueue();
523
+ },
524
+ requestApplySyncMessage: (ownerId, inputMessage) => {
525
+ const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
526
+ if (!owner)
527
+ return;
528
+ console.debug("requestApplySyncMessage", {
529
+ ownerId,
530
+ byteLength: inputMessage.byteLength,
531
+ });
532
+ queue.push({
533
+ type: "ForSharedWorker",
534
+ message: {
535
+ type: "ApplySyncMessage",
536
+ owner,
537
+ inputMessage,
538
+ },
539
+ });
540
+ runQueue();
541
+ },
542
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
543
+ });
544
+ }
545
+ catch (e_2) {
546
+ env_2.error = e_2;
547
+ env_2.hasError = true;
548
+ }
549
+ finally {
550
+ const result_2 = __disposeResources(env_2);
551
+ if (result_2)
552
+ await result_2;
553
+ }
547
554
  };
548
555
  // | (Typed<"reset"> & {
549
556
  // readonly onCompleteId: CallbackId;
@@ -567,3 +574,10 @@ const createSharedEvolu = (name) => (run) => {
567
574
  // readonly onCompleteId: CallbackId;
568
575
  // readonly reload: boolean;
569
576
  // })
577
+ // TODO: Remaining SharedWorker lifetime issues.
578
+ // - Investigate heartbeat for ports and where liveness tracking is needed.
579
+ // - Sync state for monitoring.
580
+ // - Propagate invalid protocol messages to sync state.
581
+ // - Make DbWorker logically stateless by making all requests idempotent.
582
+ // - Investigate WebKit bug 301520 relevance to DbWorker disposal. (again)
583
+ // https://bugs.webkit.org/show_bug.cgi?id=301520