cojson 0.0.11 → 0.0.13

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.
Files changed (75) hide show
  1. package/README.md +2 -2
  2. package/dist/account.d.ts +57 -0
  3. package/dist/account.js +76 -0
  4. package/dist/account.js.map +1 -0
  5. package/dist/account.test.d.ts +1 -0
  6. package/dist/account.test.js +40 -0
  7. package/dist/account.test.js.map +1 -0
  8. package/dist/coValue.d.ts +17 -36
  9. package/dist/coValue.js +53 -117
  10. package/dist/coValue.js.map +1 -1
  11. package/dist/coValue.test.js +16 -16
  12. package/dist/coValue.test.js.map +1 -1
  13. package/dist/contentType.d.ts +9 -9
  14. package/dist/contentType.js.map +1 -1
  15. package/dist/contentType.test.js +13 -17
  16. package/dist/contentType.test.js.map +1 -1
  17. package/dist/contentTypes/coList.d.ts +3 -3
  18. package/dist/contentTypes/coList.js.map +1 -1
  19. package/dist/contentTypes/coMap.d.ts +31 -21
  20. package/dist/contentTypes/coMap.js +28 -0
  21. package/dist/contentTypes/coMap.js.map +1 -1
  22. package/dist/contentTypes/coStream.d.ts +3 -3
  23. package/dist/contentTypes/coStream.js.map +1 -1
  24. package/dist/contentTypes/static.d.ts +4 -4
  25. package/dist/contentTypes/static.js.map +1 -1
  26. package/dist/crypto.d.ts +45 -39
  27. package/dist/crypto.js +68 -49
  28. package/dist/crypto.js.map +1 -1
  29. package/dist/crypto.test.js +45 -49
  30. package/dist/crypto.test.js.map +1 -1
  31. package/dist/ids.d.ts +5 -3
  32. package/dist/ids.js +3 -1
  33. package/dist/ids.js.map +1 -1
  34. package/dist/index.d.ts +12 -14
  35. package/dist/index.js +6 -8
  36. package/dist/index.js.map +1 -1
  37. package/dist/jsonValue.d.ts +2 -2
  38. package/dist/node.d.ts +25 -15
  39. package/dist/node.js +88 -33
  40. package/dist/node.js.map +1 -1
  41. package/dist/permissions.d.ts +27 -33
  42. package/dist/permissions.js +55 -47
  43. package/dist/permissions.js.map +1 -1
  44. package/dist/permissions.test.js +231 -314
  45. package/dist/permissions.test.js.map +1 -1
  46. package/dist/sync.d.ts +27 -30
  47. package/dist/sync.js +68 -64
  48. package/dist/sync.js.map +1 -1
  49. package/dist/sync.test.js +181 -305
  50. package/dist/sync.test.js.map +1 -1
  51. package/dist/testUtils.d.ts +37 -0
  52. package/dist/testUtils.js +157 -0
  53. package/dist/testUtils.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/account.test.ts +67 -0
  56. package/src/account.ts +152 -0
  57. package/src/coValue.test.ts +17 -31
  58. package/src/coValue.ts +98 -185
  59. package/src/contentType.test.ts +18 -45
  60. package/src/contentType.ts +15 -13
  61. package/src/contentTypes/coList.ts +4 -4
  62. package/src/contentTypes/coMap.ts +55 -29
  63. package/src/contentTypes/coStream.ts +4 -4
  64. package/src/contentTypes/static.ts +5 -5
  65. package/src/crypto.test.ts +53 -59
  66. package/src/crypto.ts +123 -95
  67. package/src/ids.ts +9 -3
  68. package/src/index.ts +14 -25
  69. package/src/jsonValue.ts +2 -2
  70. package/src/node.ts +189 -61
  71. package/src/permissions.test.ts +370 -404
  72. package/src/permissions.ts +126 -109
  73. package/src/sync.test.ts +262 -440
  74. package/src/sync.ts +96 -101
  75. package/src/testUtils.ts +229 -0
package/src/sync.ts CHANGED
@@ -8,43 +8,43 @@ import {
8
8
  WritableStream,
9
9
  WritableStreamDefaultWriter,
10
10
  } from "isomorphic-streams";
11
- import { RawCoValueID, SessionID } from "./ids.js";
11
+ import { RawCoID, SessionID } from "./ids.js";
12
12
 
13
13
  export type CoValueKnownState = {
14
- coValueID: RawCoValueID;
14
+ id: RawCoID;
15
15
  header: boolean;
16
16
  sessions: { [sessionID: SessionID]: number };
17
17
  };
18
18
 
19
- export function emptyKnownState(coValueID: RawCoValueID): CoValueKnownState {
19
+ export function emptyKnownState(id: RawCoID): CoValueKnownState {
20
20
  return {
21
- coValueID,
21
+ id,
22
22
  header: false,
23
23
  sessions: {},
24
24
  };
25
25
  }
26
26
 
27
27
  export type SyncMessage =
28
- | SubscribeMessage
29
- | TellKnownStateMessage
28
+ | LoadMessage
29
+ | KnownStateMessage
30
30
  | NewContentMessage
31
- | WrongAssumedKnownStateMessage
32
- | UnsubscribeMessage;
31
+ | DoneMessage;
33
32
 
34
- export type SubscribeMessage = {
35
- action: "subscribe";
33
+ export type LoadMessage = {
34
+ action: "load";
36
35
  } & CoValueKnownState;
37
36
 
38
- export type TellKnownStateMessage = {
39
- action: "tellKnownState";
40
- asDependencyOf?: RawCoValueID;
37
+ export type KnownStateMessage = {
38
+ action: "known";
39
+ asDependencyOf?: RawCoID;
40
+ isCorrection?: boolean;
41
41
  } & CoValueKnownState;
42
42
 
43
43
  export type NewContentMessage = {
44
- action: "newContent";
45
- coValueID: RawCoValueID;
44
+ action: "content";
45
+ id: RawCoID;
46
46
  header?: CoValueHeader;
47
- newContent: {
47
+ new: {
48
48
  [sessionID: SessionID]: SessionNewContent;
49
49
  };
50
50
  };
@@ -52,18 +52,11 @@ export type NewContentMessage = {
52
52
  export type SessionNewContent = {
53
53
  after: number;
54
54
  newTransactions: Transaction[];
55
- // TODO: is lastHash needed here?
56
- lastHash: Hash;
57
55
  lastSignature: Signature;
58
56
  };
59
-
60
- export type WrongAssumedKnownStateMessage = {
61
- action: "wrongAssumedKnownState";
62
- } & CoValueKnownState;
63
-
64
- export type UnsubscribeMessage = {
65
- action: "unsubscribe";
66
- coValueID: RawCoValueID;
57
+ export type DoneMessage = {
58
+ action: "done";
59
+ id: RawCoID;
67
60
  };
68
61
 
69
62
  export type PeerID = string;
@@ -77,8 +70,8 @@ export interface Peer {
77
70
 
78
71
  export interface PeerState {
79
72
  id: PeerID;
80
- optimisticKnownStates: { [coValueID: RawCoValueID]: CoValueKnownState };
81
- toldKnownState: Set<RawCoValueID>;
73
+ optimisticKnownStates: { [id: RawCoID]: CoValueKnownState };
74
+ toldKnownState: Set<RawCoID>;
82
75
  incoming: ReadableStream<SyncMessage>;
83
76
  outgoing: WritableStreamDefaultWriter<SyncMessage>;
84
77
  role: "peer" | "server" | "client";
@@ -103,7 +96,7 @@ export function combinedKnownStates(
103
96
  }
104
97
 
105
98
  return {
106
- coValueID: stateA.coValueID,
99
+ id: stateA.id,
107
100
  header: stateA.header || stateB.header,
108
101
  sessions: sessionStates,
109
102
  };
@@ -117,12 +110,12 @@ export class SyncManager {
117
110
  this.local = local;
118
111
  }
119
112
 
120
- loadFromPeers(id: RawCoValueID) {
113
+ loadFromPeers(id: RawCoID) {
121
114
  for (const peer of Object.values(this.peers)) {
122
115
  peer.outgoing
123
116
  .write({
124
- action: "subscribe",
125
- coValueID: id,
117
+ action: "load",
118
+ id: id,
126
119
  header: false,
127
120
  sessions: {},
128
121
  })
@@ -135,15 +128,17 @@ export class SyncManager {
135
128
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
136
129
  // TODO: validate
137
130
  switch (msg.action) {
138
- case "subscribe":
139
- return await this.handleSubscribe(msg, peer);
140
- case "tellKnownState":
141
- return await this.handleTellKnownState(msg, peer);
142
- case "newContent":
131
+ case "load":
132
+ return await this.handleLoad(msg, peer);
133
+ case "known":
134
+ if (msg.isCorrection) {
135
+ return await this.handleCorrection(msg, peer);
136
+ } else {
137
+ return await this.handleKnownState(msg, peer);
138
+ }
139
+ case "content":
143
140
  return await this.handleNewContent(msg, peer);
144
- case "wrongAssumedKnownState":
145
- return await this.handleWrongAssumedKnownState(msg, peer);
146
- case "unsubscribe":
141
+ case "done":
147
142
  return await this.handleUnsubscribe(msg);
148
143
  default:
149
144
  throw new Error(
@@ -155,10 +150,10 @@ export class SyncManager {
155
150
  }
156
151
 
157
152
  async subscribeToIncludingDependencies(
158
- coValueID: RawCoValueID,
153
+ id: RawCoID,
159
154
  peer: PeerState
160
155
  ) {
161
- const entry = this.local.coValues[coValueID];
156
+ const entry = this.local.coValues[id];
162
157
 
163
158
  if (!entry) {
164
159
  throw new Error(
@@ -168,8 +163,8 @@ export class SyncManager {
168
163
 
169
164
  if (entry.state === "loading") {
170
165
  await this.trySendToPeer(peer, {
171
- action: "subscribe",
172
- coValueID,
166
+ action: "load",
167
+ id,
173
168
  header: false,
174
169
  sessions: {},
175
170
  });
@@ -178,64 +173,64 @@ export class SyncManager {
178
173
 
179
174
  const coValue = entry.coValue;
180
175
 
181
- for (const coValueID of coValue.getDependedOnCoValues()) {
182
- await this.subscribeToIncludingDependencies(coValueID, peer);
176
+ for (const id of coValue.getDependedOnCoValues()) {
177
+ await this.subscribeToIncludingDependencies(id, peer);
183
178
  }
184
179
 
185
- if (!peer.toldKnownState.has(coValueID)) {
186
- peer.toldKnownState.add(coValueID);
180
+ if (!peer.toldKnownState.has(id)) {
181
+ peer.toldKnownState.add(id);
187
182
  await this.trySendToPeer(peer, {
188
- action: "subscribe",
183
+ action: "load",
189
184
  ...coValue.knownState(),
190
185
  });
191
186
  }
192
187
  }
193
188
 
194
189
  async tellUntoldKnownStateIncludingDependencies(
195
- coValueID: RawCoValueID,
190
+ id: RawCoID,
196
191
  peer: PeerState,
197
- asDependencyOf?: RawCoValueID
192
+ asDependencyOf?: RawCoID
198
193
  ) {
199
- const coValue = this.local.expectCoValueLoaded(coValueID);
194
+ const coValue = this.local.expectCoValueLoaded(id);
200
195
 
201
- for (const dependentCoValueID of coValue.getDependedOnCoValues()) {
196
+ for (const dependentCoID of coValue.getDependedOnCoValues()) {
202
197
  await this.tellUntoldKnownStateIncludingDependencies(
203
- dependentCoValueID,
198
+ dependentCoID,
204
199
  peer,
205
- asDependencyOf || coValueID
200
+ asDependencyOf || id
206
201
  );
207
202
  }
208
203
 
209
- if (!peer.toldKnownState.has(coValueID)) {
204
+ if (!peer.toldKnownState.has(id)) {
210
205
  await this.trySendToPeer(peer, {
211
- action: "tellKnownState",
206
+ action: "known",
212
207
  asDependencyOf,
213
208
  ...coValue.knownState(),
214
209
  });
215
210
 
216
- peer.toldKnownState.add(coValueID);
211
+ peer.toldKnownState.add(id);
217
212
  }
218
213
  }
219
214
 
220
215
  async sendNewContentIncludingDependencies(
221
- coValueID: RawCoValueID,
216
+ id: RawCoID,
222
217
  peer: PeerState
223
218
  ) {
224
- const coValue = this.local.expectCoValueLoaded(coValueID);
219
+ const coValue = this.local.expectCoValueLoaded(id);
225
220
 
226
- for (const coValueID of coValue.getDependedOnCoValues()) {
227
- await this.sendNewContentIncludingDependencies(coValueID, peer);
221
+ for (const id of coValue.getDependedOnCoValues()) {
222
+ await this.sendNewContentIncludingDependencies(id, peer);
228
223
  }
229
224
 
230
225
  const newContent = coValue.newContentSince(
231
- peer.optimisticKnownStates[coValueID]
226
+ peer.optimisticKnownStates[id]
232
227
  );
233
228
 
234
229
  if (newContent) {
235
230
  await this.trySendToPeer(peer, newContent);
236
- peer.optimisticKnownStates[coValueID] = combinedKnownStates(
237
- peer.optimisticKnownStates[coValueID] ||
238
- emptyKnownState(coValueID),
231
+ peer.optimisticKnownStates[id] = combinedKnownStates(
232
+ peer.optimisticKnownStates[id] ||
233
+ emptyKnownState(id),
239
234
  coValue.knownState()
240
235
  );
241
236
  }
@@ -256,11 +251,11 @@ export class SyncManager {
256
251
  const initialSync = async () => {
257
252
  for (const id of Object.keys(
258
253
  this.local.coValues
259
- ) as RawCoValueID[]) {
254
+ ) as RawCoID[]) {
260
255
  await this.subscribeToIncludingDependencies(id, peerState);
261
256
 
262
257
  peerState.optimisticKnownStates[id] = {
263
- coValueID: id,
258
+ id: id,
264
259
  header: false,
265
260
  sessions: {},
266
261
  };
@@ -295,20 +290,20 @@ export class SyncManager {
295
290
  });
296
291
  }
297
292
 
298
- async handleSubscribe(msg: SubscribeMessage, peer: PeerState) {
299
- const entry = this.local.coValues[msg.coValueID];
293
+ async handleLoad(msg: LoadMessage, peer: PeerState) {
294
+ const entry = this.local.coValues[msg.id];
300
295
 
301
296
  if (!entry || entry.state === "loading") {
302
297
  if (!entry) {
303
- this.local.coValues[msg.coValueID] = newLoadingState();
298
+ this.local.coValues[msg.id] = newLoadingState();
304
299
  }
305
300
 
306
- peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
307
- peer.toldKnownState.add(msg.coValueID);
301
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
302
+ peer.toldKnownState.add(msg.id);
308
303
 
309
304
  await this.trySendToPeer(peer, {
310
- action: "tellKnownState",
311
- coValueID: msg.coValueID,
305
+ action: "known",
306
+ id: msg.id,
312
307
  header: false,
313
308
  sessions: {},
314
309
  });
@@ -316,22 +311,22 @@ export class SyncManager {
316
311
  return;
317
312
  }
318
313
 
319
- peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
314
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
320
315
 
321
316
  await this.tellUntoldKnownStateIncludingDependencies(
322
- msg.coValueID,
317
+ msg.id,
323
318
  peer
324
319
  );
325
320
 
326
- await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
321
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
327
322
  }
328
323
 
329
- async handleTellKnownState(msg: TellKnownStateMessage, peer: PeerState) {
330
- let entry = this.local.coValues[msg.coValueID];
324
+ async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
325
+ let entry = this.local.coValues[msg.id];
331
326
 
332
- peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
333
- peer.optimisticKnownStates[msg.coValueID] ||
334
- emptyKnownState(msg.coValueID),
327
+ peer.optimisticKnownStates[msg.id] = combinedKnownStates(
328
+ peer.optimisticKnownStates[msg.id] ||
329
+ emptyKnownState(msg.id),
335
330
  knownStateIn(msg)
336
331
  );
337
332
 
@@ -340,7 +335,7 @@ export class SyncManager {
340
335
  if (this.local.coValues[msg.asDependencyOf]) {
341
336
  entry = newLoadingState();
342
337
 
343
- this.local.coValues[msg.coValueID] = entry;
338
+ this.local.coValues[msg.id] = entry;
344
339
  } else {
345
340
  throw new Error(
346
341
  "Expected coValue dependency entry to be created, missing subscribe?"
@@ -358,14 +353,14 @@ export class SyncManager {
358
353
  }
359
354
 
360
355
  await this.tellUntoldKnownStateIncludingDependencies(
361
- msg.coValueID,
356
+ msg.id,
362
357
  peer
363
358
  );
364
- await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
359
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
365
360
  }
366
361
 
367
362
  async handleNewContent(msg: NewContentMessage, peer: PeerState) {
368
- let entry = this.local.coValues[msg.coValueID];
363
+ let entry = this.local.coValues[msg.id];
369
364
 
370
365
  if (!entry) {
371
366
  throw new Error(
@@ -376,7 +371,7 @@ export class SyncManager {
376
371
  let resolveAfterDone: ((coValue: CoValue) => void) | undefined;
377
372
 
378
373
  const peerOptimisticKnownState =
379
- peer.optimisticKnownStates[msg.coValueID];
374
+ peer.optimisticKnownStates[msg.id];
380
375
 
381
376
  if (!peerOptimisticKnownState) {
382
377
  throw new Error(
@@ -400,7 +395,7 @@ export class SyncManager {
400
395
  coValue: coValue,
401
396
  };
402
397
 
403
- this.local.coValues[msg.coValueID] = entry;
398
+ this.local.coValues[msg.id] = entry;
404
399
  }
405
400
 
406
401
  const coValue = entry.coValue;
@@ -408,7 +403,7 @@ export class SyncManager {
408
403
  let invalidStateAssumed = false;
409
404
 
410
405
  for (const [sessionID, newContentForSession] of Object.entries(
411
- msg.newContent
406
+ msg.new
412
407
  ) as [SessionID, SessionNewContent][]) {
413
408
  const ourKnownTxIdx =
414
409
  coValue.sessions[sessionID]?.transactions.length;
@@ -429,7 +424,7 @@ export class SyncManager {
429
424
  const success = coValue.tryAddTransactions(
430
425
  sessionID,
431
426
  newTransactions,
432
- newContentForSession.lastHash,
427
+ undefined,
433
428
  newContentForSession.lastSignature
434
429
  );
435
430
 
@@ -451,19 +446,20 @@ export class SyncManager {
451
446
 
452
447
  if (invalidStateAssumed) {
453
448
  await this.trySendToPeer(peer, {
454
- action: "wrongAssumedKnownState",
449
+ action: "known",
450
+ isCorrection: true,
455
451
  ...coValue.knownState(),
456
452
  });
457
453
  }
458
454
  }
459
455
 
460
- async handleWrongAssumedKnownState(
461
- msg: WrongAssumedKnownStateMessage,
456
+ async handleCorrection(
457
+ msg: KnownStateMessage,
462
458
  peer: PeerState
463
459
  ) {
464
- const coValue = this.local.expectCoValueLoaded(msg.coValueID);
460
+ const coValue = this.local.expectCoValueLoaded(msg.id);
465
461
 
466
- peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
462
+ peer.optimisticKnownStates[msg.id] = combinedKnownStates(
467
463
  msg,
468
464
  coValue.knownState()
469
465
  );
@@ -475,7 +471,7 @@ export class SyncManager {
475
471
  }
476
472
  }
477
473
 
478
- handleUnsubscribe(_msg: UnsubscribeMessage) {
474
+ handleUnsubscribe(_msg: DoneMessage) {
479
475
  throw new Error("Method not implemented.");
480
476
  }
481
477
 
@@ -505,12 +501,11 @@ export class SyncManager {
505
501
 
506
502
  function knownStateIn(
507
503
  msg:
508
- | SubscribeMessage
509
- | TellKnownStateMessage
510
- | WrongAssumedKnownStateMessage
504
+ | LoadMessage
505
+ | KnownStateMessage
511
506
  ) {
512
507
  return {
513
- coValueID: msg.coValueID,
508
+ id: msg.id,
514
509
  header: msg.header,
515
510
  sessions: msg.sessions,
516
511
  };
@@ -0,0 +1,229 @@
1
+ import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js";
2
+ import { newRandomSessionID } from "./coValue.js";
3
+ import { LocalNode } from "./node.js";
4
+ import { expectTeamContent } from "./permissions.js";
5
+ import { AnonymousControlledAccount } from "./account.js";
6
+ import { SessionID } from "./ids.js";
7
+ import { ReadableStream, TransformStream, WritableStream } from "isomorphic-streams";
8
+ import { Peer, PeerID, SyncMessage } from "./sync.js";
9
+
10
+ export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccount, SessionID] {
11
+ const agentSecret = newRandomAgentSecret();
12
+
13
+ const sessionID = newRandomSessionID(getAgentID(agentSecret));
14
+
15
+ return [new AnonymousControlledAccount(agentSecret), sessionID];
16
+ }
17
+
18
+ export function newTeam() {
19
+ const [admin, sessionID] = randomAnonymousAccountAndSessionID();
20
+
21
+ const node = new LocalNode(admin, sessionID);
22
+
23
+ const team = node.createCoValue({
24
+ type: "comap",
25
+ ruleset: { type: "team", initialAdmin: admin.id },
26
+ meta: null,
27
+ ...createdNowUnique(),
28
+ });
29
+
30
+ const teamContent = expectTeamContent(team.getCurrentContent());
31
+
32
+ teamContent.edit((editable) => {
33
+ editable.set(admin.id, "admin", "trusting");
34
+ expect(editable.get(admin.id)).toEqual("admin");
35
+ });
36
+
37
+ return { node, team, admin };
38
+ }
39
+
40
+ export function teamWithTwoAdmins() {
41
+ const { team, admin, node } = newTeam();
42
+
43
+ const otherAdmin = node.createAccount("otherAdmin");
44
+
45
+ let content = expectTeamContent(team.getCurrentContent());
46
+
47
+ content.edit((editable) => {
48
+ editable.set(otherAdmin.id, "admin", "trusting");
49
+ expect(editable.get(otherAdmin.id)).toEqual("admin");
50
+ });
51
+
52
+ content = expectTeamContent(team.getCurrentContent());
53
+
54
+ if (content.type !== "comap") {
55
+ throw new Error("Expected map");
56
+ }
57
+
58
+ expect(content.get(otherAdmin.id)).toEqual("admin");
59
+ return { team, admin, otherAdmin, node };
60
+ }
61
+
62
+ export function newTeamHighLevel() {
63
+ const [admin, sessionID] = randomAnonymousAccountAndSessionID();
64
+
65
+
66
+ const node = new LocalNode(admin, sessionID);
67
+
68
+ const team = node.createTeam();
69
+
70
+ return { admin, node, team };
71
+ }
72
+
73
+ export function teamWithTwoAdminsHighLevel() {
74
+ const { admin, node, team } = newTeamHighLevel();
75
+
76
+ const otherAdmin = node.createAccount("otherAdmin");
77
+
78
+ team.addMember(otherAdmin.id, "admin");
79
+
80
+ return { admin, node, team, otherAdmin };
81
+ }
82
+
83
+ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
84
+ const queue: T[] = [];
85
+ let resolveNextItemReady: () => void = () => {};
86
+ let nextItemReady: Promise<void> = new Promise((resolve) => {
87
+ resolveNextItemReady = resolve;
88
+ });
89
+
90
+ let writerClosed = false;
91
+ let readerClosed = false;
92
+
93
+ const readable = new ReadableStream<T>({
94
+ async pull(controller) {
95
+ let retriesLeft = 3;
96
+ while (retriesLeft > 0) {
97
+ if (writerClosed) {
98
+ controller.close();
99
+ return;
100
+ }
101
+ retriesLeft--;
102
+ if (queue.length > 0) {
103
+ controller.enqueue(queue.shift()!);
104
+ if (queue.length === 0) {
105
+ nextItemReady = new Promise((resolve) => {
106
+ resolveNextItemReady = resolve;
107
+ });
108
+ }
109
+ return;
110
+ } else {
111
+ await nextItemReady;
112
+ }
113
+ }
114
+ throw new Error(
115
+ "Should only use one retry to get next item in queue."
116
+ );
117
+ },
118
+
119
+ cancel(reason) {
120
+ console.log("Manually closing reader");
121
+ readerClosed = true;
122
+ },
123
+ });
124
+
125
+ const writable = new WritableStream<T>({
126
+ write(chunk, controller) {
127
+ if (readerClosed) {
128
+ console.log("Reader closed, not writing chunk", chunk);
129
+ throw new Error("Reader closed, not writing chunk");
130
+ }
131
+ queue.push(chunk);
132
+ if (queue.length === 1) {
133
+ // make sure that await write resolves before corresponding read
134
+ process.nextTick(() => resolveNextItemReady());
135
+ }
136
+ },
137
+ abort(reason) {
138
+ console.log("Manually closing writer");
139
+ writerClosed = true;
140
+ resolveNextItemReady();
141
+ return Promise.resolve();
142
+ },
143
+ });
144
+
145
+ return [readable, writable];
146
+ }
147
+
148
+ export function shouldNotResolve<T>(
149
+ promise: Promise<T>,
150
+ ops: { timeout: number }
151
+ ): Promise<void> {
152
+ return new Promise((resolve, reject) => {
153
+ promise
154
+ .then((v) =>
155
+ reject(
156
+ new Error(
157
+ "Should not have resolved, but resolved to " +
158
+ JSON.stringify(v)
159
+ )
160
+ )
161
+ )
162
+ .catch(reject);
163
+ setTimeout(resolve, ops.timeout);
164
+ });
165
+ }
166
+
167
+ export function connectedPeers(
168
+ peer1id: PeerID,
169
+ peer2id: PeerID,
170
+ {
171
+ trace = false,
172
+ peer1role = "peer",
173
+ peer2role = "peer",
174
+ }: {
175
+ trace?: boolean;
176
+ peer1role?: Peer["role"];
177
+ peer2role?: Peer["role"];
178
+ } = {}
179
+ ): [Peer, Peer] {
180
+ const [inRx1, inTx1] = newStreamPair<SyncMessage>();
181
+ const [outRx1, outTx1] = newStreamPair<SyncMessage>();
182
+
183
+ const [inRx2, inTx2] = newStreamPair<SyncMessage>();
184
+ const [outRx2, outTx2] = newStreamPair<SyncMessage>();
185
+
186
+ void outRx2
187
+ .pipeThrough(
188
+ new TransformStream({
189
+ transform(
190
+ chunk: SyncMessage,
191
+ controller: { enqueue: (msg: SyncMessage) => void }
192
+ ) {
193
+ trace && console.log(`${peer2id} -> ${peer1id}`, JSON.stringify(chunk, null, 2));
194
+ controller.enqueue(chunk);
195
+ },
196
+ })
197
+ )
198
+ .pipeTo(inTx1);
199
+
200
+ void outRx1
201
+ .pipeThrough(
202
+ new TransformStream({
203
+ transform(
204
+ chunk: SyncMessage,
205
+ controller: { enqueue: (msg: SyncMessage) => void }
206
+ ) {
207
+ trace && console.log(`${peer1id} -> ${peer2id}`, JSON.stringify(chunk, null, 2));
208
+ controller.enqueue(chunk);
209
+ },
210
+ })
211
+ )
212
+ .pipeTo(inTx2);
213
+
214
+ const peer2AsPeer: Peer = {
215
+ id: peer2id,
216
+ incoming: inRx1,
217
+ outgoing: outTx1,
218
+ role: peer2role,
219
+ };
220
+
221
+ const peer1AsPeer: Peer = {
222
+ id: peer1id,
223
+ incoming: inRx2,
224
+ outgoing: outTx2,
225
+ role: peer1role,
226
+ };
227
+
228
+ return [peer1AsPeer, peer2AsPeer];
229
+ }