cojson 0.20.9 → 0.20.11
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/CHANGELOG.md +29 -0
- package/dist/OngoingStorageReconciliationTracker.d.ts +16 -0
- package/dist/OngoingStorageReconciliationTracker.d.ts.map +1 -0
- package/dist/OngoingStorageReconciliationTracker.js +75 -0
- package/dist/OngoingStorageReconciliationTracker.js.map +1 -0
- package/dist/PeerState.d.ts +2 -2
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/PeerState.js +3 -3
- package/dist/PeerState.js.map +1 -1
- package/dist/StorageReconciliationAckTracker.d.ts +14 -0
- package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
- package/dist/StorageReconciliationAckTracker.js +72 -0
- package/dist/StorageReconciliationAckTracker.js.map +1 -0
- package/dist/SyncStateManager.js +2 -2
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +2 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +43 -10
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/coList.d.ts +2 -0
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +28 -0
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/group.d.ts +4 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +15 -1
- package/dist/coValues/group.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +9 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +5 -1
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +7 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +13 -5
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/queue/LinkedList.d.ts +2 -0
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js +7 -0
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
- package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
- package/dist/queue/OutgoingLoadQueue.js +41 -13
- package/dist/queue/OutgoingLoadQueue.js.map +1 -1
- package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
- package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
- package/dist/queue/PriorityBasedMessageQueue.js +11 -1
- package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
- package/dist/storage/knownState.d.ts +2 -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 +10 -1
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +84 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.js +11 -0
- package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +10 -1
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +86 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +9 -2
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +19 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -2
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +20 -13
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +64 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js.map +1 -1
- package/dist/sync.d.ts +53 -2
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +300 -44
- package/dist/sync.js.map +1 -1
- package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts +2 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.d.ts.map +1 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.js +60 -0
- package/dist/tests/OngoingStorageReconciliationTracker.test.js.map +1 -0
- package/dist/tests/OutgoingLoadQueue.test.js +137 -39
- package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
- package/dist/tests/SQLiteClientAsync.test.js +1 -1
- package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +138 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +154 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
- package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
- package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
- package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
- package/dist/tests/SyncStateManager.test.js +18 -0
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coList.test.js +112 -1
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/group.test.js +44 -0
- package/dist/tests/group.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +6 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- 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.concurrentLoad.test.js +333 -1
- package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
- package/dist/tests/sync.garbageCollection.test.js +4 -0
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +19 -0
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +1 -0
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +41 -3
- package/dist/tests/sync.multipleServers.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +2 -0
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +1 -0
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
- package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
- package/dist/tests/sync.storageReconciliation.test.js +502 -0
- package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
- package/dist/tests/testUtils.d.ts +1 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +3 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/OngoingStorageReconciliationTracker.ts +97 -0
- package/src/PeerState.ts +10 -3
- package/src/StorageReconciliationAckTracker.ts +83 -0
- package/src/SyncStateManager.ts +3 -3
- package/src/coValueCore/coValueCore.ts +47 -16
- package/src/coValues/coList.ts +23 -0
- package/src/coValues/group.ts +18 -0
- package/src/config.ts +18 -0
- package/src/exports.ts +8 -0
- package/src/localNode.ts +18 -0
- package/src/permissions.ts +1 -1
- package/src/queue/LinkedList.ts +10 -0
- package/src/queue/OutgoingLoadQueue.ts +57 -15
- package/src/queue/PriorityBasedMessageQueue.ts +15 -1
- package/src/storage/knownState.ts +14 -0
- package/src/storage/sqlite/client.ts +128 -0
- package/src/storage/sqlite/sqliteMigrations.ts +11 -0
- package/src/storage/sqliteAsync/client.ts +139 -0
- package/src/storage/storageAsync.ts +37 -0
- package/src/storage/storageSync.ts +41 -16
- package/src/storage/types.ts +110 -0
- package/src/sync.ts +359 -14
- package/src/tests/OngoingStorageReconciliationTracker.test.ts +85 -0
- package/src/tests/OutgoingLoadQueue.test.ts +226 -59
- package/src/tests/SQLiteClientAsync.test.ts +1 -1
- package/src/tests/StorageApiAsync.test.ts +161 -1
- package/src/tests/StorageApiSync.test.ts +176 -0
- package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
- package/src/tests/SyncStateManager.test.ts +25 -0
- package/src/tests/coList.test.ts +138 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
- package/src/tests/group.test.ts +87 -0
- package/src/tests/knownState.lazyLoading.test.ts +36 -1
- package/src/tests/messagesTestUtils.ts +4 -0
- package/src/tests/sync.concurrentLoad.test.ts +491 -0
- package/src/tests/sync.garbageCollection.test.ts +4 -0
- package/src/tests/sync.load.test.ts +26 -0
- package/src/tests/sync.mesh.test.ts +1 -0
- package/src/tests/sync.multipleServers.test.ts +60 -2
- package/src/tests/sync.storage.test.ts +2 -0
- package/src/tests/sync.storageAsync.test.ts +1 -0
- package/src/tests/sync.storageReconciliation.test.ts +696 -0
- package/src/tests/testUtils.ts +10 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { PeerState } from "../PeerState.js";
|
|
3
|
+
import { StorageReconciliationServerAckTracker } from "../StorageReconciliationAckTracker.js";
|
|
4
|
+
import { ConnectedPeerChannel } from "../streamUtils.js";
|
|
5
|
+
import { Peer } from "../sync.js";
|
|
6
|
+
|
|
7
|
+
function createPeerState(id = "peer-1"): PeerState {
|
|
8
|
+
const peer: Peer = {
|
|
9
|
+
id,
|
|
10
|
+
role: "server",
|
|
11
|
+
persistent: true,
|
|
12
|
+
incoming: new ConnectedPeerChannel(),
|
|
13
|
+
outgoing: new ConnectedPeerChannel(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return new PeerState(peer, undefined);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("StorageReconciliationAckTracker", () => {
|
|
20
|
+
test("tracks pending acks and returns next offset on ack", () => {
|
|
21
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
22
|
+
|
|
23
|
+
tracker.trackBatch("batch-1", "peer-1", 100);
|
|
24
|
+
|
|
25
|
+
expect(tracker.pendingReconciliationAck.get("batch-1#peer-1")).toBe(100);
|
|
26
|
+
|
|
27
|
+
const nextOffset = tracker.handleAck("batch-1", "peer-1");
|
|
28
|
+
|
|
29
|
+
expect(nextOffset).toBe(100);
|
|
30
|
+
expect(tracker.pendingReconciliationAck.has("batch-1#peer-1")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("invokes registered callback when ack is received", () => {
|
|
34
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
35
|
+
const peer = createPeerState("peer-1");
|
|
36
|
+
const onAck = vi.fn();
|
|
37
|
+
|
|
38
|
+
tracker.trackBatch("batch-1", peer.id, 50);
|
|
39
|
+
tracker.waitForAck("batch-1", peer, onAck);
|
|
40
|
+
|
|
41
|
+
expect(onAck).not.toHaveBeenCalled();
|
|
42
|
+
|
|
43
|
+
tracker.handleAck("batch-1", peer.id);
|
|
44
|
+
|
|
45
|
+
expect(onAck).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("invokes callback only once even if peer closes after ack", () => {
|
|
49
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
50
|
+
const peer = createPeerState("peer-1");
|
|
51
|
+
const onAck = vi.fn();
|
|
52
|
+
|
|
53
|
+
tracker.trackBatch("batch-1", peer.id, 50);
|
|
54
|
+
tracker.waitForAck("batch-1", peer, onAck);
|
|
55
|
+
tracker.handleAck("batch-1", peer.id);
|
|
56
|
+
peer.gracefulShutdown();
|
|
57
|
+
|
|
58
|
+
expect(onAck).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("invokes all listeners registered for a batch", () => {
|
|
62
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
63
|
+
const peer = createPeerState("peer-1");
|
|
64
|
+
const first = vi.fn();
|
|
65
|
+
const second = vi.fn();
|
|
66
|
+
|
|
67
|
+
tracker.trackBatch("batch-1", peer.id, 50);
|
|
68
|
+
tracker.waitForAck("batch-1", peer, first);
|
|
69
|
+
tracker.waitForAck("batch-1", peer, second);
|
|
70
|
+
tracker.handleAck("batch-1", peer.id);
|
|
71
|
+
|
|
72
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("calls callback immediately when batch is not pending", () => {
|
|
77
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
78
|
+
const peer = createPeerState("peer-1");
|
|
79
|
+
const onAck = vi.fn();
|
|
80
|
+
|
|
81
|
+
tracker.waitForAck("missing-batch", peer, onAck);
|
|
82
|
+
|
|
83
|
+
expect(onAck).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("aborts wait on peer close and clears pending ack", () => {
|
|
87
|
+
const tracker = new StorageReconciliationServerAckTracker();
|
|
88
|
+
const peer = createPeerState("peer-1");
|
|
89
|
+
const onAck = vi.fn();
|
|
90
|
+
|
|
91
|
+
tracker.trackBatch("batch-1", peer.id, 50);
|
|
92
|
+
tracker.waitForAck("batch-1", peer, onAck);
|
|
93
|
+
|
|
94
|
+
peer.gracefulShutdown();
|
|
95
|
+
|
|
96
|
+
expect(onAck).not.toHaveBeenCalled();
|
|
97
|
+
expect(tracker.pendingReconciliationAck.has("batch-1#peer-1")).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -128,6 +128,31 @@ describe("SyncStateManager", () => {
|
|
|
128
128
|
expect(subscriptionManager.isSynced(peerState, map.core.id)).toBe(true);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
test("isSynced should stay true for garbageCollected CoValues with matching knownState", async () => {
|
|
132
|
+
const client = setupTestNode({ connected: false });
|
|
133
|
+
client.addStorage({ ourName: "client" });
|
|
134
|
+
const { peerState } = client.connectToSyncServer();
|
|
135
|
+
|
|
136
|
+
const group = client.node.createGroup();
|
|
137
|
+
const map = group.createMap();
|
|
138
|
+
map.set("key1", "value1", "trusting");
|
|
139
|
+
|
|
140
|
+
const syncState = client.node.syncManager.syncState;
|
|
141
|
+
|
|
142
|
+
await waitFor(() => syncState.isSynced(peerState, map.core.id));
|
|
143
|
+
expect(syncState.isSynced(peerState, map.core.id)).toBe(true);
|
|
144
|
+
|
|
145
|
+
const mapCore = client.node.getCoValue(map.id);
|
|
146
|
+
const mapKnownState = mapCore.knownState();
|
|
147
|
+
client.node.internalDeleteCoValue(map.id);
|
|
148
|
+
|
|
149
|
+
const garbageCollectedMap = client.node.getCoValue(map.id);
|
|
150
|
+
garbageCollectedMap.setGarbageCollectedState(mapKnownState);
|
|
151
|
+
expect(garbageCollectedMap.loadingState).toBe("garbageCollected");
|
|
152
|
+
|
|
153
|
+
expect(syncState.isSynced(peerState, map.core.id)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
131
156
|
test("unsubscribe stops receiving updates", async () => {
|
|
132
157
|
// Setup nodes
|
|
133
158
|
const client = setupTestNode({ connected: true });
|
package/src/tests/coList.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
hotSleep,
|
|
6
6
|
loadCoValueOrFail,
|
|
7
7
|
nodeWithRandomAgentAndSessionID,
|
|
8
|
+
setupTestAccount,
|
|
8
9
|
setupTestNode,
|
|
9
10
|
waitFor,
|
|
10
11
|
} from "./testUtils.js";
|
|
@@ -1024,6 +1025,143 @@ test("the list should rebuild when the group permissions change", async () => {
|
|
|
1024
1025
|
expect(listOnBob.totalValidTransactions).toEqual(1);
|
|
1025
1026
|
});
|
|
1026
1027
|
|
|
1028
|
+
describe("CoList restricted deletion", () => {
|
|
1029
|
+
test("default ownedByGroup behavior is unchanged (writers can delete)", async () => {
|
|
1030
|
+
const alice = await setupTestAccount({ connected: true });
|
|
1031
|
+
const bob = await setupTestAccount({ connected: true });
|
|
1032
|
+
|
|
1033
|
+
const group = alice.node.createGroup();
|
|
1034
|
+
group.addMember(bob.account, "writer");
|
|
1035
|
+
|
|
1036
|
+
const listCore = alice.node.createCoValue({
|
|
1037
|
+
type: "colist",
|
|
1038
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
1039
|
+
meta: null,
|
|
1040
|
+
...Crypto.createdNowUnique(),
|
|
1041
|
+
});
|
|
1042
|
+
const list = expectList(listCore.getCurrentContent());
|
|
1043
|
+
list.append("item");
|
|
1044
|
+
|
|
1045
|
+
const listOnBob = await loadCoValueOrFail(bob.node, list.id);
|
|
1046
|
+
listOnBob.delete(0);
|
|
1047
|
+
|
|
1048
|
+
await waitFor(() => {
|
|
1049
|
+
expect(list.toJSON()).toEqual([]);
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("with restrictDeletion enabled, writers can append but cannot delete or replace", async () => {
|
|
1054
|
+
const alice = await setupTestAccount({ connected: true });
|
|
1055
|
+
const bob = await setupTestAccount({ connected: true });
|
|
1056
|
+
|
|
1057
|
+
const group = alice.node.createGroup();
|
|
1058
|
+
group.addMember(bob.account, "writer");
|
|
1059
|
+
|
|
1060
|
+
const listCore = alice.node.createCoValue({
|
|
1061
|
+
type: "colist",
|
|
1062
|
+
ruleset: {
|
|
1063
|
+
type: "ownedByGroup",
|
|
1064
|
+
group: group.id,
|
|
1065
|
+
restrictDeletion: true,
|
|
1066
|
+
},
|
|
1067
|
+
meta: null,
|
|
1068
|
+
...Crypto.createdNowUnique(),
|
|
1069
|
+
});
|
|
1070
|
+
const list = expectList(listCore.getCurrentContent());
|
|
1071
|
+
list.append("seed");
|
|
1072
|
+
|
|
1073
|
+
const listOnBob = await loadCoValueOrFail(bob.node, list.id);
|
|
1074
|
+
|
|
1075
|
+
listOnBob.append("writer-append");
|
|
1076
|
+
await waitFor(() => {
|
|
1077
|
+
expect(list.toJSON()).toEqual(["seed", "writer-append"]);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
listOnBob.delete(0);
|
|
1081
|
+
await waitFor(() => {
|
|
1082
|
+
expect(list.toJSON()).toEqual(["seed", "writer-append"]);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
listOnBob.replace(1, "writer-replace-attempt");
|
|
1086
|
+
await waitFor(() => {
|
|
1087
|
+
expect(list.toJSON()).toEqual(["seed", "writer-append"]);
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
test("with restrictDeletion enabled, managers and admins can delete", async () => {
|
|
1092
|
+
const alice = await setupTestAccount({ connected: true });
|
|
1093
|
+
const bob = await setupTestAccount({ connected: true });
|
|
1094
|
+
|
|
1095
|
+
const group = alice.node.createGroup();
|
|
1096
|
+
group.addMember(bob.account, "manager");
|
|
1097
|
+
|
|
1098
|
+
const listCore = alice.node.createCoValue({
|
|
1099
|
+
type: "colist",
|
|
1100
|
+
ruleset: {
|
|
1101
|
+
type: "ownedByGroup",
|
|
1102
|
+
group: group.id,
|
|
1103
|
+
restrictDeletion: true,
|
|
1104
|
+
},
|
|
1105
|
+
meta: null,
|
|
1106
|
+
...Crypto.createdNowUnique(),
|
|
1107
|
+
});
|
|
1108
|
+
const list = expectList(listCore.getCurrentContent());
|
|
1109
|
+
list.appendItems(["first", "second", "third"]);
|
|
1110
|
+
|
|
1111
|
+
const listOnBob = await loadCoValueOrFail(bob.node, list.id);
|
|
1112
|
+
listOnBob.delete(1);
|
|
1113
|
+
|
|
1114
|
+
await waitFor(() => {
|
|
1115
|
+
expect(list.toJSON()).toEqual(["first", "third"]);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
list.delete(0);
|
|
1119
|
+
await waitFor(() => {
|
|
1120
|
+
expect(list.toJSON()).toEqual(["third"]);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("deletions made while writer stay blocked after promotion", async () => {
|
|
1125
|
+
const alice = await setupTestAccount({ connected: true });
|
|
1126
|
+
const bob = await setupTestAccount({ connected: true });
|
|
1127
|
+
|
|
1128
|
+
const group = alice.node.createGroup();
|
|
1129
|
+
group.addMember(bob.account, "writer");
|
|
1130
|
+
|
|
1131
|
+
const listCore = alice.node.createCoValue({
|
|
1132
|
+
type: "colist",
|
|
1133
|
+
ruleset: {
|
|
1134
|
+
type: "ownedByGroup",
|
|
1135
|
+
group: group.id,
|
|
1136
|
+
restrictDeletion: true,
|
|
1137
|
+
},
|
|
1138
|
+
meta: null,
|
|
1139
|
+
...Crypto.createdNowUnique(),
|
|
1140
|
+
});
|
|
1141
|
+
const list = expectList(listCore.getCurrentContent());
|
|
1142
|
+
list.append("item");
|
|
1143
|
+
|
|
1144
|
+
const listOnBob = await loadCoValueOrFail(bob.node, list.id);
|
|
1145
|
+
listOnBob.delete(0);
|
|
1146
|
+
|
|
1147
|
+
await waitFor(() => {
|
|
1148
|
+
expect(list.toJSON()).toEqual(["item"]);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
group.addMember(bob.account, "manager");
|
|
1152
|
+
|
|
1153
|
+
await waitFor(() => {
|
|
1154
|
+
expect(list.toJSON()).toEqual(["item"]);
|
|
1155
|
+
expect(listOnBob.toJSON()).toEqual(["item"]);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
listOnBob.delete(0);
|
|
1159
|
+
await waitFor(() => {
|
|
1160
|
+
expect(list.toJSON()).toEqual([]);
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1027
1165
|
test("items appended after a losing init transaction are preserved", async () => {
|
|
1028
1166
|
const alice = setupTestNode({ connected: true });
|
|
1029
1167
|
const bob = setupTestNode({ connected: true });
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { RawCoID } from "../ids";
|
|
3
3
|
import { PeerID } from "../sync";
|
|
4
|
-
import {
|
|
4
|
+
import type {
|
|
5
|
+
StorageAPI,
|
|
6
|
+
StorageReconciliationAcquireResult,
|
|
7
|
+
} from "../storage/types";
|
|
5
8
|
import {
|
|
6
9
|
createTestMetricReader,
|
|
7
10
|
createTestNode,
|
|
@@ -29,6 +32,12 @@ function setup() {
|
|
|
29
32
|
|
|
30
33
|
function createMockStorage(
|
|
31
34
|
opts: {
|
|
35
|
+
getCoValueIDs?: (
|
|
36
|
+
limit: number,
|
|
37
|
+
offset: number,
|
|
38
|
+
callback: (batch: { id: RawCoID }[]) => void,
|
|
39
|
+
) => void;
|
|
40
|
+
getCoValueCount?: (callback: (count: number) => void) => void;
|
|
32
41
|
load?: (
|
|
33
42
|
id: RawCoID,
|
|
34
43
|
callback: (data: any) => void,
|
|
@@ -50,9 +59,26 @@ function createMockStorage(
|
|
|
50
59
|
markDeleteAsValid?: (id: RawCoID) => void;
|
|
51
60
|
enableDeletedCoValuesErasure?: () => void;
|
|
52
61
|
eraseAllDeletedCoValues?: () => Promise<void>;
|
|
62
|
+
tryAcquireStorageReconciliationLock?: (
|
|
63
|
+
sessionId: string,
|
|
64
|
+
peerId: string,
|
|
65
|
+
callback: (result: StorageReconciliationAcquireResult) => void,
|
|
66
|
+
) => void;
|
|
67
|
+
renewStorageReconciliationLock?: (
|
|
68
|
+
sessionId: string,
|
|
69
|
+
peerId: string,
|
|
70
|
+
offset: number,
|
|
71
|
+
) => void;
|
|
72
|
+
releaseStorageReconciliationLock?: (
|
|
73
|
+
sessionId: string,
|
|
74
|
+
peerId: string,
|
|
75
|
+
callback?: () => void,
|
|
76
|
+
) => void;
|
|
53
77
|
} = {},
|
|
54
78
|
): StorageAPI {
|
|
55
79
|
return {
|
|
80
|
+
getCoValueIDs: opts.getCoValueIDs || vi.fn(),
|
|
81
|
+
getCoValueCount: opts.getCoValueCount || vi.fn(),
|
|
56
82
|
markDeleteAsValid: opts.markDeleteAsValid || vi.fn(),
|
|
57
83
|
enableDeletedCoValuesErasure: opts.enableDeletedCoValuesErasure || vi.fn(),
|
|
58
84
|
eraseAllDeletedCoValues: opts.eraseAllDeletedCoValues || vi.fn(),
|
|
@@ -67,6 +93,15 @@ function createMockStorage(
|
|
|
67
93
|
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
68
94
|
onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
|
|
69
95
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
96
|
+
tryAcquireStorageReconciliationLock:
|
|
97
|
+
opts.tryAcquireStorageReconciliationLock ||
|
|
98
|
+
vi.fn((_sessionId, _peerId, callback) =>
|
|
99
|
+
callback({ acquired: false as const, reason: "not_due" as const }),
|
|
100
|
+
),
|
|
101
|
+
renewStorageReconciliationLock:
|
|
102
|
+
opts.renewStorageReconciliationLock || vi.fn(),
|
|
103
|
+
releaseStorageReconciliationLock:
|
|
104
|
+
opts.releaseStorageReconciliationLock || vi.fn(),
|
|
70
105
|
};
|
|
71
106
|
}
|
|
72
107
|
|
|
@@ -518,6 +553,22 @@ describe("CoValueCore.loadFromStorage", () => {
|
|
|
518
553
|
|
|
519
554
|
expect(loadSpy).toHaveBeenCalledTimes(1);
|
|
520
555
|
});
|
|
556
|
+
|
|
557
|
+
test("should keep garbageCollected loadingState even when a peer is pending", () => {
|
|
558
|
+
const { state, node, id } = setup();
|
|
559
|
+
const storage = createMockStorage();
|
|
560
|
+
node.setStorage(storage);
|
|
561
|
+
|
|
562
|
+
state.setGarbageCollectedState({
|
|
563
|
+
id,
|
|
564
|
+
header: true,
|
|
565
|
+
sessions: {},
|
|
566
|
+
});
|
|
567
|
+
state.markPending("peer1");
|
|
568
|
+
|
|
569
|
+
expect(state.getLoadingStateForPeer("peer1")).toBe("pending");
|
|
570
|
+
expect(state.loadingState).toBe("garbageCollected");
|
|
571
|
+
});
|
|
521
572
|
});
|
|
522
573
|
|
|
523
574
|
describe("when state is onlyKnownState", () => {
|
|
@@ -576,6 +627,26 @@ describe("CoValueCore.loadFromStorage", () => {
|
|
|
576
627
|
|
|
577
628
|
expect(loadSpy).toHaveBeenCalledTimes(1);
|
|
578
629
|
});
|
|
630
|
+
|
|
631
|
+
test("should keep onlyKnownState loadingState even when a peer is pending", () => {
|
|
632
|
+
const { state, node, id } = setup();
|
|
633
|
+
const storage = createMockStorage({
|
|
634
|
+
loadKnownState: (id, callback) => {
|
|
635
|
+
callback({
|
|
636
|
+
id,
|
|
637
|
+
header: true,
|
|
638
|
+
sessions: { session1: 1 },
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
node.setStorage(storage);
|
|
643
|
+
|
|
644
|
+
state.getKnownStateFromStorage(() => {});
|
|
645
|
+
state.markPending("peer1");
|
|
646
|
+
|
|
647
|
+
expect(state.getLoadingStateForPeer("peer1")).toBe("pending");
|
|
648
|
+
expect(state.loadingState).toBe("onlyKnownState");
|
|
649
|
+
});
|
|
579
650
|
});
|
|
580
651
|
|
|
581
652
|
describe("edge cases and integration", () => {
|
package/src/tests/group.test.ts
CHANGED
|
@@ -435,6 +435,93 @@ test("Should heal the missing key_for_everyone", async () => {
|
|
|
435
435
|
);
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
+
describe("reader cannot create content", () => {
|
|
439
|
+
test("createMap throws for reader", async () => {
|
|
440
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
441
|
+
|
|
442
|
+
const group = node1.node.createGroup();
|
|
443
|
+
group.addMember(
|
|
444
|
+
await loadCoValueOrFail(node1.node, node2.accountID),
|
|
445
|
+
"reader",
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await group.core.waitForSync();
|
|
449
|
+
|
|
450
|
+
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
|
451
|
+
expect(groupOnNode2.myRole()).toBe("reader");
|
|
452
|
+
expect(() => groupOnNode2.createMap()).toThrow(
|
|
453
|
+
"does not have write permissions",
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("createList throws for reader", async () => {
|
|
458
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
459
|
+
|
|
460
|
+
const group = node1.node.createGroup();
|
|
461
|
+
group.addMember(
|
|
462
|
+
await loadCoValueOrFail(node1.node, node2.accountID),
|
|
463
|
+
"reader",
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
await group.core.waitForSync();
|
|
467
|
+
|
|
468
|
+
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
|
469
|
+
expect(() => groupOnNode2.createList()).toThrow(
|
|
470
|
+
"does not have write permissions",
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("createStream throws for reader", async () => {
|
|
475
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
476
|
+
|
|
477
|
+
const group = node1.node.createGroup();
|
|
478
|
+
group.addMember(
|
|
479
|
+
await loadCoValueOrFail(node1.node, node2.accountID),
|
|
480
|
+
"reader",
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
await group.core.waitForSync();
|
|
484
|
+
|
|
485
|
+
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
|
486
|
+
expect(() => groupOnNode2.createStream()).toThrow(
|
|
487
|
+
"does not have write permissions",
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("createBinaryStream throws for reader", async () => {
|
|
492
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
493
|
+
|
|
494
|
+
const group = node1.node.createGroup();
|
|
495
|
+
group.addMember(
|
|
496
|
+
await loadCoValueOrFail(node1.node, node2.accountID),
|
|
497
|
+
"reader",
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
await group.core.waitForSync();
|
|
501
|
+
|
|
502
|
+
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
|
503
|
+
expect(() => groupOnNode2.createBinaryStream()).toThrow(
|
|
504
|
+
"does not have write permissions",
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("writer can still create content", async () => {
|
|
509
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
510
|
+
|
|
511
|
+
const group = node1.node.createGroup();
|
|
512
|
+
group.addMember(
|
|
513
|
+
await loadCoValueOrFail(node1.node, node2.accountID),
|
|
514
|
+
"writer",
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
await group.core.waitForSync();
|
|
518
|
+
|
|
519
|
+
const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
|
|
520
|
+
const map = groupOnNode2.createMap();
|
|
521
|
+
expect(map.type).toBe("comap");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
438
525
|
describe("writeOnly", () => {
|
|
439
526
|
test("Admins can invite writeOnly members", async () => {
|
|
440
527
|
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { RawCoID, SessionID } from "../ids";
|
|
3
3
|
import { PeerID } from "../sync";
|
|
4
|
-
import {
|
|
4
|
+
import type {
|
|
5
|
+
StorageAPI,
|
|
6
|
+
StorageReconciliationAcquireResult,
|
|
7
|
+
} from "../storage/types";
|
|
5
8
|
import { CoValueKnownState, peerHasAllContent } from "../knownState";
|
|
6
9
|
import { createTestNode, createUnloadedCoValue } from "./testUtils";
|
|
7
10
|
|
|
8
11
|
function createMockStorage(
|
|
9
12
|
opts: {
|
|
13
|
+
getCoValueIDs?: (
|
|
14
|
+
limit: number,
|
|
15
|
+
offset: number,
|
|
16
|
+
callback: (batch: { id: RawCoID }[]) => void,
|
|
17
|
+
) => void;
|
|
18
|
+
getCoValueCount?: (callback: (count: number) => void) => void;
|
|
10
19
|
load?: (
|
|
11
20
|
id: RawCoID,
|
|
12
21
|
callback: (data: any) => void,
|
|
@@ -28,9 +37,26 @@ function createMockStorage(
|
|
|
28
37
|
markDeleteAsValid?: (id: RawCoID) => void;
|
|
29
38
|
enableDeletedCoValuesErasure?: () => void;
|
|
30
39
|
eraseAllDeletedCoValues?: () => Promise<void>;
|
|
40
|
+
tryAcquireStorageReconciliationLock?: (
|
|
41
|
+
sessionId: string,
|
|
42
|
+
peerId: string,
|
|
43
|
+
callback: (result: StorageReconciliationAcquireResult) => void,
|
|
44
|
+
) => void;
|
|
45
|
+
renewStorageReconciliationLock?: (
|
|
46
|
+
sessionId: string,
|
|
47
|
+
peerId: string,
|
|
48
|
+
offset: number,
|
|
49
|
+
) => void;
|
|
50
|
+
releaseStorageReconciliationLock?: (
|
|
51
|
+
sessionId: string,
|
|
52
|
+
peerId: string,
|
|
53
|
+
callback?: () => void,
|
|
54
|
+
) => void;
|
|
31
55
|
} = {},
|
|
32
56
|
): StorageAPI {
|
|
33
57
|
return {
|
|
58
|
+
getCoValueIDs: opts.getCoValueIDs || vi.fn(),
|
|
59
|
+
getCoValueCount: opts.getCoValueCount || vi.fn(),
|
|
34
60
|
markDeleteAsValid: opts.markDeleteAsValid || vi.fn(),
|
|
35
61
|
enableDeletedCoValuesErasure: opts.enableDeletedCoValuesErasure || vi.fn(),
|
|
36
62
|
eraseAllDeletedCoValues: opts.eraseAllDeletedCoValues || vi.fn(),
|
|
@@ -45,6 +71,15 @@ function createMockStorage(
|
|
|
45
71
|
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
46
72
|
onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
|
|
47
73
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
74
|
+
tryAcquireStorageReconciliationLock:
|
|
75
|
+
opts.tryAcquireStorageReconciliationLock ||
|
|
76
|
+
vi.fn((_sessionId, _peerId, callback) =>
|
|
77
|
+
callback({ acquired: false as const, reason: "not_due" as const }),
|
|
78
|
+
),
|
|
79
|
+
renewStorageReconciliationLock:
|
|
80
|
+
opts.renewStorageReconciliationLock || vi.fn(),
|
|
81
|
+
releaseStorageReconciliationLock:
|
|
82
|
+
opts.releaseStorageReconciliationLock || vi.fn(),
|
|
48
83
|
};
|
|
49
84
|
}
|
|
50
85
|
|
|
@@ -60,6 +60,10 @@ export function toSimplifiedMessages(
|
|
|
60
60
|
return `${from} -> ${to} | GET_KNOWN_STATE ${getCoValue(msg.id)}`;
|
|
61
61
|
case "lazyLoadResult":
|
|
62
62
|
return `${from} -> ${to} | GET_KNOWN_STATE_RESULT ${getCoValue(msg.id)} sessions: ${simplifySessions(msg)}`;
|
|
63
|
+
case "reconcile":
|
|
64
|
+
return `${from} -> ${to} | RECONCILE`;
|
|
65
|
+
case "reconcile-ack":
|
|
66
|
+
return `${from} -> ${to} | RECONCILE_ACK`;
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|