@freqhole/reliquary 0.0.0
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/dist/automerge/acl-filtering.d.ts +87 -0
- package/dist/automerge/acl-filtering.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.js +138 -0
- package/dist/automerge/acl-filtering.test.d.ts +2 -0
- package/dist/automerge/acl-filtering.test.d.ts.map +1 -0
- package/dist/automerge/acl-filtering.test.js +302 -0
- package/dist/automerge/index.d.ts +5 -0
- package/dist/automerge/index.d.ts.map +1 -0
- package/dist/automerge/index.js +7 -0
- package/dist/automerge/iroh-network-adapter.d.ts +178 -0
- package/dist/automerge/iroh-network-adapter.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.js +691 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts +2 -0
- package/dist/automerge/iroh-network-adapter.test.d.ts.map +1 -0
- package/dist/automerge/iroh-network-adapter.test.js +818 -0
- package/dist/automerge/types.d.ts +66 -0
- package/dist/automerge/types.d.ts.map +1 -0
- package/dist/automerge/types.js +10 -0
- package/dist/blobs/bytes-backend.d.ts +41 -0
- package/dist/blobs/bytes-backend.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.js +252 -0
- package/dist/blobs/bytes-backend.test.d.ts +2 -0
- package/dist/blobs/bytes-backend.test.d.ts.map +1 -0
- package/dist/blobs/bytes-backend.test.js +241 -0
- package/dist/blobs/db.d.ts +8 -0
- package/dist/blobs/db.d.ts.map +1 -0
- package/dist/blobs/db.js +141 -0
- package/dist/blobs/db.test.d.ts +2 -0
- package/dist/blobs/db.test.d.ts.map +1 -0
- package/dist/blobs/db.test.js +120 -0
- package/dist/blobs/index.d.ts +5 -0
- package/dist/blobs/index.d.ts.map +1 -0
- package/dist/blobs/index.js +2 -0
- package/dist/blobs/store.d.ts +47 -0
- package/dist/blobs/store.d.ts.map +1 -0
- package/dist/blobs/store.js +263 -0
- package/dist/blobs/store.test.d.ts +2 -0
- package/dist/blobs/store.test.d.ts.map +1 -0
- package/dist/blobs/store.test.js +377 -0
- package/dist/blobs/types.d.ts +55 -0
- package/dist/blobs/types.d.ts.map +1 -0
- package/dist/blobs/types.js +8 -0
- package/dist/ensure/client.d.ts +19 -0
- package/dist/ensure/client.d.ts.map +1 -0
- package/dist/ensure/client.js +74 -0
- package/dist/ensure/client.test.d.ts +2 -0
- package/dist/ensure/client.test.d.ts.map +1 -0
- package/dist/ensure/client.test.js +133 -0
- package/dist/ensure/index.d.ts +4 -0
- package/dist/ensure/index.d.ts.map +1 -0
- package/dist/ensure/index.js +4 -0
- package/dist/ensure/responder.d.ts +20 -0
- package/dist/ensure/responder.d.ts.map +1 -0
- package/dist/ensure/responder.js +100 -0
- package/dist/ensure/responder.test.d.ts +2 -0
- package/dist/ensure/responder.test.d.ts.map +1 -0
- package/dist/ensure/responder.test.js +191 -0
- package/dist/ensure/types.d.ts +19 -0
- package/dist/ensure/types.d.ts.map +1 -0
- package/dist/ensure/types.js +4 -0
- package/dist/ensure/types.test.d.ts +2 -0
- package/dist/ensure/types.test.d.ts.map +1 -0
- package/dist/ensure/types.test.js +81 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/solid/blob-url.d.ts +10 -0
- package/dist/solid/blob-url.d.ts.map +1 -0
- package/dist/solid/blob-url.js +34 -0
- package/dist/solid/blob-url.test.d.ts +2 -0
- package/dist/solid/blob-url.test.d.ts.map +1 -0
- package/dist/solid/blob-url.test.js +62 -0
- package/dist/solid/connection-summary.d.ts +25 -0
- package/dist/solid/connection-summary.d.ts.map +1 -0
- package/dist/solid/connection-summary.js +29 -0
- package/dist/solid/connection-summary.test.d.ts +2 -0
- package/dist/solid/connection-summary.test.d.ts.map +1 -0
- package/dist/solid/connection-summary.test.js +57 -0
- package/dist/solid/doc-store.d.ts +29 -0
- package/dist/solid/doc-store.d.ts.map +1 -0
- package/dist/solid/doc-store.js +79 -0
- package/dist/solid/doc-store.test.d.ts +2 -0
- package/dist/solid/doc-store.test.d.ts.map +1 -0
- package/dist/solid/doc-store.test.js +234 -0
- package/dist/solid/index.d.ts +4 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +9 -0
- package/dist/solid/transfer-progress.d.ts +21 -0
- package/dist/solid/transfer-progress.d.ts.map +1 -0
- package/dist/solid/transfer-progress.js +32 -0
- package/dist/solid/transfer-progress.test.d.ts +2 -0
- package/dist/solid/transfer-progress.test.d.ts.map +1 -0
- package/dist/solid/transfer-progress.test.js +76 -0
- package/dist/testing/blob-fixtures.d.ts +19 -0
- package/dist/testing/blob-fixtures.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.js +46 -0
- package/dist/testing/blob-fixtures.test.d.ts +2 -0
- package/dist/testing/blob-fixtures.test.d.ts.map +1 -0
- package/dist/testing/blob-fixtures.test.js +46 -0
- package/dist/testing/idb-harness.d.ts +10 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +17 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +48 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/media.d.ts +15 -0
- package/dist/testing/media.d.ts.map +1 -0
- package/dist/testing/media.js +37 -0
- package/dist/testing/media.test.d.ts +2 -0
- package/dist/testing/media.test.d.ts.map +1 -0
- package/dist/testing/media.test.js +47 -0
- package/dist/testing/mock-blob-fetch.d.ts +33 -0
- package/dist/testing/mock-blob-fetch.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.js +39 -0
- package/dist/testing/mock-blob-fetch.test.d.ts +2 -0
- package/dist/testing/mock-blob-fetch.test.d.ts.map +1 -0
- package/dist/testing/mock-blob-fetch.test.js +65 -0
- package/dist/testing/mock-node.d.ts +36 -0
- package/dist/testing/mock-node.d.ts.map +1 -0
- package/dist/testing/mock-node.js +97 -0
- package/dist/testing/mock-node.test.d.ts +2 -0
- package/dist/testing/mock-node.test.d.ts.map +1 -0
- package/dist/testing/mock-node.test.js +66 -0
- package/dist/testing/streams.d.ts +68 -0
- package/dist/testing/streams.d.ts.map +1 -0
- package/dist/testing/streams.js +99 -0
- package/dist/testing/streams.test.d.ts +2 -0
- package/dist/testing/streams.test.d.ts.map +1 -0
- package/dist/testing/streams.test.js +82 -0
- package/dist/transfer/cancellation.d.ts +8 -0
- package/dist/transfer/cancellation.d.ts.map +1 -0
- package/dist/transfer/cancellation.js +19 -0
- package/dist/transfer/index.d.ts +8 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +4 -0
- package/dist/transfer/prefetch.d.ts +38 -0
- package/dist/transfer/prefetch.d.ts.map +1 -0
- package/dist/transfer/prefetch.js +61 -0
- package/dist/transfer/prefetch.test.d.ts +2 -0
- package/dist/transfer/prefetch.test.d.ts.map +1 -0
- package/dist/transfer/prefetch.test.js +136 -0
- package/dist/transfer/serve.d.ts +66 -0
- package/dist/transfer/serve.d.ts.map +1 -0
- package/dist/transfer/serve.js +126 -0
- package/dist/transfer/serve.test.d.ts +2 -0
- package/dist/transfer/serve.test.d.ts.map +1 -0
- package/dist/transfer/serve.test.js +101 -0
- package/dist/transfer/snatch.d.ts +39 -0
- package/dist/transfer/snatch.d.ts.map +1 -0
- package/dist/transfer/snatch.js +289 -0
- package/dist/transfer/snatch.test.d.ts +2 -0
- package/dist/transfer/snatch.test.d.ts.map +1 -0
- package/dist/transfer/snatch.test.js +337 -0
- package/dist/transfer/types.d.ts +85 -0
- package/dist/transfer/types.d.ts.map +1 -0
- package/dist/transfer/types.js +15 -0
- package/dist/utils/hash.d.ts +13 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +31 -0
- package/dist/utils/hash.test.d.ts +2 -0
- package/dist/utils/hash.test.d.ts.map +1 -0
- package/dist/utils/hash.test.js +56 -0
- package/dist/utils/image-utils.d.ts +28 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +113 -0
- package/dist/utils/image-utils.test.d.ts +2 -0
- package/dist/utils/image-utils.test.d.ts.map +1 -0
- package/dist/utils/image-utils.test.js +181 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/log.d.ts +22 -0
- package/dist/utils/log.d.ts.map +1 -0
- package/dist/utils/log.js +100 -0
- package/dist/utils/log.test.d.ts +2 -0
- package/dist/utils/log.test.d.ts.map +1 -0
- package/dist/utils/log.test.js +113 -0
- package/dist/worker/blob-worker-client.d.ts +118 -0
- package/dist/worker/blob-worker-client.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.js +361 -0
- package/dist/worker/blob-worker-client.test.d.ts +2 -0
- package/dist/worker/blob-worker-client.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-client.test.js +137 -0
- package/dist/worker/blob-worker-logic.d.ts +120 -0
- package/dist/worker/blob-worker-logic.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.js +368 -0
- package/dist/worker/blob-worker-logic.test.d.ts +2 -0
- package/dist/worker/blob-worker-logic.test.d.ts.map +1 -0
- package/dist/worker/blob-worker-logic.test.js +191 -0
- package/dist/worker/blob-worker.d.ts +21 -0
- package/dist/worker/blob-worker.d.ts.map +1 -0
- package/dist/worker/blob-worker.js +44 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +6 -0
- package/dist/worker/midden-blake3.d.ts +23 -0
- package/dist/worker/midden-blake3.d.ts.map +1 -0
- package/dist/worker/midden-blake3.js +47 -0
- package/dist/worker/midden-blake3.test.d.ts +2 -0
- package/dist/worker/midden-blake3.test.d.ts.map +1 -0
- package/dist/worker/midden-blake3.test.js +25 -0
- package/dist/worker/midden-not-bundled.stub.d.ts +1 -0
- package/dist/worker/midden-not-bundled.stub.d.ts.map +1 -0
- package/dist/worker/midden-not-bundled.stub.js +10 -0
- package/dist/worker/midden-stub.d.ts +10 -0
- package/dist/worker/midden-stub.d.ts.map +1 -0
- package/dist/worker/midden-stub.js +31 -0
- package/dist/worker/midden-stub.test.d.ts +2 -0
- package/dist/worker/midden-stub.test.d.ts.map +1 -0
- package/dist/worker/midden-stub.test.js +25 -0
- package/dist/worker/midden-worker-client.d.ts +103 -0
- package/dist/worker/midden-worker-client.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.js +246 -0
- package/dist/worker/midden-worker-client.test.d.ts +2 -0
- package/dist/worker/midden-worker-client.test.d.ts.map +1 -0
- package/dist/worker/midden-worker-client.test.js +151 -0
- package/dist/worker/midden-worker-contract.d.ts +87 -0
- package/dist/worker/midden-worker-contract.d.ts.map +1 -0
- package/dist/worker/midden-worker-contract.js +30 -0
- package/package.json +90 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// iroh network adapter for automerge-repo
|
|
3
|
+
//
|
|
4
|
+
// bridges iroh QUIC bidirectional streams (via a midden-shaped node) with
|
|
5
|
+
// automerge-repo's sync protocol. messages are CBOR-encoded and
|
|
6
|
+
// length-delimited (4-byte BE u32 prefix), matching the framing used by
|
|
7
|
+
// the iroh-automerge-repo example.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
import { cbor, NetworkAdapter, } from "@automerge/automerge-repo";
|
|
10
|
+
import { log } from "../utils/log.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** base delay for reconnection backoff (ms). */
|
|
15
|
+
const RECONNECT_BASE_DELAY_MS = 1000;
|
|
16
|
+
/** maximum delay between reconnection attempts (ms). */
|
|
17
|
+
const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
18
|
+
/** maximum number of reconnection attempts before giving up. */
|
|
19
|
+
const RECONNECT_MAX_ATTEMPTS = 8;
|
|
20
|
+
/** upper bound of random jitter added to each reconnect delay (ms). */
|
|
21
|
+
const RECONNECT_JITTER_MS = 1000;
|
|
22
|
+
/** ALPN protocol identifier for automerge-repo sync over iroh. */
|
|
23
|
+
export const SYNC_ALPN = "iroh/automerge-repo/1";
|
|
24
|
+
/** console log prefix. */
|
|
25
|
+
const TAG = "automerge.iroh-adapter";
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// adapter
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* automerge-repo NetworkAdapter that uses iroh QUIC streams for transport.
|
|
31
|
+
*
|
|
32
|
+
* usage:
|
|
33
|
+
* const adapter = new IrohNetworkAdapter({
|
|
34
|
+
* getNode: () => getMiddenNode(),
|
|
35
|
+
* getIdentity: () => getStoredIdentity(),
|
|
36
|
+
* onIdentityChange: (cb) => subscribeToIdentityChanges(cb),
|
|
37
|
+
* });
|
|
38
|
+
* const repo = new Repo({ network: [broadcastAdapter, adapter] });
|
|
39
|
+
* // later, to connect to a peer:
|
|
40
|
+
* await adapter.addPeer("abc123...def");
|
|
41
|
+
*/
|
|
42
|
+
export class IrohNetworkAdapter extends NetworkAdapter {
|
|
43
|
+
opts;
|
|
44
|
+
syncAlpn;
|
|
45
|
+
midden = null;
|
|
46
|
+
streams = new Map();
|
|
47
|
+
readLoops = new Map(); // peerId -> active flag
|
|
48
|
+
_ready = false;
|
|
49
|
+
_readyPromise;
|
|
50
|
+
_resolveReady;
|
|
51
|
+
_disconnected = false;
|
|
52
|
+
_acceptLoopRunning = false;
|
|
53
|
+
identityUnsub = null;
|
|
54
|
+
/** peers explicitly added via addPeer() that we should stay connected to. */
|
|
55
|
+
intendedPeers = new Set();
|
|
56
|
+
/** per-peer reconnection state tracking attempt count and pending timer. */
|
|
57
|
+
reconnectState = new Map();
|
|
58
|
+
/** listeners for connection state changes. */
|
|
59
|
+
connectionStateListeners = [];
|
|
60
|
+
/** peers that exceeded max reconnection attempts. */
|
|
61
|
+
failedPeers = new Set();
|
|
62
|
+
/** external handlers registered for non-sync ALPNs via registerAlpnHandler(). */
|
|
63
|
+
alpnHandlers = new Map();
|
|
64
|
+
/** listeners notified when a peer stream is established. */
|
|
65
|
+
peerConnectListeners = [];
|
|
66
|
+
/** listeners notified when a peer stream is removed. */
|
|
67
|
+
peerDisconnectListeners = [];
|
|
68
|
+
_stopped = false;
|
|
69
|
+
_endpointState = "off";
|
|
70
|
+
endpointStateListeners = [];
|
|
71
|
+
constructor(opts) {
|
|
72
|
+
super();
|
|
73
|
+
this.opts = opts;
|
|
74
|
+
this.syncAlpn = opts.syncAlpn ?? SYNC_ALPN;
|
|
75
|
+
this._readyPromise = new Promise((resolve) => {
|
|
76
|
+
this._resolveReady = resolve;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// --- NetworkAdapter interface ---
|
|
80
|
+
isReady() {
|
|
81
|
+
return this._ready;
|
|
82
|
+
}
|
|
83
|
+
whenReady() {
|
|
84
|
+
return this._readyPromise;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* called by the Repo to start the adapter.
|
|
88
|
+
*
|
|
89
|
+
* marks the adapter ready (so automerge-repo can use it immediately)
|
|
90
|
+
* and begins transport setup if an identity already exists.
|
|
91
|
+
*/
|
|
92
|
+
connect(peerId, peerMetadata) {
|
|
93
|
+
this.peerId = peerId;
|
|
94
|
+
this.peerMetadata = peerMetadata;
|
|
95
|
+
// the adapter is always "ready" from automerge-repo's perspective -
|
|
96
|
+
// it can participate in the network subsystem even before the node
|
|
97
|
+
// starts. actual p2p only begins once an identity exists.
|
|
98
|
+
this._ready = true;
|
|
99
|
+
this._resolveReady();
|
|
100
|
+
this.checkIdentityAndStart().catch((err) => {
|
|
101
|
+
log.error(TAG, "identity check failed:", err);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* send a message to a peer.
|
|
106
|
+
*
|
|
107
|
+
* CBOR-encodes the message and writes it as a length-delimited frame
|
|
108
|
+
* to the peer's QUIC stream.
|
|
109
|
+
*/
|
|
110
|
+
send(message) {
|
|
111
|
+
const targetId = message.targetId;
|
|
112
|
+
const stream = this.streams.get(targetId);
|
|
113
|
+
if (!stream) {
|
|
114
|
+
log.warn(TAG, "no stream for peer:", targetId.slice(0, 16) + "...");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const encoded = cbor.encode(message);
|
|
118
|
+
const bytes = new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength);
|
|
119
|
+
stream.write_message(bytes).catch((err) => {
|
|
120
|
+
log.error(TAG, "write failed for peer:", targetId.slice(0, 16) + "...", err);
|
|
121
|
+
this.removePeer(targetId);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* disconnect from all peers and stop accepting connections.
|
|
126
|
+
*/
|
|
127
|
+
disconnect() {
|
|
128
|
+
this._disconnected = true;
|
|
129
|
+
for (const [peerId, stream] of this.streams) {
|
|
130
|
+
stream.close();
|
|
131
|
+
this.emit("peer-disconnected", { peerId: peerId });
|
|
132
|
+
}
|
|
133
|
+
this.streams.clear();
|
|
134
|
+
this.readLoops.clear();
|
|
135
|
+
this.intendedPeers.clear();
|
|
136
|
+
this.failedPeers.clear();
|
|
137
|
+
this.alpnHandlers.clear();
|
|
138
|
+
for (const [, state] of this.reconnectState) {
|
|
139
|
+
if (state.timer !== null) {
|
|
140
|
+
clearTimeout(state.timer);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.reconnectState.clear();
|
|
144
|
+
this.connectionStateListeners.length = 0;
|
|
145
|
+
this.peerConnectListeners.length = 0;
|
|
146
|
+
this.peerDisconnectListeners.length = 0;
|
|
147
|
+
this.endpointStateListeners.length = 0;
|
|
148
|
+
if (this.identityUnsub) {
|
|
149
|
+
this.identityUnsub();
|
|
150
|
+
this.identityUnsub = null;
|
|
151
|
+
}
|
|
152
|
+
this.setEndpointState("off");
|
|
153
|
+
this.emit("close");
|
|
154
|
+
}
|
|
155
|
+
// --- public API (beyond NetworkAdapter) ---
|
|
156
|
+
/**
|
|
157
|
+
* connect to a peer by node ID.
|
|
158
|
+
*
|
|
159
|
+
* opens a bidirectional QUIC stream to the peer on the automerge sync
|
|
160
|
+
* ALPN, starts a read loop, and emits a peer-candidate event so
|
|
161
|
+
* automerge-repo begins syncing with this peer.
|
|
162
|
+
*/
|
|
163
|
+
async addPeer(nodeId) {
|
|
164
|
+
if (this._disconnected) {
|
|
165
|
+
throw new Error("adapter is disconnected");
|
|
166
|
+
}
|
|
167
|
+
// track this peer as one we intend to stay connected to
|
|
168
|
+
this.intendedPeers.add(nodeId);
|
|
169
|
+
this.failedPeers.delete(nodeId);
|
|
170
|
+
this.emitConnectionStateChange();
|
|
171
|
+
// clear any pending reconnection state - this is a fresh attempt
|
|
172
|
+
this.clearReconnectState(nodeId);
|
|
173
|
+
// remember the peer but don't connect while stopped
|
|
174
|
+
if (this._stopped)
|
|
175
|
+
return;
|
|
176
|
+
// skip if already connected
|
|
177
|
+
if (this.streams.has(nodeId)) {
|
|
178
|
+
log.debug(TAG, "already connected to:", nodeId.slice(0, 16) + "...");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const midden = await this.ensureMidden();
|
|
182
|
+
const stream = await this.openBiWithRetry(midden, nodeId);
|
|
183
|
+
this.registerStream(nodeId, stream, "outbound");
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* dial a peer with a short retry loop for the initial connection attempt.
|
|
187
|
+
*
|
|
188
|
+
* discovery (relay-assisted address exchange) can lag a peer's node-id
|
|
189
|
+
* registration by a second or so right after both endpoints come
|
|
190
|
+
* online - dialing during that window fails with "no addressing
|
|
191
|
+
* information available" even though the peer is reachable moments
|
|
192
|
+
* later. retry a handful of times with a short fixed delay before
|
|
193
|
+
* giving up; this is a much tighter loop than the general reconnect
|
|
194
|
+
* backoff below, which is for peers that were connected and then
|
|
195
|
+
* dropped.
|
|
196
|
+
*/
|
|
197
|
+
async openBiWithRetry(midden, nodeId, maxAttempts = 4, delayMs = 750) {
|
|
198
|
+
let lastErr;
|
|
199
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
200
|
+
try {
|
|
201
|
+
return await midden.open_bi(nodeId, this.syncAlpn);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
lastErr = err;
|
|
205
|
+
log.debug(TAG, `open_bi attempt ${attempt}/${maxAttempts} failed for`, nodeId.slice(0, 16) + "...", err);
|
|
206
|
+
if (attempt < maxAttempts) {
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
throw lastErr;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* stop maintaining a connection to a peer.
|
|
215
|
+
*
|
|
216
|
+
* removes the peer from intendedPeers, cancels any pending reconnection,
|
|
217
|
+
* and closes any existing stream. use this when you intentionally want
|
|
218
|
+
* to stop connecting to a peer (as opposed to a transient failure).
|
|
219
|
+
*/
|
|
220
|
+
forgetPeer(nodeId) {
|
|
221
|
+
// remove from intended set first so removePeer() won't schedule a reconnect
|
|
222
|
+
this.intendedPeers.delete(nodeId);
|
|
223
|
+
this.failedPeers.delete(nodeId);
|
|
224
|
+
this.clearReconnectState(nodeId);
|
|
225
|
+
// delegate stream cleanup and peer-disconnected emission to removePeer
|
|
226
|
+
this.removePeer(nodeId);
|
|
227
|
+
this.emitConnectionStateChange();
|
|
228
|
+
}
|
|
229
|
+
/** check whether we have an active stream to a peer (transport-level connectivity check). */
|
|
230
|
+
isConnected(nodeId) {
|
|
231
|
+
return this.streams.has(nodeId);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* get a summary of the current connection state.
|
|
235
|
+
* used by the UI to show stoplight-style indicators.
|
|
236
|
+
*/
|
|
237
|
+
getConnectionSummary() {
|
|
238
|
+
let connected = 0;
|
|
239
|
+
let reconnecting = 0;
|
|
240
|
+
for (const peerId of this.intendedPeers) {
|
|
241
|
+
if (this.streams.has(peerId)) {
|
|
242
|
+
connected++;
|
|
243
|
+
}
|
|
244
|
+
else if (this.reconnectState.has(peerId)) {
|
|
245
|
+
reconnecting++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { connected, reconnecting, failed: this.failedPeers.size };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* subscribe to connection state changes.
|
|
252
|
+
* fires whenever a peer connects, disconnects, starts reconnecting,
|
|
253
|
+
* or gives up. returns an unsubscribe function.
|
|
254
|
+
*/
|
|
255
|
+
onConnectionStateChange(handler) {
|
|
256
|
+
this.connectionStateListeners.push(handler);
|
|
257
|
+
return () => {
|
|
258
|
+
const idx = this.connectionStateListeners.indexOf(handler);
|
|
259
|
+
if (idx >= 0)
|
|
260
|
+
this.connectionStateListeners.splice(idx, 1);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* subscribe to peer connect events. fires when a stream is
|
|
265
|
+
* successfully established with a peer. returns an unsubscribe function.
|
|
266
|
+
*/
|
|
267
|
+
onPeerConnect(handler) {
|
|
268
|
+
this.peerConnectListeners.push(handler);
|
|
269
|
+
return () => {
|
|
270
|
+
const idx = this.peerConnectListeners.indexOf(handler);
|
|
271
|
+
if (idx >= 0)
|
|
272
|
+
this.peerConnectListeners.splice(idx, 1);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* subscribe to peer disconnect events. fires when a peer's stream
|
|
277
|
+
* is removed (closed or errored). returns an unsubscribe function.
|
|
278
|
+
*/
|
|
279
|
+
onPeerDisconnect(handler) {
|
|
280
|
+
this.peerDisconnectListeners.push(handler);
|
|
281
|
+
return () => {
|
|
282
|
+
const idx = this.peerDisconnectListeners.indexOf(handler);
|
|
283
|
+
if (idx >= 0)
|
|
284
|
+
this.peerDisconnectListeners.splice(idx, 1);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* retry connection to all failed peers.
|
|
289
|
+
* re-adds them to intendedPeers and starts fresh reconnection attempts.
|
|
290
|
+
*/
|
|
291
|
+
retryFailedPeers() {
|
|
292
|
+
if (this._disconnected)
|
|
293
|
+
return;
|
|
294
|
+
const failed = [...this.failedPeers];
|
|
295
|
+
this.failedPeers.clear();
|
|
296
|
+
for (const peerId of failed) {
|
|
297
|
+
this.intendedPeers.add(peerId);
|
|
298
|
+
this.scheduleReconnect(peerId);
|
|
299
|
+
}
|
|
300
|
+
this.emitConnectionStateChange();
|
|
301
|
+
}
|
|
302
|
+
/** get the current iroh endpoint state for UI display. */
|
|
303
|
+
getEndpointState() {
|
|
304
|
+
return this._endpointState;
|
|
305
|
+
}
|
|
306
|
+
/** subscribe to endpoint state changes; returns an unsubscribe function. */
|
|
307
|
+
onEndpointStateChange(handler) {
|
|
308
|
+
this.endpointStateListeners.push(handler);
|
|
309
|
+
return () => {
|
|
310
|
+
const idx = this.endpointStateListeners.indexOf(handler);
|
|
311
|
+
if (idx >= 0)
|
|
312
|
+
this.endpointStateListeners.splice(idx, 1);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* stop all P2P transport gracefully.
|
|
317
|
+
* closes all peer streams and cancels reconnections, but preserves
|
|
318
|
+
* intendedPeers so restart() can reconnect them. the node reference is
|
|
319
|
+
* dropped so restart() creates a fresh one via getNode().
|
|
320
|
+
*/
|
|
321
|
+
stop() {
|
|
322
|
+
if (this._stopped || this._disconnected)
|
|
323
|
+
return;
|
|
324
|
+
this._stopped = true;
|
|
325
|
+
for (const [peerId, stream] of this.streams) {
|
|
326
|
+
stream.close();
|
|
327
|
+
this.emit("peer-disconnected", { peerId: peerId });
|
|
328
|
+
}
|
|
329
|
+
this.streams.clear();
|
|
330
|
+
this.readLoops.clear();
|
|
331
|
+
this.failedPeers.clear();
|
|
332
|
+
for (const [, state] of this.reconnectState) {
|
|
333
|
+
if (state.timer !== null)
|
|
334
|
+
clearTimeout(state.timer);
|
|
335
|
+
}
|
|
336
|
+
this.reconnectState.clear();
|
|
337
|
+
this.midden = null;
|
|
338
|
+
this.setEndpointState("off");
|
|
339
|
+
this.emitConnectionStateChange();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* restart the P2P transport after stop().
|
|
343
|
+
* re-initializes the node and reconnects all remembered peers.
|
|
344
|
+
*/
|
|
345
|
+
async restart() {
|
|
346
|
+
if (!this._stopped || this._disconnected)
|
|
347
|
+
return;
|
|
348
|
+
this._stopped = false;
|
|
349
|
+
await this.initialize();
|
|
350
|
+
for (const peerId of this.intendedPeers) {
|
|
351
|
+
this.addPeer(peerId).catch((err) => {
|
|
352
|
+
log.warn(TAG, "restart: failed to re-add peer:", peerId.slice(0, 16) + "...", err);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* register a handler for incoming streams with a specific ALPN.
|
|
358
|
+
* the accept loop will dispatch matching streams to this handler
|
|
359
|
+
* instead of closing them. used to support additional protocols
|
|
360
|
+
* alongside automerge sync without forking this adapter.
|
|
361
|
+
*/
|
|
362
|
+
registerAlpnHandler(alpn, handler) {
|
|
363
|
+
this.alpnHandlers.set(alpn, handler);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* get the midden stream node, initializing it lazily if needed.
|
|
367
|
+
* exposed for use by protocol handlers that need to open outbound
|
|
368
|
+
* streams on a different ALPN.
|
|
369
|
+
*/
|
|
370
|
+
async getNode() {
|
|
371
|
+
return this.ensureMidden();
|
|
372
|
+
}
|
|
373
|
+
// --- internals ---
|
|
374
|
+
async checkIdentityAndStart() {
|
|
375
|
+
const identity = await this.opts.getIdentity();
|
|
376
|
+
if (identity) {
|
|
377
|
+
// an identity already exists - start the node and begin accepting
|
|
378
|
+
// connections right away
|
|
379
|
+
log.debug(TAG, "identity found, starting P2P transport");
|
|
380
|
+
await this.initialize();
|
|
381
|
+
}
|
|
382
|
+
else if (this.opts.onIdentityChange) {
|
|
383
|
+
// no identity yet - subscribe to identity changes so transport
|
|
384
|
+
// starts once one is created
|
|
385
|
+
log.debug(TAG, "no identity yet, deferring P2P transport");
|
|
386
|
+
this.identityUnsub = this.opts.onIdentityChange((newIdentity) => {
|
|
387
|
+
if (newIdentity && !this.midden && !this._disconnected) {
|
|
388
|
+
log.debug(TAG, "identity created, starting P2P transport");
|
|
389
|
+
this.initialize().catch((err) => {
|
|
390
|
+
log.error(TAG, "deferred initialization failed:", err);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// no identity and no subscription - adapter stays passive
|
|
397
|
+
log.debug(TAG, "no identity and no onIdentityChange provided, adapter is passive");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async initialize() {
|
|
401
|
+
this.setEndpointState("starting");
|
|
402
|
+
try {
|
|
403
|
+
await this.ensureMidden();
|
|
404
|
+
this.startAcceptLoop();
|
|
405
|
+
this.setEndpointState("online");
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
log.error(TAG, "failed to initialize node:", err);
|
|
409
|
+
this.setEndpointState("error");
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async ensureMidden() {
|
|
414
|
+
if (this.midden)
|
|
415
|
+
return this.midden;
|
|
416
|
+
this.midden = await this.opts.getNode();
|
|
417
|
+
return this.midden;
|
|
418
|
+
}
|
|
419
|
+
startAcceptLoop() {
|
|
420
|
+
if (this._acceptLoopRunning)
|
|
421
|
+
return;
|
|
422
|
+
this._acceptLoopRunning = true;
|
|
423
|
+
const loop_ = async () => {
|
|
424
|
+
const midden = await this.ensureMidden();
|
|
425
|
+
while (!this._disconnected && !this._stopped) {
|
|
426
|
+
try {
|
|
427
|
+
const stream = await midden.accept();
|
|
428
|
+
if (!stream) {
|
|
429
|
+
// endpoint closed
|
|
430
|
+
log.debug(TAG, "accept loop: endpoint closed");
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
const alpn = stream.alpn();
|
|
434
|
+
const peerId = stream.peer_node_id();
|
|
435
|
+
if (alpn === this.syncAlpn) {
|
|
436
|
+
// automerge-repo sync - handle internally
|
|
437
|
+
log.debug(TAG, "accepted sync connection from:", peerId.slice(0, 16) + "...");
|
|
438
|
+
this.registerStream(peerId, stream, "inbound");
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const handler = this.alpnHandlers.get(alpn);
|
|
442
|
+
if (handler) {
|
|
443
|
+
log.debug(TAG, "dispatching", alpn, "stream from:", peerId.slice(0, 16) + "...");
|
|
444
|
+
handler(stream);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// no handler registered - close the stream
|
|
448
|
+
log.warn(TAG, "dropping inbound stream - no handler registered for ALPN:", alpn, "registered handlers:", Array.from(this.alpnHandlers.keys()));
|
|
449
|
+
stream.close();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
if (this._disconnected || this._stopped)
|
|
455
|
+
break;
|
|
456
|
+
log.error(TAG, "accept loop error:", err);
|
|
457
|
+
// brief pause before retrying to avoid tight error loops
|
|
458
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this._acceptLoopRunning = false;
|
|
462
|
+
};
|
|
463
|
+
loop_().catch((err) => {
|
|
464
|
+
log.error(TAG, "accept loop crashed:", err);
|
|
465
|
+
this._acceptLoopRunning = false;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
registerStream(peerId, stream, direction) {
|
|
469
|
+
// duplicate-stream handling (simultaneous connect, peer restart): the
|
|
470
|
+
// NEWEST stream always takes over for writes, and the replaced stream
|
|
471
|
+
// is NEVER closed - its read loop keeps draining it until it actually
|
|
472
|
+
// dies (guarded cleanup below ignores non-active streams).
|
|
473
|
+
//
|
|
474
|
+
// why not close the old one? when both peers dial each other at once,
|
|
475
|
+
// "last one wins + close the loser" resolves differently on each side
|
|
476
|
+
// - each side keeps its own dial and closes the stream the other side
|
|
477
|
+
// kept, killing BOTH connections and any in-flight sync requests. and
|
|
478
|
+
// why not a deterministic keep-the-incumbent tiebreak? the incumbent
|
|
479
|
+
// can be a zombie whose death hasn't been detected yet (the peer
|
|
480
|
+
// process was killed and restarted): writes routed to it silently
|
|
481
|
+
// vanish and recovery deadlocks. writing to the newest stream and
|
|
482
|
+
// reading from all of them is correct in every case - sync flows
|
|
483
|
+
// regardless of which stream each side picks for its writes, and dead
|
|
484
|
+
// streams get reaped by their own read-loop errors.
|
|
485
|
+
const existing = this.streams.get(peerId);
|
|
486
|
+
let reannounceDelayMs = 0;
|
|
487
|
+
if (existing) {
|
|
488
|
+
log.debug(TAG, `duplicate stream for ${peerId.slice(0, 16)}...: new ${direction} stream takes writes; old stream stays open for reads until it dies`);
|
|
489
|
+
// cycle the repo-level peer: automerge-repo ignores a repeated
|
|
490
|
+
// peer-candidate for a peer it already considers connected, so a
|
|
491
|
+
// superseding stream (e.g. a restarted peer redialing before we've
|
|
492
|
+
// noticed the old stream died) would otherwise never restart doc
|
|
493
|
+
// sync. an explicit disconnected -> candidate cycle resets the
|
|
494
|
+
// repo's sync state for this peer; the candidate is re-announced a
|
|
495
|
+
// tick later so the repo's own async disconnect cleanup finishes
|
|
496
|
+
// first.
|
|
497
|
+
if (!this._disconnected && !this._stopped) {
|
|
498
|
+
this.emit("peer-disconnected", { peerId: peerId });
|
|
499
|
+
reannounceDelayMs = 100;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.streams.set(peerId, stream);
|
|
503
|
+
// connection established - clear any reconnection backoff state
|
|
504
|
+
this.clearReconnectState(peerId);
|
|
505
|
+
this.emitConnectionStateChange();
|
|
506
|
+
// notify peer-connect listeners
|
|
507
|
+
for (const h of this.peerConnectListeners)
|
|
508
|
+
h(peerId);
|
|
509
|
+
// emit peer-candidate so automerge-repo starts syncing. the read loop
|
|
510
|
+
// starts first either way - sync messages can arrive as soon as the
|
|
511
|
+
// candidate is announced.
|
|
512
|
+
this.startReadLoop(peerId, stream);
|
|
513
|
+
const announce = () => {
|
|
514
|
+
if (this._disconnected || this._stopped)
|
|
515
|
+
return;
|
|
516
|
+
if (this.streams.get(peerId) !== stream)
|
|
517
|
+
return; // superseded already
|
|
518
|
+
this.emit("peer-candidate", {
|
|
519
|
+
peerId: peerId,
|
|
520
|
+
peerMetadata: { isEphemeral: false },
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
if (reannounceDelayMs > 0) {
|
|
524
|
+
setTimeout(announce, reannounceDelayMs);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
announce();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
startReadLoop(peerId, stream) {
|
|
531
|
+
this.readLoops.set(peerId, true);
|
|
532
|
+
const loop_ = async () => {
|
|
533
|
+
while (this.readLoops.get(peerId) && !this._disconnected && !this._stopped) {
|
|
534
|
+
try {
|
|
535
|
+
const data = await stream.read_message();
|
|
536
|
+
if (!data) {
|
|
537
|
+
// stream closed cleanly
|
|
538
|
+
log.debug(TAG, "stream closed by peer:", peerId.slice(0, 16) + "...");
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
// CBOR-decode the message
|
|
542
|
+
const message = cbor.decode(data);
|
|
543
|
+
// ensure the senderId is set to the peer's node ID
|
|
544
|
+
message.senderId = peerId;
|
|
545
|
+
this.emit("message", message);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
if (this._disconnected || this._stopped)
|
|
549
|
+
break;
|
|
550
|
+
log.error(TAG, "read error from peer:", peerId.slice(0, 16) + "...", err);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// only clean up the PEER if this stream is still the active one. if
|
|
555
|
+
// registerStream() superseded us with a newer stream, that one owns
|
|
556
|
+
// the peer now - calling removePeer here would incorrectly kill it.
|
|
557
|
+
// either way, close our own stream so its resources are released.
|
|
558
|
+
if (this.streams.get(peerId) === stream) {
|
|
559
|
+
this.removePeer(peerId);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
stream.close();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
loop_().catch((err) => {
|
|
566
|
+
log.error(TAG, "read loop crashed for peer:", peerId.slice(0, 16) + "...", err);
|
|
567
|
+
if (this.streams.get(peerId) === stream) {
|
|
568
|
+
this.removePeer(peerId);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
stream.close();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
removePeer(peerId) {
|
|
576
|
+
const stream = this.streams.get(peerId);
|
|
577
|
+
if (stream) {
|
|
578
|
+
stream.close();
|
|
579
|
+
this.streams.delete(peerId);
|
|
580
|
+
this.emitConnectionStateChange();
|
|
581
|
+
// notify peer-disconnect listeners
|
|
582
|
+
for (const h of this.peerDisconnectListeners)
|
|
583
|
+
h(peerId);
|
|
584
|
+
}
|
|
585
|
+
this.readLoops.delete(peerId);
|
|
586
|
+
if (!this._disconnected && !this._stopped) {
|
|
587
|
+
this.emit("peer-disconnected", { peerId: peerId });
|
|
588
|
+
// if this was an intended peer, schedule a reconnection attempt
|
|
589
|
+
if (this.intendedPeers.has(peerId)) {
|
|
590
|
+
this.scheduleReconnect(peerId);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// --- reconnection logic ---
|
|
595
|
+
/**
|
|
596
|
+
* schedule a reconnection attempt for a peer using exponential backoff
|
|
597
|
+
* with random jitter.
|
|
598
|
+
*/
|
|
599
|
+
scheduleReconnect(peerId) {
|
|
600
|
+
if (this._disconnected || this._stopped)
|
|
601
|
+
return;
|
|
602
|
+
// already reconnected while we were setting up
|
|
603
|
+
if (this.streams.has(peerId))
|
|
604
|
+
return;
|
|
605
|
+
// get or create reconnect state for this peer
|
|
606
|
+
let state = this.reconnectState.get(peerId);
|
|
607
|
+
if (!state) {
|
|
608
|
+
state = { attempt: 0, timer: null };
|
|
609
|
+
this.reconnectState.set(peerId, state);
|
|
610
|
+
}
|
|
611
|
+
// give up after max attempts
|
|
612
|
+
if (state.attempt >= RECONNECT_MAX_ATTEMPTS) {
|
|
613
|
+
log.warn(TAG, "giving up reconnection to peer after", RECONNECT_MAX_ATTEMPTS, "attempts:", peerId.slice(0, 16) + "...");
|
|
614
|
+
this.intendedPeers.delete(peerId);
|
|
615
|
+
this.failedPeers.add(peerId);
|
|
616
|
+
this.clearReconnectState(peerId);
|
|
617
|
+
this.emitConnectionStateChange();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// exponential backoff: baseDelay * 2^attempt, capped at maxDelay
|
|
621
|
+
const exponentialDelay = RECONNECT_BASE_DELAY_MS * Math.pow(2, state.attempt);
|
|
622
|
+
const cappedDelay = Math.min(exponentialDelay, RECONNECT_MAX_DELAY_MS);
|
|
623
|
+
// add random jitter to avoid simultaneous-open races
|
|
624
|
+
const jitter = Math.floor(Math.random() * RECONNECT_JITTER_MS);
|
|
625
|
+
const delay = cappedDelay + jitter;
|
|
626
|
+
log.debug(TAG, "scheduling reconnect to peer:", peerId.slice(0, 16) + "...", `(attempt ${state.attempt + 1}/${RECONNECT_MAX_ATTEMPTS}, delay ${delay}ms)`);
|
|
627
|
+
// clear any existing timer (shouldn't happen, but be safe)
|
|
628
|
+
if (state.timer !== null) {
|
|
629
|
+
clearTimeout(state.timer);
|
|
630
|
+
}
|
|
631
|
+
state.timer = setTimeout(() => {
|
|
632
|
+
state.timer = null;
|
|
633
|
+
this.attemptReconnect(peerId);
|
|
634
|
+
}, delay);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* attempt to reconnect to a peer. called by the scheduled timer from
|
|
638
|
+
* scheduleReconnect(). on failure, schedules the next attempt.
|
|
639
|
+
*/
|
|
640
|
+
async attemptReconnect(peerId) {
|
|
641
|
+
if (this._disconnected || this._stopped)
|
|
642
|
+
return;
|
|
643
|
+
// already reconnected (e.g. peer connected to us via accept loop)
|
|
644
|
+
if (this.streams.has(peerId)) {
|
|
645
|
+
this.clearReconnectState(peerId);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// peer was removed from intended set while we were waiting
|
|
649
|
+
if (!this.intendedPeers.has(peerId))
|
|
650
|
+
return;
|
|
651
|
+
const state = this.reconnectState.get(peerId);
|
|
652
|
+
if (state) {
|
|
653
|
+
state.attempt += 1;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const midden = await this.ensureMidden();
|
|
657
|
+
const stream = await midden.open_bi(peerId, this.syncAlpn);
|
|
658
|
+
log.debug(TAG, "reconnected to peer:", peerId.slice(0, 16) + "...");
|
|
659
|
+
// registerStream calls clearReconnectState, so no need to do it here
|
|
660
|
+
this.registerStream(peerId, stream, "outbound");
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
log.warn(TAG, "reconnect attempt failed for peer:", peerId.slice(0, 16) + "...", err);
|
|
664
|
+
// schedule next attempt with increased backoff
|
|
665
|
+
this.scheduleReconnect(peerId);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** notify all connection state listeners. */
|
|
669
|
+
emitConnectionStateChange() {
|
|
670
|
+
for (const handler of this.connectionStateListeners) {
|
|
671
|
+
handler();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
setEndpointState(state) {
|
|
675
|
+
if (this._endpointState === state)
|
|
676
|
+
return;
|
|
677
|
+
this._endpointState = state;
|
|
678
|
+
for (const h of this.endpointStateListeners)
|
|
679
|
+
h(state);
|
|
680
|
+
}
|
|
681
|
+
/** cancel and remove any pending reconnect timer for a peer. */
|
|
682
|
+
clearReconnectState(peerId) {
|
|
683
|
+
const state = this.reconnectState.get(peerId);
|
|
684
|
+
if (state) {
|
|
685
|
+
if (state.timer !== null) {
|
|
686
|
+
clearTimeout(state.timer);
|
|
687
|
+
}
|
|
688
|
+
this.reconnectState.delete(peerId);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iroh-network-adapter.test.d.ts","sourceRoot":"","sources":["../../src/automerge/iroh-network-adapter.test.ts"],"names":[],"mappings":""}
|