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