cojson 0.0.8 → 0.0.10
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 +18 -9
- package/dist/sync.js.map +1 -1
- package/dist/sync.test.js +107 -1
- package/dist/sync.test.js.map +1 -1
- package/package.json +22 -4
- package/src/sync.test.ts +140 -1
- package/src/sync.ts +29 -18
- package/yarn-error.log +2864 -0
package/src/sync.test.ts
CHANGED
|
@@ -980,6 +980,123 @@ 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
|
+
|
|
983
1100
|
function teamContentEx(team: Team) {
|
|
984
1101
|
return {
|
|
985
1102
|
action: "newContent",
|
|
@@ -1015,10 +1132,17 @@ function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
1015
1132
|
resolveNextItemReady = resolve;
|
|
1016
1133
|
});
|
|
1017
1134
|
|
|
1135
|
+
let writerClosed = false;
|
|
1136
|
+
let readerClosed = false;
|
|
1137
|
+
|
|
1018
1138
|
const readable = new ReadableStream<T>({
|
|
1019
1139
|
async pull(controller) {
|
|
1020
1140
|
let retriesLeft = 3;
|
|
1021
1141
|
while (retriesLeft > 0) {
|
|
1142
|
+
if (writerClosed) {
|
|
1143
|
+
controller.close();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1022
1146
|
retriesLeft--;
|
|
1023
1147
|
if (queue.length > 0) {
|
|
1024
1148
|
controller.enqueue(queue.shift()!);
|
|
@@ -1034,16 +1158,31 @@ function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
1034
1158
|
}
|
|
1035
1159
|
throw new Error("Should only use one retry to get next item in queue.")
|
|
1036
1160
|
},
|
|
1161
|
+
|
|
1162
|
+
cancel(reason) {
|
|
1163
|
+
console.log("Manually closing reader")
|
|
1164
|
+
readerClosed = true;
|
|
1165
|
+
},
|
|
1037
1166
|
});
|
|
1038
1167
|
|
|
1039
1168
|
const writable = new WritableStream<T>({
|
|
1040
|
-
write(chunk) {
|
|
1169
|
+
write(chunk, controller) {
|
|
1170
|
+
if (readerClosed) {
|
|
1171
|
+
console.log("Reader closed, not writing chunk", chunk);
|
|
1172
|
+
throw new Error("Reader closed, not writing chunk");
|
|
1173
|
+
}
|
|
1041
1174
|
queue.push(chunk);
|
|
1042
1175
|
if (queue.length === 1) {
|
|
1043
1176
|
// make sure that await write resolves before corresponding read
|
|
1044
1177
|
process.nextTick(() => resolveNextItemReady());
|
|
1045
1178
|
}
|
|
1046
1179
|
},
|
|
1180
|
+
abort(reason) {
|
|
1181
|
+
console.log("Manually closing writer")
|
|
1182
|
+
writerClosed = true;
|
|
1183
|
+
resolveNextItemReady();
|
|
1184
|
+
return Promise.resolve();
|
|
1185
|
+
},
|
|
1047
1186
|
});
|
|
1048
1187
|
|
|
1049
1188
|
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;
|
|
@@ -162,7 +166,7 @@ export class SyncManager {
|
|
|
162
166
|
|
|
163
167
|
if (!peer.toldKnownState.has(coValueID)) {
|
|
164
168
|
peer.toldKnownState.add(coValueID);
|
|
165
|
-
await
|
|
169
|
+
await this.trySendToPeer(peer, {
|
|
166
170
|
action: "subscribe",
|
|
167
171
|
...coValue.knownState(),
|
|
168
172
|
});
|
|
@@ -185,7 +189,7 @@ export class SyncManager {
|
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
if (!peer.toldKnownState.has(coValueID)) {
|
|
188
|
-
await
|
|
192
|
+
await this.trySendToPeer(peer, {
|
|
189
193
|
action: "tellKnownState",
|
|
190
194
|
asDependencyOf,
|
|
191
195
|
...coValue.knownState(),
|
|
@@ -210,7 +214,7 @@ export class SyncManager {
|
|
|
210
214
|
);
|
|
211
215
|
|
|
212
216
|
if (newContent) {
|
|
213
|
-
await
|
|
217
|
+
await this.trySendToPeer(peer, newContent);
|
|
214
218
|
peer.optimisticKnownStates[coValueID] = combinedKnownStates(
|
|
215
219
|
peer.optimisticKnownStates[coValueID] ||
|
|
216
220
|
emptyKnownState(coValueID),
|
|
@@ -264,11 +268,20 @@ export class SyncManager {
|
|
|
264
268
|
);
|
|
265
269
|
}
|
|
266
270
|
}
|
|
271
|
+
console.log("Peer disconnected:", peer.id);
|
|
272
|
+
delete this.peers[peer.id];
|
|
267
273
|
};
|
|
268
274
|
|
|
269
275
|
void readIncoming();
|
|
270
276
|
}
|
|
271
277
|
|
|
278
|
+
trySendToPeer(peer: PeerState, msg: SyncMessage) {
|
|
279
|
+
return peer.outgoing.write(msg).catch((e) => {
|
|
280
|
+
console.error("Error writing to peer, disconnecting", e);
|
|
281
|
+
delete this.peers[peer.id];
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
272
285
|
async handleSubscribe(msg: SubscribeMessage, peer: PeerState) {
|
|
273
286
|
const entry = this.local.coValues[msg.coValueID];
|
|
274
287
|
|
|
@@ -280,7 +293,7 @@ export class SyncManager {
|
|
|
280
293
|
peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
|
|
281
294
|
peer.toldKnownState.add(msg.coValueID);
|
|
282
295
|
|
|
283
|
-
await
|
|
296
|
+
await this.trySendToPeer(peer, {
|
|
284
297
|
action: "tellKnownState",
|
|
285
298
|
coValueID: msg.coValueID,
|
|
286
299
|
header: false,
|
|
@@ -304,7 +317,8 @@ export class SyncManager {
|
|
|
304
317
|
let entry = this.local.coValues[msg.coValueID];
|
|
305
318
|
|
|
306
319
|
peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
|
|
307
|
-
peer.optimisticKnownStates[msg.coValueID] ||
|
|
320
|
+
peer.optimisticKnownStates[msg.coValueID] ||
|
|
321
|
+
emptyKnownState(msg.coValueID),
|
|
308
322
|
knownStateIn(msg)
|
|
309
323
|
);
|
|
310
324
|
|
|
@@ -423,7 +437,7 @@ export class SyncManager {
|
|
|
423
437
|
await this.syncCoValue(coValue);
|
|
424
438
|
|
|
425
439
|
if (invalidStateAssumed) {
|
|
426
|
-
await
|
|
440
|
+
await this.trySendToPeer(peer, {
|
|
427
441
|
action: "wrongAssumedKnownState",
|
|
428
442
|
...coValue.knownState(),
|
|
429
443
|
});
|
|
@@ -444,7 +458,7 @@ export class SyncManager {
|
|
|
444
458
|
const newContent = coValue.newContentSince(msg);
|
|
445
459
|
|
|
446
460
|
if (newContent) {
|
|
447
|
-
await
|
|
461
|
+
await this.trySendToPeer(peer, newContent);
|
|
448
462
|
}
|
|
449
463
|
}
|
|
450
464
|
|
|
@@ -466,10 +480,7 @@ export class SyncManager {
|
|
|
466
480
|
peer
|
|
467
481
|
);
|
|
468
482
|
} else if (peer.role === "server") {
|
|
469
|
-
await this.subscribeToIncludingDependencies(
|
|
470
|
-
coValue.id,
|
|
471
|
-
peer
|
|
472
|
-
);
|
|
483
|
+
await this.subscribeToIncludingDependencies(coValue.id, peer);
|
|
473
484
|
await this.sendNewContentIncludingDependencies(
|
|
474
485
|
coValue.id,
|
|
475
486
|
peer
|