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/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 './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;
@@ -154,7 +158,25 @@ export class SyncManager {
154
158
  coValueID: RawCoValueID,
155
159
  peer: PeerState
156
160
  ) {
157
- const coValue = this.local.expectCoValueLoaded(coValueID);
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 peer.outgoing.write({
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 peer.outgoing.write({
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 peer.outgoing.write(newContent);
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 entry of Object.values(this.local.coValues)) {
236
- if (entry.state === "loading") {
237
- continue;
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[entry.coValue.id] = {
246
- coValueID: entry.coValue.id,
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 peer.outgoing.write({
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] || emptyKnownState(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 peer.outgoing.write({
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 peer.outgoing.write(newContent);
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