cojson 0.7.11 → 0.7.14

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.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  import { Signature } from "./crypto/crypto.js";
2
2
  import { CoValueHeader, Transaction } from "./coValueCore.js";
3
3
  import { CoValueCore } from "./coValueCore.js";
4
- import { LocalNode } from "./localNode.js";
5
- import {
6
- ReadableStream,
7
- WritableStream,
8
- WritableStreamDefaultWriter,
9
- } from "isomorphic-streams";
4
+ import { LocalNode, newLoadingState } from "./localNode.js";
10
5
  import { RawCoID, SessionID } from "./ids.js";
6
+ import { Effect, Queue, Stream } from "effect";
11
7
 
12
8
  export type CoValueKnownState = {
13
9
  id: RawCoID;
@@ -60,10 +56,27 @@ export type DoneMessage = {
60
56
 
61
57
  export type PeerID = string;
62
58
 
59
+ export class DisconnectedError extends Error {
60
+ readonly _tag = "DisconnectedError";
61
+ constructor(public message: string) {
62
+ super(message);
63
+ }
64
+ }
65
+
66
+ export class PingTimeoutError extends Error {
67
+ readonly _tag = "PingTimeoutError";
68
+ }
69
+
70
+ export type IncomingSyncStream = Stream.Stream<
71
+ SyncMessage,
72
+ DisconnectedError | PingTimeoutError
73
+ >;
74
+ export type OutgoingSyncQueue = Queue.Enqueue<SyncMessage>;
75
+
63
76
  export interface Peer {
64
77
  id: PeerID;
65
- incoming: ReadableStream<SyncMessage>;
66
- outgoing: WritableStream<SyncMessage>;
78
+ incoming: IncomingSyncStream;
79
+ outgoing: OutgoingSyncQueue;
67
80
  role: "peer" | "server" | "client";
68
81
  delayOnError?: number;
69
82
  priority?: number;
@@ -73,8 +86,8 @@ export interface PeerState {
73
86
  id: PeerID;
74
87
  optimisticKnownStates: { [id: RawCoID]: CoValueKnownState };
75
88
  toldKnownState: Set<RawCoID>;
76
- incoming: ReadableStream<SyncMessage>;
77
- outgoing: WritableStreamDefaultWriter<SyncMessage>;
89
+ incoming: IncomingSyncStream;
90
+ outgoing: OutgoingSyncQueue;
78
91
  role: "peer" | "server" | "client";
79
92
  delayOnError?: number;
80
93
  priority?: number;
@@ -127,25 +140,24 @@ export class SyncManager {
127
140
  });
128
141
  }
129
142
 
130
- async loadFromPeers(id: RawCoID, excludePeer?: PeerID) {
131
- for (const peer of this.peersInPriorityOrder()) {
132
- if (peer.id === excludePeer) {
133
- continue;
134
- }
135
- if (peer.role !== "server") {
136
- continue;
137
- }
143
+ async loadFromPeers(id: RawCoID, forPeer?: PeerID) {
144
+ const eligiblePeers = this.peersInPriorityOrder().filter(
145
+ (peer) => peer.id !== forPeer && peer.role === "server",
146
+ );
147
+
148
+ for (const peer of eligiblePeers) {
138
149
  // console.log("loading", id, "from", peer.id);
139
- peer.outgoing
140
- .write({
150
+ Effect.runPromise(
151
+ Queue.offer(peer.outgoing, {
141
152
  action: "load",
142
153
  id: id,
143
154
  header: false,
144
155
  sessions: {},
145
- })
146
- .catch((e) => {
147
- console.error("Error writing to peer", e);
148
- });
156
+ }),
157
+ ).catch((e) => {
158
+ console.error("Error writing to peer", e);
159
+ });
160
+
149
161
  const coValueEntry = this.local.coValues[id];
150
162
  if (coValueEntry?.state !== "loading") {
151
163
  continue;
@@ -297,7 +309,9 @@ export class SyncManager {
297
309
  let lastYield = performance.now();
298
310
  for (const [_i, piece] of newContentPieces.entries()) {
299
311
  // console.log(
300
- // `${id} -> ${peer.id}: Sending content piece ${i + 1}/${newContentPieces.length} header: ${!!piece.header}`,
312
+ // `${id} -> ${peer.id}: Sending content piece ${i + 1}/${
313
+ // newContentPieces.length
314
+ // } header: ${!!piece.header}`,
301
315
  // // Object.values(piece.new).map((s) => s.newTransactions)
302
316
  // );
303
317
  await this.trySendToPeer(peer, piece);
@@ -328,7 +342,7 @@ export class SyncManager {
328
342
  id: peer.id,
329
343
  optimisticKnownStates: {},
330
344
  incoming: peer.incoming,
331
- outgoing: peer.outgoing.getWriter(),
345
+ outgoing: peer.outgoing,
332
346
  toldKnownState: new Set(),
333
347
  role: peer.role,
334
348
  delayOnError: peer.delayOnError,
@@ -354,91 +368,55 @@ export class SyncManager {
354
368
  void initialSync();
355
369
  }
356
370
 
357
- const readIncoming = async () => {
358
- try {
359
- for await (const msg of peerState.incoming) {
360
- try {
361
- // await this.handleSyncMessage(msg, peerState);
362
- this.handleSyncMessage(msg, peerState).catch((e) => {
363
- console.error(
364
- new Date(),
365
- `Error reading from peer ${peer.id}, handling msg`,
366
- JSON.stringify(msg, (k, v) =>
367
- k === "changes" || k === "encryptedChanges"
368
- ? v.slice(0, 20) + "..."
369
- : v,
370
- ),
371
- e,
372
- );
373
- });
374
- // await new Promise<void>((resolve) => {
375
- // setTimeout(resolve, 0);
376
- // });
377
- } catch (e) {
378
- console.error(
379
- new Date(),
380
- `Error reading from peer ${peer.id}, handling msg`,
381
- JSON.stringify(msg, (k, v) =>
382
- k === "changes" || k === "encryptedChanges"
383
- ? v.slice(0, 20) + "..."
384
- : v,
371
+ void Effect.runPromise(
372
+ peerState.incoming.pipe(
373
+ Stream.ensuring(
374
+ Effect.sync(() => {
375
+ console.log("Peer disconnected:", peer.id);
376
+ delete this.peers[peer.id];
377
+ }),
378
+ ),
379
+ Stream.runForEach((msg) =>
380
+ Effect.tryPromise({
381
+ try: () => this.handleSyncMessage(msg, peerState),
382
+ catch: (e) =>
383
+ new Error(
384
+ `Error reading from peer ${
385
+ peer.id
386
+ }, handling msg\n\n${JSON.stringify(
387
+ msg,
388
+ (k, v) =>
389
+ k === "changes" ||
390
+ k === "encryptedChanges"
391
+ ? v.slice(0, 20) + "..."
392
+ : v,
393
+ )}`,
394
+ { cause: e },
385
395
  ),
386
- e,
387
- );
388
- if (peerState.delayOnError) {
389
- await new Promise<void>((resolve) => {
390
- setTimeout(resolve, peerState.delayOnError);
391
- });
392
- }
393
- }
394
- }
395
- } catch (e) {
396
- console.error(`Error reading from peer ${peer.id}`, e);
397
- }
398
-
399
- console.log("Peer disconnected:", peer.id);
400
- delete this.peers[peer.id];
401
- };
402
-
403
- void readIncoming();
396
+ }).pipe(
397
+ Effect.timeoutFail({
398
+ duration: 10000,
399
+ onTimeout: () =>
400
+ new Error("Took >10s to process message"),
401
+ }),
402
+ ),
403
+ ),
404
+ Effect.catchAll((e) =>
405
+ Effect.logError(
406
+ "Error in peer",
407
+ peer.id,
408
+ e.message,
409
+ typeof e.cause === "object" &&
410
+ e.cause instanceof Error &&
411
+ e.cause.message,
412
+ ),
413
+ ),
414
+ ),
415
+ );
404
416
  }
405
417
 
406
418
  trySendToPeer(peer: PeerState, msg: SyncMessage) {
407
- if (!this.peers[peer.id]) {
408
- // already disconnected, return to drain potential queue
409
- return Promise.resolve();
410
- }
411
-
412
- return new Promise<void>((resolve) => {
413
- const start = Date.now();
414
- peer.outgoing
415
- .write(msg)
416
- .then(() => {
417
- const end = Date.now();
418
- if (end - start > 1000) {
419
- // console.error(
420
- // new Error(
421
- // `Writing to peer "${peer.id}" took ${
422
- // Math.round((Date.now() - start) / 100) / 10
423
- // }s - this should never happen as write should resolve quickly or error`
424
- // )
425
- // );
426
- } else {
427
- resolve();
428
- }
429
- })
430
- .catch((e) => {
431
- console.error(
432
- new Error(
433
- `Error writing to peer ${peer.id}, disconnecting`,
434
- {
435
- cause: e,
436
- },
437
- ),
438
- );
439
- delete this.peers[peer.id];
440
- });
441
- });
419
+ return Effect.runPromise(Queue.offer(peer.outgoing, msg));
442
420
  }
443
421
 
444
422
  async handleLoad(msg: LoadMessage, peer: PeerState) {
@@ -447,21 +425,50 @@ export class SyncManager {
447
425
 
448
426
  if (!entry) {
449
427
  // console.log(`Loading ${msg.id} from all peers except ${peer.id}`);
450
- this.local
451
- .loadCoValueCore(msg.id, {
452
- dontLoadFrom: peer.id,
453
- dontWaitFor: peer.id,
454
- })
455
- .catch((e) => {
456
- console.error("Error loading coValue in handleLoad", e);
457
- });
428
+
429
+ // special case: we should be able to solve this much more neatly
430
+ // with an explicit state machine in the future
431
+ const eligiblePeers = this.peersInPriorityOrder().filter(
432
+ (other) => other.id !== peer.id && peer.role === "server",
433
+ );
434
+ if (eligiblePeers.length === 0) {
435
+ if (msg.header || Object.keys(msg.sessions).length > 0) {
436
+ this.local.coValues[msg.id] = newLoadingState(
437
+ new Set([peer.id]),
438
+ );
439
+ this.trySendToPeer(peer, {
440
+ action: "known",
441
+ id: msg.id,
442
+ header: false,
443
+ sessions: {},
444
+ }).catch((e) => {
445
+ console.error("Error sending known state back", e);
446
+ });
447
+ }
448
+ return;
449
+ } else {
450
+ this.local
451
+ .loadCoValueCore(msg.id, {
452
+ dontLoadFrom: peer.id,
453
+ dontWaitFor: peer.id,
454
+ })
455
+ .catch((e) => {
456
+ console.error("Error loading coValue in handleLoad", e);
457
+ });
458
+ }
458
459
 
459
460
  entry = this.local.coValues[msg.id]!;
460
461
  }
461
462
 
462
463
  if (entry.state === "loading") {
464
+ console.log(
465
+ "Waiting for loaded",
466
+ msg.id,
467
+ "after message from",
468
+ peer.id,
469
+ );
463
470
  const loaded = await entry.done;
464
-
471
+ console.log("Loaded", msg.id, loaded);
465
472
  if (loaded === "unavailable") {
466
473
  peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
467
474
  peer.toldKnownState.add(msg.id);
@@ -508,7 +515,7 @@ export class SyncManager {
508
515
  }
509
516
  } else {
510
517
  throw new Error(
511
- "Expected coValue entry to be created, missing subscribe?",
518
+ `Expected coValue entry for ${msg.id} to be created on known state, missing subscribe?`,
512
519
  );
513
520
  }
514
521
  }
@@ -549,7 +556,7 @@ export class SyncManager {
549
556
 
550
557
  if (!entry) {
551
558
  throw new Error(
552
- "Expected coValue entry to be created, missing subscribe?",
559
+ `Expected coValue entry for ${msg.id} to be created on new content, missing subscribe?`,
553
560
  );
554
561
  }
555
562
 
@@ -3,6 +3,7 @@ import { newRandomSessionID } from "../coValueCore.js";
3
3
  import { LocalNode } from "../localNode.js";
4
4
  import { connectedPeers } from "../streamUtils.js";
5
5
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
6
+ import { Effect } from "effect";
6
7
 
7
8
  const Crypto = await WasmCrypto.create();
8
9
 
@@ -52,11 +53,13 @@ test("Can create account with one node, and then load it on another", async () =
52
53
  map.set("foo", "bar", "private");
53
54
  expect(map.get("foo")).toEqual("bar");
54
55
 
55
- const [node1asPeer, node2asPeer] = connectedPeers("node1", "node2", {
56
+ const [node1asPeer, node2asPeer] = await Effect.runPromise(connectedPeers("node1", "node2", {
56
57
  trace: true,
57
58
  peer1role: "server",
58
59
  peer2role: "client",
59
- });
60
+ }));
61
+
62
+ console.log("After connected peers")
60
63
 
61
64
  node.syncManager.addPeer(node2asPeer);
62
65