@evolu/common 8.10.0 → 8.12.0

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 (152) hide show
  1. package/dist/src/Bytes.d.ts +39 -2
  2. package/dist/src/Bytes.d.ts.map +1 -1
  3. package/dist/src/Bytes.js +50 -2
  4. package/dist/src/Config.d.ts +22 -22
  5. package/dist/src/Config.d.ts.map +1 -1
  6. package/dist/src/Console.d.ts +62 -7
  7. package/dist/src/Console.d.ts.map +1 -1
  8. package/dist/src/Console.js +20 -4
  9. package/dist/src/Crypto.d.ts +76 -4
  10. package/dist/src/Crypto.d.ts.map +1 -1
  11. package/dist/src/Crypto.js +55 -4
  12. package/dist/src/Error.d.ts +45 -0
  13. package/dist/src/Error.d.ts.map +1 -1
  14. package/dist/src/Error.js +69 -0
  15. package/dist/src/Fs.d.ts +92 -18
  16. package/dist/src/Fs.d.ts.map +1 -1
  17. package/dist/src/Fs.js +2 -0
  18. package/dist/src/Identicon.d.ts +2 -2
  19. package/dist/src/Identicon.js +2 -2
  20. package/dist/src/LeakDetector.d.ts +22 -3
  21. package/dist/src/LeakDetector.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.js +12 -2
  23. package/dist/src/LockManager.d.ts +8 -0
  24. package/dist/src/LockManager.d.ts.map +1 -1
  25. package/dist/src/LockManager.js +6 -0
  26. package/dist/src/Object.d.ts.map +1 -1
  27. package/dist/src/Object.js +5 -0
  28. package/dist/src/Platform.d.ts +47 -7
  29. package/dist/src/Platform.d.ts.map +1 -1
  30. package/dist/src/Platform.js +24 -5
  31. package/dist/src/Random.d.ts +25 -2
  32. package/dist/src/Random.d.ts.map +1 -1
  33. package/dist/src/Random.js +14 -2
  34. package/dist/src/Resource.d.ts +156 -1
  35. package/dist/src/Resource.d.ts.map +1 -1
  36. package/dist/src/Resource.js +201 -72
  37. package/dist/src/Schedule.d.ts +11 -10
  38. package/dist/src/Schedule.d.ts.map +1 -1
  39. package/dist/src/Schedule.js +1 -1
  40. package/dist/src/Sqlite.d.ts +132 -16
  41. package/dist/src/Sqlite.d.ts.map +1 -1
  42. package/dist/src/Sqlite.js +63 -9
  43. package/dist/src/Task.d.ts +15 -4
  44. package/dist/src/Task.d.ts.map +1 -1
  45. package/dist/src/Task.js +41 -15
  46. package/dist/src/Test.d.ts +9 -0
  47. package/dist/src/Test.d.ts.map +1 -1
  48. package/dist/src/Test.js +4 -0
  49. package/dist/src/Time.d.ts +106 -9
  50. package/dist/src/Time.d.ts.map +1 -1
  51. package/dist/src/Time.js +55 -4
  52. package/dist/src/Type.d.ts +1455 -1310
  53. package/dist/src/Type.d.ts.map +1 -1
  54. package/dist/src/Type.js +1274 -517
  55. package/dist/src/WebSocket.d.ts +164 -13
  56. package/dist/src/WebSocket.d.ts.map +1 -1
  57. package/dist/src/WebSocket.js +133 -24
  58. package/dist/src/Worker.d.ts +90 -8
  59. package/dist/src/Worker.d.ts.map +1 -1
  60. package/dist/src/Worker.js +28 -2
  61. package/dist/src/index.d.ts +6 -7
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +2 -3
  64. package/dist/src/local-first/Db.d.ts +52 -3
  65. package/dist/src/local-first/Db.d.ts.map +1 -1
  66. package/dist/src/local-first/Db.js +412 -137
  67. package/dist/src/local-first/Evolu.d.ts +412 -213
  68. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  69. package/dist/src/local-first/Evolu.js +181 -18
  70. package/dist/src/local-first/Owner.d.ts +13 -30
  71. package/dist/src/local-first/Owner.d.ts.map +1 -1
  72. package/dist/src/local-first/Owner.js +13 -30
  73. package/dist/src/local-first/Protocol.d.ts +106 -19
  74. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  75. package/dist/src/local-first/Protocol.js +162 -60
  76. package/dist/src/local-first/Query.d.ts +8 -15
  77. package/dist/src/local-first/Query.d.ts.map +1 -1
  78. package/dist/src/local-first/Relay.d.ts.map +1 -1
  79. package/dist/src/local-first/Relay.js +4 -2
  80. package/dist/src/local-first/Schema.d.ts +346 -23
  81. package/dist/src/local-first/Schema.d.ts.map +1 -1
  82. package/dist/src/local-first/Schema.js +214 -17
  83. package/dist/src/local-first/Shared.d.ts +537 -22
  84. package/dist/src/local-first/Shared.d.ts.map +1 -1
  85. package/dist/src/local-first/Shared.js +1437 -234
  86. package/dist/src/local-first/Storage.d.ts +195 -17
  87. package/dist/src/local-first/Storage.d.ts.map +1 -1
  88. package/dist/src/local-first/Storage.js +85 -22
  89. package/dist/src/local-first/Timestamp.d.ts +392 -41
  90. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  91. package/dist/src/local-first/Timestamp.js +403 -81
  92. package/dist/src/local-first/index.d.ts +0 -1
  93. package/dist/src/local-first/index.d.ts.map +1 -1
  94. package/dist/src/local-first/index.js +0 -1
  95. package/package.json +1 -1
  96. package/src/Assert.test.ts +2 -5
  97. package/src/Bytes.test.ts +27 -0
  98. package/src/Bytes.ts +58 -2
  99. package/src/Config.test.ts +2 -6
  100. package/src/Config.ts +133 -133
  101. package/src/Console.ts +62 -7
  102. package/src/Crypto.ts +76 -4
  103. package/src/Eq.test.ts +2 -3
  104. package/src/Error.test.ts +76 -3
  105. package/src/Error.ts +71 -0
  106. package/src/Fs.ts +92 -18
  107. package/src/Identicon.ts +2 -2
  108. package/src/LeakDetector.ts +22 -3
  109. package/src/LockManager.ts +8 -0
  110. package/src/Object.test.ts +27 -12
  111. package/src/Object.ts +5 -0
  112. package/src/Platform.ts +50 -8
  113. package/src/Random.ts +25 -2
  114. package/src/Resource.test.ts +837 -0
  115. package/src/Resource.ts +235 -15
  116. package/src/Schedule.test.ts +50 -12
  117. package/src/Schedule.ts +24 -14
  118. package/src/Sqlite.ts +137 -17
  119. package/src/Task.test.ts +189 -8
  120. package/src/Task.ts +56 -17
  121. package/src/Test.ts +9 -0
  122. package/src/Time.ts +106 -9
  123. package/src/Type.test.ts +946 -1028
  124. package/src/Type.ts +4195 -3136
  125. package/src/Types.test.ts +4 -14
  126. package/src/WebSocket.ts +313 -40
  127. package/src/Worker.ts +90 -8
  128. package/src/index.ts +20 -6
  129. package/src/local-first/Db.ts +644 -339
  130. package/src/local-first/Evolu.test.ts +994 -22
  131. package/src/local-first/Evolu.ts +625 -232
  132. package/src/local-first/Owner.ts +13 -30
  133. package/src/local-first/Protocol.test.ts +634 -10
  134. package/src/local-first/Protocol.ts +255 -109
  135. package/src/local-first/Query.ts +8 -15
  136. package/src/local-first/Relay.ts +4 -2
  137. package/src/local-first/Schema.test.ts +143 -0
  138. package/src/local-first/Schema.ts +376 -26
  139. package/src/local-first/Shared.test.ts +7731 -559
  140. package/src/local-first/Shared.ts +2036 -267
  141. package/src/local-first/Storage.ts +224 -36
  142. package/src/local-first/Timestamp.test.ts +344 -70
  143. package/src/local-first/Timestamp.ts +434 -118
  144. package/src/local-first/index.ts +0 -1
  145. package/dist/src/local-first/Error.d.ts +0 -12
  146. package/dist/src/local-first/Error.d.ts.map +0 -1
  147. package/dist/src/local-first/Error.js +0 -6
  148. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  149. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  150. package/dist/src/local-first/LocalAuth.js +0 -179
  151. package/src/local-first/Error.ts +0 -17
  152. package/src/local-first/LocalAuth.ts +0 -457
@@ -6,6 +6,7 @@ import {
6
6
  assertFalse,
7
7
  assertInstanceOf,
8
8
  assertLength,
9
+ assertNotSame,
9
10
  assertNotUndefined,
10
11
  assertOk,
11
12
  assertRejects,
@@ -20,6 +21,7 @@ import {
20
21
  createSharedResourceByKeyWithClaims,
21
22
  createSharedResource,
22
23
  createSharedResourceByKey,
24
+ createResettableResource,
23
25
  type BorrowedResource,
24
26
  type ClaimLease,
25
27
  type Lease,
@@ -30,10 +32,12 @@ import {
30
32
  import { err, ok } from "./Result.ts";
31
33
  import {
32
34
  AbortError,
35
+ createAbortError,
33
36
  createGate,
34
37
  runDisposedAbortReason,
35
38
  testAbortReason,
36
39
  testCreateRun,
40
+ type AbortableFiber,
37
41
  type Task,
38
42
  } from "./Task.ts";
39
43
  import { assertType } from "./Type.ts";
@@ -1346,6 +1350,839 @@ describe("SharedResource", () => {
1346
1350
  });
1347
1351
  });
1348
1352
 
1353
+ describe("ResettableResource", () => {
1354
+ it("creates the first resource before returning", async () => {
1355
+ await using run = testCreateRun();
1356
+ const resources = createTestResources();
1357
+
1358
+ await using resettable = await run.ok(
1359
+ createResettableResource(resources.create),
1360
+ );
1361
+
1362
+ assertEqual(resources.getCreateCount(), 1);
1363
+ assertNotUndefined(resettable.get());
1364
+ assertType<
1365
+ ReturnType<typeof resettable.get>,
1366
+ BorrowedResource<Disposable> | undefined
1367
+ >();
1368
+ });
1369
+
1370
+ it("leaves nothing running when the first creation aborts", async () => {
1371
+ await using run = testCreateRun();
1372
+ // The creation observes an abort of its own, such as a dependency's Run.
1373
+ const create: Task<Disposable> = () => {
1374
+ // oxlint-disable-next-line typescript/only-throw-error -- AbortError is Task abort control flow.
1375
+ throw createAbortError(testAbortReason);
1376
+ };
1377
+
1378
+ const result = await run.abortable(createResettableResource(create));
1379
+
1380
+ assertErr(result);
1381
+ assertType(AbortError, result.error);
1382
+ assertSame(result.error.reason, testAbortReason);
1383
+ assertLength(run.snapshot().children, 0);
1384
+ });
1385
+
1386
+ it("cleans up before returning when the caller aborts initial creation", async () => {
1387
+ await using run = testCreateRun();
1388
+ const createStarted = Promise.withResolvers<void>();
1389
+ const continueCreate = Promise.withResolvers<void>();
1390
+ const disposalStarted = Promise.withResolvers<void>();
1391
+ const continueDisposal = Promise.withResolvers<void>();
1392
+ let disposeCount = 0;
1393
+ let disposalFinished = false;
1394
+ const create: Task<AsyncDisposable> = async () => {
1395
+ createStarted.resolve();
1396
+ await continueCreate.promise;
1397
+ return ok({
1398
+ [Symbol.asyncDispose]: async () => {
1399
+ disposeCount++;
1400
+ disposalStarted.resolve();
1401
+ await continueDisposal.promise;
1402
+ disposalFinished = true;
1403
+ },
1404
+ });
1405
+ };
1406
+
1407
+ const creating = run.abortable(createResettableResource(create));
1408
+ await createStarted.promise;
1409
+ creating.abort(testAbortReason);
1410
+ continueCreate.resolve();
1411
+
1412
+ const settled = creating.then(() => "settled" as const);
1413
+ try {
1414
+ assertSame(
1415
+ await Promise.race([
1416
+ disposalStarted.promise.then(() => "disposing" as const),
1417
+ settled,
1418
+ ]),
1419
+ "disposing",
1420
+ );
1421
+ assertSame(
1422
+ await Promise.race([settled, Promise.resolve("pending")]),
1423
+ "pending",
1424
+ );
1425
+ assertFalse(disposalFinished);
1426
+ } finally {
1427
+ continueDisposal.resolve();
1428
+ }
1429
+
1430
+ const result = await creating;
1431
+ assertErr(result);
1432
+ assertType(AbortError, result.error);
1433
+ assertSame(result.error.reason, testAbortReason);
1434
+ assertTrue(disposalFinished);
1435
+ assertEqual(disposeCount, 1);
1436
+ assertLength(run.snapshot().children, 0);
1437
+ assertSame(run.getState().type, "Running");
1438
+ assertEqual(
1439
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
1440
+ 0,
1441
+ );
1442
+ assertEqual(run.deps.reportDefect.getDefectsSnapshot(), []);
1443
+ });
1444
+
1445
+ it("disposes the current resource before creating the next", async () => {
1446
+ await using run = testCreateRun();
1447
+ const events: Array<string> = [];
1448
+ const create: Task<Disposable> = () => {
1449
+ events.push("create");
1450
+ return ok({
1451
+ [Symbol.dispose]: () => {
1452
+ events.push("dispose");
1453
+ },
1454
+ });
1455
+ };
1456
+
1457
+ await using resettable = await run.ok(createResettableResource(create));
1458
+ const first = resettable.get();
1459
+
1460
+ assertOk(await run(resettable.reset(first)), undefined);
1461
+
1462
+ assertEqual(events, ["create", "dispose", "create"]);
1463
+ assertNotUndefined(resettable.get());
1464
+ assertNotSame(resettable.get(), first);
1465
+ });
1466
+
1467
+ it("waits for async disposal before creating one shared reset", async () => {
1468
+ await using run = testCreateRun();
1469
+ const disposalStarted = Promise.withResolvers<void>();
1470
+ const continueDisposal = Promise.withResolvers<void>();
1471
+ const events: Array<string> = [];
1472
+ let createCount = 0;
1473
+ const create: Task<AsyncDisposable> = () => {
1474
+ const id = ++createCount;
1475
+ events.push(`create ${id}`);
1476
+ return ok({
1477
+ [Symbol.asyncDispose]: async () => {
1478
+ events.push(`dispose start ${id}`);
1479
+ if (id === 1) {
1480
+ disposalStarted.resolve();
1481
+ await continueDisposal.promise;
1482
+ }
1483
+ events.push(`dispose end ${id}`);
1484
+ },
1485
+ });
1486
+ };
1487
+
1488
+ await using resettable = await run.ok(createResettableResource(create));
1489
+ const observed = resettable.get();
1490
+ const first = run(resettable.reset(observed));
1491
+ await disposalStarted.promise;
1492
+ const second = run(resettable.reset(observed));
1493
+
1494
+ try {
1495
+ assertSame(resettable.get(), undefined);
1496
+ assertEqual(createCount, 1);
1497
+ } finally {
1498
+ continueDisposal.resolve();
1499
+ }
1500
+
1501
+ assertOk(await first, undefined);
1502
+ assertOk(await second, undefined);
1503
+ assertNotUndefined(resettable.get());
1504
+ assertEqual(events, [
1505
+ "create 1",
1506
+ "dispose start 1",
1507
+ "dispose end 1",
1508
+ "create 2",
1509
+ ]);
1510
+ });
1511
+
1512
+ it("has no current resource while a reset is in progress", async () => {
1513
+ await using run = testCreateRun();
1514
+ const createStarted = Promise.withResolvers<void>();
1515
+ const gate = createGate();
1516
+ let createCount = 0;
1517
+ const create: Task<Disposable> = async (run) => {
1518
+ if (++createCount > 1) {
1519
+ createStarted.resolve();
1520
+ await run.ok(gate.wait);
1521
+ }
1522
+ return ok({ [Symbol.dispose]: constVoid });
1523
+ };
1524
+
1525
+ await using resettable = await run.ok(createResettableResource(create));
1526
+ const resetting = run(resettable.reset(resettable.get()));
1527
+ await createStarted.promise;
1528
+
1529
+ assertSame(resettable.get(), undefined);
1530
+
1531
+ gate.open();
1532
+ assertOk(await resetting, undefined);
1533
+ assertNotUndefined(resettable.get());
1534
+ });
1535
+
1536
+ it("shares one reset between concurrent requests", async () => {
1537
+ await using run = testCreateRun();
1538
+ const resources = createTestResources();
1539
+
1540
+ await using resettable = await run.ok(
1541
+ createResettableResource(resources.create),
1542
+ );
1543
+ const observed = resettable.get();
1544
+ const first = run(resettable.reset(observed));
1545
+ const second = run(resettable.reset(observed));
1546
+
1547
+ assertOk(await first, undefined);
1548
+ assertOk(await second, undefined);
1549
+ assertEqual(resources.getCreateCount(), 2);
1550
+ assertEqual(resources.getDisposeCount(), 1);
1551
+
1552
+ // A request for the current resource after the completed reset starts
1553
+ // another one.
1554
+ assertOk(await run(resettable.reset(resettable.get())), undefined);
1555
+ assertEqual(resources.getCreateCount(), 3);
1556
+ assertEqual(resources.getDisposeCount(), 2);
1557
+ });
1558
+
1559
+ it("skips a reset when the observed resource is no longer current", async () => {
1560
+ await using run = testCreateRun();
1561
+ const resources = createTestResources();
1562
+
1563
+ await using resettable = await run.ok(
1564
+ createResettableResource(resources.create),
1565
+ );
1566
+ const first = resettable.get();
1567
+ assertOk(await run(resettable.reset(first)), undefined);
1568
+ const second = resettable.get();
1569
+ assertNotSame(second, first);
1570
+
1571
+ // A late observer of the first resource does not reset the second.
1572
+ assertOk(await run(resettable.reset(first)), undefined);
1573
+
1574
+ assertSame(resettable.get(), second);
1575
+ assertEqual(resources.getCreateCount(), 2);
1576
+ assertEqual(resources.getDisposeCount(), 1);
1577
+ });
1578
+
1579
+ it("skips a request that observed no resource once the replacement succeeds", async () => {
1580
+ await using run = testCreateRun();
1581
+ const resources = createTestResources();
1582
+ const createStarted = Promise.withResolvers<void>();
1583
+ const gate = createGate();
1584
+ let createCount = 0;
1585
+ const create: Task<Disposable> = async (run) => {
1586
+ if (++createCount === 2) {
1587
+ createStarted.resolve();
1588
+ await run.ok(gate.wait);
1589
+ }
1590
+ return run(resources.create);
1591
+ };
1592
+
1593
+ await using resettable = await run.ok(createResettableResource(create));
1594
+ const active = run(resettable.reset(resettable.get()));
1595
+ await createStarted.promise;
1596
+ const observed = resettable.get();
1597
+ assertSame(observed, undefined);
1598
+ const queued = run(resettable.reset(observed));
1599
+ gate.open();
1600
+
1601
+ assertOk(await active, undefined);
1602
+ assertOk(await queued, undefined);
1603
+ assertNotUndefined(resettable.get());
1604
+ assertEqual(resources.getCreateCount(), 2);
1605
+ assertEqual(resources.getDisposeCount(), 1);
1606
+ });
1607
+
1608
+ it("create returning a reused object identity is a programmer error", async () => {
1609
+ await using run = testCreateRun();
1610
+ const resource: Disposable = { [Symbol.dispose]: constVoid };
1611
+
1612
+ const resettable = await run.ok(
1613
+ createResettableResource(() => ok(resource)),
1614
+ );
1615
+ const first = resettable.get();
1616
+
1617
+ const result = await run.abortable(resettable.reset(first));
1618
+
1619
+ assertErr(result);
1620
+ assertType(AbortError, result.error);
1621
+ assertSame(result.error.reason.type, "PanicAbortReason");
1622
+ assertInstanceOf(result.error.reason.defect, Error);
1623
+ assertEqual(
1624
+ result.error.reason.defect.message,
1625
+ "ResettableResource create must return a fresh object identity.",
1626
+ );
1627
+ // The reused object was disposed before the check, so nothing is current.
1628
+ assertSame(resettable.get(), undefined);
1629
+ const defects = run.deps.reportDefect.getDefectsSnapshot();
1630
+ assertLength(defects, 1);
1631
+ assertSame(defects[0], result.error);
1632
+ });
1633
+
1634
+ it("runs a reset to completion when the caller aborts its Fiber", async () => {
1635
+ await using run = testCreateRun();
1636
+ const resources = createTestResources();
1637
+ const createStarted = Promise.withResolvers<void>();
1638
+ const gate = createGate();
1639
+ let createCount = 0;
1640
+ const create: Task<Disposable> = async (run) => {
1641
+ if (++createCount > 1) {
1642
+ createStarted.resolve();
1643
+ await run.ok(gate.wait);
1644
+ }
1645
+ return run(resources.create);
1646
+ };
1647
+
1648
+ await using resettable = await run.ok(createResettableResource(create));
1649
+ const first = resettable.get();
1650
+ const resetting = run.abortable(resettable.reset(first));
1651
+ await createStarted.promise;
1652
+
1653
+ resetting.abort(testAbortReason);
1654
+ gate.open();
1655
+
1656
+ assertOk(await resetting, undefined);
1657
+ assertNotUndefined(resettable.get());
1658
+ assertNotSame(resettable.get(), first);
1659
+ assertEqual(resources.getCreateCount(), 2);
1660
+ assertEqual(resources.getDisposeCount(), 1);
1661
+ });
1662
+
1663
+ it("overprovided reset deps do not replace deps captured for create", async () => {
1664
+ await using run = testCreateRun();
1665
+
1666
+ interface TestResource extends Disposable {
1667
+ readonly value: string;
1668
+ }
1669
+ interface TestDep {
1670
+ readonly value: string;
1671
+ }
1672
+
1673
+ await using resettable = await run.ok(
1674
+ createResettableResource<TestResource, TestDep>(({ deps }) =>
1675
+ ok({ value: deps.value, [Symbol.dispose]: constVoid }),
1676
+ ),
1677
+ { value: "captured" },
1678
+ );
1679
+ const first = resettable.get();
1680
+ assertEqual(first?.value, "captured");
1681
+
1682
+ assertOk(
1683
+ await run(resettable.reset(first), { value: "replacement" }),
1684
+ undefined,
1685
+ );
1686
+
1687
+ assertEqual(resettable.get()?.value, "captured");
1688
+ assertNotSame(resettable.get(), first);
1689
+ });
1690
+
1691
+ it("disposes the current resource", async () => {
1692
+ await using run = testCreateRun();
1693
+ const resources = createTestResources();
1694
+
1695
+ const resettable = await run.ok(createResettableResource(resources.create));
1696
+ await resettable[Symbol.asyncDispose]();
1697
+
1698
+ assertEqual(resources.getDisposeCount(), 1);
1699
+ assertSame(resettable.get(), undefined);
1700
+ });
1701
+
1702
+ it("repeated disposal waits for the same async cleanup", async () => {
1703
+ await using run = testCreateRun();
1704
+ const disposalStarted = Promise.withResolvers<void>();
1705
+ const continueDisposal = Promise.withResolvers<void>();
1706
+ let disposeCount = 0;
1707
+ let disposalFinished = false;
1708
+ const resettable = await run.ok(
1709
+ createResettableResource(() =>
1710
+ ok({
1711
+ [Symbol.asyncDispose]: async () => {
1712
+ disposeCount++;
1713
+ disposalStarted.resolve();
1714
+ await continueDisposal.promise;
1715
+ disposalFinished = true;
1716
+ },
1717
+ }),
1718
+ ),
1719
+ );
1720
+
1721
+ const first = resettable[Symbol.asyncDispose]();
1722
+ await disposalStarted.promise;
1723
+ const second = resettable[Symbol.asyncDispose]();
1724
+
1725
+ try {
1726
+ assertSame(
1727
+ await Promise.race([second, Promise.resolve("pending")]),
1728
+ "pending",
1729
+ );
1730
+ assertFalse(disposalFinished);
1731
+ } finally {
1732
+ continueDisposal.resolve();
1733
+ await Promise.all([first, second]);
1734
+ }
1735
+
1736
+ assertTrue(disposalFinished);
1737
+ assertSame(resettable.get(), undefined);
1738
+ await resettable[Symbol.asyncDispose]();
1739
+ assertEqual(disposeCount, 1);
1740
+ });
1741
+
1742
+ it("reentrant disposal waits for the same cleanup", async () => {
1743
+ await using run = testCreateRun();
1744
+ const resources = createTestResources();
1745
+ const createStarted = Promise.withResolvers<void>();
1746
+ const continueCreate = Promise.withResolvers<void>();
1747
+ let createCount = 0;
1748
+ let repeatedDisposal: PromiseLike<void> | undefined;
1749
+ const create: Task<Disposable> = async (run) => {
1750
+ const resource = await run.ok(resources.create);
1751
+ if (++createCount > 1) {
1752
+ using _onAbort = run.onAbort(() => {
1753
+ repeatedDisposal = resettable[Symbol.asyncDispose]();
1754
+ });
1755
+ createStarted.resolve();
1756
+ await continueCreate.promise;
1757
+ }
1758
+ return ok(resource);
1759
+ };
1760
+
1761
+ const resettable = await run.ok(createResettableResource(create));
1762
+ const resetting = run.abortable(resettable.reset(resettable.get()));
1763
+ await createStarted.promise;
1764
+ const disposal = resettable[Symbol.asyncDispose]();
1765
+
1766
+ try {
1767
+ assertNotUndefined(repeatedDisposal);
1768
+ assertSame(
1769
+ await Promise.race([repeatedDisposal, Promise.resolve("pending")]),
1770
+ "pending",
1771
+ );
1772
+ } finally {
1773
+ continueCreate.resolve();
1774
+ await Promise.all([disposal, repeatedDisposal]);
1775
+ }
1776
+
1777
+ assertErr(await resetting);
1778
+ assertSame(resettable.get(), undefined);
1779
+ assertEqual(resources.getDisposeCount(), 2);
1780
+ });
1781
+
1782
+ it("repeated disposal preserves the cleanup defect", async () => {
1783
+ await using run = testCreateRun();
1784
+ const defect = new Error("dispose failed");
1785
+ let disposeCount = 0;
1786
+ const resettable = await run.ok(
1787
+ createResettableResource(() =>
1788
+ ok({
1789
+ [Symbol.dispose]: () => {
1790
+ disposeCount++;
1791
+ throw defect;
1792
+ },
1793
+ }),
1794
+ ),
1795
+ );
1796
+
1797
+ assertEqual(
1798
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
1799
+ 1,
1800
+ );
1801
+ const first = resettable[Symbol.asyncDispose]();
1802
+ let disposalError: unknown;
1803
+ await assertRejects(first, (error) => {
1804
+ assertType(AbortError, error);
1805
+ assertSame(error.reason.type, "PanicAbortReason");
1806
+ assertSame(error.reason.defect, defect);
1807
+ disposalError = error;
1808
+ });
1809
+
1810
+ const second = resettable[Symbol.asyncDispose]();
1811
+ await assertRejects(second, (error) => {
1812
+ assertSame(error, disposalError);
1813
+ });
1814
+ assertEqual(disposeCount, 1);
1815
+ const reportedDefects = run.deps.reportDefect.getDefectsSnapshot();
1816
+ assertLength(reportedDefects, 1);
1817
+ assertSame(reportedDefects[0], disposalError);
1818
+ assertEqual(
1819
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
1820
+ 0,
1821
+ );
1822
+ });
1823
+
1824
+ it("is bounded by its Run", async () => {
1825
+ const resources = createTestResources();
1826
+ {
1827
+ await using run = testCreateRun();
1828
+ const resettable = await run.ok(
1829
+ createResettableResource(resources.create),
1830
+ );
1831
+ assertNotUndefined(resettable.get());
1832
+ }
1833
+ assertEqual(resources.getDisposeCount(), 1);
1834
+ });
1835
+
1836
+ it("aborts active and queued resets when disposed during async resource disposal", async () => {
1837
+ await using run = testCreateRun();
1838
+ const disposalStarted = Promise.withResolvers<void>();
1839
+ const continueDisposal = Promise.withResolvers<void>();
1840
+ let createCount = 0;
1841
+ let disposeCount = 0;
1842
+ const create: Task<AsyncDisposable> = () => {
1843
+ createCount++;
1844
+ return ok({
1845
+ [Symbol.asyncDispose]: async () => {
1846
+ disposeCount++;
1847
+ disposalStarted.resolve();
1848
+ await continueDisposal.promise;
1849
+ },
1850
+ });
1851
+ };
1852
+
1853
+ await using resettable = await run.ok(createResettableResource(create));
1854
+ const active = run.abortable(resettable.reset(resettable.get()));
1855
+ await disposalStarted.promise;
1856
+ const queued = run.abortable(resettable.reset(resettable.get()));
1857
+ const disposal = resettable[Symbol.asyncDispose]();
1858
+
1859
+ try {
1860
+ // Queued work aborts even while the active disposer is still blocked.
1861
+ const result = await queued;
1862
+ assertErr(result);
1863
+ assertType(AbortError, result.error);
1864
+ assertSame(result.error.reason, runDisposedAbortReason);
1865
+ assertSame(resettable.get(), undefined);
1866
+ } finally {
1867
+ continueDisposal.resolve();
1868
+ }
1869
+
1870
+ const result = await active;
1871
+ assertErr(result);
1872
+ assertType(AbortError, result.error);
1873
+ assertSame(result.error.reason, runDisposedAbortReason);
1874
+ await disposal;
1875
+
1876
+ assertEqual(createCount, 1);
1877
+ assertEqual(disposeCount, 1);
1878
+ assertSame(resettable.get(), undefined);
1879
+ });
1880
+
1881
+ it("never exposes a resource returned after disposal started", async () => {
1882
+ await using run = testCreateRun();
1883
+ const resources = createTestResources();
1884
+ const createStarted = Promise.withResolvers<void>();
1885
+ const continueCreate = Promise.withResolvers<void>();
1886
+ let createCount = 0;
1887
+ const create: Task<Disposable> = async (run) => {
1888
+ const resource = await run.ok(resources.create);
1889
+ if (++createCount > 1) {
1890
+ createStarted.resolve();
1891
+ await continueCreate.promise;
1892
+ }
1893
+ return ok(resource);
1894
+ };
1895
+
1896
+ const resettable = await run.ok(createResettableResource(create));
1897
+ const resetting = run.abortable(resettable.reset(resettable.get()));
1898
+ await createStarted.promise;
1899
+
1900
+ const disposal = resettable[Symbol.asyncDispose]();
1901
+ continueCreate.resolve();
1902
+ // The reset settles through microtasks only; none of them may
1903
+ // expose the late resource. The bound only guards against a hang.
1904
+ const settled = resetting.then(() => "settled" as const);
1905
+ for (let tick = 0; tick < 1000; tick++) {
1906
+ assertSame(resettable.get(), undefined);
1907
+ const outcome = await Promise.race([
1908
+ settled,
1909
+ Promise.resolve("tick" as const),
1910
+ ]);
1911
+ if (outcome === "settled") break;
1912
+ }
1913
+
1914
+ const result = await resetting;
1915
+ await disposal;
1916
+ assertErr(result);
1917
+ assertType(AbortError, result.error);
1918
+ assertSame(result.error.reason, runDisposedAbortReason);
1919
+ // The resource returned after disposal started never became current.
1920
+ assertEqual(resources.getCreateCount(), 2);
1921
+ assertEqual(resources.getDisposeCount(), 2);
1922
+ assertSame(resettable.get(), undefined);
1923
+ });
1924
+
1925
+ it("reset after disposal is a programmer error", async () => {
1926
+ await using run = testCreateRun();
1927
+ const resources = createTestResources();
1928
+
1929
+ const resettable = await run.ok(createResettableResource(resources.create));
1930
+ await resettable[Symbol.asyncDispose]();
1931
+
1932
+ const result = await run.abortable(resettable.reset(resettable.get()));
1933
+
1934
+ assertErr(result);
1935
+ assertType(AbortError, result.error);
1936
+ assertSame(result.error.reason.type, "PanicAbortReason");
1937
+ assertInstanceOf(result.error.reason.defect, Error);
1938
+ assertEqual(
1939
+ result.error.reason.defect.message,
1940
+ "Cannot use a disposed object.",
1941
+ );
1942
+ assertSame(resettable.get(), undefined);
1943
+ assertEqual(resources.getCreateCount(), 1);
1944
+ const defects = run.deps.reportDefect.getDefectsSnapshot();
1945
+ assertLength(defects, 1);
1946
+ assertSame(defects[0], result.error);
1947
+ });
1948
+
1949
+ it("reset after disposal starts is a programmer error", async () => {
1950
+ await using run = testCreateRun();
1951
+ const resources = createTestResources();
1952
+
1953
+ const resettable = await run.ok(createResettableResource(resources.create));
1954
+ // Disposal rejects new work immediately, before its cleanup finishes.
1955
+ const disposal = resettable[Symbol.asyncDispose]();
1956
+
1957
+ const result = await run.abortable(resettable.reset(resettable.get()));
1958
+ await disposal;
1959
+
1960
+ assertErr(result);
1961
+ assertType(AbortError, result.error);
1962
+ assertSame(result.error.reason.type, "PanicAbortReason");
1963
+ assertInstanceOf(result.error.reason.defect, Error);
1964
+ assertEqual(
1965
+ result.error.reason.defect.message,
1966
+ "Cannot use a disposed object.",
1967
+ );
1968
+ assertEqual(resources.getCreateCount(), 1);
1969
+ assertEqual(resources.getDisposeCount(), 1);
1970
+ const defects = run.deps.reportDefect.getDefectsSnapshot();
1971
+ assertLength(defects, 1);
1972
+ assertSame(defects[0], result.error);
1973
+ });
1974
+
1975
+ it("reset from an abort callback during disposal is a programmer error", async () => {
1976
+ await using run = testCreateRun();
1977
+ const resources = createTestResources();
1978
+ const createStarted = Promise.withResolvers<void>();
1979
+ const continueCreate = Promise.withResolvers<void>();
1980
+ let createCount = 0;
1981
+ let resetFromAbort: AbortableFiber<void> | undefined;
1982
+ const create: Task<Disposable> = async (creationRun) => {
1983
+ const resource = await creationRun.ok(resources.create);
1984
+ if (++createCount > 1) {
1985
+ using _onAbort = creationRun.onAbort(() => {
1986
+ resetFromAbort = run.abortable(resettable.reset(undefined));
1987
+ });
1988
+ createStarted.resolve();
1989
+ await continueCreate.promise;
1990
+ }
1991
+ return ok(resource);
1992
+ };
1993
+
1994
+ const resettable = await run.ok(createResettableResource(create));
1995
+ const resetting = run.abortable(resettable.reset(resettable.get()));
1996
+ await createStarted.promise;
1997
+ const disposal = resettable[Symbol.asyncDispose]();
1998
+
1999
+ try {
2000
+ assertNotUndefined(resetFromAbort);
2001
+ const result = await resetFromAbort;
2002
+ assertErr(result);
2003
+ assertType(AbortError, result.error);
2004
+ assertSame(result.error.reason.type, "PanicAbortReason");
2005
+ assertInstanceOf(result.error.reason.defect, Error);
2006
+ assertEqual(
2007
+ result.error.reason.defect.message,
2008
+ "Cannot use a disposed object.",
2009
+ );
2010
+ assertSame(run.getState().type, "Aborted");
2011
+ const defects = run.deps.reportDefect.getDefectsSnapshot();
2012
+ assertLength(defects, 1);
2013
+ assertSame(defects[0], result.error);
2014
+ } finally {
2015
+ continueCreate.resolve();
2016
+ await disposal;
2017
+ }
2018
+
2019
+ const result = await resetting;
2020
+ assertErr(result);
2021
+ assertType(AbortError, result.error);
2022
+ assertSame(result.error.reason, runDisposedAbortReason);
2023
+ assertSame(resettable.get(), undefined);
2024
+ assertEqual(resources.getCreateCount(), 2);
2025
+ assertEqual(resources.getDisposeCount(), 2);
2026
+ });
2027
+
2028
+ it("aborts a reset requested during root abort propagation", async () => {
2029
+ await using owner = testCreateRun();
2030
+ await using caller = testCreateRun();
2031
+ const resources = createTestResources();
2032
+ const createStarted = Promise.withResolvers<void>();
2033
+ const continueCreate = Promise.withResolvers<void>();
2034
+ let createCount = 0;
2035
+ let resetFromAbort: AbortableFiber<void> | undefined;
2036
+ const create: Task<Disposable> = async (creationRun) => {
2037
+ const resource = await creationRun.ok(resources.create);
2038
+ if (++createCount > 1) {
2039
+ using _onAbort = creationRun.onAbort(() => {
2040
+ resetFromAbort = caller.abortable(resettable.reset(undefined));
2041
+ });
2042
+ createStarted.resolve();
2043
+ await continueCreate.promise;
2044
+ }
2045
+ return ok(resource);
2046
+ };
2047
+
2048
+ await using resettable = await owner.ok(createResettableResource(create));
2049
+ const resetting = caller.abortable(resettable.reset(resettable.get()));
2050
+ await createStarted.promise;
2051
+ const queued = caller.abortable(resettable.reset(resettable.get()));
2052
+ const disposal = owner[Symbol.asyncDispose]();
2053
+
2054
+ try {
2055
+ assertNotUndefined(resetFromAbort);
2056
+ const result = await resetFromAbort;
2057
+ assertErr(result);
2058
+ assertType(AbortError, result.error);
2059
+ assertSame(result.error.reason, runDisposedAbortReason);
2060
+ assertSame(caller.getState().type, "Running");
2061
+ assertEqual(owner.deps.reportDefect.getDefectsSnapshot(), []);
2062
+ assertEqual(caller.deps.reportDefect.getDefectsSnapshot(), []);
2063
+ } finally {
2064
+ continueCreate.resolve();
2065
+ await disposal;
2066
+ }
2067
+
2068
+ for (const result of [await resetting, await queued]) {
2069
+ assertErr(result);
2070
+ assertType(AbortError, result.error);
2071
+ assertSame(result.error.reason, runDisposedAbortReason);
2072
+ }
2073
+ assertSame(resettable.get(), undefined);
2074
+ assertEqual(resources.getCreateCount(), 2);
2075
+ assertEqual(resources.getDisposeCount(), 2);
2076
+ });
2077
+
2078
+ it("leaves no current resource when a reset's creation aborts", async () => {
2079
+ await using run = testCreateRun();
2080
+ const resources = createTestResources();
2081
+ let createCount = 0;
2082
+ // The second creation aborts on its own, as a dependency's Run would.
2083
+ const create: Task<Disposable> = (run) => {
2084
+ if (++createCount === 2) {
2085
+ // oxlint-disable-next-line typescript/only-throw-error -- AbortError is Task abort control flow.
2086
+ throw createAbortError(testAbortReason);
2087
+ }
2088
+ return run(resources.create);
2089
+ };
2090
+
2091
+ await using resettable = await run.ok(createResettableResource(create));
2092
+ const first = resettable.get();
2093
+
2094
+ const result = await run.abortable(resettable.reset(first));
2095
+ assertErr(result);
2096
+ assertType(AbortError, result.error);
2097
+ assertSame(result.error.reason, testAbortReason);
2098
+ assertSame(resettable.get(), undefined);
2099
+ assertEqual(resources.getDisposeCount(), 1);
2100
+
2101
+ // Nothing is current, so a later request for the first resource creates
2102
+ // the next one.
2103
+ assertOk(await run(resettable.reset(first)), undefined);
2104
+ assertNotUndefined(resettable.get());
2105
+ assertEqual(resources.getCreateCount(), 2);
2106
+ });
2107
+
2108
+ it("runs a queued reset when the active reset's creation aborts", async () => {
2109
+ await using run = testCreateRun();
2110
+ const resources = createTestResources();
2111
+ const createStarted = Promise.withResolvers<void>();
2112
+ const gate = createGate();
2113
+ let createCount = 0;
2114
+ const create: Task<Disposable> = async (run) => {
2115
+ if (++createCount === 2) {
2116
+ createStarted.resolve();
2117
+ await run.ok(gate.wait);
2118
+ // oxlint-disable-next-line typescript/only-throw-error -- AbortError is Task abort control flow.
2119
+ throw createAbortError(testAbortReason);
2120
+ }
2121
+ return run(resources.create);
2122
+ };
2123
+
2124
+ await using resettable = await run.ok(createResettableResource(create));
2125
+ const active = run.abortable(resettable.reset(resettable.get()));
2126
+ await createStarted.promise;
2127
+ // The queued request observes the absence during replacement.
2128
+ const observed = resettable.get();
2129
+ assertSame(observed, undefined);
2130
+ const queued = run.abortable(resettable.reset(observed));
2131
+ gate.open();
2132
+
2133
+ assertErr(await active);
2134
+ // Nothing answered the queued request, so it creates the next resource.
2135
+ assertOk(await queued, undefined);
2136
+ assertNotUndefined(resettable.get());
2137
+ assertEqual(resources.getCreateCount(), 2);
2138
+ });
2139
+
2140
+ describe("leak detection", () => {
2141
+ it("warns when an undisposed ResettableResource is garbage-collected", async () => {
2142
+ await using run = testCreateRun();
2143
+
2144
+ await run.ok(createResettableResource(createTestResources().create));
2145
+
2146
+ assertEqual(run.deps.leakDetector.collect(), 1);
2147
+ const entries = run.deps.console.getEntriesSnapshot();
2148
+ assertLength(entries, 1);
2149
+ assertEqual(entries[0].method, "warn");
2150
+ assertEqual(
2151
+ entries[0].args[0],
2152
+ "ResettableResource was garbage-collected without cleanup. Tracked at:",
2153
+ );
2154
+ });
2155
+
2156
+ for (const owner of ["resource", "Run"]) {
2157
+ it(`untracks after ${owner} disposal and stays tracked through resets`, async () => {
2158
+ await using run = testCreateRun();
2159
+ const resettable = await run.ok(
2160
+ createResettableResource(createTestResources().create),
2161
+ );
2162
+
2163
+ assertEqual(
2164
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
2165
+ 1,
2166
+ );
2167
+ await run.ok(resettable.reset(resettable.get()));
2168
+ assertEqual(
2169
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
2170
+ 1,
2171
+ );
2172
+
2173
+ await (owner === "resource" ? resettable : run)[Symbol.asyncDispose]();
2174
+
2175
+ assertEqual(
2176
+ run.deps.leakDetector.getTrackedCount({ name: "ResettableResource" }),
2177
+ 0,
2178
+ );
2179
+ assertEqual(run.deps.leakDetector.collect(), 0);
2180
+ assertEqual(run.deps.console.getEntriesSnapshot(), []);
2181
+ });
2182
+ }
2183
+ });
2184
+ });
2185
+
1349
2186
  describe("SharedResourceByKey", () => {
1350
2187
  describe("acquire", () => {
1351
2188
  it("lazily creates a resource per key", async () => {