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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/coValueCore/coValueCore.d.ts +9 -0
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +21 -0
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +10 -10
- package/dist/coValues/account.js.map +1 -1
- package/dist/ids.d.ts +1 -1
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/knownState.d.ts +5 -0
- package/dist/knownState.d.ts.map +1 -1
- package/dist/knownState.js +15 -0
- package/dist/knownState.js.map +1 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +8 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/knownState.d.ts +5 -0
- package/dist/storage/knownState.d.ts.map +1 -1
- package/dist/storage/knownState.js +11 -0
- package/dist/storage/knownState.js.map +1 -1
- package/dist/storage/sqlite/client.d.ts +2 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +18 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +2 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +20 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +2 -0
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +40 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +1 -0
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +15 -0
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +18 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +17 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +111 -41
- package/dist/sync.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +91 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +91 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
- package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
- package/dist/tests/knownState.lazyLoading.test.js +166 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
- package/dist/tests/messagesTestUtils.d.ts +5 -2
- package/dist/tests/messagesTestUtils.d.ts.map +1 -1
- package/dist/tests/messagesTestUtils.js +4 -0
- package/dist/tests/messagesTestUtils.js.map +1 -1
- package/dist/tests/sync.load.test.js +388 -0
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +4 -4
- package/dist/tests/testStorage.js +36 -0
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +14 -2
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/coValueCore/coValueCore.ts +26 -0
- package/src/coValues/account.ts +12 -14
- package/src/ids.ts +1 -1
- package/src/knownState.ts +24 -0
- package/src/localNode.ts +9 -2
- package/src/storage/knownState.ts +12 -0
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +51 -0
- package/src/storage/storageSync.ts +22 -0
- package/src/storage/types.ts +26 -0
- package/src/sync.ts +126 -42
- package/src/tests/StorageApiAsync.test.ts +136 -0
- package/src/tests/StorageApiSync.test.ts +132 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +3 -0
- package/src/tests/knownState.lazyLoading.test.ts +217 -0
- package/src/tests/messagesTestUtils.ts +10 -3
- package/src/tests/sync.load.test.ts +483 -1
- package/src/tests/sync.mesh.test.ts +4 -4
- package/src/tests/testStorage.ts +38 -0
- 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 |
|
|
565
|
-
"storage -> core |
|
|
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 |
|
|
568
|
-
"storage -> core |
|
|
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",
|
package/src/tests/testStorage.ts
CHANGED
|
@@ -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({
|
package/src/tests/testUtils.ts
CHANGED
|
@@ -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: {
|