@evolu/common 8.0.0-next.3 → 8.0.0-next.5

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 (81) hide show
  1. package/dist/src/Assert.d.ts +10 -5
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +10 -5
  4. package/dist/src/Error.d.ts +5 -3
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +16 -0
  7. package/dist/src/Identicon.js +4 -4
  8. package/dist/src/LockManager.d.ts +74 -0
  9. package/dist/src/LockManager.d.ts.map +1 -0
  10. package/dist/src/LockManager.js +103 -0
  11. package/dist/src/Microtask.d.ts.map +1 -1
  12. package/dist/src/Microtask.js +90 -26
  13. package/dist/src/Object.d.ts.map +1 -1
  14. package/dist/src/RefCount.d.ts.map +1 -1
  15. package/dist/src/RefCount.js +153 -68
  16. package/dist/src/Resource.d.ts +6 -6
  17. package/dist/src/Resource.d.ts.map +1 -1
  18. package/dist/src/Resource.js +211 -162
  19. package/dist/src/Schedule.d.ts +5 -5
  20. package/dist/src/Schedule.d.ts.map +1 -1
  21. package/dist/src/Schedule.js +53 -44
  22. package/dist/src/Sqlite.d.ts +2 -0
  23. package/dist/src/Sqlite.d.ts.map +1 -1
  24. package/dist/src/Sqlite.js +66 -41
  25. package/dist/src/Task.d.ts +99 -123
  26. package/dist/src/Task.d.ts.map +1 -1
  27. package/dist/src/Task.js +273 -256
  28. package/dist/src/Test.d.ts +14 -15
  29. package/dist/src/Test.d.ts.map +1 -1
  30. package/dist/src/Time.d.ts +5 -0
  31. package/dist/src/Time.d.ts.map +1 -1
  32. package/dist/src/Time.js +5 -0
  33. package/dist/src/Type.d.ts +7 -0
  34. package/dist/src/Type.d.ts.map +1 -1
  35. package/dist/src/Type.js +7 -0
  36. package/dist/src/Types.d.ts +0 -8
  37. package/dist/src/Types.d.ts.map +1 -1
  38. package/dist/src/WebSocket.d.ts.map +1 -1
  39. package/dist/src/WebSocket.js +7 -6
  40. package/dist/src/Worker.d.ts +42 -8
  41. package/dist/src/Worker.d.ts.map +1 -1
  42. package/dist/src/Worker.js +292 -112
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +1 -0
  46. package/dist/src/local-first/Db.d.ts +5 -4
  47. package/dist/src/local-first/Db.d.ts.map +1 -1
  48. package/dist/src/local-first/Db.js +43 -37
  49. package/dist/src/local-first/Evolu.d.ts +3 -2
  50. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  51. package/dist/src/local-first/Evolu.js +90 -80
  52. package/dist/src/local-first/Owner.d.ts.map +1 -1
  53. package/dist/src/local-first/Shared.d.ts +65 -54
  54. package/dist/src/local-first/Shared.d.ts.map +1 -1
  55. package/dist/src/local-first/Shared.js +390 -376
  56. package/package.json +1 -1
  57. package/src/Array.ts +8 -8
  58. package/src/Assert.ts +11 -6
  59. package/src/Error.ts +5 -4
  60. package/src/Identicon.ts +4 -4
  61. package/src/LockManager.ts +181 -0
  62. package/src/Microtask.ts +17 -11
  63. package/src/Object.ts +3 -3
  64. package/src/RefCount.ts +25 -21
  65. package/src/Resource.ts +63 -56
  66. package/src/Schedule.ts +70 -48
  67. package/src/Set.ts +4 -4
  68. package/src/Sqlite.ts +43 -31
  69. package/src/Task.ts +392 -430
  70. package/src/Test.ts +14 -15
  71. package/src/Time.ts +7 -0
  72. package/src/Type.ts +9 -0
  73. package/src/Types.ts +0 -9
  74. package/src/WebSocket.ts +7 -6
  75. package/src/Worker.ts +195 -53
  76. package/src/index.ts +1 -0
  77. package/src/local-first/Db.ts +86 -61
  78. package/src/local-first/Evolu.ts +74 -68
  79. package/src/local-first/Owner.ts +1 -1
  80. package/src/local-first/Query.ts +1 -1
  81. package/src/local-first/Shared.ts +485 -476
@@ -56,67 +56,49 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
56
56
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
57
57
  });
58
58
  import { emptyArray, firstInArray, isNonEmptyArray, shiftFromArray, } from "../Array.js";
59
- import { assert, assertNotDisposed } from "../Assert.js";
59
+ import { assert, assertNotAborted, assertNotDisposed } from "../Assert.js";
60
60
  import { createCallbacks } from "../Callbacks.js";
61
- import { exhaustiveCheck } from "../Function.js";
61
+ import { acquireLeaderLock } from "../LockManager.js";
62
62
  import { structuralLookup } from "../Lookup.js";
63
63
  import { createRefCountByKey } from "../RefCount.js";
64
64
  import { createSharedResourceByKey, createSharedResourceByKeyWithClaims, } from "../Resource.js";
65
65
  import { ok } from "../Result.js";
66
- import { spaced } from "../Schedule.js";
67
- import { repeat, unabortable, } from "../Task.js";
68
- import { createId } from "../Type.js";
66
+ import { createStore } from "../Store.js";
67
+ import { AbortError, createMutex, unabortable, } from "../Task.js";
68
+ import {} from "../Type.js";
69
69
  import { createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, parseProtocolHeader, } from "./Protocol.js";
70
70
  import { makePatches, } from "./Query.js";
71
+ export const consoleEntryOrErrorBroadcastChannelName = "evolu:console-entry-or-error";
71
72
  export const initSharedWorker = (self) => async (run) => {
72
73
  const env_1 = { stack: [], error: void 0, hasError: false };
73
74
  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);
75
+ const { deps } = run;
76
+ const console = deps.console.child("SharedWorker");
77
+ const disposer = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
78
+ const tabLeaderPortStore = disposer.use(createStore(null));
79
+ const consoleEntryOrErrorBroadcastChannel = disposer.use(deps.createBroadcastChannel(consoleEntryOrErrorBroadcastChannelName));
80
+ const postConsoleEntryOrError = (output) => {
81
+ consoleEntryOrErrorBroadcastChannel.postMessage(output);
88
82
  };
89
- stack.defer(consoleStoreOutputEntry.subscribe(() => {
90
- const entry = consoleStoreOutputEntry.get();
91
- if (entry)
92
- postTabOutput({ type: "OnConsoleEntry", entry });
93
- }));
83
+ const sharedWorkerReady = Promise.withResolvers();
94
84
  // Register ASAP so the worker does not miss connections.
95
85
  self.onConnect = (port) => {
96
86
  void sharedWorkerReady.promise.then(() => {
97
87
  // The underlying port buffers messages until onMessage is assigned.
98
88
  port.onMessage = (message) => {
99
89
  switch (message.type) {
100
- case "InitTab": {
101
- /**
102
- * One SharedWorker serves multiple tabs. The most recently
103
- * initialized tab's level wins.
104
- */
90
+ case "AnnounceTabLeader": {
105
91
  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
- }
92
+ tabLeaderPortStore.set(port);
93
+ console.info("tabLeaderAnnounced");
112
94
  break;
113
95
  }
114
96
  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)));
97
+ void sharedWorkerRun(async (run) => {
98
+ const tenant = await run(unabortable(tenantsByName.acquire(message)));
99
+ if (!tenant.ok)
100
+ return tenant;
101
+ tenant.value.addInstance(message, () => void sharedWorkerRun(tenantsByName.release(message)));
120
102
  return ok();
121
103
  });
122
104
  break;
@@ -127,8 +109,12 @@ export const initSharedWorker = (self) => async (run) => {
127
109
  };
128
110
  });
129
111
  };
130
- const initSharedWorkerRun = run.create();
131
- const transports = stack.use(await initSharedWorkerRun.orThrow(createSharedResourceByKeyWithClaims((transport) => createWebSocket(transport.url, {
112
+ disposer.defer(deps.consoleStoreOutputEntry.subscribe(() => {
113
+ const entry = deps.consoleStoreOutputEntry.get();
114
+ if (entry)
115
+ postConsoleEntryOrError({ type: "ConsoleEntry", entry });
116
+ }));
117
+ const transports = disposer.use(await run.orThrow(createSharedResourceByKeyWithClaims((transport) => deps.createWebSocket(transport.url, {
132
118
  binaryType: "arraybuffer",
133
119
  onOpen: () => {
134
120
  const ownerIds = transports.getClaimsForResource(transport);
@@ -136,38 +122,37 @@ export const initSharedWorker = (self) => async (run) => {
136
122
  url: transport.url,
137
123
  ownerIds: [...ownerIds],
138
124
  });
139
- forEachSharedEvolu((sharedEvolu) => {
140
- sharedEvolu.requestCreateSyncMessages(ownerIds);
125
+ forEachTenant((tenant) => {
126
+ tenant.requestCreateSyncMessages(ownerIds);
141
127
  });
142
128
  },
143
129
  onMessage(data) {
144
130
  if (!(data instanceof ArrayBuffer))
145
131
  return;
146
- const inputMessage = new globalThis.Uint8Array(data);
147
- const headerResult = parseProtocolHeader(new globalThis.Uint8Array(data));
132
+ const message = new Uint8Array(data);
133
+ const headerResult = parseProtocolHeader(message);
148
134
  if (!headerResult.ok) {
149
135
  console.debug("transportInvalidProtocolMessage", {
150
136
  url: transport.url,
151
- byteLength: inputMessage.byteLength,
137
+ byteLength: message.byteLength,
152
138
  });
153
- // TODO: Propagate invalid protocol messages to sync state.
154
139
  return;
155
140
  }
156
141
  console.debug("transportProtocolMessage", {
157
142
  url: transport.url,
158
143
  ownerId: headerResult.value.ownerId,
159
- byteLength: inputMessage.byteLength,
144
+ byteLength: message.byteLength,
160
145
  });
161
- forEachSharedEvolu((sharedEvolu) => {
162
- sharedEvolu.requestApplySyncMessage(headerResult.value.ownerId, inputMessage);
146
+ forEachTenant((tenant) => {
147
+ tenant.requestApplySyncMessage(headerResult.value.ownerId, message);
163
148
  });
164
149
  },
165
150
  }), {
166
151
  onFirstClaimAdded: (ownerId, webSocket) => {
167
152
  if (!webSocket.isOpen())
168
153
  return;
169
- forEachSharedEvolu((sharedEvolu) => {
170
- sharedEvolu.requestCreateSyncMessages(new Set([ownerId]));
154
+ forEachTenant((tenant) => {
155
+ tenant.requestCreateSyncMessages(new Set([ownerId]));
171
156
  });
172
157
  },
173
158
  onLastClaimRemoved: (ownerId, webSocket) => {
@@ -177,23 +162,23 @@ export const initSharedWorker = (self) => async (run) => {
177
162
  idleDisposeAfter: "3s",
178
163
  resourceLookup: structuralLookup,
179
164
  })));
180
- const sharedWorkerRunWithSharedEvoluDeps = initSharedWorkerRun.addDeps({
165
+ const sharedWorkerRun = run.create({
166
+ ...deps,
167
+ postConsoleEntryOrError,
168
+ tabLeaderPortStore,
181
169
  transports,
182
- postTabOutput,
183
170
  });
184
- const sharedEvolusByName = stack.use(await sharedWorkerRunWithSharedEvoluDeps.orThrow(createSharedResourceByKey(createSharedEvolu, {
185
- lookup: structuralLookup,
171
+ const tenantsByName = disposer.use(await sharedWorkerRun.orThrow(createSharedResourceByKey(createEvoluTenant, {
172
+ idleDisposeAfter: "3s",
173
+ lookup: (message) => message.name,
186
174
  })));
187
- const forEachSharedEvolu = (callback) => {
188
- for (const sharedEvolu of sharedEvolusByName
189
- .snapshot()
190
- .resourcesByKey.values()) {
191
- callback(sharedEvolu);
175
+ sharedWorkerReady.resolve();
176
+ const forEachTenant = (callback) => {
177
+ for (const tenant of tenantsByName.snapshot().resourcesByKey.values()) {
178
+ callback(tenant);
192
179
  }
193
180
  };
194
- sharedWorkerReady.resolve();
195
- console.info("initSharedWorker");
196
- return ok(stack.move());
181
+ return ok(disposer.move());
197
182
  }
198
183
  catch (e_1) {
199
184
  env_1.error = e_1;
@@ -205,91 +190,156 @@ export const initSharedWorker = (self) => async (run) => {
205
190
  await result_1;
206
191
  }
207
192
  };
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;
193
+ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, memoryOnly, }) => async (run) => {
194
+ const env_2 = { stack: [], error: void 0, hasError: false };
195
+ try {
196
+ const disposer = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
197
+ const tenantRun = disposer.use(run.create());
198
+ const { deps } = run;
199
+ const console = deps.console.child(name).child("SharedWorker");
200
+ const instancesById = disposer.adopt(new Map(), async (instancesById) => {
201
+ const env_3 = { stack: [], error: void 0, hasError: false };
202
+ try {
203
+ const disposer = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
204
+ for (const instance of [...instancesById.values()]) {
205
+ disposer.use(instance);
234
206
  }
235
- case "ForSharedWorker":
236
- handleResponseForSharedWorker(response);
237
- break;
238
- default:
239
- exhaustiveCheck(response);
240
207
  }
241
- // Complete the current queue item and continue with the next one.
242
- shiftFromArray(queue);
243
- ensureQueueProcessing();
208
+ catch (e_3) {
209
+ env_3.error = e_3;
210
+ env_3.hasError = true;
211
+ }
212
+ finally {
213
+ const result_3 = __disposeResources(env_3);
214
+ if (result_3)
215
+ await result_3;
216
+ }
244
217
  });
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));
218
+ let dbWorkerPort = null;
219
+ const dbWorkerInited = Promise.withResolvers();
220
+ const initDbWorker = () => {
221
+ const tabLeaderPort = deps.tabLeaderPortStore.get();
222
+ assert(tabLeaderPort, "Expected tab leader port.");
223
+ const dbWorkerChannel = deps.createMessageChannel();
224
+ const currentDbWorkerPort = dbWorkerChannel.port2;
225
+ currentDbWorkerPort.onMessage = (message) => {
226
+ switch (message.type) {
227
+ case "LeaderAcquired": {
228
+ assert(dbWorkerPort !== currentDbWorkerPort, "Expected a new DbWorker port.");
229
+ dbWorkerPort?.[Symbol.dispose]();
230
+ dbWorkerPort = currentDbWorkerPort;
231
+ queueRequestInFlight = false;
232
+ console.info("leaderAcquired");
233
+ dbWorkerInited.resolve();
234
+ runQueue();
235
+ break;
236
+ }
237
+ case "OnQueuedResponse": {
238
+ callbacks.execute(message.callbackId, message);
239
+ break;
240
+ }
270
241
  }
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
- });
242
+ };
243
+ tabLeaderPort.postMessage({
244
+ type: "DbWorkerInit",
245
+ name,
246
+ consoleLevel,
247
+ sqliteSchema,
248
+ encryptionKey,
249
+ memoryOnly,
250
+ port: dbWorkerChannel.port1.native,
251
+ }, [dbWorkerChannel.port1.native]);
252
+ };
253
+ const queue = [];
254
+ const callbacks = disposer.use(createCallbacks(run.deps));
255
+ let queueRequestInFlight = false;
256
+ const runQueue = () => {
257
+ if (queueRequestInFlight || !isNonEmptyArray(queue) || !dbWorkerPort) {
258
+ return;
259
+ }
260
+ const request = firstInArray(queue);
261
+ const callbackId = callbacks.register(({ response }) => {
262
+ switch (response.type) {
263
+ case "ForEvolu": {
264
+ handleResponseForEvolu(response, request);
265
+ break;
287
266
  }
288
- sharedEvoluRun((run) => {
267
+ case "ForSharedWorker":
268
+ handleResponseForSharedWorker(response);
269
+ break;
270
+ }
271
+ // Complete the current queue item and continue with the next one.
272
+ shiftFromArray(queue);
273
+ queueRequestInFlight = false;
274
+ runQueue();
275
+ });
276
+ queueRequestInFlight = true;
277
+ dbWorkerPort.postMessage({ type: "Request", callbackId, request });
278
+ };
279
+ disposer.defer(async () => {
280
+ const env_4 = { stack: [], error: void 0, hasError: false };
281
+ try {
282
+ dbWorkerPort?.postMessage({ type: "Dispose" });
283
+ dbWorkerPort = null;
284
+ queueRequestInFlight = false;
285
+ // The DbWorker holds this tenant leader lock while it is alive. Tenant
286
+ // disposal sends Dispose, then acquires the same lock to wait until the
287
+ // DbWorker releases it: either because Dispose was delivered or because
288
+ // the hosting tab closed. The wait is unabortable because tenant disposal
289
+ // must finish even after tenantRun receives an abort request.
290
+ const lock = await tenantRun(acquireLeaderLock(name));
291
+ // AbortError here means tenantRun was already disposed before this
292
+ // cleanup could start, violating the disposal ordering above.
293
+ assertNotAborted(lock);
294
+ const _ = __addDisposableResource(env_4, lock.value, true);
295
+ }
296
+ catch (e_4) {
297
+ env_4.error = e_4;
298
+ env_4.hasError = true;
299
+ }
300
+ finally {
301
+ const result_4 = __disposeResources(env_4);
302
+ if (result_4)
303
+ await result_4;
304
+ }
305
+ });
306
+ disposer.defer(deps.tabLeaderPortStore.subscribe(initDbWorker));
307
+ initDbWorker();
308
+ await dbWorkerInited.promise;
309
+ const disposables = disposer.move();
310
+ const handleResponseForEvolu = (response, first) => {
311
+ const instance = instancesById.get(response.id);
312
+ if (!instance)
313
+ return;
314
+ switch (response.message.type) {
315
+ case "Mutate":
316
+ case "Query": {
317
+ const nextRowsByQuery = new Map(instance.rowsByQuery);
318
+ const patchesByQuery = new Map();
319
+ for (const [query, rows] of response.message.rowsByQuery) {
320
+ nextRowsByQuery.set(query, rows);
321
+ patchesByQuery.set(query, makePatches(instance.rowsByQuery.get(query), rows));
322
+ }
323
+ instance.rowsByQuery = nextRowsByQuery;
324
+ instance.port.postMessage({
325
+ type: "OnPatchesByQuery",
326
+ patchesByQuery,
327
+ onCompleteIds: first.message.type === "Mutate"
328
+ ? first.message.onCompleteIds
329
+ : emptyArray,
330
+ });
331
+ if (response.message.type === "Mutate") {
332
+ for (const [instanceId, instance] of instancesById) {
333
+ if (instanceId === response.id)
334
+ continue;
335
+ instance.port.postMessage({
336
+ type: "RefreshQueries",
337
+ });
338
+ }
289
339
  const protocolMessagesByOwnerId = new Map();
290
340
  for (const syncOwner of instance.usedSyncOwners.keys()) {
291
341
  const { owner } = syncOwner;
292
- const messages = mutateResponse.messagesByOwnerId.get(owner.id);
342
+ const messages = response.message.messagesByOwnerId.get(owner.id);
293
343
  // Skip owners this instance does not currently sync for
294
344
  // writing. Read-only owners cannot produce protocol
295
345
  // messages because they do not have a write key.
@@ -298,252 +348,209 @@ const createSharedEvolu = (name) => (run) => {
298
348
  protocolMessagesByOwnerId.set(owner.id, createProtocolMessageFromCrdtMessages(run.deps)(owner, messages));
299
349
  }
300
350
  sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
301
- return ok();
302
- });
351
+ }
352
+ break;
303
353
  }
304
- break;
354
+ case "Export":
355
+ instance.port.postMessage({ type: "OnExport", file: response.message.file }, [response.message.file.buffer]);
356
+ break;
305
357
  }
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
- });
358
+ };
359
+ const handleResponseForSharedWorker = (response) => {
360
+ switch (response.message.type) {
361
+ case "CreateSyncMessages":
362
+ sendProtocolMessagesByOwnerId(response.message.protocolMessagesByOwnerId);
363
+ break;
364
+ case "ApplySyncMessage":
365
+ if (response.message.didWriteMessages) {
366
+ for (const instance of instancesById.values()) {
367
+ instance.port.postMessage({ type: "RefreshQueries" });
368
+ }
330
369
  }
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);
370
+ if (!response.message.result.ok) {
371
+ if (response.message.result.error.type !== "AbortError") {
372
+ deps.postConsoleEntryOrError({
373
+ type: "Error",
374
+ error: response.message.result.error,
375
+ });
376
+ }
377
+ }
378
+ else {
379
+ switch (response.message.result.value.type) {
380
+ case "Response":
381
+ sendProtocolMessagesByOwnerId(new Map([
382
+ [
383
+ response.message.ownerId,
384
+ response.message.result.value.message,
385
+ ],
386
+ ]));
387
+ break;
388
+ case "Broadcast":
389
+ case "NoResponse":
390
+ break;
391
+ }
347
392
  }
393
+ break;
394
+ }
395
+ };
396
+ const sendProtocolMessagesByOwnerId = (protocolMessagesByOwnerId) => {
397
+ for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
398
+ for (const transport of deps.transports.getResourceKeysForClaim(ownerId)) {
399
+ const webSocket = deps.transports.getResource(transport);
400
+ if (!webSocket?.isOpen())
401
+ continue;
402
+ console.debug("sendProtocolMessage", {
403
+ ownerId,
404
+ url: transport.url,
405
+ byteLength: protocolMessage.byteLength,
406
+ });
407
+ webSocket.send(protocolMessage);
348
408
  }
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
409
  }
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);
410
+ };
411
+ const getUsedOwnersById = (ownerIds) => {
412
+ const ownersById = new Map();
413
+ for (const instance of instancesById.values()) {
414
+ for (const { owner } of instance.usedSyncOwners.keys()) {
415
+ if (!ownerIds.has(owner.id) || !("writeKey" in owner))
416
+ continue;
417
+ ownersById.set(owner.id, owner);
418
+ }
381
419
  }
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"));
420
+ return ownersById;
421
+ };
422
+ const toggleSyncOwner = (instance, syncOwner, action) => async (run) => {
423
+ if (action === "add") {
424
+ instance.usedSyncOwners.increment(syncOwner);
425
+ return await run(deps.transports.addClaim(syncOwner.owner.id, syncOwner.transports));
389
426
  }
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;
462
- }
463
- case "UseOwner": {
464
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
465
- if (!evoluInstance)
427
+ else {
428
+ instance.usedSyncOwners.decrement(syncOwner);
429
+ return await run(deps.transports.removeClaim(syncOwner.owner.id, syncOwner.transports));
430
+ }
431
+ };
432
+ return ok({
433
+ addInstance: (message, onDisposed) => {
434
+ assertNotDisposed(disposables);
435
+ const instance = {
436
+ id: message.id,
437
+ port: deps.createMessagePort(message.evoluPort),
438
+ onDisposed,
439
+ rowsByQuery: new Map(),
440
+ useOwnerMutex: createMutex(),
441
+ usedSyncOwners: createRefCountByKey({
442
+ lookup: (syncOwner) => syncOwner.owner.id,
443
+ }),
444
+ [Symbol.asyncDispose]: () => disposer.disposeAsync(),
445
+ };
446
+ instancesById.set(instance.id, instance);
447
+ const disposer = new AsyncDisposableStack();
448
+ disposer.defer(instance.onDisposed);
449
+ disposer.use(instance.usedSyncOwners);
450
+ disposer.use(instance.useOwnerMutex);
451
+ disposer.defer(async () => {
452
+ await tenantRun(instance.useOwnerMutex.withLock(async (run) => {
453
+ for (const syncOwner of instance.usedSyncOwners.keys()) {
454
+ while (instance.usedSyncOwners.has(syncOwner)) {
455
+ await run(toggleSyncOwner(instance, syncOwner, "remove"));
456
+ }
457
+ }
458
+ return ok();
459
+ }));
460
+ });
461
+ disposer.defer(() => {
462
+ instancesById.delete(instance.id);
463
+ console.info("evoluDispose", { name, id: instance.id });
464
+ });
465
+ disposer.use(instance.port);
466
+ // The main-thread Evolu instance holds this per-instance leader lock
467
+ // while it is alive. Acquiring the same lock here means the main
468
+ // thread instance was disposed or its tab closed, so the tenant-side
469
+ // instance must dispose itself.
470
+ void tenantRun(acquireLeaderLock(message.id)).then((lock) => {
471
+ if (!lock.ok)
472
+ return;
473
+ disposer.use(lock.value);
474
+ return disposer.disposeAsync();
475
+ });
476
+ instance.port.onMessage = (message) => {
477
+ switch (message.type) {
478
+ case "Query":
479
+ case "Export": {
480
+ queue.push({ type: "ForEvolu", id: instance.id, message });
481
+ runQueue();
466
482
  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
483
  }
476
- break;
477
- }
478
- case "Dispose": {
479
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
480
- if (!evoluInstance)
484
+ case "Mutate": {
485
+ // TODO: Delegate do vsech evolu instances, co to pouzivaji
486
+ queue.push({ type: "ForEvolu", id: instance.id, message });
487
+ runQueue();
481
488
  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;
489
+ }
490
+ case "UseOwner": {
491
+ void tenantRun(instance.useOwnerMutex.withLock(async (run) => {
492
+ for (const { owner, action } of message.actions) {
493
+ console.debug("useOwner", {
494
+ id: instance.id,
495
+ action,
496
+ ownerId: owner.owner.id,
497
+ transportUrls: owner.transports.map(({ url }) => url),
498
+ });
499
+ await run(toggleSyncOwner(instance, owner, action));
500
+ }
501
+ return ok();
502
+ }));
503
+ break;
504
+ }
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