cojson 0.20.7 → 0.20.8
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 +18 -0
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +0 -2
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/base64url.d.ts +15 -0
- package/dist/base64url.d.ts.map +1 -1
- package/dist/base64url.js +101 -5
- package/dist/base64url.js.map +1 -1
- package/dist/base64url.test.js +76 -1
- package/dist/base64url.test.js.map +1 -1
- package/dist/coValue.d.ts +2 -1
- package/dist/coValue.d.ts.map +1 -1
- package/dist/coValue.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +9 -11
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +92 -65
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +38 -7
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +226 -30
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/binaryCoStream.d.ts +63 -0
- package/dist/coValues/binaryCoStream.d.ts.map +1 -0
- package/dist/coValues/binaryCoStream.js +125 -0
- package/dist/coValues/binaryCoStream.js.map +1 -0
- package/dist/coValues/coList.d.ts +3 -1
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +15 -6
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.d.ts +1 -1
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coMap.js +2 -2
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/coStream.d.ts +0 -38
- package/dist/coValues/coStream.d.ts.map +1 -1
- package/dist/coValues/coStream.js +0 -86
- package/dist/coValues/coStream.js.map +1 -1
- package/dist/coValues/group.d.ts +44 -6
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +198 -17
- package/dist/coValues/group.js.map +1 -1
- package/dist/coreToCoValue.d.ts +2 -1
- package/dist/coreToCoValue.d.ts.map +1 -1
- package/dist/coreToCoValue.js +2 -1
- package/dist/coreToCoValue.js.map +1 -1
- package/dist/crypto/NapiCrypto.d.ts +18 -24
- package/dist/crypto/NapiCrypto.d.ts.map +1 -1
- package/dist/crypto/NapiCrypto.js +98 -60
- package/dist/crypto/NapiCrypto.js.map +1 -1
- package/dist/crypto/RNCrypto.d.ts +16 -3
- package/dist/crypto/RNCrypto.d.ts.map +1 -1
- package/dist/crypto/RNCrypto.js +117 -54
- package/dist/crypto/RNCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.d.ts +18 -24
- package/dist/crypto/WasmCrypto.d.ts.map +1 -1
- package/dist/crypto/WasmCrypto.js +100 -61
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/crypto.d.ts +55 -19
- package/dist/crypto/crypto.d.ts.map +1 -1
- package/dist/crypto/crypto.js +14 -3
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/exports.d.ts +7 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +4 -2
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +3 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +10 -3
- package/dist/localNode.js.map +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/permissions.d.ts +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +19 -3
- package/dist/permissions.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +24 -12
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +70 -58
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/sqliteAsync/types.d.ts +1 -1
- package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
- package/dist/storage/types.d.ts +1 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/tests/CojsonMessageChannel.test.js +2 -2
- package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
- package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
- package/dist/tests/SQLiteClientAsync.test.js +64 -0
- package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
- package/dist/tests/StorageApiAsync.test.js +2 -8
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +2 -2
- package/dist/tests/WasmCrypto.test.js +1 -15
- package/dist/tests/WasmCrypto.test.js.map +1 -1
- package/dist/tests/coList.test.js +24 -5
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coStream.test.js +4 -3
- package/dist/tests/coStream.test.js.map +1 -1
- package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
- package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
- package/dist/tests/coValueCore.initTransaction.test.js +438 -0
- package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
- package/dist/tests/coValueCore.test.js +11 -19
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/crypto.test.js +83 -0
- package/dist/tests/crypto.test.js.map +1 -1
- package/dist/tests/deleteCoValue.test.js +5 -5
- package/dist/tests/deleteCoValue.test.js.map +1 -1
- package/dist/tests/group.inheritance.test.js +11 -0
- package/dist/tests/group.inheritance.test.js.map +1 -1
- package/dist/tests/group.test.js +24 -1
- package/dist/tests/group.test.js.map +1 -1
- package/dist/tests/groupSealer.test.d.ts +2 -0
- package/dist/tests/groupSealer.test.d.ts.map +1 -0
- package/dist/tests/groupSealer.test.js +913 -0
- package/dist/tests/groupSealer.test.js.map +1 -0
- package/dist/tests/setup.js +5 -0
- package/dist/tests/setup.js.map +1 -1
- package/dist/tests/sync.auth.test.js +10 -10
- package/dist/tests/sync.concurrentLoad.test.js +12 -12
- package/dist/tests/sync.deleted.test.js +8 -8
- package/dist/tests/sync.garbageCollection.test.js +10 -10
- package/dist/tests/sync.invite.test.js +12 -12
- package/dist/tests/sync.known.test.js +2 -2
- package/dist/tests/sync.load.test.js +107 -107
- package/dist/tests/sync.mesh.test.js +164 -46
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +43 -43
- package/dist/tests/sync.peerReconciliation.test.js +29 -29
- package/dist/tests/sync.sharding.test.js +3 -3
- package/dist/tests/sync.storage.test.js +104 -104
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +56 -56
- package/dist/tests/sync.upload.test.js +22 -22
- package/dist/tests/testStorage.d.ts +2 -0
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +30 -6
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/typeUtils/isCoValue.js +1 -1
- package/dist/typeUtils/isCoValue.js.map +1 -1
- package/package.json +4 -4
- package/src/SyncStateManager.ts +0 -2
- package/src/base64url.test.ts +89 -1
- package/src/base64url.ts +134 -6
- package/src/coValue.ts +2 -1
- package/src/coValueCore/coValueCore.ts +126 -84
- package/src/coValueCore/verifiedState.ts +335 -53
- package/src/coValues/binaryCoStream.ts +217 -0
- package/src/coValues/coList.ts +21 -8
- package/src/coValues/coMap.ts +3 -0
- package/src/coValues/coStream.ts +0 -170
- package/src/coValues/group.ts +270 -21
- package/src/coreToCoValue.ts +2 -1
- package/src/crypto/NapiCrypto.ts +198 -95
- package/src/crypto/RNCrypto.ts +229 -102
- package/src/crypto/WasmCrypto.ts +201 -95
- package/src/crypto/crypto.ts +118 -45
- package/src/exports.ts +11 -5
- package/src/localNode.ts +17 -1
- package/src/media.ts +1 -1
- package/src/permissions.ts +30 -7
- package/src/storage/sqliteAsync/client.ts +136 -115
- package/src/storage/sqliteAsync/types.ts +3 -1
- package/src/storage/types.ts +4 -0
- package/src/sync.ts +10 -1
- package/src/tests/CojsonMessageChannel.test.ts +2 -2
- package/src/tests/SQLiteClientAsync.test.ts +75 -0
- package/src/tests/StorageApiAsync.test.ts +4 -9
- package/src/tests/SyncStateManager.test.ts +2 -2
- package/src/tests/WasmCrypto.test.ts +1 -25
- package/src/tests/coList.test.ts +39 -5
- package/src/tests/coStream.test.ts +4 -5
- package/src/tests/coValueCore.initTransaction.test.ts +836 -0
- package/src/tests/coValueCore.test.ts +11 -22
- package/src/tests/crypto.test.ts +107 -0
- package/src/tests/deleteCoValue.test.ts +5 -5
- package/src/tests/group.inheritance.test.ts +16 -0
- package/src/tests/group.test.ts +29 -1
- package/src/tests/groupSealer.test.ts +1473 -0
- package/src/tests/setup.ts +6 -0
- package/src/tests/sync.auth.test.ts +10 -10
- package/src/tests/sync.concurrentLoad.test.ts +12 -12
- package/src/tests/sync.deleted.test.ts +8 -8
- package/src/tests/sync.garbageCollection.test.ts +10 -10
- package/src/tests/sync.invite.test.ts +12 -12
- package/src/tests/sync.known.test.ts +2 -2
- package/src/tests/sync.load.test.ts +107 -107
- package/src/tests/sync.mesh.test.ts +189 -46
- package/src/tests/sync.multipleServers.test.ts +43 -43
- package/src/tests/sync.peerReconciliation.test.ts +29 -29
- package/src/tests/sync.sharding.test.ts +3 -3
- package/src/tests/sync.storage.test.ts +104 -104
- package/src/tests/sync.storageAsync.test.ts +56 -56
- package/src/tests/sync.upload.test.ts +22 -22
- package/src/tests/testStorage.ts +39 -9
- package/src/typeUtils/isCoValue.ts +1 -1
- package/dist/coValueCore/SessionMap.d.ts +0 -55
- package/dist/coValueCore/SessionMap.d.ts.map +0 -1
- package/dist/coValueCore/SessionMap.js +0 -206
- package/dist/coValueCore/SessionMap.js.map +0 -1
- package/dist/tests/coreWasm.test.d.ts +0 -2
- package/dist/tests/coreWasm.test.d.ts.map +0 -1
- package/dist/tests/coreWasm.test.js +0 -203
- package/dist/tests/coreWasm.test.js.map +0 -1
- package/src/coValueCore/SessionMap.ts +0 -394
- package/src/tests/coreWasm.test.ts +0 -452
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
3
|
+
import {
|
|
4
|
+
createTwoConnectedNodes,
|
|
5
|
+
importContentIntoNode,
|
|
6
|
+
loadCoValueOrFail,
|
|
7
|
+
setupTestNode,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from "./testUtils.js";
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
setupTestNode({ isSyncServer: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("init transaction meta", () => {
|
|
16
|
+
test("a transaction with init meta is parsed correctly", () => {
|
|
17
|
+
const client = setupTestNode();
|
|
18
|
+
const group = client.node.createGroup();
|
|
19
|
+
const map = group.createMap();
|
|
20
|
+
|
|
21
|
+
map.core.makeTransaction(
|
|
22
|
+
[{ op: "set", key: "hello", value: "world" }],
|
|
23
|
+
"trusting",
|
|
24
|
+
{ fww: "init" },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const transactions = map.core.getValidSortedTransactions();
|
|
28
|
+
expect(transactions).toHaveLength(1);
|
|
29
|
+
expect(transactions[0]?.meta).toEqual({ fww: "init" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("first-init-wins: only the first init transaction is valid", () => {
|
|
33
|
+
const client = setupTestNode();
|
|
34
|
+
const group = client.node.createGroup();
|
|
35
|
+
const map = group.createMap();
|
|
36
|
+
|
|
37
|
+
// Make two init transactions with different timestamps
|
|
38
|
+
// The first one (earlier madeAt) should win
|
|
39
|
+
const earlierTime = Date.now();
|
|
40
|
+
map.core.makeTransaction(
|
|
41
|
+
[{ op: "set", key: "version", value: "first" }],
|
|
42
|
+
"trusting",
|
|
43
|
+
{ fww: "init" },
|
|
44
|
+
earlierTime,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const laterTime = earlierTime + 100;
|
|
48
|
+
map.core.makeTransaction(
|
|
49
|
+
[{ op: "set", key: "version", value: "second" }],
|
|
50
|
+
"trusting",
|
|
51
|
+
{ fww: "init" },
|
|
52
|
+
laterTime,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const validTransactions = map.core.getValidSortedTransactions();
|
|
56
|
+
|
|
57
|
+
// Only the first init transaction should be valid
|
|
58
|
+
expect(validTransactions).toHaveLength(1);
|
|
59
|
+
expect(validTransactions[0]?.meta).toEqual({ fww: "init" });
|
|
60
|
+
|
|
61
|
+
// The first transaction (earlier madeAt) should be the valid one
|
|
62
|
+
expect(validTransactions[0]?.madeAt).toBe(earlierTime);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("first-init-wins: transactions without init meta are not affected", () => {
|
|
66
|
+
const client = setupTestNode();
|
|
67
|
+
const group = client.node.createGroup();
|
|
68
|
+
const map = group.createMap();
|
|
69
|
+
|
|
70
|
+
// Make an init transaction
|
|
71
|
+
map.core.makeTransaction(
|
|
72
|
+
[{ op: "set", key: "version", value: "init" }],
|
|
73
|
+
"trusting",
|
|
74
|
+
{ fww: "init" },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Make a regular transaction (no init meta)
|
|
78
|
+
map.core.makeTransaction(
|
|
79
|
+
[{ op: "set", key: "hello", value: "world" }],
|
|
80
|
+
"trusting",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const validTransactions = map.core.getValidSortedTransactions();
|
|
84
|
+
|
|
85
|
+
// Both transactions should be valid
|
|
86
|
+
expect(validTransactions).toHaveLength(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("late-arriving winner triggers content rebuild", async () => {
|
|
90
|
+
const client = setupTestNode({ connected: true });
|
|
91
|
+
const clientSession2 = client.spawnNewSession();
|
|
92
|
+
const group = client.node.createGroup();
|
|
93
|
+
const map = group.createMap();
|
|
94
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
95
|
+
clientSession2.node,
|
|
96
|
+
map.id,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Make an init transaction with a later timestamp
|
|
100
|
+
const laterTime = Date.now() + 1000;
|
|
101
|
+
map.core.makeTransaction(
|
|
102
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
103
|
+
"trusting",
|
|
104
|
+
{ fww: "init" },
|
|
105
|
+
laterTime,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(map.get("version")).toBe("later");
|
|
109
|
+
|
|
110
|
+
const rebuildSpy = vi.spyOn(map, "rebuildFromCore");
|
|
111
|
+
|
|
112
|
+
// Now make an init transaction with an earlier timestamp (this should win)
|
|
113
|
+
const earlierTime = laterTime - 500;
|
|
114
|
+
mapOnClientSession2.core.makeTransaction(
|
|
115
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
116
|
+
"trusting",
|
|
117
|
+
{ fww: "init" },
|
|
118
|
+
earlierTime,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(map.core.knownState()).toEqual(
|
|
123
|
+
mapOnClientSession2.core.knownState(),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// The content should have been rebuilt
|
|
128
|
+
expect(rebuildSpy).toHaveBeenCalled();
|
|
129
|
+
|
|
130
|
+
expect(map.get("version")).toBe("earlier");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("two init transactions coming together do not trigger content rebuild", async () => {
|
|
134
|
+
const alice = setupTestNode({ connected: true });
|
|
135
|
+
const aliceSession2 = alice.spawnNewSession();
|
|
136
|
+
const bob = setupTestNode({ connected: false });
|
|
137
|
+
const group = alice.node.createGroup();
|
|
138
|
+
group.addMember("everyone", "writer");
|
|
139
|
+
const map = group.createMap();
|
|
140
|
+
|
|
141
|
+
importContentIntoNode(group.core, bob.node);
|
|
142
|
+
importContentIntoNode(map.core, bob.node);
|
|
143
|
+
|
|
144
|
+
const mapOnBob = bob.node.getCoValue(map.id);
|
|
145
|
+
const rebuildSpy = vi.spyOn(mapOnBob, "scheduleContentRebuild");
|
|
146
|
+
|
|
147
|
+
const mapOnAliceSession2 = await loadCoValueOrFail(
|
|
148
|
+
aliceSession2.node,
|
|
149
|
+
map.id,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Make an init transaction with a later timestamp
|
|
153
|
+
const laterTime = Date.now() + 1000;
|
|
154
|
+
map.core.makeTransaction(
|
|
155
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
156
|
+
"trusting",
|
|
157
|
+
{ fww: "init" },
|
|
158
|
+
laterTime,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(map.get("version")).toBe("later");
|
|
162
|
+
|
|
163
|
+
// Now make an init transaction with an earlier timestamp (this should win)
|
|
164
|
+
const earlierTime = laterTime - 500;
|
|
165
|
+
mapOnAliceSession2.core.makeTransaction(
|
|
166
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
167
|
+
"trusting",
|
|
168
|
+
{ fww: "init" },
|
|
169
|
+
earlierTime,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(map.core.knownState()).toEqual(
|
|
174
|
+
mapOnAliceSession2.core.knownState(),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
importContentIntoNode(map.core, bob.node);
|
|
179
|
+
|
|
180
|
+
// The content should have been rebuilt
|
|
181
|
+
expect(rebuildSpy).not.toHaveBeenCalled();
|
|
182
|
+
|
|
183
|
+
expect(mapOnBob.getCurrentContent().toJSON()).toEqual({
|
|
184
|
+
version: "earlier",
|
|
185
|
+
});
|
|
186
|
+
expect(map.get("version")).toBe("earlier");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("content reflects the winning init transaction after rebuild", async () => {
|
|
190
|
+
const client = setupTestNode({ connected: true });
|
|
191
|
+
const clientSession2 = client.spawnNewSession();
|
|
192
|
+
const group = client.node.createGroup();
|
|
193
|
+
const map = group.createMap();
|
|
194
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
195
|
+
clientSession2.node,
|
|
196
|
+
map.id,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Make an init transaction with a later timestamp
|
|
200
|
+
const laterTime = Date.now() + 1000;
|
|
201
|
+
map.core.makeTransaction(
|
|
202
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
203
|
+
"trusting",
|
|
204
|
+
{ fww: "init" },
|
|
205
|
+
laterTime,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Verify initial content
|
|
209
|
+
expect(map.get("version")).toBe("later");
|
|
210
|
+
|
|
211
|
+
// Now make an init transaction with an earlier timestamp (this should win)
|
|
212
|
+
const earlierTime = laterTime - 500;
|
|
213
|
+
mapOnClientSession2.core.makeTransaction(
|
|
214
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
215
|
+
"trusting",
|
|
216
|
+
{ fww: "init" },
|
|
217
|
+
earlierTime,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(map.core.knownState()).toEqual(
|
|
222
|
+
mapOnClientSession2.core.knownState(),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// The content should reflect the earlier (winning) init transaction
|
|
227
|
+
expect(map.get("version")).toBe("earlier");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("losing init transaction is marked as invalid (different sessions)", async () => {
|
|
231
|
+
const client = setupTestNode({ connected: true });
|
|
232
|
+
const clientSession2 = client.spawnNewSession();
|
|
233
|
+
const group = client.node.createGroup();
|
|
234
|
+
const map = group.createMap();
|
|
235
|
+
|
|
236
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
237
|
+
clientSession2.node,
|
|
238
|
+
map.id,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const earlierTime = Date.now();
|
|
242
|
+
map.core.makeTransaction(
|
|
243
|
+
[{ op: "set", key: "version", value: "first" }],
|
|
244
|
+
"trusting",
|
|
245
|
+
{ fww: "init" },
|
|
246
|
+
earlierTime,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const laterTime = earlierTime + 100;
|
|
250
|
+
mapOnClientSession2.core.makeTransaction(
|
|
251
|
+
[{ op: "set", key: "version", value: "second" }],
|
|
252
|
+
"trusting",
|
|
253
|
+
{ fww: "init" },
|
|
254
|
+
laterTime,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await waitFor(() => {
|
|
258
|
+
expect(map.core.knownState()).toEqual(
|
|
259
|
+
mapOnClientSession2.core.knownState(),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Check the raw verified transactions
|
|
264
|
+
const allTransactions = map.core.verifiedTransactions;
|
|
265
|
+
expect(allTransactions).toHaveLength(2);
|
|
266
|
+
|
|
267
|
+
// The first transaction should be valid
|
|
268
|
+
const firstTx = allTransactions.find((tx) => tx.madeAt === earlierTime);
|
|
269
|
+
expect(firstTx?.isValid).toBe(true);
|
|
270
|
+
|
|
271
|
+
// The second transaction should be invalid
|
|
272
|
+
const secondTx = allTransactions.find((tx) => tx.madeAt === laterTime);
|
|
273
|
+
expect(secondTx?.isValid).toBe(false);
|
|
274
|
+
expect(secondTx?.validationErrorMessage).toBe(
|
|
275
|
+
`Transaction is not the first writer for fww key "init"`,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("losing init transaction is marked as invalid (same session)", () => {
|
|
280
|
+
const client = setupTestNode();
|
|
281
|
+
const group = client.node.createGroup();
|
|
282
|
+
const map = group.createMap();
|
|
283
|
+
|
|
284
|
+
const earlierTime = Date.now();
|
|
285
|
+
map.core.makeTransaction(
|
|
286
|
+
[{ op: "set", key: "version", value: "first" }],
|
|
287
|
+
"trusting",
|
|
288
|
+
{ fww: "init" },
|
|
289
|
+
earlierTime,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const laterTime = earlierTime + 100;
|
|
293
|
+
map.core.makeTransaction(
|
|
294
|
+
[{ op: "set", key: "version", value: "second" }],
|
|
295
|
+
"trusting",
|
|
296
|
+
{ fww: "init" },
|
|
297
|
+
laterTime,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Check the raw verified transactions
|
|
301
|
+
const allTransactions = map.core.verifiedTransactions;
|
|
302
|
+
expect(allTransactions).toHaveLength(2);
|
|
303
|
+
|
|
304
|
+
// The first transaction should be valid
|
|
305
|
+
const firstTx = allTransactions.find((tx) => tx.madeAt === earlierTime);
|
|
306
|
+
expect(firstTx?.isValid).toBe(true);
|
|
307
|
+
|
|
308
|
+
// The second transaction should be invalid
|
|
309
|
+
const secondTx = allTransactions.find((tx) => tx.madeAt === laterTime);
|
|
310
|
+
expect(secondTx?.isValid).toBe(false);
|
|
311
|
+
expect(secondTx?.validationErrorMessage).toBe(
|
|
312
|
+
`Transaction is not the first writer for fww key "init"`,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("validity change on processed transaction dispatches rebuild", async () => {
|
|
317
|
+
const client = setupTestNode({ connected: true });
|
|
318
|
+
const clientSession2 = client.spawnNewSession();
|
|
319
|
+
const group = client.node.createGroup();
|
|
320
|
+
const map = group.createMap();
|
|
321
|
+
|
|
322
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
323
|
+
clientSession2.node,
|
|
324
|
+
map.id,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Make an init transaction and process it
|
|
328
|
+
const laterTime = Date.now() + 1000;
|
|
329
|
+
map.core.makeTransaction(
|
|
330
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
331
|
+
"trusting",
|
|
332
|
+
{ fww: "init" },
|
|
333
|
+
laterTime,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Check the transaction is marked as processed
|
|
337
|
+
const laterTx = map.core.verifiedTransactions.find(
|
|
338
|
+
(tx) => tx.madeAt === laterTime,
|
|
339
|
+
);
|
|
340
|
+
expect(laterTx?.stage).toBe("processed");
|
|
341
|
+
expect(laterTx?.isValid).toBe(true);
|
|
342
|
+
|
|
343
|
+
// Add a new init transaction with an earlier timestamp
|
|
344
|
+
const earlierTime = laterTime - 500;
|
|
345
|
+
mapOnClientSession2.core.makeTransaction(
|
|
346
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
347
|
+
"trusting",
|
|
348
|
+
{ fww: "init" },
|
|
349
|
+
earlierTime,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(map.core.knownState()).toEqual(
|
|
354
|
+
mapOnClientSession2.core.knownState(),
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// The later transaction should now be invalid
|
|
359
|
+
expect(laterTx?.isValid).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("synced init transactions resolve correctly across nodes", async () => {
|
|
363
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
364
|
+
|
|
365
|
+
const group = node1.node.createGroup();
|
|
366
|
+
group.addMember("everyone", "writer");
|
|
367
|
+
|
|
368
|
+
const map = group.createMap();
|
|
369
|
+
|
|
370
|
+
// Make an init transaction on node1
|
|
371
|
+
map.core.makeTransaction(
|
|
372
|
+
[{ op: "set", key: "version", value: "node1" }],
|
|
373
|
+
"trusting",
|
|
374
|
+
{ fww: "init" },
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
await map.core.waitForSync();
|
|
378
|
+
|
|
379
|
+
// Load the map on node2
|
|
380
|
+
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
|
381
|
+
|
|
382
|
+
await waitFor(() => {
|
|
383
|
+
expect(mapOnNode2.get("version")).toBe("node1");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Both nodes should have the same valid transaction
|
|
387
|
+
const node1ValidTxs = map.core.getValidSortedTransactions();
|
|
388
|
+
const node2ValidTxs = mapOnNode2.core.getValidSortedTransactions();
|
|
389
|
+
|
|
390
|
+
expect(node1ValidTxs).toHaveLength(1);
|
|
391
|
+
expect(node2ValidTxs).toHaveLength(1);
|
|
392
|
+
expect(node1ValidTxs[0]?.madeAt).toBe(node2ValidTxs[0]?.madeAt);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("concurrent init transactions from different nodes resolve deterministically", async () => {
|
|
396
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
397
|
+
|
|
398
|
+
const group = node1.node.createGroup();
|
|
399
|
+
group.addMember("everyone", "writer");
|
|
400
|
+
|
|
401
|
+
const map = group.createMap();
|
|
402
|
+
|
|
403
|
+
await map.core.waitForSync();
|
|
404
|
+
|
|
405
|
+
// Load the map on node2
|
|
406
|
+
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
|
407
|
+
|
|
408
|
+
// Make init transactions on both nodes with different timestamps
|
|
409
|
+
const node1Time = Date.now();
|
|
410
|
+
const node2Time = node1Time + 1000; // node2 is later
|
|
411
|
+
|
|
412
|
+
map.core.makeTransaction(
|
|
413
|
+
[{ op: "set", key: "version", value: "node1" }],
|
|
414
|
+
"trusting",
|
|
415
|
+
{ fww: "init" },
|
|
416
|
+
node1Time,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
mapOnNode2.core.makeTransaction(
|
|
420
|
+
[{ op: "set", key: "version", value: "node2" }],
|
|
421
|
+
"trusting",
|
|
422
|
+
{ fww: "init" },
|
|
423
|
+
node2Time,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Wait for sync
|
|
427
|
+
await map.core.waitForSync();
|
|
428
|
+
await mapOnNode2.core.waitForSync();
|
|
429
|
+
|
|
430
|
+
// Wait for microtasks
|
|
431
|
+
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
432
|
+
|
|
433
|
+
// Both nodes should converge to the same winner (node1 with earlier timestamp)
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
expect(map.get("version")).toBe("node1");
|
|
436
|
+
expect(mapOnNode2.get("version")).toBe("node1");
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("subscription is notified when init transaction changes the content", async () => {
|
|
441
|
+
const client = setupTestNode();
|
|
442
|
+
const group = client.node.createGroup();
|
|
443
|
+
const map = group.createMap();
|
|
444
|
+
|
|
445
|
+
// Make an init transaction with a later timestamp
|
|
446
|
+
const laterTime = Date.now() + 1000;
|
|
447
|
+
map.core.makeTransaction(
|
|
448
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
449
|
+
"trusting",
|
|
450
|
+
{ fww: "init" },
|
|
451
|
+
laterTime,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Subscribe to changes
|
|
455
|
+
const subscriptionSpy = vi.fn();
|
|
456
|
+
const unsubscribe = map.subscribe(subscriptionSpy);
|
|
457
|
+
|
|
458
|
+
subscriptionSpy.mockClear();
|
|
459
|
+
|
|
460
|
+
// Add a new init transaction with an earlier timestamp
|
|
461
|
+
const earlierTime = laterTime - 500;
|
|
462
|
+
map.core.makeTransaction(
|
|
463
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
464
|
+
"trusting",
|
|
465
|
+
{ fww: "init" },
|
|
466
|
+
earlierTime,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Wait for notifications
|
|
470
|
+
await waitFor(() => {
|
|
471
|
+
expect(subscriptionSpy).toHaveBeenCalled();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
unsubscribe();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("getValidTransactions returns discarded init transactions when includeInvalidMetaTransactions is true", async () => {
|
|
478
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
479
|
+
|
|
480
|
+
const group = node1.node.createGroup();
|
|
481
|
+
group.addMember("everyone", "writer");
|
|
482
|
+
|
|
483
|
+
const map = group.createMap();
|
|
484
|
+
|
|
485
|
+
const earlierTime = Date.now();
|
|
486
|
+
const laterTime = earlierTime + 1000;
|
|
487
|
+
|
|
488
|
+
map.core.makeTransaction(
|
|
489
|
+
[{ op: "set", key: "version", value: "later" }],
|
|
490
|
+
"trusting",
|
|
491
|
+
{ fww: "init" },
|
|
492
|
+
laterTime,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
await map.core.waitForSync();
|
|
496
|
+
|
|
497
|
+
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
|
498
|
+
|
|
499
|
+
await waitFor(() => {
|
|
500
|
+
expect(mapOnNode2.get("version")).toBe("later");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
mapOnNode2.core.makeTransaction(
|
|
504
|
+
[{ op: "set", key: "version", value: "earlier" }],
|
|
505
|
+
"trusting",
|
|
506
|
+
{ fww: "init" },
|
|
507
|
+
earlierTime,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await waitFor(() => {
|
|
511
|
+
expect(map.core.knownState()).toEqual(mapOnNode2.core.knownState());
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Without flag: only valid transactions
|
|
515
|
+
const validOnly = map.core.getValidSortedTransactions();
|
|
516
|
+
expect(validOnly).toHaveLength(1);
|
|
517
|
+
expect(validOnly[0]?.madeAt).toBe(earlierTime);
|
|
518
|
+
|
|
519
|
+
// With flag: includes invalid init transactions
|
|
520
|
+
const withInvalid = map.core.getValidSortedTransactions({
|
|
521
|
+
includeInvalidMetaTransactions: true,
|
|
522
|
+
ignorePrivateTransactions: false,
|
|
523
|
+
});
|
|
524
|
+
expect(withInvalid).toHaveLength(2);
|
|
525
|
+
expect(withInvalid.filter((tx) => tx.isValid)).toHaveLength(1);
|
|
526
|
+
expect(withInvalid.filter((tx) => !tx.isValid)).toHaveLength(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("getValidTransactions({includeInvalidMetaTransactions: true}) does not return permission-invalid transactions", async () => {
|
|
530
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
531
|
+
|
|
532
|
+
const group = node1.node.createGroup();
|
|
533
|
+
const map = group.createMap();
|
|
534
|
+
|
|
535
|
+
group.addMember(node2.node.getCurrentAgent(), "reader");
|
|
536
|
+
|
|
537
|
+
map.set("key", "admin-value", "trusting");
|
|
538
|
+
|
|
539
|
+
await map.core.waitForSync();
|
|
540
|
+
|
|
541
|
+
const mapOnReader = await loadCoValueOrFail(node2.node, map.id);
|
|
542
|
+
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(mapOnReader.get("key")).toBe("admin-value");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
mapOnReader.set("key", "reader-value", "trusting");
|
|
548
|
+
|
|
549
|
+
// Permission-invalid transactions are never included
|
|
550
|
+
const allTx = mapOnReader.core.getValidSortedTransactions({
|
|
551
|
+
includeInvalidMetaTransactions: true,
|
|
552
|
+
ignorePrivateTransactions: false,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(allTx).toHaveLength(1);
|
|
556
|
+
expect(mapOnReader.get("key")).toBe("admin-value");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("different FWW keys are independent", () => {
|
|
560
|
+
const client = setupTestNode();
|
|
561
|
+
const group = client.node.createGroup();
|
|
562
|
+
const map = group.createMap();
|
|
563
|
+
|
|
564
|
+
// Make two FWW transactions with different keys
|
|
565
|
+
map.core.makeTransaction(
|
|
566
|
+
[{ op: "set", key: "keyA", value: "valueA" }],
|
|
567
|
+
"trusting",
|
|
568
|
+
{ fww: "keyA" },
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
map.core.makeTransaction(
|
|
572
|
+
[{ op: "set", key: "keyB", value: "valueB" }],
|
|
573
|
+
"trusting",
|
|
574
|
+
{ fww: "keyB" },
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const validTransactions = map.core.getValidSortedTransactions();
|
|
578
|
+
|
|
579
|
+
// Both transactions should be valid since they have different FWW keys
|
|
580
|
+
expect(validTransactions).toHaveLength(2);
|
|
581
|
+
expect(validTransactions[0]?.meta).toEqual({ fww: "keyA" });
|
|
582
|
+
expect(validTransactions[1]?.meta).toEqual({ fww: "keyB" });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("FWW at non-zero txIndex is valid", () => {
|
|
586
|
+
const client = setupTestNode();
|
|
587
|
+
const group = client.node.createGroup();
|
|
588
|
+
const map = group.createMap();
|
|
589
|
+
|
|
590
|
+
// Make a regular transaction first (no FWW meta)
|
|
591
|
+
map.core.makeTransaction(
|
|
592
|
+
[{ op: "set", key: "setup", value: "regular" }],
|
|
593
|
+
"trusting",
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// Make an FWW transaction at txIndex > 0
|
|
597
|
+
map.core.makeTransaction(
|
|
598
|
+
[{ op: "set", key: "version", value: "init" }],
|
|
599
|
+
"trusting",
|
|
600
|
+
{ fww: "init" },
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const validTransactions = map.core.getValidSortedTransactions();
|
|
604
|
+
|
|
605
|
+
// Both transactions should be valid
|
|
606
|
+
expect(validTransactions).toHaveLength(2);
|
|
607
|
+
|
|
608
|
+
// The FWW transaction should be valid even though it's not at txIndex 0
|
|
609
|
+
const fwwTx = validTransactions.find((tx) => tx.meta?.fww === "init");
|
|
610
|
+
expect(fwwTx).toBeDefined();
|
|
611
|
+
expect(fwwTx?.isValid).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("multiple FWW keys with different winners", async () => {
|
|
615
|
+
const client = setupTestNode({ connected: true });
|
|
616
|
+
const clientSession2 = client.spawnNewSession();
|
|
617
|
+
const group = client.node.createGroup();
|
|
618
|
+
const map = group.createMap();
|
|
619
|
+
|
|
620
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
621
|
+
clientSession2.node,
|
|
622
|
+
map.id,
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
const earlierTime = Date.now();
|
|
626
|
+
const laterTime = earlierTime + 100;
|
|
627
|
+
|
|
628
|
+
// Session 1: Make FWW transaction for "keyA" with earlier time
|
|
629
|
+
map.core.makeTransaction(
|
|
630
|
+
[{ op: "set", key: "keyA", value: "session1" }],
|
|
631
|
+
"trusting",
|
|
632
|
+
{ fww: "keyA" },
|
|
633
|
+
earlierTime,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Session 1: Make FWW transaction for "keyB" with later time
|
|
637
|
+
map.core.makeTransaction(
|
|
638
|
+
[{ op: "set", key: "keyB", value: "session1" }],
|
|
639
|
+
"trusting",
|
|
640
|
+
{ fww: "keyB" },
|
|
641
|
+
laterTime,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
// Session 2: Make FWW transaction for "keyA" with later time
|
|
645
|
+
mapOnClientSession2.core.makeTransaction(
|
|
646
|
+
[{ op: "set", key: "keyA", value: "session2" }],
|
|
647
|
+
"trusting",
|
|
648
|
+
{ fww: "keyA" },
|
|
649
|
+
laterTime,
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Session 2: Make FWW transaction for "keyB" with earlier time
|
|
653
|
+
mapOnClientSession2.core.makeTransaction(
|
|
654
|
+
[{ op: "set", key: "keyB", value: "session2" }],
|
|
655
|
+
"trusting",
|
|
656
|
+
{ fww: "keyB" },
|
|
657
|
+
earlierTime,
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
await waitFor(() => {
|
|
661
|
+
expect(map.core.knownState()).toEqual(
|
|
662
|
+
mapOnClientSession2.core.knownState(),
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Each key should independently pick its winner
|
|
667
|
+
// keyA: session1 wins (earlier time)
|
|
668
|
+
// keyB: session2 wins (earlier time)
|
|
669
|
+
const validTransactions = map.core.getValidSortedTransactions();
|
|
670
|
+
expect(validTransactions).toHaveLength(2);
|
|
671
|
+
|
|
672
|
+
const keyAWinner = validTransactions.find((tx) => tx.meta?.fww === "keyA");
|
|
673
|
+
const keyBWinner = validTransactions.find((tx) => tx.meta?.fww === "keyB");
|
|
674
|
+
|
|
675
|
+
expect(keyAWinner).toBeDefined();
|
|
676
|
+
expect(keyBWinner).toBeDefined();
|
|
677
|
+
|
|
678
|
+
// Verify the correct values are in the map
|
|
679
|
+
expect(map.get("keyA")).toBe("session1");
|
|
680
|
+
expect(map.get("keyB")).toBe("session2");
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("multiple FWW keys resolve independently across synced nodes", async () => {
|
|
684
|
+
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
|
685
|
+
|
|
686
|
+
const group = node1.node.createGroup();
|
|
687
|
+
group.addMember("everyone", "writer");
|
|
688
|
+
|
|
689
|
+
const map = group.createMap();
|
|
690
|
+
|
|
691
|
+
await map.core.waitForSync();
|
|
692
|
+
|
|
693
|
+
// Load the map on node2
|
|
694
|
+
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
|
|
695
|
+
|
|
696
|
+
const earlierTime = Date.now();
|
|
697
|
+
const laterTime = earlierTime + 1000;
|
|
698
|
+
|
|
699
|
+
// On node1: create FWW transaction for "keyA" with earlier time, "keyB" with later time
|
|
700
|
+
map.core.makeTransaction(
|
|
701
|
+
[{ op: "set", key: "keyA", value: "node1" }],
|
|
702
|
+
"trusting",
|
|
703
|
+
{ fww: "keyA" },
|
|
704
|
+
earlierTime,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
map.core.makeTransaction(
|
|
708
|
+
[{ op: "set", key: "keyB", value: "node1" }],
|
|
709
|
+
"trusting",
|
|
710
|
+
{ fww: "keyB" },
|
|
711
|
+
laterTime,
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// On node2: create FWW transaction for "keyA" with later time, "keyB" with earlier time
|
|
715
|
+
mapOnNode2.core.makeTransaction(
|
|
716
|
+
[{ op: "set", key: "keyA", value: "node2" }],
|
|
717
|
+
"trusting",
|
|
718
|
+
{ fww: "keyA" },
|
|
719
|
+
laterTime,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
mapOnNode2.core.makeTransaction(
|
|
723
|
+
[{ op: "set", key: "keyB", value: "node2" }],
|
|
724
|
+
"trusting",
|
|
725
|
+
{ fww: "keyB" },
|
|
726
|
+
earlierTime,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Wait for sync
|
|
730
|
+
await map.core.waitForSync();
|
|
731
|
+
await mapOnNode2.core.waitForSync();
|
|
732
|
+
|
|
733
|
+
// Wait for microtasks
|
|
734
|
+
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
735
|
+
|
|
736
|
+
// Both nodes should converge to the same state
|
|
737
|
+
// keyA: node1 wins (earlier timestamp)
|
|
738
|
+
// keyB: node2 wins (earlier timestamp)
|
|
739
|
+
await waitFor(() => {
|
|
740
|
+
expect(map.get("keyA")).toBe("node1");
|
|
741
|
+
expect(map.get("keyB")).toBe("node2");
|
|
742
|
+
expect(mapOnNode2.get("keyA")).toBe("node1");
|
|
743
|
+
expect(mapOnNode2.get("keyB")).toBe("node2");
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Verify both nodes have exactly 2 valid transactions (one per key)
|
|
747
|
+
const node1ValidTxs = map.core.getValidSortedTransactions();
|
|
748
|
+
const node2ValidTxs = mapOnNode2.core.getValidSortedTransactions();
|
|
749
|
+
|
|
750
|
+
expect(node1ValidTxs).toHaveLength(2);
|
|
751
|
+
expect(node2ValidTxs).toHaveLength(2);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("late-arriving winner for one key does not affect other keys", async () => {
|
|
755
|
+
const client = setupTestNode({ connected: true });
|
|
756
|
+
const clientSession2 = client.spawnNewSession();
|
|
757
|
+
const group = client.node.createGroup();
|
|
758
|
+
const map = group.createMap();
|
|
759
|
+
|
|
760
|
+
const mapOnClientSession2 = await loadCoValueOrFail(
|
|
761
|
+
clientSession2.node,
|
|
762
|
+
map.id,
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const earlierTime = Date.now();
|
|
766
|
+
const laterTime = earlierTime + 1000;
|
|
767
|
+
|
|
768
|
+
// Session 1: Create FWW transaction for "keyA" with later time
|
|
769
|
+
map.core.makeTransaction(
|
|
770
|
+
[{ op: "set", key: "keyA", value: "session1" }],
|
|
771
|
+
"trusting",
|
|
772
|
+
{ fww: "keyA" },
|
|
773
|
+
laterTime,
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// Session 1: Create FWW transaction for "keyB" (any time)
|
|
777
|
+
map.core.makeTransaction(
|
|
778
|
+
[{ op: "set", key: "keyB", value: "session1" }],
|
|
779
|
+
"trusting",
|
|
780
|
+
{ fww: "keyB" },
|
|
781
|
+
earlierTime,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// Verify initial state
|
|
785
|
+
expect(map.get("keyA")).toBe("session1");
|
|
786
|
+
expect(map.get("keyB")).toBe("session1");
|
|
787
|
+
|
|
788
|
+
// Session 2: Create FWW transaction for "keyA" with earlier time (this should win)
|
|
789
|
+
mapOnClientSession2.core.makeTransaction(
|
|
790
|
+
[{ op: "set", key: "keyA", value: "session2" }],
|
|
791
|
+
"trusting",
|
|
792
|
+
{ fww: "keyA" },
|
|
793
|
+
earlierTime,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
await waitFor(() => {
|
|
797
|
+
expect(map.core.knownState()).toEqual(
|
|
798
|
+
mapOnClientSession2.core.knownState(),
|
|
799
|
+
);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// After sync:
|
|
803
|
+
// keyA: session2's transaction wins (earlier timestamp), session1's is invalidated
|
|
804
|
+
// keyB: session1's transaction remains valid (unaffected)
|
|
805
|
+
expect(map.get("keyA")).toBe("session2");
|
|
806
|
+
expect(map.get("keyB")).toBe("session1");
|
|
807
|
+
|
|
808
|
+
// Check that both keys have exactly one valid transaction each
|
|
809
|
+
const allTransactions = map.core.getValidSortedTransactions({
|
|
810
|
+
includeInvalidMetaTransactions: true,
|
|
811
|
+
ignorePrivateTransactions: false,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Should have 3 total transactions: 2 for keyA (one invalid), 1 for keyB (valid)
|
|
815
|
+
expect(allTransactions).toHaveLength(3);
|
|
816
|
+
|
|
817
|
+
// keyA transactions
|
|
818
|
+
const keyATransactions = allTransactions.filter(
|
|
819
|
+
(tx) => tx.meta?.fww === "keyA",
|
|
820
|
+
);
|
|
821
|
+
expect(keyATransactions).toHaveLength(2);
|
|
822
|
+
expect(keyATransactions.filter((tx) => tx.isValid)).toHaveLength(1);
|
|
823
|
+
expect(keyATransactions.filter((tx) => !tx.isValid)).toHaveLength(1);
|
|
824
|
+
|
|
825
|
+
// The winning keyA transaction should be from session2 (earlier time)
|
|
826
|
+
const validKeyA = keyATransactions.find((tx) => tx.isValid);
|
|
827
|
+
expect(validKeyA?.madeAt).toBe(earlierTime);
|
|
828
|
+
|
|
829
|
+
// keyB transaction should be valid (unaffected by keyA's late-arriving winner)
|
|
830
|
+
const keyBTransactions = allTransactions.filter(
|
|
831
|
+
(tx) => tx.meta?.fww === "keyB",
|
|
832
|
+
);
|
|
833
|
+
expect(keyBTransactions).toHaveLength(1);
|
|
834
|
+
expect(keyBTransactions[0]?.isValid).toBe(true);
|
|
835
|
+
});
|
|
836
|
+
});
|