cojson 0.0.10 → 0.0.12

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 +16 -35
  9. package/dist/coValue.js +49 -112
  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 +26 -28
  47. package/dist/sync.js +81 -67
  48. package/dist/sync.js.map +1 -1
  49. package/dist/sync.test.js +194 -293
  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 +93 -179
  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 +284 -426
  74. package/src/sync.ts +115 -105
  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
  };
@@ -56,14 +56,9 @@ export type SessionNewContent = {
56
56
  lastHash: Hash;
57
57
  lastSignature: Signature;
58
58
  };
59
-
60
- export type WrongAssumedKnownStateMessage = {
61
- action: "wrongAssumedKnownState";
62
- } & CoValueKnownState;
63
-
64
- export type UnsubscribeMessage = {
65
- action: "unsubscribe";
66
- coValueID: RawCoValueID;
59
+ export type DoneMessage = {
60
+ action: "done";
61
+ id: RawCoID;
67
62
  };
68
63
 
69
64
  export type PeerID = string;
@@ -77,8 +72,8 @@ export interface Peer {
77
72
 
78
73
  export interface PeerState {
79
74
  id: PeerID;
80
- optimisticKnownStates: { [coValueID: RawCoValueID]: CoValueKnownState };
81
- toldKnownState: Set<RawCoValueID>;
75
+ optimisticKnownStates: { [id: RawCoID]: CoValueKnownState };
76
+ toldKnownState: Set<RawCoID>;
82
77
  incoming: ReadableStream<SyncMessage>;
83
78
  outgoing: WritableStreamDefaultWriter<SyncMessage>;
84
79
  role: "peer" | "server" | "client";
@@ -103,7 +98,7 @@ export function combinedKnownStates(
103
98
  }
104
99
 
105
100
  return {
106
- coValueID: stateA.coValueID,
101
+ id: stateA.id,
107
102
  header: stateA.header || stateB.header,
108
103
  sessions: sessionStates,
109
104
  };
@@ -117,12 +112,12 @@ export class SyncManager {
117
112
  this.local = local;
118
113
  }
119
114
 
120
- loadFromPeers(id: RawCoValueID) {
115
+ loadFromPeers(id: RawCoID) {
121
116
  for (const peer of Object.values(this.peers)) {
122
117
  peer.outgoing
123
118
  .write({
124
- action: "subscribe",
125
- coValueID: id,
119
+ action: "load",
120
+ id: id,
126
121
  header: false,
127
122
  sessions: {},
128
123
  })
@@ -135,15 +130,17 @@ export class SyncManager {
135
130
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
136
131
  // TODO: validate
137
132
  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":
133
+ case "load":
134
+ return await this.handleLoad(msg, peer);
135
+ case "known":
136
+ if (msg.isCorrection) {
137
+ return await this.handleCorrection(msg, peer);
138
+ } else {
139
+ return await this.handleKnownState(msg, peer);
140
+ }
141
+ case "content":
143
142
  return await this.handleNewContent(msg, peer);
144
- case "wrongAssumedKnownState":
145
- return await this.handleWrongAssumedKnownState(msg, peer);
146
- case "unsubscribe":
143
+ case "done":
147
144
  return await this.handleUnsubscribe(msg);
148
145
  default:
149
146
  throw new Error(
@@ -155,69 +152,87 @@ export class SyncManager {
155
152
  }
156
153
 
157
154
  async subscribeToIncludingDependencies(
158
- coValueID: RawCoValueID,
155
+ id: RawCoID,
159
156
  peer: PeerState
160
157
  ) {
161
- const coValue = this.local.expectCoValueLoaded(coValueID);
158
+ const entry = this.local.coValues[id];
162
159
 
163
- for (const coValueID of coValue.getDependedOnCoValues()) {
164
- await this.subscribeToIncludingDependencies(coValueID, peer);
160
+ if (!entry) {
161
+ throw new Error(
162
+ "Expected coValue entry on subscribe"
163
+ );
165
164
  }
166
165
 
167
- if (!peer.toldKnownState.has(coValueID)) {
168
- peer.toldKnownState.add(coValueID);
166
+ if (entry.state === "loading") {
169
167
  await this.trySendToPeer(peer, {
170
- action: "subscribe",
168
+ action: "load",
169
+ id,
170
+ header: false,
171
+ sessions: {},
172
+ });
173
+ return;
174
+ }
175
+
176
+ const coValue = entry.coValue;
177
+
178
+ for (const id of coValue.getDependedOnCoValues()) {
179
+ await this.subscribeToIncludingDependencies(id, peer);
180
+ }
181
+
182
+ if (!peer.toldKnownState.has(id)) {
183
+ peer.toldKnownState.add(id);
184
+ await this.trySendToPeer(peer, {
185
+ action: "load",
171
186
  ...coValue.knownState(),
172
187
  });
173
188
  }
174
189
  }
175
190
 
176
191
  async tellUntoldKnownStateIncludingDependencies(
177
- coValueID: RawCoValueID,
192
+ id: RawCoID,
178
193
  peer: PeerState,
179
- asDependencyOf?: RawCoValueID
194
+ asDependencyOf?: RawCoID
180
195
  ) {
181
- const coValue = this.local.expectCoValueLoaded(coValueID);
196
+ const coValue = this.local.expectCoValueLoaded(id);
182
197
 
183
- for (const dependentCoValueID of coValue.getDependedOnCoValues()) {
198
+ for (const dependentCoID of coValue.getDependedOnCoValues()) {
184
199
  await this.tellUntoldKnownStateIncludingDependencies(
185
- dependentCoValueID,
200
+ dependentCoID,
186
201
  peer,
187
- asDependencyOf || coValueID
202
+ asDependencyOf || id
188
203
  );
189
204
  }
190
205
 
191
- if (!peer.toldKnownState.has(coValueID)) {
206
+ if (!peer.toldKnownState.has(id)) {
192
207
  await this.trySendToPeer(peer, {
193
- action: "tellKnownState",
208
+ action: "known",
194
209
  asDependencyOf,
195
210
  ...coValue.knownState(),
196
211
  });
197
212
 
198
- peer.toldKnownState.add(coValueID);
213
+ peer.toldKnownState.add(id);
199
214
  }
200
215
  }
201
216
 
202
217
  async sendNewContentIncludingDependencies(
203
- coValueID: RawCoValueID,
218
+ id: RawCoID,
204
219
  peer: PeerState
205
220
  ) {
206
- const coValue = this.local.expectCoValueLoaded(coValueID);
221
+ const coValue = this.local.expectCoValueLoaded(id);
207
222
 
208
- for (const coValueID of coValue.getDependedOnCoValues()) {
209
- await this.sendNewContentIncludingDependencies(coValueID, peer);
223
+ for (const id of coValue.getDependedOnCoValues()) {
224
+ await this.sendNewContentIncludingDependencies(id, peer);
210
225
  }
211
226
 
212
227
  const newContent = coValue.newContentSince(
213
- peer.optimisticKnownStates[coValueID]
228
+ peer.optimisticKnownStates[id]
214
229
  );
215
230
 
216
231
  if (newContent) {
217
232
  await this.trySendToPeer(peer, newContent);
218
- peer.optimisticKnownStates[coValueID] = combinedKnownStates(
219
- peer.optimisticKnownStates[coValueID] ||
220
- emptyKnownState(coValueID),
233
+ peer.optimisticKnownStates[id] = combinedKnownStates(
234
+ peer.optimisticKnownStates[id] ||
235
+ emptyKnownState(id),
221
236
  coValue.knownState()
222
237
  );
223
238
  }
@@ -236,18 +251,13 @@ export class SyncManager {
236
251
 
237
252
  if (peer.role === "server") {
238
253
  const initialSync = async () => {
239
- for (const entry of Object.values(this.local.coValues)) {
240
- if (entry.state === "loading") {
241
- continue;
242
- }
243
-
244
- await this.subscribeToIncludingDependencies(
245
- entry.coValue.id,
246
- peerState
247
- );
254
+ for (const id of Object.keys(
255
+ this.local.coValues
256
+ ) as RawCoID[]) {
257
+ await this.subscribeToIncludingDependencies(id, peerState);
248
258
 
249
- peerState.optimisticKnownStates[entry.coValue.id] = {
250
- coValueID: entry.coValue.id,
259
+ peerState.optimisticKnownStates[id] = {
260
+ id: id,
251
261
  header: false,
252
262
  sessions: {},
253
263
  };
@@ -282,20 +292,20 @@ export class SyncManager {
282
292
  });
283
293
  }
284
294
 
285
- async handleSubscribe(msg: SubscribeMessage, peer: PeerState) {
286
- const entry = this.local.coValues[msg.coValueID];
295
+ async handleLoad(msg: LoadMessage, peer: PeerState) {
296
+ const entry = this.local.coValues[msg.id];
287
297
 
288
298
  if (!entry || entry.state === "loading") {
289
299
  if (!entry) {
290
- this.local.coValues[msg.coValueID] = newLoadingState();
300
+ this.local.coValues[msg.id] = newLoadingState();
291
301
  }
292
302
 
293
- peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
294
- peer.toldKnownState.add(msg.coValueID);
303
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
304
+ peer.toldKnownState.add(msg.id);
295
305
 
296
306
  await this.trySendToPeer(peer, {
297
- action: "tellKnownState",
298
- coValueID: msg.coValueID,
307
+ action: "known",
308
+ id: msg.id,
299
309
  header: false,
300
310
  sessions: {},
301
311
  });
@@ -303,22 +313,22 @@ export class SyncManager {
303
313
  return;
304
314
  }
305
315
 
306
- peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
316
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
307
317
 
308
318
  await this.tellUntoldKnownStateIncludingDependencies(
309
- msg.coValueID,
319
+ msg.id,
310
320
  peer
311
321
  );
312
322
 
313
- await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
323
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
314
324
  }
315
325
 
316
- async handleTellKnownState(msg: TellKnownStateMessage, peer: PeerState) {
317
- let entry = this.local.coValues[msg.coValueID];
326
+ async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
327
+ let entry = this.local.coValues[msg.id];
318
328
 
319
- peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
320
- peer.optimisticKnownStates[msg.coValueID] ||
321
- emptyKnownState(msg.coValueID),
329
+ peer.optimisticKnownStates[msg.id] = combinedKnownStates(
330
+ peer.optimisticKnownStates[msg.id] ||
331
+ emptyKnownState(msg.id),
322
332
  knownStateIn(msg)
323
333
  );
324
334
 
@@ -327,7 +337,7 @@ export class SyncManager {
327
337
  if (this.local.coValues[msg.asDependencyOf]) {
328
338
  entry = newLoadingState();
329
339
 
330
- this.local.coValues[msg.coValueID] = entry;
340
+ this.local.coValues[msg.id] = entry;
331
341
  } else {
332
342
  throw new Error(
333
343
  "Expected coValue dependency entry to be created, missing subscribe?"
@@ -345,14 +355,14 @@ export class SyncManager {
345
355
  }
346
356
 
347
357
  await this.tellUntoldKnownStateIncludingDependencies(
348
- msg.coValueID,
358
+ msg.id,
349
359
  peer
350
360
  );
351
- await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
361
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
352
362
  }
353
363
 
354
364
  async handleNewContent(msg: NewContentMessage, peer: PeerState) {
355
- let entry = this.local.coValues[msg.coValueID];
365
+ let entry = this.local.coValues[msg.id];
356
366
 
357
367
  if (!entry) {
358
368
  throw new Error(
@@ -363,7 +373,7 @@ export class SyncManager {
363
373
  let resolveAfterDone: ((coValue: CoValue) => void) | undefined;
364
374
 
365
375
  const peerOptimisticKnownState =
366
- peer.optimisticKnownStates[msg.coValueID];
376
+ peer.optimisticKnownStates[msg.id];
367
377
 
368
378
  if (!peerOptimisticKnownState) {
369
379
  throw new Error(
@@ -387,7 +397,7 @@ export class SyncManager {
387
397
  coValue: coValue,
388
398
  };
389
399
 
390
- this.local.coValues[msg.coValueID] = entry;
400
+ this.local.coValues[msg.id] = entry;
391
401
  }
392
402
 
393
403
  const coValue = entry.coValue;
@@ -395,7 +405,7 @@ export class SyncManager {
395
405
  let invalidStateAssumed = false;
396
406
 
397
407
  for (const [sessionID, newContentForSession] of Object.entries(
398
- msg.newContent
408
+ msg.new
399
409
  ) as [SessionID, SessionNewContent][]) {
400
410
  const ourKnownTxIdx =
401
411
  coValue.sessions[sessionID]?.transactions.length;
@@ -438,19 +448,20 @@ export class SyncManager {
438
448
 
439
449
  if (invalidStateAssumed) {
440
450
  await this.trySendToPeer(peer, {
441
- action: "wrongAssumedKnownState",
451
+ action: "known",
452
+ isCorrection: true,
442
453
  ...coValue.knownState(),
443
454
  });
444
455
  }
445
456
  }
446
457
 
447
- async handleWrongAssumedKnownState(
448
- msg: WrongAssumedKnownStateMessage,
458
+ async handleCorrection(
459
+ msg: KnownStateMessage,
449
460
  peer: PeerState
450
461
  ) {
451
- const coValue = this.local.expectCoValueLoaded(msg.coValueID);
462
+ const coValue = this.local.expectCoValueLoaded(msg.id);
452
463
 
453
- peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
464
+ peer.optimisticKnownStates[msg.id] = combinedKnownStates(
454
465
  msg,
455
466
  coValue.knownState()
456
467
  );
@@ -462,7 +473,7 @@ export class SyncManager {
462
473
  }
463
474
  }
464
475
 
465
- handleUnsubscribe(_msg: UnsubscribeMessage) {
476
+ handleUnsubscribe(_msg: DoneMessage) {
466
477
  throw new Error("Method not implemented.");
467
478
  }
468
479
 
@@ -492,12 +503,11 @@ export class SyncManager {
492
503
 
493
504
  function knownStateIn(
494
505
  msg:
495
- | SubscribeMessage
496
- | TellKnownStateMessage
497
- | WrongAssumedKnownStateMessage
506
+ | LoadMessage
507
+ | KnownStateMessage
498
508
  ) {
499
509
  return {
500
- coValueID: msg.coValueID,
510
+ id: msg.id,
501
511
  header: msg.header,
502
512
  sessions: msg.sessions,
503
513
  };
@@ -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
+ }