cojson 0.0.9 → 0.0.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/dist/sync.d.ts +6 -5
- package/dist/sync.js +36 -17
- package/dist/sync.js.map +1 -1
- package/dist/sync.test.js +125 -1
- package/dist/sync.test.js.map +1 -1
- package/package.json +20 -2
- package/src/sync.test.ts +172 -1
- package/src/sync.ts +54 -30
- package/yarn-error.log +2864 -0
package/src/sync.test.ts
CHANGED
|
@@ -980,6 +980,155 @@ test("Can sync a coValue with private transactions through a server to another c
|
|
|
980
980
|
);
|
|
981
981
|
});
|
|
982
982
|
|
|
983
|
+
test("When a peer's incoming/readable stream closes, we remove the peer", async () => {
|
|
984
|
+
const admin = newRandomAgentCredential("admin");
|
|
985
|
+
const adminID = getAgentID(getAgent(admin));
|
|
986
|
+
|
|
987
|
+
const node = new LocalNode(admin, newRandomSessionID(adminID));
|
|
988
|
+
|
|
989
|
+
const team = node.createTeam();
|
|
990
|
+
|
|
991
|
+
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
|
992
|
+
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
|
993
|
+
|
|
994
|
+
node.sync.addPeer({
|
|
995
|
+
id: "test",
|
|
996
|
+
incoming: inRx,
|
|
997
|
+
outgoing: outTx,
|
|
998
|
+
role: "server",
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
const reader = outRx.getReader();
|
|
1002
|
+
expect((await reader.read()).value).toMatchObject({
|
|
1003
|
+
action: "subscribe",
|
|
1004
|
+
coValueID: adminID,
|
|
1005
|
+
});
|
|
1006
|
+
expect((await reader.read()).value).toMatchObject({
|
|
1007
|
+
action: "subscribe",
|
|
1008
|
+
coValueID: team.teamMap.coValue.id,
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const map = team.createMap();
|
|
1012
|
+
|
|
1013
|
+
const mapSubscribeMsg = await reader.read();
|
|
1014
|
+
|
|
1015
|
+
expect(mapSubscribeMsg.value).toEqual({
|
|
1016
|
+
action: "subscribe",
|
|
1017
|
+
...map.coValue.knownState(),
|
|
1018
|
+
} satisfies SyncMessage);
|
|
1019
|
+
|
|
1020
|
+
expect((await reader.read()).value).toMatchObject(admContEx(adminID));
|
|
1021
|
+
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
|
1022
|
+
|
|
1023
|
+
const mapContentMsg = await reader.read();
|
|
1024
|
+
|
|
1025
|
+
expect(mapContentMsg.value).toEqual({
|
|
1026
|
+
action: "newContent",
|
|
1027
|
+
coValueID: map.coValue.id,
|
|
1028
|
+
header: map.coValue.header,
|
|
1029
|
+
newContent: {},
|
|
1030
|
+
} satisfies SyncMessage);
|
|
1031
|
+
|
|
1032
|
+
await inTx.abort();
|
|
1033
|
+
|
|
1034
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1035
|
+
|
|
1036
|
+
expect(node.sync.peers["test"]).toBeUndefined();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
test("When a peer's outgoing/writable stream closes, we remove the peer", async () => {
|
|
1040
|
+
const admin = newRandomAgentCredential("admin");
|
|
1041
|
+
const adminID = getAgentID(getAgent(admin));
|
|
1042
|
+
|
|
1043
|
+
const node = new LocalNode(admin, newRandomSessionID(adminID));
|
|
1044
|
+
|
|
1045
|
+
const team = node.createTeam();
|
|
1046
|
+
|
|
1047
|
+
const [inRx, inTx] = newStreamPair<SyncMessage>();
|
|
1048
|
+
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
|
1049
|
+
|
|
1050
|
+
node.sync.addPeer({
|
|
1051
|
+
id: "test",
|
|
1052
|
+
incoming: inRx,
|
|
1053
|
+
outgoing: outTx,
|
|
1054
|
+
role: "server",
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
const reader = outRx.getReader();
|
|
1058
|
+
expect((await reader.read()).value).toMatchObject({
|
|
1059
|
+
action: "subscribe",
|
|
1060
|
+
coValueID: adminID,
|
|
1061
|
+
});
|
|
1062
|
+
expect((await reader.read()).value).toMatchObject({
|
|
1063
|
+
action: "subscribe",
|
|
1064
|
+
coValueID: team.teamMap.coValue.id,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const map = team.createMap();
|
|
1068
|
+
|
|
1069
|
+
const mapSubscribeMsg = await reader.read();
|
|
1070
|
+
|
|
1071
|
+
expect(mapSubscribeMsg.value).toEqual({
|
|
1072
|
+
action: "subscribe",
|
|
1073
|
+
...map.coValue.knownState(),
|
|
1074
|
+
} satisfies SyncMessage);
|
|
1075
|
+
|
|
1076
|
+
expect((await reader.read()).value).toMatchObject(admContEx(adminID));
|
|
1077
|
+
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
|
|
1078
|
+
|
|
1079
|
+
const mapContentMsg = await reader.read();
|
|
1080
|
+
|
|
1081
|
+
expect(mapContentMsg.value).toEqual({
|
|
1082
|
+
action: "newContent",
|
|
1083
|
+
coValueID: map.coValue.id,
|
|
1084
|
+
header: map.coValue.header,
|
|
1085
|
+
newContent: {},
|
|
1086
|
+
} satisfies SyncMessage);
|
|
1087
|
+
|
|
1088
|
+
reader.releaseLock();
|
|
1089
|
+
await outRx.cancel();
|
|
1090
|
+
|
|
1091
|
+
map.edit((editable) => {
|
|
1092
|
+
editable.set("hello", "world", "trusting");
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1096
|
+
|
|
1097
|
+
expect(node.sync.peers["test"]).toBeUndefined();
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
|
|
1101
|
+
const admin = newRandomAgentCredential("admin");
|
|
1102
|
+
const adminID = getAgentID(getAgent(admin));
|
|
1103
|
+
|
|
1104
|
+
const node1 = new LocalNode(admin, newRandomSessionID(adminID));
|
|
1105
|
+
|
|
1106
|
+
const team = node1.createTeam();
|
|
1107
|
+
|
|
1108
|
+
const map = team.createMap();
|
|
1109
|
+
map.edit((editable) => {
|
|
1110
|
+
editable.set("hello", "world", "trusting");
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const node2 = new LocalNode(admin, newRandomSessionID(adminID));
|
|
1114
|
+
|
|
1115
|
+
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {peer1role: 'server', peer2role: 'client', trace: true});
|
|
1116
|
+
|
|
1117
|
+
node1.sync.addPeer(node2asPeer);
|
|
1118
|
+
|
|
1119
|
+
const mapOnNode2Promise = node2.loadCoValue(map.coValue.id);
|
|
1120
|
+
|
|
1121
|
+
expect(node2.coValues[map.coValue.id]?.state).toEqual("loading");
|
|
1122
|
+
|
|
1123
|
+
node2.sync.addPeer(node1asPeer);
|
|
1124
|
+
|
|
1125
|
+
const mapOnNode2 = await mapOnNode2Promise;
|
|
1126
|
+
|
|
1127
|
+
expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual(
|
|
1128
|
+
"world"
|
|
1129
|
+
);
|
|
1130
|
+
})
|
|
1131
|
+
|
|
983
1132
|
function teamContentEx(team: Team) {
|
|
984
1133
|
return {
|
|
985
1134
|
action: "newContent",
|
|
@@ -1015,10 +1164,17 @@ function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
1015
1164
|
resolveNextItemReady = resolve;
|
|
1016
1165
|
});
|
|
1017
1166
|
|
|
1167
|
+
let writerClosed = false;
|
|
1168
|
+
let readerClosed = false;
|
|
1169
|
+
|
|
1018
1170
|
const readable = new ReadableStream<T>({
|
|
1019
1171
|
async pull(controller) {
|
|
1020
1172
|
let retriesLeft = 3;
|
|
1021
1173
|
while (retriesLeft > 0) {
|
|
1174
|
+
if (writerClosed) {
|
|
1175
|
+
controller.close();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1022
1178
|
retriesLeft--;
|
|
1023
1179
|
if (queue.length > 0) {
|
|
1024
1180
|
controller.enqueue(queue.shift()!);
|
|
@@ -1034,16 +1190,31 @@ function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
1034
1190
|
}
|
|
1035
1191
|
throw new Error("Should only use one retry to get next item in queue.")
|
|
1036
1192
|
},
|
|
1193
|
+
|
|
1194
|
+
cancel(reason) {
|
|
1195
|
+
console.log("Manually closing reader")
|
|
1196
|
+
readerClosed = true;
|
|
1197
|
+
},
|
|
1037
1198
|
});
|
|
1038
1199
|
|
|
1039
1200
|
const writable = new WritableStream<T>({
|
|
1040
|
-
write(chunk) {
|
|
1201
|
+
write(chunk, controller) {
|
|
1202
|
+
if (readerClosed) {
|
|
1203
|
+
console.log("Reader closed, not writing chunk", chunk);
|
|
1204
|
+
throw new Error("Reader closed, not writing chunk");
|
|
1205
|
+
}
|
|
1041
1206
|
queue.push(chunk);
|
|
1042
1207
|
if (queue.length === 1) {
|
|
1043
1208
|
// make sure that await write resolves before corresponding read
|
|
1044
1209
|
process.nextTick(() => resolveNextItemReady());
|
|
1045
1210
|
}
|
|
1046
1211
|
},
|
|
1212
|
+
abort(reason) {
|
|
1213
|
+
console.log("Manually closing writer")
|
|
1214
|
+
writerClosed = true;
|
|
1215
|
+
resolveNextItemReady();
|
|
1216
|
+
return Promise.resolve();
|
|
1217
|
+
},
|
|
1047
1218
|
});
|
|
1048
1219
|
|
|
1049
1220
|
return [readable, writable];
|
package/src/sync.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Hash, Signature } from
|
|
2
|
-
import { CoValueHeader, Transaction } from
|
|
3
|
-
import { CoValue } from
|
|
4
|
-
import { LocalNode } from
|
|
5
|
-
import { newLoadingState } from
|
|
6
|
-
import {
|
|
7
|
-
|
|
1
|
+
import { Hash, Signature } from "./crypto.js";
|
|
2
|
+
import { CoValueHeader, Transaction } from "./coValue.js";
|
|
3
|
+
import { CoValue } from "./coValue.js";
|
|
4
|
+
import { LocalNode } from "./node.js";
|
|
5
|
+
import { newLoadingState } from "./node.js";
|
|
6
|
+
import {
|
|
7
|
+
ReadableStream,
|
|
8
|
+
WritableStream,
|
|
9
|
+
WritableStreamDefaultWriter,
|
|
10
|
+
} from "isomorphic-streams";
|
|
11
|
+
import { RawCoValueID, SessionID } from "./ids.js";
|
|
8
12
|
|
|
9
13
|
export type CoValueKnownState = {
|
|
10
14
|
coValueID: RawCoValueID;
|
|
@@ -154,7 +158,25 @@ export class SyncManager {
|
|
|
154
158
|
coValueID: RawCoValueID,
|
|
155
159
|
peer: PeerState
|
|
156
160
|
) {
|
|
157
|
-
const
|
|
161
|
+
const entry = this.local.coValues[coValueID];
|
|
162
|
+
|
|
163
|
+
if (!entry) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"Expected coValue entry on subscribe"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (entry.state === "loading") {
|
|
170
|
+
await this.trySendToPeer(peer, {
|
|
171
|
+
action: "subscribe",
|
|
172
|
+
coValueID,
|
|
173
|
+
header: false,
|
|
174
|
+
sessions: {},
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const coValue = entry.coValue;
|
|
158
180
|
|
|
159
181
|
for (const coValueID of coValue.getDependedOnCoValues()) {
|
|
160
182
|
await this.subscribeToIncludingDependencies(coValueID, peer);
|
|
@@ -162,7 +184,7 @@ export class SyncManager {
|
|
|
162
184
|
|
|
163
185
|
if (!peer.toldKnownState.has(coValueID)) {
|
|
164
186
|
peer.toldKnownState.add(coValueID);
|
|
165
|
-
await
|
|
187
|
+
await this.trySendToPeer(peer, {
|
|
166
188
|
action: "subscribe",
|
|
167
189
|
...coValue.knownState(),
|
|
168
190
|
});
|
|
@@ -185,7 +207,7 @@ export class SyncManager {
|
|
|
185
207
|
}
|
|
186
208
|
|
|
187
209
|
if (!peer.toldKnownState.has(coValueID)) {
|
|
188
|
-
await
|
|
210
|
+
await this.trySendToPeer(peer, {
|
|
189
211
|
action: "tellKnownState",
|
|
190
212
|
asDependencyOf,
|
|
191
213
|
...coValue.knownState(),
|
|
@@ -210,7 +232,7 @@ export class SyncManager {
|
|
|
210
232
|
);
|
|
211
233
|
|
|
212
234
|
if (newContent) {
|
|
213
|
-
await
|
|
235
|
+
await this.trySendToPeer(peer, newContent);
|
|
214
236
|
peer.optimisticKnownStates[coValueID] = combinedKnownStates(
|
|
215
237
|
peer.optimisticKnownStates[coValueID] ||
|
|
216
238
|
emptyKnownState(coValueID),
|
|
@@ -232,18 +254,13 @@ export class SyncManager {
|
|
|
232
254
|
|
|
233
255
|
if (peer.role === "server") {
|
|
234
256
|
const initialSync = async () => {
|
|
235
|
-
for (const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
await this.subscribeToIncludingDependencies(
|
|
241
|
-
entry.coValue.id,
|
|
242
|
-
peerState
|
|
243
|
-
);
|
|
257
|
+
for (const id of Object.keys(
|
|
258
|
+
this.local.coValues
|
|
259
|
+
) as RawCoValueID[]) {
|
|
260
|
+
await this.subscribeToIncludingDependencies(id, peerState);
|
|
244
261
|
|
|
245
|
-
peerState.optimisticKnownStates[
|
|
246
|
-
coValueID:
|
|
262
|
+
peerState.optimisticKnownStates[id] = {
|
|
263
|
+
coValueID: id,
|
|
247
264
|
header: false,
|
|
248
265
|
sessions: {},
|
|
249
266
|
};
|
|
@@ -264,11 +281,20 @@ export class SyncManager {
|
|
|
264
281
|
);
|
|
265
282
|
}
|
|
266
283
|
}
|
|
284
|
+
console.log("Peer disconnected:", peer.id);
|
|
285
|
+
delete this.peers[peer.id];
|
|
267
286
|
};
|
|
268
287
|
|
|
269
288
|
void readIncoming();
|
|
270
289
|
}
|
|
271
290
|
|
|
291
|
+
trySendToPeer(peer: PeerState, msg: SyncMessage) {
|
|
292
|
+
return peer.outgoing.write(msg).catch((e) => {
|
|
293
|
+
console.error("Error writing to peer, disconnecting", e);
|
|
294
|
+
delete this.peers[peer.id];
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
272
298
|
async handleSubscribe(msg: SubscribeMessage, peer: PeerState) {
|
|
273
299
|
const entry = this.local.coValues[msg.coValueID];
|
|
274
300
|
|
|
@@ -280,7 +306,7 @@ export class SyncManager {
|
|
|
280
306
|
peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
|
|
281
307
|
peer.toldKnownState.add(msg.coValueID);
|
|
282
308
|
|
|
283
|
-
await
|
|
309
|
+
await this.trySendToPeer(peer, {
|
|
284
310
|
action: "tellKnownState",
|
|
285
311
|
coValueID: msg.coValueID,
|
|
286
312
|
header: false,
|
|
@@ -304,7 +330,8 @@ export class SyncManager {
|
|
|
304
330
|
let entry = this.local.coValues[msg.coValueID];
|
|
305
331
|
|
|
306
332
|
peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
|
|
307
|
-
peer.optimisticKnownStates[msg.coValueID] ||
|
|
333
|
+
peer.optimisticKnownStates[msg.coValueID] ||
|
|
334
|
+
emptyKnownState(msg.coValueID),
|
|
308
335
|
knownStateIn(msg)
|
|
309
336
|
);
|
|
310
337
|
|
|
@@ -423,7 +450,7 @@ export class SyncManager {
|
|
|
423
450
|
await this.syncCoValue(coValue);
|
|
424
451
|
|
|
425
452
|
if (invalidStateAssumed) {
|
|
426
|
-
await
|
|
453
|
+
await this.trySendToPeer(peer, {
|
|
427
454
|
action: "wrongAssumedKnownState",
|
|
428
455
|
...coValue.knownState(),
|
|
429
456
|
});
|
|
@@ -444,7 +471,7 @@ export class SyncManager {
|
|
|
444
471
|
const newContent = coValue.newContentSince(msg);
|
|
445
472
|
|
|
446
473
|
if (newContent) {
|
|
447
|
-
await
|
|
474
|
+
await this.trySendToPeer(peer, newContent);
|
|
448
475
|
}
|
|
449
476
|
}
|
|
450
477
|
|
|
@@ -466,10 +493,7 @@ export class SyncManager {
|
|
|
466
493
|
peer
|
|
467
494
|
);
|
|
468
495
|
} else if (peer.role === "server") {
|
|
469
|
-
await this.subscribeToIncludingDependencies(
|
|
470
|
-
coValue.id,
|
|
471
|
-
peer
|
|
472
|
-
);
|
|
496
|
+
await this.subscribeToIncludingDependencies(coValue.id, peer);
|
|
473
497
|
await this.sendNewContentIncludingDependencies(
|
|
474
498
|
coValue.id,
|
|
475
499
|
peer
|