@evolu/common 8.0.0-next.4 → 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.
package/src/Test.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  type Random,
18
18
  type RandomLibDep,
19
19
  } from "./Random.js";
20
- import { createRun, type Run, type RunDeps } from "./Task.js";
20
+ import { createRun, type Run, type RunDefaultDeps } from "./Task.js";
21
21
  import {
22
22
  minMillis,
23
23
  setTimeout,
@@ -35,14 +35,14 @@ export type TestCreateId = <B extends string = never>() => [B] extends [never]
35
35
  /**
36
36
  * Cheap deterministic baseline deps for tests.
37
37
  *
38
- * `TestDeps` includes test-friendly replacements for {@link RunDeps}, such as
39
- * {@link TestConsole} and {@link TestTime}, plus extra helpers commonly useful in
40
- * tests and fixtures, such as `randomLib` (only that for now).
38
+ * `TestDeps` includes test-friendly replacements for {@link RunDefaultDeps},
39
+ * such as {@link TestConsole} and {@link TestTime}, plus extra helpers commonly
40
+ * useful in tests and fixtures, such as `randomLib` (only that for now).
41
41
  *
42
42
  * Use it directly for synchronous test setup, fixtures, and helpers. It is also
43
43
  * intentionally the base deps used by {@link testCreateRun}.
44
44
  */
45
- export type TestDeps = Omit<RunDeps, "console" | "time"> &
45
+ export type TestDeps = Omit<RunDefaultDeps, "console" | "time"> &
46
46
  TestConsoleDep &
47
47
  TestTimeDep &
48
48
  RandomLibDep;
@@ -103,7 +103,7 @@ export const testCreateDeps = (options?: {
103
103
  * ### Example
104
104
  *
105
105
  * ```ts
106
- * // Built-in TestTime
106
+ * // Default TestTime
107
107
  * await using run = testCreateRun();
108
108
  * const fiber = run(sleep("1s"));
109
109
  * run.deps.time.advance("1s");
@@ -112,9 +112,7 @@ export const testCreateDeps = (options?: {
112
112
  * // For a single test, create the dependency using the Run.
113
113
  * await using run = testCreateRun();
114
114
  * await using foo = await run.orThrow(createFoo());
115
- * const runWithFoo = run.addDeps({ foo });
116
- *
117
- * expect(runWithFoo.deps.foo).toBe(foo);
115
+ * await run.orThrow(taskThatNeedsFoo, { foo });
118
116
  *
119
117
  * // If multiple tests need the same setup, create a disposable helper.
120
118
  * // Then `await using setup` disposes everything the helper owns.
@@ -122,14 +120,15 @@ export const testCreateDeps = (options?: {
122
120
  * await using disposer = new AsyncDisposableStack();
123
121
  * const run = disposer.use(testCreateRun());
124
122
  * const foo = disposer.use(await run.orThrow(createFoo()));
123
+ * const runWithFoo = disposer.use(run.create({ ...run.deps, foo }));
125
124
  * const disposables = disposer.move();
126
125
  *
127
126
  * // Return whatever the tests need: a Run with the dependency,
128
127
  * // the dependency itself, or both.
129
128
  * return {
130
- * run: run.addDeps({ foo }),
129
+ * run: runWithFoo,
131
130
  * foo,
132
- * [Symbol.asyncDispose]: () => moved.disposeAsync(),
131
+ * [Symbol.asyncDispose]: () => disposables.disposeAsync(),
133
132
  * };
134
133
  * };
135
134
  *
package/src/Types.ts CHANGED
@@ -359,12 +359,3 @@ export type ExtractType<
359
359
  TUnion extends { readonly type: TypeName },
360
360
  TType extends TUnion["type"],
361
361
  > = Extract<TUnion, { readonly type: TType }>;
362
-
363
- /**
364
- * Constrains `T` to only contain keys not present in `Existing`.
365
- *
366
- * Use as a generic constraint to prevent overriding existing properties.
367
- */
368
- export type NewKeys<T, Existing> = {
369
- [K in keyof T]: K extends keyof Existing ? never : T[K];
370
- };
@@ -140,12 +140,11 @@ export const startDbWorker =
140
140
  async (run) => {
141
141
  await using disposer = new AsyncDisposableStack();
142
142
  disposer.use(self);
143
- const dbWorkerRun = disposer.use(run.create());
144
- const { deps } = dbWorkerRun;
143
+ const { deps } = run;
145
144
 
146
145
  let initialized = false;
147
146
 
148
- const initMessage = await dbWorkerRun.orThrow(
147
+ const initMessage = await run.orThrow(
149
148
  callback<DbWorkerInit>(({ ok }) => {
150
149
  self.onMessage = (message) => {
151
150
  assert(!initialized, "DbWorker must be initialized only once");
@@ -179,14 +178,12 @@ export const startDbWorker =
179
178
  );
180
179
  console.debug("createDbWorker");
181
180
 
182
- disposer.use(
183
- await dbWorkerRun.orThrow(acquireLeaderLock(initMessage.name)),
184
- );
181
+ disposer.use(await run.orThrow(acquireLeaderLock(initMessage.name)));
185
182
 
186
183
  port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
187
184
 
188
185
  const sqlite = disposer.use(
189
- await dbWorkerRun.orThrow(
186
+ await run.orThrow(
190
187
  createSqlite(
191
188
  initMessage.name,
192
189
  initMessage.memoryOnly
@@ -199,16 +196,17 @@ export const startDbWorker =
199
196
 
200
197
  const baseSqliteStorage = createBaseSqliteStorage({
201
198
  sqlite,
202
- ...dbWorkerRun.deps,
199
+ ...deps,
203
200
  });
204
201
 
205
202
  const dbDeps = {
206
- ...dbWorkerRun.deps,
203
+ ...deps,
207
204
  sqlite,
208
205
  sqliteSchema: initMessage.sqliteSchema,
209
206
  baseSqliteStorage,
210
207
  timestampConfig: { maxDrift: defaultTimestampMaxDrift },
211
208
  };
209
+ const dbWorkerRun = disposer.use(run.create());
212
210
 
213
211
  const currentSchema = getEvoluSqliteSchema(dbDeps)();
214
212
  const dbIsInitialized = "evolu_version" in currentSchema.tables;
@@ -230,7 +228,6 @@ export const startDbWorker =
230
228
  });
231
229
 
232
230
  const disposables = disposer.move();
233
- const dbWorkerRunWithStorage = dbWorkerRun.addDeps({ storage });
234
231
 
235
232
  port.onMessage = (input) => {
236
233
  if (input.type === "Dispose") {
@@ -330,29 +327,34 @@ export const startDbWorker =
330
327
  case "ApplySyncMessage": {
331
328
  const { owner, inputMessage } = request.message;
332
329
 
333
- dbWorkerRunWithStorage<void, never>(async (run) => {
334
- storage.setRequestContext(owner.encryptionKey);
330
+ dbWorkerRun<void, never, { readonly storage: ClientStorage }>(
331
+ async (run) => {
332
+ const { storage } = run.deps;
335
333
 
336
- const result = await run(
337
- applyProtocolMessageAsClient(inputMessage, {
338
- writeKey: owner.writeKey,
339
- }),
340
- );
334
+ storage.setRequestContext(owner.encryptionKey);
341
335
 
342
- const didWriteMessages = storage.didWriteMessages();
336
+ const result = await run(
337
+ applyProtocolMessageAsClient(inputMessage, {
338
+ writeKey: owner.writeKey,
339
+ }),
340
+ );
343
341
 
344
- postQueuedResponse({
345
- type: "ForSharedWorker",
346
- message: {
347
- type: "ApplySyncMessage",
348
- ownerId: owner.id,
349
- didWriteMessages,
350
- result,
351
- },
352
- });
342
+ const didWriteMessages = storage.didWriteMessages();
353
343
 
354
- return ok();
355
- });
344
+ postQueuedResponse({
345
+ type: "ForSharedWorker",
346
+ message: {
347
+ type: "ApplySyncMessage",
348
+ ownerId: owner.id,
349
+ didWriteMessages,
350
+ result,
351
+ },
352
+ });
353
+
354
+ return ok();
355
+ },
356
+ { storage },
357
+ );
356
358
  break;
357
359
  }
358
360
 
@@ -11,12 +11,11 @@ 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
15
  import type { Brand } from "../Brand.js";
16
16
  import { createCallbacks } from "../Callbacks.js";
17
17
  import type { ConsoleEntry, ConsoleLevel } from "../Console.js";
18
18
  import type { EncryptionKey } from "../Crypto.js";
19
- import { exhaustiveCheck } from "../Function.js";
20
19
  import { acquireLeaderLock, type LockManagerDep } from "../LockManager.js";
21
20
  import { structuralLookup, type StructuralLookupKey } from "../Lookup.js";
22
21
  import { createRefCountByKey, type RefCountByKey } from "../RefCount.js";
@@ -30,7 +29,13 @@ import { ok, type Result } from "../Result.js";
30
29
  import type { NonEmptyReadonlySet } from "../Set.js";
31
30
  import type { SqliteSchema } from "../Sqlite.js";
32
31
  import { createStore, type Store } from "../Store.js";
33
- import { AbortError, createMutex, type Mutex, type Task } from "../Task.js";
32
+ import {
33
+ AbortError,
34
+ createMutex,
35
+ unabortable,
36
+ type Mutex,
37
+ type Task,
38
+ } from "../Task.js";
34
39
  import { type Id, type Name, type Typed } from "../Type.js";
35
40
  import type { Callback, ExtractType } from "../Types.js";
36
41
  import type { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
@@ -308,10 +313,12 @@ export const initSharedWorker =
308
313
 
309
314
  case "CreateEvolu": {
310
315
  void sharedWorkerRun(async (run) => {
311
- const tenant = await run.orThrow(
312
- tenantsByName.acquire(message),
316
+ const tenant = await run(
317
+ unabortable(tenantsByName.acquire(message)),
313
318
  );
314
- tenant.addInstance(
319
+ if (!tenant.ok) return tenant;
320
+
321
+ tenant.value.addInstance(
315
322
  message,
316
323
  () => void sharedWorkerRun(tenantsByName.release(message)),
317
324
  );
@@ -405,7 +412,8 @@ export const initSharedWorker =
405
412
  ),
406
413
  );
407
414
 
408
- const sharedWorkerRun = run.create().addDeps({
415
+ const sharedWorkerRun = run.create({
416
+ ...deps,
409
417
  postConsoleEntryOrError,
410
418
  tabLeaderPortStore,
411
419
  transports,
@@ -414,6 +422,7 @@ export const initSharedWorker =
414
422
  const tenantsByName = disposer.use(
415
423
  await sharedWorkerRun.orThrow(
416
424
  createSharedResourceByKey(createEvoluTenant, {
425
+ idleDisposeAfter: "3s",
417
426
  lookup: (message) => message.name,
418
427
  }),
419
428
  ),
@@ -505,8 +514,6 @@ const createEvoluTenant =
505
514
  callbacks.execute(message.callbackId, message);
506
515
  break;
507
516
  }
508
- default:
509
- exhaustiveCheck(message);
510
517
  }
511
518
  };
512
519
 
@@ -549,9 +556,6 @@ const createEvoluTenant =
549
556
  case "ForSharedWorker":
550
557
  handleResponseForSharedWorker(response);
551
558
  break;
552
-
553
- default:
554
- exhaustiveCheck(response);
555
559
  }
556
560
 
557
561
  // Complete the current queue item and continue with the next one.
@@ -569,7 +573,16 @@ const createEvoluTenant =
569
573
  dbWorkerPort = null;
570
574
  queueRequestInFlight = false;
571
575
 
572
- await using _ = await tenantRun.orThrow(acquireLeaderLock(name));
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;
573
586
  });
574
587
 
575
588
  disposer.defer(deps.tabLeaderPortStore.subscribe(initDbWorker));
@@ -652,9 +665,6 @@ const createEvoluTenant =
652
665
  [response.message.file.buffer],
653
666
  );
654
667
  break;
655
-
656
- default:
657
- exhaustiveCheck(response.message);
658
668
  }
659
669
  };
660
670
 
@@ -698,15 +708,9 @@ const createEvoluTenant =
698
708
  case "Broadcast":
699
709
  case "NoResponse":
700
710
  break;
701
-
702
- default:
703
- exhaustiveCheck(response.message.result.value);
704
711
  }
705
712
  }
706
713
  break;
707
-
708
- default:
709
- exhaustiveCheck(response.message);
710
714
  }
711
715
  };
712
716
 
@@ -812,15 +816,14 @@ const createEvoluTenant =
812
816
  });
813
817
  disposer.use(instance.port);
814
818
 
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]();
821
- }
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;
822
825
 
823
- disposer.use(lock);
826
+ disposer.use(lock.value);
824
827
  return disposer.disposeAsync();
825
828
  });
826
829
 
@@ -857,8 +860,6 @@ const createEvoluTenant =
857
860
  );
858
861
  break;
859
862
  }
860
- default:
861
- exhaustiveCheck(message);
862
863
  }
863
864
  };
864
865
  },