@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
@@ -11,10 +11,12 @@ import {
11
11
  shiftFromArray,
12
12
  type NonEmptyReadonlyArray,
13
13
  } from "../Array.js";
14
- import { assert, assertNotDisposed } from "../Assert.js";
14
+ import { assert, assertNotAborted, assertNotDisposed } from "../Assert.js";
15
+ import type { Brand } from "../Brand.js";
15
16
  import { createCallbacks } from "../Callbacks.js";
16
17
  import type { ConsoleEntry, ConsoleLevel } from "../Console.js";
17
- import { exhaustiveCheck } from "../Function.js";
18
+ import type { EncryptionKey } from "../Crypto.js";
19
+ import { acquireLeaderLock, type LockManagerDep } from "../LockManager.js";
18
20
  import { structuralLookup, type StructuralLookupKey } from "../Lookup.js";
19
21
  import { createRefCountByKey, type RefCountByKey } from "../RefCount.js";
20
22
  import {
@@ -24,25 +26,29 @@ import {
24
26
  type SharedResourceByKeyWithClaims,
25
27
  } from "../Resource.js";
26
28
  import { ok, type Result } from "../Result.js";
27
- import { spaced } from "../Schedule.js";
28
29
  import type { NonEmptyReadonlySet } from "../Set.js";
30
+ import type { SqliteSchema } from "../Sqlite.js";
31
+ import { createStore, type Store } from "../Store.js";
29
32
  import {
30
- repeat,
33
+ AbortError,
34
+ createMutex,
31
35
  unabortable,
32
- type AbortError,
33
- type Fiber,
36
+ type Mutex,
34
37
  type Task,
35
38
  } from "../Task.js";
36
- import { createId, type Id, type Name } from "../Type.js";
39
+ import { type Id, type Name, type Typed } from "../Type.js";
37
40
  import type { Callback, ExtractType } from "../Types.js";
38
41
  import type { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
39
42
  import type {
40
43
  SharedWorker as CommonSharedWorker,
44
+ CreateBroadcastChannelDep,
45
+ CreateMessageChannelDep,
41
46
  MessagePort,
42
47
  NativeMessagePort,
43
48
  SharedWorkerSelf,
44
49
  WorkerDeps,
45
50
  } from "../Worker.js";
51
+ import type { DbWorkerInit } from "./Db.js";
46
52
  import type { EvoluError } from "./Error.js";
47
53
  import type { Owner, OwnerId, OwnerTransport, SyncOwner } from "./Owner.js";
48
54
  import {
@@ -57,42 +63,52 @@ import {
57
63
  makePatches,
58
64
  type Patch,
59
65
  type Query,
66
+ type Row,
60
67
  type RowsByQueryMap,
61
68
  } from "./Query.js";
62
69
  import type { MutationChange } from "./Schema.js";
63
70
  import type { CrdtMessage } from "./Storage.js";
64
71
 
65
- export type SharedWorker = CommonSharedWorker<SharedWorkerInput>;
72
+ export type SharedWorker = CommonSharedWorker<
73
+ SharedWorkerInput,
74
+ SharedWorkerOutput
75
+ >;
66
76
 
67
77
  export interface SharedWorkerDep {
68
78
  readonly sharedWorker: SharedWorker;
69
79
  }
70
80
 
71
- export type SharedWorkerDeps = WorkerDeps & CreateWebSocketDep;
72
-
73
81
  export type SharedWorkerInput =
74
82
  | {
75
- readonly type: "InitTab";
83
+ readonly type: "AnnounceTabLeader";
76
84
  readonly consoleLevel: ConsoleLevel;
77
- readonly port: NativeMessagePort<EvoluTabOutput>;
78
85
  }
79
86
  | {
80
87
  readonly type: "CreateEvolu";
81
88
  readonly name: Name;
89
+ readonly id: EvoluInstanceId;
90
+ readonly consoleLevel: ConsoleLevel;
91
+ readonly sqliteSchema: SqliteSchema;
92
+ readonly encryptionKey: EncryptionKey;
93
+ readonly memoryOnly: boolean;
82
94
  readonly evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>;
83
- readonly dbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>;
84
95
  };
85
96
 
86
- export type EvoluTabOutput =
97
+ export type SharedWorkerOutput = DbWorkerInit;
98
+
99
+ export type ConsoleEntryOrError =
87
100
  | {
88
- readonly type: "OnConsoleEntry";
101
+ readonly type: "ConsoleEntry";
89
102
  readonly entry: ConsoleEntry;
90
103
  }
91
104
  | {
92
- readonly type: "OnError";
105
+ readonly type: "Error";
93
106
  readonly error: EvoluError;
94
107
  };
95
108
 
109
+ export const consoleEntryOrErrorBroadcastChannelName =
110
+ "evolu:console-entry-or-error";
111
+
96
112
  export type EvoluInput =
97
113
  | {
98
114
  readonly type: "Mutate";
@@ -113,9 +129,6 @@ export type EvoluInput =
113
129
  }
114
130
  | {
115
131
  readonly type: "Export";
116
- }
117
- | {
118
- readonly type: "Dispose";
119
132
  };
120
133
 
121
134
  export type EvoluOutput =
@@ -132,32 +145,158 @@ export type EvoluOutput =
132
145
  readonly file: Uint8Array<ArrayBuffer>;
133
146
  };
134
147
 
148
+ export type DbWorkerInput =
149
+ | (Typed<"Request"> & {
150
+ readonly callbackId: Id;
151
+ readonly request: DbWorkerRequest;
152
+ })
153
+ | Typed<"Dispose">;
154
+
155
+ export type DbWorkerRequest =
156
+ | {
157
+ readonly type: "ForEvolu";
158
+ readonly id: EvoluInstanceId;
159
+ readonly message: ExtractType<EvoluInput, "Mutate" | "Query" | "Export">;
160
+ }
161
+ | {
162
+ readonly type: "ForSharedWorker";
163
+ readonly message:
164
+ | {
165
+ readonly type: "CreateSyncMessages";
166
+ readonly owners: NonEmptyReadonlyArray<Owner>;
167
+ }
168
+ | {
169
+ readonly type: "ApplySyncMessage";
170
+ readonly owner: Owner;
171
+ readonly inputMessage: Uint8Array;
172
+ };
173
+ };
174
+
175
+ export type DbWorkerOutput =
176
+ | {
177
+ readonly type: "LeaderAcquired";
178
+ readonly name: Name;
179
+ }
180
+ | {
181
+ readonly type: "OnQueuedResponse";
182
+ readonly callbackId: Id;
183
+ readonly response: DbWorkerQueuedResponse;
184
+ };
185
+
186
+ export type DbWorkerQueuedResponse =
187
+ | {
188
+ readonly type: "ForEvolu";
189
+ readonly id: EvoluInstanceId;
190
+ readonly message:
191
+ | {
192
+ readonly type: "Mutate";
193
+ readonly messagesByOwnerId: ReadonlyMap<
194
+ OwnerId,
195
+ NonEmptyReadonlyArray<CrdtMessage>
196
+ >;
197
+ readonly rowsByQuery: RowsByQueryMap;
198
+ }
199
+ | {
200
+ readonly type: "Query";
201
+ readonly rowsByQuery: RowsByQueryMap;
202
+ }
203
+ | {
204
+ readonly type: "Export";
205
+ readonly file: Uint8Array<ArrayBuffer>;
206
+ };
207
+ }
208
+ | {
209
+ readonly type: "ForSharedWorker";
210
+ readonly message:
211
+ | {
212
+ readonly type: "CreateSyncMessages";
213
+ readonly protocolMessagesByOwnerId: ReadonlyMap<
214
+ OwnerId,
215
+ ProtocolMessage
216
+ >;
217
+ }
218
+ | {
219
+ readonly type: "ApplySyncMessage";
220
+ readonly ownerId: OwnerId;
221
+ readonly didWriteMessages: boolean;
222
+ readonly result: Result<
223
+ ApplyProtocolMessageAsClientResult,
224
+ ProtocolError | AbortError
225
+ >;
226
+ };
227
+ };
228
+
229
+ export type SharedWorkerDeps = WorkerDeps &
230
+ CreateBroadcastChannelDep &
231
+ CreateMessageChannelDep &
232
+ CreateWebSocketDep &
233
+ LockManagerDep;
234
+
235
+ interface EvoluTenant extends AsyncDisposable {
236
+ readonly addInstance: (
237
+ message: ExtractType<SharedWorkerInput, "CreateEvolu">,
238
+ onDisposed: () => void,
239
+ ) => void;
240
+
241
+ readonly requestCreateSyncMessages: (ownerIds: ReadonlySet<OwnerId>) => void;
242
+
243
+ readonly requestApplySyncMessage: (
244
+ ownerId: OwnerId,
245
+ inputMessage: Uint8Array,
246
+ ) => void;
247
+ }
248
+
249
+ type EvoluTenantDeps = SharedWorkerDeps &
250
+ PostConsoleEntryOrErrorDep &
251
+ TabLeaderPortStoreDep &
252
+ TransportsDep;
253
+
254
+ interface PostConsoleEntryOrErrorDep {
255
+ readonly postConsoleEntryOrError: Callback<ConsoleEntryOrError>;
256
+ }
257
+
258
+ interface TabLeaderPortStoreDep {
259
+ readonly tabLeaderPortStore: Store<TabLeaderPort | null>;
260
+ }
261
+
262
+ type TabLeaderPort = Pick<MessagePort<DbWorkerInit>, "postMessage">;
263
+
264
+ interface TransportsDep {
265
+ readonly transports: SharedResourceByKeyWithClaims<
266
+ OwnerTransport,
267
+ OwnerId,
268
+ WebSocket,
269
+ SharedWorkerDeps
270
+ >;
271
+ }
272
+
273
+ export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
274
+
275
+ export type SyncState = 123;
276
+
135
277
  export const initSharedWorker =
136
278
  (
137
- self: SharedWorkerSelf<SharedWorkerInput>,
279
+ self: SharedWorkerSelf<SharedWorkerInput, SharedWorkerOutput>,
138
280
  ): Task<AsyncDisposableStack, never, SharedWorkerDeps> =>
139
281
  async (run) => {
140
- const { createMessagePort, consoleStoreOutputEntry, createWebSocket } =
141
- run.deps;
142
- const console = run.deps.console.child("SharedWorker");
143
- await using stack = new AsyncDisposableStack();
144
- const sharedWorkerReady = Promise.withResolvers<void>();
282
+ const { deps } = run;
283
+ const console = deps.console.child("SharedWorker");
145
284
 
146
- // TODO: Use heartbeat to detect and prune dead ports.
147
- const tabPorts = new Set<MessagePort<EvoluTabOutput>>();
285
+ await using disposer = new AsyncDisposableStack();
148
286
 
149
- // Buffer console/error outputs until the first tab connects its port.
150
- const queuedTabOutputs: Array<EvoluTabOutput> = [];
151
- const postTabOutput = (output: EvoluTabOutput): void => {
152
- if (tabPorts.size === 0) queuedTabOutputs.push(output);
153
- else for (const port of tabPorts) port.postMessage(output);
154
- };
155
- stack.defer(
156
- consoleStoreOutputEntry.subscribe(() => {
157
- const entry = consoleStoreOutputEntry.get();
158
- if (entry) postTabOutput({ type: "OnConsoleEntry", entry });
159
- }),
287
+ const tabLeaderPortStore = disposer.use(
288
+ createStore<TabLeaderPort | null>(null),
289
+ );
290
+ const consoleEntryOrErrorBroadcastChannel = disposer.use(
291
+ deps.createBroadcastChannel<ConsoleEntryOrError>(
292
+ consoleEntryOrErrorBroadcastChannelName,
293
+ ),
160
294
  );
295
+ const postConsoleEntryOrError = (output: ConsoleEntryOrError): void => {
296
+ consoleEntryOrErrorBroadcastChannel.postMessage(output);
297
+ };
298
+
299
+ const sharedWorkerReady = Promise.withResolvers<void>();
161
300
 
162
301
  // Register ASAP so the worker does not miss connections.
163
302
  self.onConnect = (port) => {
@@ -165,35 +304,23 @@ export const initSharedWorker =
165
304
  // The underlying port buffers messages until onMessage is assigned.
166
305
  port.onMessage = (message) => {
167
306
  switch (message.type) {
168
- case "InitTab": {
169
- /**
170
- * One SharedWorker serves multiple tabs. The most recently
171
- * initialized tab's level wins.
172
- */
307
+ case "AnnounceTabLeader": {
173
308
  console.setLevel(message.consoleLevel);
174
-
175
- const tabPort = createMessagePort<EvoluTabOutput>(message.port);
176
- tabPorts.add(tabPort);
177
- if (queuedTabOutputs.length > 0) {
178
- queuedTabOutputs.forEach(postTabOutput);
179
- queuedTabOutputs.length = 0;
180
- }
309
+ tabLeaderPortStore.set(port);
310
+ console.info("tabLeaderAnnounced");
181
311
  break;
182
312
  }
183
313
 
184
314
  case "CreateEvolu": {
185
- void sharedWorkerRunWithSharedEvoluDeps(async (run) => {
186
- const sharedEvoluResult = await run(
187
- sharedEvolusByName.acquire(message.name),
315
+ void sharedWorkerRun(async (run) => {
316
+ const tenant = await run(
317
+ unabortable(tenantsByName.acquire(message)),
188
318
  );
189
- if (!sharedEvoluResult.ok) return sharedEvoluResult;
190
- sharedEvoluResult.value.createInstance(
191
- message.evoluPort,
192
- message.dbWorkerPort,
193
- () =>
194
- void sharedWorkerRunWithSharedEvoluDeps(
195
- sharedEvolusByName.release(message.name),
196
- ),
319
+ if (!tenant.ok) return tenant;
320
+
321
+ tenant.value.addInstance(
322
+ message,
323
+ () => void sharedWorkerRun(tenantsByName.release(message)),
197
324
  );
198
325
  return ok();
199
326
  });
@@ -206,10 +333,15 @@ export const initSharedWorker =
206
333
  });
207
334
  };
208
335
 
209
- const initSharedWorkerRun = run.create();
336
+ disposer.defer(
337
+ deps.consoleStoreOutputEntry.subscribe(() => {
338
+ const entry = deps.consoleStoreOutputEntry.get();
339
+ if (entry) postConsoleEntryOrError({ type: "ConsoleEntry", entry });
340
+ }),
341
+ );
210
342
 
211
- const transports = stack.use(
212
- await initSharedWorkerRun.orThrow(
343
+ const transports = disposer.use(
344
+ await run.orThrow(
213
345
  createSharedResourceByKeyWithClaims<
214
346
  WebSocket,
215
347
  OwnerTransport,
@@ -218,7 +350,7 @@ export const initSharedWorker =
218
350
  StructuralLookupKey
219
351
  >(
220
352
  (transport) =>
221
- createWebSocket(transport.url, {
353
+ deps.createWebSocket(transport.url, {
222
354
  binaryType: "arraybuffer",
223
355
 
224
356
  onOpen: () => {
@@ -228,39 +360,35 @@ export const initSharedWorker =
228
360
  ownerIds: [...ownerIds],
229
361
  });
230
362
 
231
- forEachSharedEvolu((sharedEvolu) => {
232
- sharedEvolu.requestCreateSyncMessages(ownerIds);
363
+ forEachTenant((tenant) => {
364
+ tenant.requestCreateSyncMessages(ownerIds);
233
365
  });
234
366
  },
235
367
 
236
368
  onMessage(data) {
237
369
  if (!(data instanceof ArrayBuffer)) return;
238
370
 
239
- const inputMessage = new globalThis.Uint8Array(data);
240
- const headerResult = parseProtocolHeader(
241
- new globalThis.Uint8Array(data),
242
- );
371
+ const message = new Uint8Array(data);
372
+ const headerResult = parseProtocolHeader(message);
243
373
 
244
374
  if (!headerResult.ok) {
245
375
  console.debug("transportInvalidProtocolMessage", {
246
376
  url: transport.url,
247
- byteLength: inputMessage.byteLength,
377
+ byteLength: message.byteLength,
248
378
  });
249
-
250
- // TODO: Propagate invalid protocol messages to sync state.
251
379
  return;
252
380
  }
253
381
 
254
382
  console.debug("transportProtocolMessage", {
255
383
  url: transport.url,
256
384
  ownerId: headerResult.value.ownerId,
257
- byteLength: inputMessage.byteLength,
385
+ byteLength: message.byteLength,
258
386
  });
259
387
 
260
- forEachSharedEvolu((sharedEvolu) => {
261
- sharedEvolu.requestApplySyncMessage(
388
+ forEachTenant((tenant) => {
389
+ tenant.requestApplySyncMessage(
262
390
  headerResult.value.ownerId,
263
- inputMessage,
391
+ message,
264
392
  );
265
393
  });
266
394
  },
@@ -268,8 +396,8 @@ export const initSharedWorker =
268
396
  {
269
397
  onFirstClaimAdded: (ownerId, webSocket) => {
270
398
  if (!webSocket.isOpen()) return;
271
- forEachSharedEvolu((sharedEvolu) => {
272
- sharedEvolu.requestCreateSyncMessages(new Set([ownerId]));
399
+ forEachTenant((tenant) => {
400
+ tenant.requestCreateSyncMessages(new Set([ownerId]));
273
401
  });
274
402
  },
275
403
 
@@ -284,258 +412,209 @@ export const initSharedWorker =
284
412
  ),
285
413
  );
286
414
 
287
- const sharedWorkerRunWithSharedEvoluDeps = initSharedWorkerRun.addDeps({
415
+ const sharedWorkerRun = run.create({
416
+ ...deps,
417
+ postConsoleEntryOrError,
418
+ tabLeaderPortStore,
288
419
  transports,
289
- postTabOutput,
290
420
  });
291
421
 
292
- const sharedEvolusByName = stack.use(
293
- await sharedWorkerRunWithSharedEvoluDeps.orThrow(
294
- createSharedResourceByKey(createSharedEvolu, {
295
- lookup: structuralLookup,
422
+ const tenantsByName = disposer.use(
423
+ await sharedWorkerRun.orThrow(
424
+ createSharedResourceByKey(createEvoluTenant, {
425
+ idleDisposeAfter: "3s",
426
+ lookup: (message) => message.name,
296
427
  }),
297
428
  ),
298
429
  );
299
430
 
300
- const forEachSharedEvolu = (
301
- callback: Callback<BorrowedResource<SharedEvolu>>,
431
+ sharedWorkerReady.resolve();
432
+
433
+ const forEachTenant = (
434
+ callback: Callback<BorrowedResource<EvoluTenant>>,
302
435
  ): void => {
303
- for (const sharedEvolu of sharedEvolusByName
304
- .snapshot()
305
- .resourcesByKey.values()) {
306
- callback(sharedEvolu);
436
+ for (const tenant of tenantsByName.snapshot().resourcesByKey.values()) {
437
+ callback(tenant);
307
438
  }
308
439
  };
309
440
 
310
- sharedWorkerReady.resolve();
311
- console.info("initSharedWorker");
312
-
313
- return ok(stack.move());
441
+ return ok(disposer.move());
314
442
  };
315
443
 
316
- interface SharedEvolu extends AsyncDisposable {
317
- readonly createInstance: (
318
- evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>,
319
- dbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>,
320
- releaseSharedEvolu: () => void,
321
- ) => void;
322
-
323
- readonly requestCreateSyncMessages: (ownerIds: ReadonlySet<OwnerId>) => void;
324
-
325
- readonly requestApplySyncMessage: (
326
- ownerId: OwnerId,
327
- inputMessage: Uint8Array,
328
- ) => void;
329
- }
444
+ const createEvoluTenant =
445
+ ({
446
+ name,
447
+ consoleLevel,
448
+ sqliteSchema,
449
+ encryptionKey,
450
+ memoryOnly,
451
+ }: ExtractType<SharedWorkerInput, "CreateEvolu">): Task<
452
+ EvoluTenant,
453
+ never,
454
+ EvoluTenantDeps
455
+ > =>
456
+ async (run) => {
457
+ await using disposer = new AsyncDisposableStack();
458
+ const tenantRun = disposer.use(run.create());
330
459
 
331
- type SharedEvoluDeps = SharedWorkerDeps & PostTabOutputDep & TransportsDep;
460
+ const { deps } = run;
461
+ const console = deps.console.child(name).child("SharedWorker");
332
462
 
333
- interface PostTabOutputDep {
334
- readonly postTabOutput: Callback<EvoluTabOutput>;
335
- }
463
+ interface EvoluInstance extends AsyncDisposable {
464
+ readonly id: EvoluInstanceId;
465
+ readonly onDisposed: () => void;
466
+ readonly port: MessagePort<EvoluOutput, EvoluInput>;
467
+ readonly useOwnerMutex: Mutex;
468
+ readonly usedSyncOwners: RefCountByKey<SyncOwner>;
469
+ rowsByQuery: Map<Query, ReadonlyArray<Row>>;
470
+ }
336
471
 
337
- interface TransportsDep {
338
- readonly transports: SharedResourceByKeyWithClaims<
339
- OwnerTransport,
340
- OwnerId,
341
- WebSocket,
342
- SharedWorkerDeps
343
- >;
344
- }
472
+ const instancesById = disposer.adopt(
473
+ new Map<EvoluInstanceId, EvoluInstance>(),
474
+ async (instancesById) => {
475
+ await using disposer = new AsyncDisposableStack();
476
+ for (const instance of [...instancesById.values()]) {
477
+ disposer.use(instance);
478
+ }
479
+ },
480
+ );
345
481
 
346
- export interface DbWorkerInput {
347
- readonly callbackId: Id;
348
- readonly request:
349
- | {
350
- readonly type: "ForEvolu";
351
- readonly evoluPortId: Id;
352
- readonly message: ExtractType<
353
- EvoluInput,
354
- "Mutate" | "Query" | "Export"
355
- >;
356
- }
357
- | {
358
- readonly type: "ForSharedWorker";
359
- readonly message:
360
- | {
361
- readonly type: "CreateSyncMessages";
362
- readonly owners: NonEmptyReadonlyArray<Owner>;
363
- }
364
- | {
365
- readonly type: "ApplySyncMessage";
366
- readonly owner: Owner;
367
- readonly inputMessage: Uint8Array;
368
- };
482
+ let dbWorkerPort = null as MessagePort<
483
+ DbWorkerInput,
484
+ DbWorkerOutput
485
+ > | null;
486
+ const dbWorkerInited = Promise.withResolvers<void>();
487
+
488
+ const initDbWorker = (): void => {
489
+ const tabLeaderPort = deps.tabLeaderPortStore.get();
490
+ assert(tabLeaderPort, "Expected tab leader port.");
491
+
492
+ const dbWorkerChannel = deps.createMessageChannel<
493
+ DbWorkerOutput,
494
+ DbWorkerInput
495
+ >();
496
+ const currentDbWorkerPort = dbWorkerChannel.port2;
497
+
498
+ currentDbWorkerPort.onMessage = (message) => {
499
+ switch (message.type) {
500
+ case "LeaderAcquired": {
501
+ assert(
502
+ dbWorkerPort !== currentDbWorkerPort,
503
+ "Expected a new DbWorker port.",
504
+ );
505
+ dbWorkerPort?.[Symbol.dispose]();
506
+ dbWorkerPort = currentDbWorkerPort;
507
+ queueRequestInFlight = false;
508
+ console.info("leaderAcquired");
509
+ dbWorkerInited.resolve();
510
+ runQueue();
511
+ break;
512
+ }
513
+ case "OnQueuedResponse": {
514
+ callbacks.execute(message.callbackId, message);
515
+ break;
516
+ }
517
+ }
369
518
  };
370
- }
371
519
 
372
- export type DbWorkerOutput =
373
- | EvoluTabOutput
374
- | {
375
- readonly type: "LeaderAcquired";
376
- readonly name: Name;
377
- }
378
- | {
379
- readonly type: "OnQueuedResponse";
380
- readonly callbackId: Id;
381
- readonly response:
382
- | {
383
- readonly type: "ForEvolu";
384
- readonly evoluPortId: Id;
385
- readonly message:
386
- | {
387
- readonly type: "Mutate";
388
- readonly messagesByOwnerId: ReadonlyMap<
389
- OwnerId,
390
- NonEmptyReadonlyArray<CrdtMessage>
391
- >;
392
- readonly rowsByQuery: RowsByQueryMap;
393
- }
394
- | {
395
- readonly type: "Query";
396
- readonly rowsByQuery: RowsByQueryMap;
397
- }
398
- | {
399
- readonly type: "Export";
400
- readonly file: Uint8Array<ArrayBuffer>;
401
- };
402
- }
403
- | {
404
- readonly type: "ForSharedWorker";
405
- readonly message:
406
- | {
407
- readonly type: "CreateSyncMessages";
408
- readonly protocolMessagesByOwnerId: ReadonlyMap<
409
- OwnerId,
410
- ProtocolMessage
411
- >;
412
- }
413
- | {
414
- readonly type: "ApplySyncMessage";
415
- readonly ownerId: OwnerId;
416
- readonly didWriteMessages: boolean;
417
- readonly result: Result<
418
- ApplyProtocolMessageAsClientResult,
419
- ProtocolError | AbortError
420
- >;
421
- };
422
- };
520
+ tabLeaderPort.postMessage(
521
+ {
522
+ type: "DbWorkerInit",
523
+ name,
524
+ consoleLevel,
525
+ sqliteSchema,
526
+ encryptionKey,
527
+ memoryOnly,
528
+ port: dbWorkerChannel.port1.native,
529
+ },
530
+ [dbWorkerChannel.port1.native],
531
+ );
423
532
  };
424
533
 
425
- export type SyncState = 123;
426
-
427
- const createSharedEvolu =
428
- (name: Name): Task<SharedEvolu, never, SharedEvoluDeps> =>
429
- (run) => {
430
- const stack = new AsyncDisposableStack();
431
- const sharedEvoluRun = run.create();
432
-
433
- const console = run.deps.console.child(name).child("SharedWorker");
434
- const { createMessagePort, postTabOutput, transports } = run.deps;
435
-
436
- interface EvoluInstanceState {
437
- readonly evoluPort: MessagePort<EvoluOutput, EvoluInput>;
438
- readonly dbWorkerPort: MessagePort<DbWorkerInput, DbWorkerOutput>;
439
- readonly releaseSharedEvolu: () => void;
440
- rowsByQuery: RowsByQueryMap;
441
- readonly usedSyncOwners: RefCountByKey<SyncOwner>;
442
- }
443
-
444
- const evoluInstancesByPortId = new Map<Id, EvoluInstanceState>();
445
- const queue: Array<DbWorkerInput["request"]> = [];
446
- const callbacks = stack.use(
534
+ const queue: Array<DbWorkerRequest> = [];
535
+ const callbacks = disposer.use(
447
536
  createCallbacks<ExtractType<DbWorkerOutput, "OnQueuedResponse">>(
448
537
  run.deps,
449
538
  ),
450
539
  );
540
+ let queueRequestInFlight = false;
451
541
 
452
- let leaderDbWorkerPort = null as MessagePort<
453
- DbWorkerInput,
454
- DbWorkerOutput
455
- > | null;
456
- let queueProcessingFiber: Fiber<void, never, WorkerDeps> | null = null;
457
-
458
- stack.use(sharedEvoluRun);
459
- const moved = stack.move();
460
-
461
- const ensureQueueProcessing = (): void => {
462
- if (
463
- queueProcessingFiber ||
464
- !isNonEmptyArray(queue) ||
465
- !leaderDbWorkerPort
466
- ) {
542
+ const runQueue = (): void => {
543
+ if (queueRequestInFlight || !isNonEmptyArray(queue) || !dbWorkerPort) {
467
544
  return;
468
545
  }
469
546
 
470
- const first = firstInArray(queue);
547
+ const request = firstInArray(queue);
471
548
 
472
549
  const callbackId = callbacks.register(({ response }) => {
473
- queueProcessingFiber?.abort();
474
- queueProcessingFiber = null;
475
-
476
550
  switch (response.type) {
477
551
  case "ForEvolu": {
478
- handleResponseForEvolu(response, first);
552
+ handleResponseForEvolu(response, request);
479
553
  break;
480
554
  }
481
555
 
482
556
  case "ForSharedWorker":
483
557
  handleResponseForSharedWorker(response);
484
558
  break;
485
-
486
- default:
487
- exhaustiveCheck(response);
488
559
  }
489
560
 
490
561
  // Complete the current queue item and continue with the next one.
491
562
  shiftFromArray(queue);
492
- ensureQueueProcessing();
563
+ queueRequestInFlight = false;
564
+ runQueue();
493
565
  });
494
566
 
495
- /**
496
- * The leader DbWorker can disappear at any time when its tab closes, so
497
- * keep resending the current request until some leader answers. The
498
- * response handler aborts this retry loop and advances the queue.
499
- */
500
- queueProcessingFiber = sharedEvoluRun.daemon(
501
- repeat(() => {
502
- assert(leaderDbWorkerPort, "Expected a leader DbWorker");
503
- leaderDbWorkerPort.postMessage({ callbackId, request: first });
504
- return ok();
505
- }, spaced("5s")),
506
- );
567
+ queueRequestInFlight = true;
568
+ dbWorkerPort.postMessage({ type: "Request", callbackId, request });
507
569
  };
508
570
 
571
+ disposer.defer(async () => {
572
+ dbWorkerPort?.postMessage({ type: "Dispose" });
573
+ dbWorkerPort = null;
574
+ queueRequestInFlight = false;
575
+
576
+ // The DbWorker holds this tenant leader lock while it is alive. Tenant
577
+ // disposal sends Dispose, then acquires the same lock to wait until the
578
+ // DbWorker releases it: either because Dispose was delivered or because
579
+ // the hosting tab closed. The wait is unabortable because tenant disposal
580
+ // must finish even after tenantRun receives an abort request.
581
+ const lock = await tenantRun(acquireLeaderLock(name));
582
+ // AbortError here means tenantRun was already disposed before this
583
+ // cleanup could start, violating the disposal ordering above.
584
+ assertNotAborted(lock);
585
+ await using _ = lock.value;
586
+ });
587
+
588
+ disposer.defer(deps.tabLeaderPortStore.subscribe(initDbWorker));
589
+
590
+ initDbWorker();
591
+ await dbWorkerInited.promise;
592
+ const disposables = disposer.move();
593
+
509
594
  const handleResponseForEvolu = (
510
- response: ExtractType<
511
- ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
512
- "ForEvolu"
513
- >,
514
- first: DbWorkerInput["request"],
595
+ response: ExtractType<DbWorkerQueuedResponse, "ForEvolu">,
596
+ first: DbWorkerRequest,
515
597
  ): void => {
516
- const instance = evoluInstancesByPortId.get(response.evoluPortId);
517
- const port = instance?.evoluPort;
518
-
519
- if (!port) return;
598
+ const instance = instancesById.get(response.id);
599
+ if (!instance) return;
520
600
 
521
601
  switch (response.message.type) {
522
602
  case "Mutate":
523
603
  case "Query": {
524
- const previousRowsByQuery = instance.rowsByQuery;
525
- const nextRowsByQuery = new Map(previousRowsByQuery);
604
+ const nextRowsByQuery = new Map(instance.rowsByQuery);
526
605
  const patchesByQuery = new Map<Query, ReadonlyArray<Patch>>();
527
606
 
528
607
  for (const [query, rows] of response.message.rowsByQuery) {
529
608
  nextRowsByQuery.set(query, rows);
530
609
  patchesByQuery.set(
531
610
  query,
532
- makePatches(previousRowsByQuery.get(query), rows),
611
+ makePatches(instance.rowsByQuery.get(query), rows),
533
612
  );
534
613
  }
535
614
 
536
615
  instance.rowsByQuery = nextRowsByQuery;
537
616
 
538
- instance.evoluPort.postMessage({
617
+ instance.port.postMessage({
539
618
  type: "OnPatchesByQuery",
540
619
  patchesByQuery,
541
620
  onCompleteIds:
@@ -545,66 +624,52 @@ const createSharedEvolu =
545
624
  });
546
625
 
547
626
  if (response.message.type === "Mutate") {
548
- const mutateResponse = response.message;
549
-
550
- for (const [
551
- otherEvoluPortId,
552
- otherEvoluInstance,
553
- ] of evoluInstancesByPortId) {
554
- if (otherEvoluPortId === response.evoluPortId) continue;
555
- otherEvoluInstance.evoluPort.postMessage({
627
+ for (const [instanceId, instance] of instancesById) {
628
+ if (instanceId === response.id) continue;
629
+ instance.port.postMessage({
556
630
  type: "RefreshQueries",
557
631
  });
558
632
  }
559
633
 
560
- sharedEvoluRun<void, never>((run) => {
561
- const protocolMessagesByOwnerId = new Map<
562
- OwnerId,
563
- ProtocolMessage
564
- >();
565
-
566
- for (const syncOwner of instance.usedSyncOwners.keys()) {
567
- const { owner } = syncOwner;
568
- const messages = mutateResponse.messagesByOwnerId.get(owner.id);
569
-
570
- // Skip owners this instance does not currently sync for
571
- // writing. Read-only owners cannot produce protocol
572
- // messages because they do not have a write key.
573
- if (!messages || !("writeKey" in owner)) continue;
574
-
575
- protocolMessagesByOwnerId.set(
576
- owner.id,
577
- createProtocolMessageFromCrdtMessages(run.deps)(
578
- owner,
579
- messages,
580
- ),
581
- );
582
- }
583
-
584
- sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
634
+ const protocolMessagesByOwnerId = new Map<
635
+ OwnerId,
636
+ ProtocolMessage
637
+ >();
638
+
639
+ for (const syncOwner of instance.usedSyncOwners.keys()) {
640
+ const { owner } = syncOwner;
641
+ const messages = response.message.messagesByOwnerId.get(owner.id);
642
+
643
+ // Skip owners this instance does not currently sync for
644
+ // writing. Read-only owners cannot produce protocol
645
+ // messages because they do not have a write key.
646
+ if (!messages || !("writeKey" in owner)) continue;
647
+
648
+ protocolMessagesByOwnerId.set(
649
+ owner.id,
650
+ createProtocolMessageFromCrdtMessages(run.deps)(
651
+ owner,
652
+ messages,
653
+ ),
654
+ );
655
+ }
585
656
 
586
- return ok();
587
- });
657
+ sendProtocolMessagesByOwnerId(protocolMessagesByOwnerId);
588
658
  }
589
659
  break;
590
660
  }
591
661
 
592
662
  case "Export":
593
- port.postMessage({ type: "OnExport", file: response.message.file }, [
594
- response.message.file.buffer,
595
- ]);
663
+ instance.port.postMessage(
664
+ { type: "OnExport", file: response.message.file },
665
+ [response.message.file.buffer],
666
+ );
596
667
  break;
597
-
598
- default:
599
- exhaustiveCheck(response.message);
600
668
  }
601
669
  };
602
670
 
603
671
  const handleResponseForSharedWorker = (
604
- response: ExtractType<
605
- ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
606
- "ForSharedWorker"
607
- >,
672
+ response: ExtractType<DbWorkerQueuedResponse, "ForSharedWorker">,
608
673
  ): void => {
609
674
  switch (response.message.type) {
610
675
  case "CreateSyncMessages":
@@ -615,13 +680,15 @@ const createSharedEvolu =
615
680
 
616
681
  case "ApplySyncMessage":
617
682
  if (response.message.didWriteMessages) {
618
- refreshQueries();
683
+ for (const instance of instancesById.values()) {
684
+ instance.port.postMessage({ type: "RefreshQueries" });
685
+ }
619
686
  }
620
687
 
621
688
  if (!response.message.result.ok) {
622
689
  if (response.message.result.error.type !== "AbortError") {
623
- postTabOutput({
624
- type: "OnError",
690
+ deps.postConsoleEntryOrError({
691
+ type: "Error",
625
692
  error: response.message.result.error,
626
693
  });
627
694
  }
@@ -641,15 +708,9 @@ const createSharedEvolu =
641
708
  case "Broadcast":
642
709
  case "NoResponse":
643
710
  break;
644
-
645
- default:
646
- exhaustiveCheck(response.message.result.value);
647
711
  }
648
712
  }
649
713
  break;
650
-
651
- default:
652
- exhaustiveCheck(response.message);
653
714
  }
654
715
  };
655
716
 
@@ -657,8 +718,10 @@ const createSharedEvolu =
657
718
  protocolMessagesByOwnerId: ReadonlyMap<OwnerId, ProtocolMessage>,
658
719
  ): void => {
659
720
  for (const [ownerId, protocolMessage] of protocolMessagesByOwnerId) {
660
- for (const transport of transports.getResourceKeysForClaim(ownerId)) {
661
- const webSocket = transports.getResource(transport);
721
+ for (const transport of deps.transports.getResourceKeysForClaim(
722
+ ownerId,
723
+ )) {
724
+ const webSocket = deps.transports.getResource(transport);
662
725
  if (!webSocket?.isOpen()) continue;
663
726
 
664
727
  console.debug("sendProtocolMessage", {
@@ -672,192 +735,131 @@ const createSharedEvolu =
672
735
  }
673
736
  };
674
737
 
675
- const refreshQueries = (): void => {
676
- for (const evoluInstance of evoluInstancesByPortId.values()) {
677
- evoluInstance.evoluPort.postMessage({ type: "RefreshQueries" });
678
- }
679
- };
680
-
681
738
  const getUsedOwnersById = (
682
739
  ownerIds: ReadonlySet<OwnerId>,
683
740
  ): ReadonlyMap<OwnerId, Owner> => {
684
741
  const ownersById = new Map<OwnerId, Owner>();
685
-
686
- for (const evoluInstance of evoluInstancesByPortId.values()) {
687
- for (const { owner } of evoluInstance.usedSyncOwners.keys()) {
742
+ for (const instance of instancesById.values()) {
743
+ for (const { owner } of instance.usedSyncOwners.keys()) {
688
744
  if (!ownerIds.has(owner.id) || !("writeKey" in owner)) continue;
689
-
690
745
  ownersById.set(owner.id, owner);
691
746
  }
692
747
  }
693
-
694
748
  return ownersById;
695
749
  };
696
750
 
697
- const removeAllUsedSyncOwners = (
698
- evoluInstance: EvoluInstanceState,
699
- ): Task<void, never, SharedEvoluDeps> =>
700
- unabortable(async (run) => {
701
- for (const syncOwner of evoluInstance.usedSyncOwners.keys()) {
702
- while (evoluInstance.usedSyncOwners.has(syncOwner)) {
703
- await run(toggleUsedSyncOwner(evoluInstance, syncOwner, "remove"));
704
- }
705
- }
706
- return ok();
707
- });
708
-
709
- const toggleUsedSyncOwner = (
710
- evoluInstance: EvoluInstanceState,
711
- syncOwner: SyncOwner,
712
- action: "add" | "remove",
713
- ): Task<void, never, SharedEvoluDeps> =>
714
- unabortable(async (run) => {
751
+ const toggleSyncOwner =
752
+ (
753
+ instance: EvoluInstance,
754
+ syncOwner: SyncOwner,
755
+ action: "add" | "remove",
756
+ ): Task<void, never, EvoluTenantDeps> =>
757
+ async (run) => {
715
758
  if (action === "add") {
716
- evoluInstance.usedSyncOwners.increment(syncOwner);
717
- await run(
718
- transports.addClaim(syncOwner.owner.id, syncOwner.transports),
759
+ instance.usedSyncOwners.increment(syncOwner);
760
+ return await run(
761
+ deps.transports.addClaim(syncOwner.owner.id, syncOwner.transports),
719
762
  );
720
763
  } else {
721
- evoluInstance.usedSyncOwners.decrement(syncOwner);
722
- await run(
723
- transports.removeClaim(syncOwner.owner.id, syncOwner.transports),
764
+ instance.usedSyncOwners.decrement(syncOwner);
765
+ return await run(
766
+ deps.transports.removeClaim(
767
+ syncOwner.owner.id,
768
+ syncOwner.transports,
769
+ ),
724
770
  );
725
771
  }
726
- return ok();
727
- });
772
+ };
728
773
 
729
774
  return ok({
730
- createInstance: (
731
- nativeEvoluPort: NativeMessagePort<EvoluOutput, EvoluInput>,
732
- nativeDbWorkerPort: NativeMessagePort<DbWorkerInput, DbWorkerOutput>,
733
- releaseSharedEvolu: () => void,
734
- ): void => {
735
- assertNotDisposed(moved);
736
-
737
- const evoluPort = createMessagePort<EvoluOutput, EvoluInput>(
738
- nativeEvoluPort,
739
- );
740
- const dbWorkerPort = createMessagePort<DbWorkerInput, DbWorkerOutput>(
741
- nativeDbWorkerPort,
742
- );
743
-
744
- const evoluPortId = createId(run.deps);
745
-
746
- const evoluInstance: EvoluInstanceState = {
747
- evoluPort,
748
- dbWorkerPort,
749
- releaseSharedEvolu,
750
- rowsByQuery: new Map(),
775
+ addInstance: (message, onDisposed) => {
776
+ assertNotDisposed(disposables);
777
+
778
+ const instance: EvoluInstance = {
779
+ id: message.id,
780
+ port: deps.createMessagePort<EvoluOutput, EvoluInput>(
781
+ message.evoluPort,
782
+ ),
783
+ onDisposed,
784
+ rowsByQuery: new Map<Query, ReadonlyArray<Row>>(),
785
+ useOwnerMutex: createMutex(),
751
786
  usedSyncOwners: createRefCountByKey<SyncOwner, OwnerId>({
752
- lookup: ({ owner: { id } }) => id,
787
+ lookup: (syncOwner) => syncOwner.owner.id,
753
788
  }),
789
+ [Symbol.asyncDispose]: () => disposer.disposeAsync(),
754
790
  };
755
791
 
756
- evoluInstancesByPortId.set(evoluPortId, evoluInstance);
792
+ instancesById.set(instance.id, instance);
757
793
 
758
- dbWorkerPort.onMessage = (message) => {
759
- switch (message.type) {
760
- case "LeaderAcquired": {
761
- leaderDbWorkerPort = evoluInstance.dbWorkerPort;
762
- console.info("leaderAcquired");
763
- ensureQueueProcessing();
764
- break;
765
- }
794
+ const disposer = new AsyncDisposableStack();
766
795
 
767
- case "OnQueuedResponse": {
768
- callbacks.execute(message.callbackId, message);
769
- break;
770
- }
796
+ disposer.defer(instance.onDisposed);
797
+ disposer.use(instance.usedSyncOwners);
798
+ disposer.use(instance.useOwnerMutex);
771
799
 
772
- case "OnConsoleEntry":
773
- case "OnError": {
774
- postTabOutput(message);
775
- break;
776
- }
800
+ disposer.defer(async () => {
801
+ await tenantRun(
802
+ instance.useOwnerMutex.withLock(async (run) => {
803
+ for (const syncOwner of instance.usedSyncOwners.keys()) {
804
+ while (instance.usedSyncOwners.has(syncOwner)) {
805
+ await run(toggleSyncOwner(instance, syncOwner, "remove"));
806
+ }
807
+ }
808
+ return ok();
809
+ }),
810
+ );
811
+ });
777
812
 
778
- default:
779
- exhaustiveCheck(message);
780
- }
781
- };
813
+ disposer.defer(() => {
814
+ instancesById.delete(instance.id);
815
+ console.info("evoluDispose", { name, id: instance.id });
816
+ });
817
+ disposer.use(instance.port);
818
+
819
+ // The main-thread Evolu instance holds this per-instance leader lock
820
+ // while it is alive. Acquiring the same lock here means the main
821
+ // thread instance was disposed or its tab closed, so the tenant-side
822
+ // instance must dispose itself.
823
+ void tenantRun(acquireLeaderLock(message.id)).then((lock) => {
824
+ if (!lock.ok) return;
825
+
826
+ disposer.use(lock.value);
827
+ return disposer.disposeAsync();
828
+ });
782
829
 
783
- evoluPort.onMessage = (evoluMessage) => {
784
- switch (evoluMessage.type) {
830
+ instance.port.onMessage = (message) => {
831
+ switch (message.type) {
785
832
  case "Query":
786
833
  case "Export": {
787
- queue.push({
788
- type: "ForEvolu",
789
- evoluPortId,
790
- message: evoluMessage,
791
- });
792
- ensureQueueProcessing();
834
+ queue.push({ type: "ForEvolu", id: instance.id, message });
835
+ runQueue();
793
836
  break;
794
837
  }
795
-
796
838
  case "Mutate": {
797
839
  // TODO: Delegate do vsech evolu instances, co to pouzivaji
798
- queue.push({
799
- type: "ForEvolu",
800
- evoluPortId,
801
- message: evoluMessage,
802
- });
803
- ensureQueueProcessing();
840
+ queue.push({ type: "ForEvolu", id: instance.id, message });
841
+ runQueue();
804
842
  break;
805
843
  }
806
-
807
844
  case "UseOwner": {
808
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
809
- if (!evoluInstance) break;
810
-
811
- for (const { owner, action } of evoluMessage.actions) {
812
- console.debug("useOwner", {
813
- evoluPortId,
814
- action,
815
- ownerId: owner.owner.id,
816
- transportUrls: owner.transports.map(({ url }) => url),
817
- });
818
-
819
- void sharedEvoluRun(
820
- toggleUsedSyncOwner(evoluInstance, owner, action),
821
- );
822
- }
845
+ void tenantRun(
846
+ instance.useOwnerMutex.withLock(async (run) => {
847
+ for (const { owner, action } of message.actions) {
848
+ console.debug("useOwner", {
849
+ id: instance.id,
850
+ action,
851
+ ownerId: owner.owner.id,
852
+ transportUrls: owner.transports.map(({ url }) => url),
853
+ });
854
+
855
+ await run(toggleSyncOwner(instance, owner, action));
856
+ }
857
+
858
+ return ok();
859
+ }),
860
+ );
823
861
  break;
824
862
  }
825
-
826
- case "Dispose": {
827
- const evoluInstance = evoluInstancesByPortId.get(evoluPortId);
828
- if (!evoluInstance) break;
829
-
830
- evoluInstancesByPortId.delete(evoluPortId);
831
- const hadLastPort = evoluInstancesByPortId.size === 0;
832
- console.info("evoluDispose", {
833
- name,
834
- evoluPortId,
835
- hadLastPort,
836
- });
837
-
838
- // Potential plan: keep DbWorker ports in a SharedResource
839
- // abstraction instead of deleting them eagerly here. DbWorkers use
840
- // leader election because SQLite WASM needs a single active owner.
841
- // When the last Evolu instance is disposed, broadcast shutdown to
842
- // all DbWorkers so the current leader can dispose itself and the
843
- // followers can clean up consistently.
844
- sharedEvoluRun<void, never>(async (run) => {
845
- await run(removeAllUsedSyncOwners(evoluInstance));
846
- evoluInstance.usedSyncOwners[Symbol.dispose]();
847
- return ok();
848
- });
849
-
850
- // if (hadLastPort) evoluInstance.releaseSharedEvolu();
851
-
852
- // TODO: Dispose SharedEvolu on the last port.
853
-
854
- // TODO: Decided what to do with DbWorker but probably dispose it, but
855
- // https://bugs.webkit.org/show_bug.cgi?id=301520
856
- break;
857
- }
858
-
859
- default:
860
- exhaustiveCheck(evoluMessage);
861
863
  }
862
864
  };
863
865
  },
@@ -879,12 +881,11 @@ const createSharedEvolu =
879
881
  },
880
882
  });
881
883
 
882
- ensureQueueProcessing();
884
+ runQueue();
883
885
  },
884
886
 
885
887
  requestApplySyncMessage: (ownerId, inputMessage): void => {
886
888
  const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
887
-
888
889
  if (!owner) return;
889
890
 
890
891
  console.debug("requestApplySyncMessage", {
@@ -901,10 +902,10 @@ const createSharedEvolu =
901
902
  },
902
903
  });
903
904
 
904
- ensureQueueProcessing();
905
+ runQueue();
905
906
  },
906
907
 
907
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
908
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
908
909
  });
909
910
  };
910
911
 
@@ -930,3 +931,11 @@ const createSharedEvolu =
930
931
  // readonly onCompleteId: CallbackId;
931
932
  // readonly reload: boolean;
932
933
  // })
934
+
935
+ // TODO: Remaining SharedWorker lifetime issues.
936
+ // - Investigate heartbeat for ports and where liveness tracking is needed.
937
+ // - Sync state for monitoring.
938
+ // - Propagate invalid protocol messages to sync state.
939
+ // - Make DbWorker logically stateless by making all requests idempotent.
940
+ // - Investigate WebKit bug 301520 relevance to DbWorker disposal. (again)
941
+ // https://bugs.webkit.org/show_bug.cgi?id=301520