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/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 './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 { ReadableStream, WritableStream, WritableStreamDefaultWriter } from "isomorphic-streams";
7
- import { RawCoValueID, SessionID } from './ids.js';
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 peer.outgoing.write({
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 peer.outgoing.write({
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 peer.outgoing.write(newContent);
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 peer.outgoing.write({
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] || emptyKnownState(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 peer.outgoing.write({
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 peer.outgoing.write(newContent);
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