cojson 0.19.20 → 0.19.21

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 (88) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/coValueCore/coValueCore.d.ts +9 -0
  3. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  4. package/dist/coValueCore/coValueCore.js +21 -0
  5. package/dist/coValueCore/coValueCore.js.map +1 -1
  6. package/dist/coValues/account.d.ts.map +1 -1
  7. package/dist/coValues/account.js +10 -10
  8. package/dist/coValues/account.js.map +1 -1
  9. package/dist/ids.d.ts +1 -1
  10. package/dist/ids.d.ts.map +1 -1
  11. package/dist/ids.js.map +1 -1
  12. package/dist/knownState.d.ts +5 -0
  13. package/dist/knownState.d.ts.map +1 -1
  14. package/dist/knownState.js +15 -0
  15. package/dist/knownState.js.map +1 -1
  16. package/dist/localNode.d.ts.map +1 -1
  17. package/dist/localNode.js +8 -2
  18. package/dist/localNode.js.map +1 -1
  19. package/dist/storage/knownState.d.ts +5 -0
  20. package/dist/storage/knownState.d.ts.map +1 -1
  21. package/dist/storage/knownState.js +11 -0
  22. package/dist/storage/knownState.js.map +1 -1
  23. package/dist/storage/sqlite/client.d.ts +2 -0
  24. package/dist/storage/sqlite/client.d.ts.map +1 -1
  25. package/dist/storage/sqlite/client.js +18 -0
  26. package/dist/storage/sqlite/client.js.map +1 -1
  27. package/dist/storage/sqliteAsync/client.d.ts +2 -0
  28. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  29. package/dist/storage/sqliteAsync/client.js +20 -0
  30. package/dist/storage/sqliteAsync/client.js.map +1 -1
  31. package/dist/storage/storageAsync.d.ts +2 -0
  32. package/dist/storage/storageAsync.d.ts.map +1 -1
  33. package/dist/storage/storageAsync.js +40 -0
  34. package/dist/storage/storageAsync.js.map +1 -1
  35. package/dist/storage/storageSync.d.ts +1 -0
  36. package/dist/storage/storageSync.d.ts.map +1 -1
  37. package/dist/storage/storageSync.js +15 -0
  38. package/dist/storage/storageSync.js.map +1 -1
  39. package/dist/storage/types.d.ts +18 -0
  40. package/dist/storage/types.d.ts.map +1 -1
  41. package/dist/sync.d.ts +17 -0
  42. package/dist/sync.d.ts.map +1 -1
  43. package/dist/sync.js +111 -41
  44. package/dist/sync.js.map +1 -1
  45. package/dist/tests/StorageApiAsync.test.js +91 -0
  46. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  47. package/dist/tests/StorageApiSync.test.js +91 -0
  48. package/dist/tests/StorageApiSync.test.js.map +1 -1
  49. package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
  50. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  51. package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
  52. package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
  53. package/dist/tests/knownState.lazyLoading.test.js +166 -0
  54. package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
  55. package/dist/tests/messagesTestUtils.d.ts +5 -2
  56. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  57. package/dist/tests/messagesTestUtils.js +4 -0
  58. package/dist/tests/messagesTestUtils.js.map +1 -1
  59. package/dist/tests/sync.load.test.js +388 -0
  60. package/dist/tests/sync.load.test.js.map +1 -1
  61. package/dist/tests/sync.mesh.test.js +4 -4
  62. package/dist/tests/testStorage.js +36 -0
  63. package/dist/tests/testStorage.js.map +1 -1
  64. package/dist/tests/testUtils.d.ts +14 -2
  65. package/dist/tests/testUtils.d.ts.map +1 -1
  66. package/dist/tests/testUtils.js.map +1 -1
  67. package/package.json +4 -4
  68. package/src/coValueCore/coValueCore.ts +26 -0
  69. package/src/coValues/account.ts +12 -14
  70. package/src/ids.ts +1 -1
  71. package/src/knownState.ts +24 -0
  72. package/src/localNode.ts +9 -2
  73. package/src/storage/knownState.ts +12 -0
  74. package/src/storage/sqlite/client.ts +31 -0
  75. package/src/storage/sqliteAsync/client.ts +35 -0
  76. package/src/storage/storageAsync.ts +51 -0
  77. package/src/storage/storageSync.ts +22 -0
  78. package/src/storage/types.ts +26 -0
  79. package/src/sync.ts +126 -42
  80. package/src/tests/StorageApiAsync.test.ts +136 -0
  81. package/src/tests/StorageApiSync.test.ts +132 -0
  82. package/src/tests/coValueCore.loadFromStorage.test.ts +3 -0
  83. package/src/tests/knownState.lazyLoading.test.ts +217 -0
  84. package/src/tests/messagesTestUtils.ts +10 -3
  85. package/src/tests/sync.load.test.ts +483 -1
  86. package/src/tests/sync.mesh.test.ts +4 -4
  87. package/src/tests/testStorage.ts +38 -0
  88. package/src/tests/testUtils.ts +14 -2
@@ -4,7 +4,7 @@ import {
4
4
  CO_VALUE_LOADING_CONFIG,
5
5
  setCoValueLoadingRetryDelay,
6
6
  } from "../config";
7
- import { RawCoMap } from "../exports";
7
+ import { CojsonInternalTypes, RawCoMap, SessionID } from "../exports";
8
8
  import {
9
9
  SyncMessagesLog,
10
10
  TEST_NODE_CONFIG,
@@ -1452,3 +1452,485 @@ describe("loading coValues from server", () => {
1452
1452
  expect(shardedCoreNode.hasCoValue(group.id)).toBe(false);
1453
1453
  });
1454
1454
  });
1455
+
1456
+ describe("lazy storage load optimization", () => {
1457
+ test("handleLoad skips full load when peer already has all content", async () => {
1458
+ // Setup server with storage
1459
+ const { storage } = jazzCloud.addStorage({ ourName: "server" });
1460
+
1461
+ // Create content on server and sync to storage
1462
+ const group = jazzCloud.node.createGroup();
1463
+ const map = group.createMap();
1464
+ map.set("hello", "world", "trusting");
1465
+ await map.core.waitForSync();
1466
+
1467
+ // Setup client and load the content (client now has everything)
1468
+ const client = setupTestNode({
1469
+ connected: true,
1470
+ });
1471
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
1472
+ expect(mapOnClient.get("hello")).toEqual("world");
1473
+
1474
+ // Disconnect client
1475
+ client.disconnect();
1476
+
1477
+ // Restart the server to clear memory (keeping storage)
1478
+ // Now the server has no CoValues in memory, only in storage
1479
+ jazzCloud.restart();
1480
+ jazzCloud.node.setStorage(storage);
1481
+
1482
+ SyncMessagesLog.clear();
1483
+
1484
+ // Reconnect client - it will send LOAD with its knownState
1485
+ // Server should use LAZY_LOAD to check storage and see peer already has everything
1486
+ client.connectToSyncServer();
1487
+
1488
+ await client.node.syncManager.waitForAllCoValuesSync();
1489
+
1490
+ // Verify the flow: LAZY_LOAD checks storage to get knownState,
1491
+ // sees peer already has everything, responds with KNOWN (skips full LOAD)
1492
+ expect(
1493
+ SyncMessagesLog.getMessages({
1494
+ Group: group.core,
1495
+ Map: map.core,
1496
+ }),
1497
+ ).toMatchInlineSnapshot(`
1498
+ [
1499
+ "client -> server | LOAD Group sessions: header/3",
1500
+ "client -> server | LOAD Map sessions: header/1",
1501
+ "server -> storage | GET_KNOWN_STATE Group",
1502
+ "storage -> server | GET_KNOWN_STATE_RESULT Group sessions: header/3",
1503
+ "server -> client | KNOWN Group sessions: header/3",
1504
+ "server -> storage | GET_KNOWN_STATE Map",
1505
+ "storage -> server | GET_KNOWN_STATE_RESULT Map sessions: header/1",
1506
+ "server -> client | KNOWN Map sessions: header/1",
1507
+ ]
1508
+ `);
1509
+ });
1510
+
1511
+ test("handleLoad does full load when peer needs content", async () => {
1512
+ // Setup server with storage
1513
+ const { storage } = jazzCloud.addStorage({ ourName: "server" });
1514
+
1515
+ // Create content on server and sync to storage
1516
+ const group = jazzCloud.node.createGroup();
1517
+ const map = group.createMap();
1518
+ map.set("hello", "world", "trusting");
1519
+ await map.core.waitForSync();
1520
+
1521
+ // Restart the server to clear memory (keeping storage)
1522
+ jazzCloud.restart();
1523
+ jazzCloud.node.setStorage(storage);
1524
+
1525
+ SyncMessagesLog.clear();
1526
+
1527
+ // Setup client without any data
1528
+ const client = setupTestNode({
1529
+ connected: true,
1530
+ });
1531
+
1532
+ // Client requests a load - server needs to load from storage and send content
1533
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
1534
+ expect(mapOnClient.get("hello")).toEqual("world");
1535
+
1536
+ // Verify the flow:
1537
+ // 1. Client sends LOAD with empty sessions (no header)
1538
+ // 2. Server skips LAZY_LOAD since peer has no content - goes directly to full LOAD
1539
+ // 3. Server sends CONTENT to client
1540
+ expect(
1541
+ SyncMessagesLog.getMessages({
1542
+ Group: group.core,
1543
+ Map: map.core,
1544
+ }),
1545
+ ).toMatchInlineSnapshot(`
1546
+ [
1547
+ "client -> server | LOAD Map sessions: empty",
1548
+ "server -> storage | LOAD Map sessions: empty",
1549
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 3",
1550
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1551
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
1552
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
1553
+ "client -> server | KNOWN Group sessions: header/3",
1554
+ "client -> server | KNOWN Map sessions: header/1",
1555
+ ]
1556
+ `);
1557
+ });
1558
+
1559
+ test("handleLoad falls back to peers when not in storage", async () => {
1560
+ // Setup server WITHOUT storage
1561
+ // Create content on server (in memory only)
1562
+ const group = jazzCloud.node.createGroup();
1563
+ const map = group.createMap();
1564
+ map.set("hello", "world", "trusting");
1565
+
1566
+ SyncMessagesLog.clear();
1567
+
1568
+ // Setup client
1569
+ const client = setupTestNode({
1570
+ connected: true,
1571
+ });
1572
+
1573
+ // Client requests a load - server should respond from memory
1574
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
1575
+ expect(mapOnClient.get("hello")).toEqual("world");
1576
+
1577
+ // Verify the content was delivered
1578
+ expect(
1579
+ SyncMessagesLog.getMessages({
1580
+ Group: group.core,
1581
+ Map: map.core,
1582
+ }),
1583
+ ).toMatchInlineSnapshot(`
1584
+ [
1585
+ "client -> server | LOAD Map sessions: empty",
1586
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
1587
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
1588
+ "client -> server | KNOWN Group sessions: header/3",
1589
+ "client -> server | KNOWN Map sessions: header/1",
1590
+ ]
1591
+ `);
1592
+ });
1593
+
1594
+ test("handleNewContent loads from storage for garbage-collected CoValues", async () => {
1595
+ // Setup server with storage
1596
+ jazzCloud.addStorage({ ourName: "server" });
1597
+
1598
+ // Create content on server and sync to storage
1599
+ const group = jazzCloud.node.createGroup();
1600
+ group.addMember("everyone", "writer");
1601
+ const map = group.createMap();
1602
+ map.set("initial", "value", "trusting");
1603
+ await map.core.waitForSync();
1604
+
1605
+ // Verify storage has the data
1606
+ expect(jazzCloud.node.storage).toBeDefined();
1607
+
1608
+ // Load the content on a client first to get the knownState
1609
+ const client1 = setupTestNode({
1610
+ connected: true,
1611
+ });
1612
+ const mapOnClient1 = await loadCoValueOrFail(client1.node, map.id);
1613
+ expect(mapOnClient1.get("initial")).toEqual("value");
1614
+
1615
+ // Now simulate the CoValue being garbage collected from server memory
1616
+ // by removing it and then receiving new content from a different client
1617
+ jazzCloud.node.internalDeleteCoValue(map.id);
1618
+
1619
+ // Clear messages to track what happens next
1620
+ SyncMessagesLog.clear();
1621
+
1622
+ // Have client1 make an update - this should trigger handleNewContent
1623
+ // which should load from storage since the CoValue was "garbage collected"
1624
+ mapOnClient1.set("new", "update", "trusting");
1625
+
1626
+ await waitFor(() => {
1627
+ // The server should have reloaded from storage and processed the update
1628
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1629
+ return serverMap.isAvailable();
1630
+ });
1631
+
1632
+ // Verify the server has the updated content
1633
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1634
+ expect(serverMap.isAvailable()).toBe(true);
1635
+
1636
+ // Verify that the server did a full load from storage
1637
+ expect(
1638
+ SyncMessagesLog.getMessages({
1639
+ Group: group.core,
1640
+ Map: map.core,
1641
+ }),
1642
+ ).toMatchInlineSnapshot(`
1643
+ [
1644
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1645
+ "server -> storage | LOAD Map sessions: empty",
1646
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1647
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1648
+ "server -> client | KNOWN Map sessions: header/2",
1649
+ "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
1650
+ ]
1651
+ `);
1652
+ });
1653
+
1654
+ test("handleNewContent loads large CoValue from storage when garbage-collected", async () => {
1655
+ // Setup server with storage
1656
+ jazzCloud.addStorage({ ourName: "server" });
1657
+
1658
+ // Create a large map on server and sync to storage
1659
+ const group = jazzCloud.node.createGroup();
1660
+ group.addMember("everyone", "writer");
1661
+ const largeMap = group.createMap();
1662
+ fillCoMapWithLargeData(largeMap);
1663
+ await largeMap.core.waitForSync();
1664
+
1665
+ // Verify storage has the data
1666
+ expect(jazzCloud.node.storage).toBeDefined();
1667
+
1668
+ // Load the content on a client first
1669
+ const client1 = setupTestNode({
1670
+ connected: true,
1671
+ });
1672
+ const mapOnClient1 = await loadCoValueOrFail(client1.node, largeMap.id);
1673
+ await mapOnClient1.core.waitForFullStreaming();
1674
+
1675
+ // Simulate the CoValue being garbage collected from server memory
1676
+ jazzCloud.node.internalDeleteCoValue(largeMap.id);
1677
+
1678
+ // Clear messages to track what happens next
1679
+ SyncMessagesLog.clear();
1680
+
1681
+ // Have client1 make an update - this should trigger handleNewContent
1682
+ // which should load from storage (streaming) since the CoValue was "garbage collected"
1683
+ mapOnClient1.set("new", "update", "trusting");
1684
+
1685
+ await waitFor(() => {
1686
+ // The server should have reloaded from storage and processed the update
1687
+ const serverMap = jazzCloud.node.getCoValue(largeMap.id);
1688
+ return serverMap.isAvailable();
1689
+ });
1690
+
1691
+ // Verify the server has the updated content
1692
+ const serverMap = jazzCloud.node.getCoValue(largeMap.id);
1693
+ expect(serverMap.isAvailable()).toBe(true);
1694
+
1695
+ // Verify that the server did a full load from storage (with streaming for large data)
1696
+ expect(
1697
+ SyncMessagesLog.getMessages({
1698
+ Group: group.core,
1699
+ Map: largeMap.core,
1700
+ }),
1701
+ ).toMatchInlineSnapshot(`
1702
+ [
1703
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1704
+ "server -> storage | LOAD Map sessions: empty",
1705
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1706
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 73 expectContentUntil: header/201",
1707
+ "server -> client | KNOWN Map sessions: header/74",
1708
+ "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
1709
+ "storage -> server | CONTENT Map header: true new: After: 73 New: 73",
1710
+ "storage -> server | CONTENT Map header: true new: After: 146 New: 54",
1711
+ ]
1712
+ `);
1713
+ });
1714
+
1715
+ test("handleNewContent loads CoValue from storage when group is large and garbage-collected", async () => {
1716
+ // Setup server with storage
1717
+ jazzCloud.addStorage({ ourName: "server" });
1718
+
1719
+ // Create a group with large data
1720
+ const group = jazzCloud.node.createGroup();
1721
+ group.addMember("everyone", "writer");
1722
+
1723
+ // Add large data to the group itself
1724
+ for (let i = 0; i < 200; i++) {
1725
+ const value = Buffer.alloc(1024, `value${i}`).toString("base64");
1726
+ group.set(`key${i}` as any, value as never, "trusting");
1727
+ }
1728
+
1729
+ const map = group.createMap();
1730
+ map.set("initial", "value", "trusting");
1731
+
1732
+ await map.core.waitForSync();
1733
+ await group.core.waitForSync();
1734
+
1735
+ // Verify storage has the data
1736
+ expect(jazzCloud.node.storage).toBeDefined();
1737
+
1738
+ // Load the content on a client first
1739
+ const client1 = setupTestNode({
1740
+ connected: true,
1741
+ });
1742
+ const mapOnClient1 = await loadCoValueOrFail(client1.node, map.id);
1743
+ expect(mapOnClient1.get("initial")).toEqual("value");
1744
+
1745
+ // Wait for the group to finish streaming
1746
+ const groupOnClient1 = client1.node.getCoValue(group.id);
1747
+ await groupOnClient1.waitForAvailableOrUnavailable();
1748
+
1749
+ // Simulate the map being garbage collected from server memory
1750
+ // The group should also be deleted to force reload from storage
1751
+ jazzCloud.node.internalDeleteCoValue(map.id);
1752
+ jazzCloud.node.internalDeleteCoValue(group.id);
1753
+
1754
+ // Clear messages to track what happens next
1755
+ SyncMessagesLog.clear();
1756
+
1757
+ // Have client1 make an update - this should trigger handleNewContent
1758
+ // which should load from storage (with the large group streaming)
1759
+ mapOnClient1.set("new", "update", "trusting");
1760
+
1761
+ await waitFor(() => {
1762
+ // The server should have reloaded from storage and processed the update
1763
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1764
+ return serverMap.isAvailable();
1765
+ });
1766
+
1767
+ // Verify the server has the updated content
1768
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1769
+ expect(serverMap.isAvailable()).toBe(true);
1770
+
1771
+ // Verify that the server did a full load from storage
1772
+ // Note: No LAZY_LOAD here because the content message has no header (it's an update),
1773
+ // so the server goes directly to full LOAD
1774
+ expect(
1775
+ SyncMessagesLog.getMessages({
1776
+ Group: group.core,
1777
+ Map: map.core,
1778
+ }),
1779
+ ).toMatchInlineSnapshot(`
1780
+ [
1781
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1782
+ "server -> storage | LOAD Map sessions: empty",
1783
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 78 expectContentUntil: header/205",
1784
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1785
+ "server -> client | KNOWN Map sessions: header/2",
1786
+ "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
1787
+ "storage -> server | CONTENT Group header: true new: After: 78 New: 73",
1788
+ "storage -> server | CONTENT Group header: true new: After: 151 New: 54",
1789
+ ]
1790
+ `);
1791
+ });
1792
+
1793
+ test("handleNewContent loads CoValue from storage when parent group is large and garbage-collected", async () => {
1794
+ // Setup server with storage
1795
+ jazzCloud.addStorage({ ourName: "server" });
1796
+
1797
+ // Create parent group with large data
1798
+ const parentGroup = jazzCloud.node.createGroup();
1799
+ parentGroup.addMember("everyone", "reader");
1800
+
1801
+ // Add large data to the parent group
1802
+ fillCoMapWithLargeData(parentGroup);
1803
+
1804
+ // Create child group that extends parent
1805
+ const group = jazzCloud.node.createGroup();
1806
+ group.addMember("everyone", "writer");
1807
+ group.extend(parentGroup);
1808
+
1809
+ const map = group.createMap();
1810
+ map.set("initial", "value", "trusting");
1811
+
1812
+ await map.core.waitForSync();
1813
+ await group.core.waitForSync();
1814
+ await parentGroup.core.waitForSync();
1815
+
1816
+ // Verify storage has the data
1817
+ expect(jazzCloud.node.storage).toBeDefined();
1818
+
1819
+ // Load the content on a client first
1820
+ const client1 = setupTestNode({
1821
+ connected: true,
1822
+ });
1823
+ const mapOnClient1 = await loadCoValueOrFail(client1.node, map.id);
1824
+ expect(mapOnClient1.get("initial")).toEqual("value");
1825
+
1826
+ // Wait for the parent group to finish streaming
1827
+ const parentGroupOnClient1 = client1.node.getCoValue(parentGroup.id);
1828
+ await parentGroupOnClient1.waitForAvailableOrUnavailable();
1829
+ if (parentGroupOnClient1.isAvailable()) {
1830
+ await parentGroupOnClient1.waitForFullStreaming();
1831
+ }
1832
+
1833
+ // Simulate CoValues being garbage collected from server memory
1834
+ jazzCloud.node.internalDeleteCoValue(map.id);
1835
+ jazzCloud.node.internalDeleteCoValue(group.id);
1836
+ jazzCloud.node.internalDeleteCoValue(parentGroup.id);
1837
+
1838
+ // Clear messages to track what happens next
1839
+ SyncMessagesLog.clear();
1840
+
1841
+ // Have client1 make an update - this should trigger handleNewContent
1842
+ // which should load from storage (with the large parent group streaming)
1843
+ mapOnClient1.set("new", "update", "trusting");
1844
+
1845
+ await waitFor(() => {
1846
+ // The server should have reloaded from storage and processed the update
1847
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1848
+ return serverMap.isAvailable();
1849
+ });
1850
+
1851
+ // Verify the server has the updated content
1852
+ const serverMap = jazzCloud.node.getCoValue(map.id);
1853
+ expect(serverMap.isAvailable()).toBe(true);
1854
+
1855
+ // Verify that the server did a full load from storage for all CoValues
1856
+ // The snapshot shows the complete flow: loading Map triggers loading its dependencies
1857
+ expect(
1858
+ SyncMessagesLog.getMessages({
1859
+ ParentGroup: parentGroup.core,
1860
+ Group: group.core,
1861
+ Map: map.core,
1862
+ }),
1863
+ ).toMatchInlineSnapshot(`
1864
+ [
1865
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1866
+ "server -> storage | LOAD Map sessions: empty",
1867
+ "storage -> server | CONTENT ParentGroup header: true new: After: 0 New: 78 expectContentUntil: header/205",
1868
+ "storage -> server | CONTENT Group header: true new: After: 0 New: 7",
1869
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1870
+ "server -> client | KNOWN Map sessions: header/2",
1871
+ "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
1872
+ "storage -> server | CONTENT ParentGroup header: true new: After: 78 New: 73",
1873
+ "storage -> server | CONTENT ParentGroup header: true new: After: 151 New: 54",
1874
+ ]
1875
+ `);
1876
+ });
1877
+
1878
+ test("handles gracefully when CoValue is garbage collected mid-stream from storage", async () => {
1879
+ // This test verifies the edge case where:
1880
+ // 1. Storage is streaming a large CoValue in chunks
1881
+ // 2. The CoValue is garbage collected mid-stream
1882
+ // 3. Subsequent chunks from storage (without header) should be handled gracefully
1883
+
1884
+ // Setup server with storage
1885
+ jazzCloud.addStorage({ ourName: "server" });
1886
+
1887
+ // Create a large CoValue that will stream
1888
+ const group = jazzCloud.node.createGroup();
1889
+ group.addMember("everyone", "writer");
1890
+ const largeMap = group.createMap();
1891
+ fillCoMapWithLargeData(largeMap);
1892
+ await largeMap.core.waitForSync();
1893
+
1894
+ // Get the sessions from the largeMap to create a realistic streaming chunk
1895
+ const sessions = Object.entries(largeMap.core.knownState().sessions);
1896
+ const [sessionId, txCount] = sessions[0]!;
1897
+
1898
+ // Simulate receiving a streaming chunk from storage for a non-existent CoValue
1899
+ // This happens when:
1900
+ // 1. A large CoValue starts streaming from storage
1901
+ // 2. The CoValue gets garbage collected mid-stream
1902
+ // 3. Remaining chunks arrive with no header (they're continuation chunks)
1903
+
1904
+ // First, ensure the CoValue doesn't exist in server memory
1905
+ jazzCloud.node.internalDeleteCoValue(largeMap.id);
1906
+ expect(jazzCloud.node.hasCoValue(largeMap.id)).toBe(false);
1907
+
1908
+ // Now simulate a streaming chunk arriving from storage without a header
1909
+ // This is what happens when GC runs between streaming chunks
1910
+ const streamingChunk = {
1911
+ action: "content" as const,
1912
+ id: largeMap.id,
1913
+ header: undefined, // No header - it's a continuation chunk
1914
+ priority: 0 as const,
1915
+ new: {
1916
+ [sessionId as SessionID]: {
1917
+ after: Math.floor(txCount / 2), // Middle of the stream
1918
+ newTransactions: [],
1919
+ lastSignature: "test" as CojsonInternalTypes.Signature,
1920
+ },
1921
+ },
1922
+ };
1923
+
1924
+ // Call handleNewContent directly with the storage message
1925
+ // This should NOT crash, just log a warning and return early
1926
+ jazzCloud.node.syncManager.handleNewContent(streamingChunk, "storage");
1927
+
1928
+ // The CoValue entry gets created by getOrCreateCoValue, but it should
1929
+ // NOT be available (the chunk was ignored because it had no header)
1930
+ const coValue = jazzCloud.node.getCoValue(largeMap.id);
1931
+ expect(coValue).toBeDefined();
1932
+ expect(coValue?.isAvailable()).toBe(false);
1933
+
1934
+ // Test passes if we reach here without crashing
1935
+ });
1936
+ });
@@ -561,11 +561,11 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
561
561
  "edge -> client | CONTENT Map header: false new: After: 63 New: 21 expectContentUntil: header/100",
562
562
  "storage -> edge | CONTENT Map header: true new: After: 84 New: 16",
563
563
  "edge -> client | CONTENT Map header: false new: After: 84 New: 16",
564
- "core -> storage | LOAD Group sessions: empty",
565
- "storage -> core | KNOWN Group sessions: empty",
564
+ "core -> storage | GET_KNOWN_STATE Group",
565
+ "storage -> core | GET_KNOWN_STATE_RESULT Group sessions: empty",
566
566
  "core -> edge | KNOWN Group sessions: empty",
567
- "core -> storage | LOAD Map sessions: empty",
568
- "storage -> core | KNOWN Map sessions: empty",
567
+ "core -> storage | GET_KNOWN_STATE Map",
568
+ "storage -> core | GET_KNOWN_STATE_RESULT Map sessions: empty",
569
569
  "core -> edge | KNOWN Map sessions: empty",
570
570
  "client -> edge | KNOWN Group sessions: header/5",
571
571
  "client -> storage | CONTENT Group header: true new: After: 0 New: 5",
@@ -150,6 +150,44 @@ function trackStorageMessages(
150
150
  ) {
151
151
  const originalStore = storage.store;
152
152
  const originalLoad = storage.load;
153
+ const originalLoadKnownState = storage.loadKnownState;
154
+
155
+ storage.loadKnownState = function (id, callback) {
156
+ SyncMessagesLog.add({
157
+ from: nodeName,
158
+ to: storageName,
159
+ msg: {
160
+ action: "lazyLoad",
161
+ id: id as RawCoID,
162
+ },
163
+ });
164
+
165
+ return originalLoadKnownState.call(storage, id, (knownState) => {
166
+ if (knownState) {
167
+ SyncMessagesLog.add({
168
+ from: storageName,
169
+ to: nodeName,
170
+ msg: {
171
+ action: "lazyLoadResult",
172
+ ...knownState,
173
+ },
174
+ });
175
+ } else {
176
+ SyncMessagesLog.add({
177
+ from: storageName,
178
+ to: nodeName,
179
+ msg: {
180
+ action: "lazyLoadResult",
181
+ id: id as RawCoID,
182
+ header: false,
183
+ sessions: {},
184
+ },
185
+ });
186
+ }
187
+
188
+ return callback(knownState);
189
+ });
190
+ };
153
191
 
154
192
  storage.store = function (data, correctionCallback) {
155
193
  SyncMessagesLog.add({
@@ -19,7 +19,7 @@ import {
19
19
  type RawCoValue,
20
20
  StorageAPI,
21
21
  } from "../exports.js";
22
- import type { SessionID } from "../ids.js";
22
+ import type { RawCoID, SessionID } from "../ids.js";
23
23
  import { LocalNode } from "../localNode.js";
24
24
  import { connectedPeers } from "../streamUtils.js";
25
25
  import type { Peer, SyncMessage, SyncWhen } from "../sync.js";
@@ -679,10 +679,22 @@ export async function setupTestAccount(
679
679
  };
680
680
  }
681
681
 
682
+ export type LazyLoadMessage = {
683
+ action: "lazyLoad";
684
+ id: RawCoID;
685
+ };
686
+
687
+ export type LazyLoadResultMessage = {
688
+ action: "lazyLoadResult";
689
+ id: RawCoID;
690
+ header: boolean;
691
+ sessions: { [sessionID: string]: number };
692
+ };
693
+
682
694
  export type SyncTestMessage = {
683
695
  from: string;
684
696
  to: string;
685
- msg: SyncMessage;
697
+ msg: SyncMessage | LazyLoadMessage | LazyLoadResultMessage;
686
698
  };
687
699
 
688
700
  export function connectedPeersWithMessagesTracking(opts: {