@peerbit/stream 1.0.1
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/LICENSE +202 -0
- package/README.md +2 -0
- package/lib/esm/index.d.ts +240 -0
- package/lib/esm/index.js +1175 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/logger.d.ts +1 -0
- package/lib/esm/logger.js +3 -0
- package/lib/esm/logger.js.map +1 -0
- package/lib/esm/metrics.d.ts +26 -0
- package/lib/esm/metrics.js +61 -0
- package/lib/esm/metrics.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/lib/esm/peer-map.d.ts +1 -0
- package/lib/esm/peer-map.js +2 -0
- package/lib/esm/peer-map.js.map +1 -0
- package/lib/esm/routes.d.ts +39 -0
- package/lib/esm/routes.js +227 -0
- package/lib/esm/routes.js.map +1 -0
- package/lib/esm/topology.d.ts +24 -0
- package/lib/esm/topology.js +39 -0
- package/lib/esm/topology.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +1556 -0
- package/src/logger.ts +2 -0
- package/src/metrics.ts +70 -0
- package/src/peer-map.ts +1 -0
- package/src/routes.ts +283 -0
- package/src/topology.ts +58 -0
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import { EventEmitter, CustomEvent } from "@libp2p/interfaces/events";
|
|
2
|
+
import { pipe } from "it-pipe";
|
|
3
|
+
import Queue from "p-queue";
|
|
4
|
+
import { pushable } from "it-pushable";
|
|
5
|
+
import { Uint8ArrayList } from "uint8arraylist";
|
|
6
|
+
import { abortableSource } from "abortable-iterator";
|
|
7
|
+
import * as lp from "it-length-prefixed";
|
|
8
|
+
import { Routes } from "./routes.js";
|
|
9
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
10
|
+
import { delay, TimeoutError, waitFor } from "@peerbit/time";
|
|
11
|
+
import { getKeypairFromPeerId, getPublicKeyFromPeerId, PublicSignKey, } from "@peerbit/crypto";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
import { Cache } from "@peerbit/cache";
|
|
14
|
+
import { createTopology } from "./topology.js";
|
|
15
|
+
export { logger };
|
|
16
|
+
import { Message as Message, Goodbye, Hello, DataMessage, Ping, PingPong, Pong, getMsgId, } from "@peerbit/stream-interface";
|
|
17
|
+
const isWebsocketConnection = (c) => c.remoteAddr.protoNames().find((x) => x === "ws" || x === "wss");
|
|
18
|
+
/**
|
|
19
|
+
* Thin wrapper around a peer's inbound / outbound pubsub streams
|
|
20
|
+
*/
|
|
21
|
+
export class PeerStreams extends EventEmitter {
|
|
22
|
+
counter = 0;
|
|
23
|
+
peerId;
|
|
24
|
+
publicKey;
|
|
25
|
+
protocol;
|
|
26
|
+
/**
|
|
27
|
+
* Write stream - it's preferable to use the write method
|
|
28
|
+
*/
|
|
29
|
+
outboundStream;
|
|
30
|
+
/**
|
|
31
|
+
* Read stream
|
|
32
|
+
*/
|
|
33
|
+
inboundStream;
|
|
34
|
+
/**
|
|
35
|
+
* The raw outbound stream, as retrieved from conn.newStream
|
|
36
|
+
*/
|
|
37
|
+
_rawOutboundStream;
|
|
38
|
+
/**
|
|
39
|
+
* The raw inbound stream, as retrieved from the callback from libp2p.handle
|
|
40
|
+
*/
|
|
41
|
+
_rawInboundStream;
|
|
42
|
+
/**
|
|
43
|
+
* An AbortController for controlled shutdown of the treams
|
|
44
|
+
*/
|
|
45
|
+
inboundAbortController;
|
|
46
|
+
closed;
|
|
47
|
+
pingJob;
|
|
48
|
+
pingLatency;
|
|
49
|
+
connId;
|
|
50
|
+
constructor(init) {
|
|
51
|
+
super();
|
|
52
|
+
this.peerId = init.peerId;
|
|
53
|
+
this.publicKey = init.publicKey;
|
|
54
|
+
this.protocol = init.protocol;
|
|
55
|
+
this.inboundAbortController = new AbortController();
|
|
56
|
+
this.closed = false;
|
|
57
|
+
this.connId = init.connId;
|
|
58
|
+
this.counter = 1;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Do we have a connection to read from?
|
|
62
|
+
*/
|
|
63
|
+
get isReadable() {
|
|
64
|
+
return Boolean(this.inboundStream);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Do we have a connection to write on?
|
|
68
|
+
*/
|
|
69
|
+
get isWritable() {
|
|
70
|
+
return Boolean(this.outboundStream);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Send a message to this peer.
|
|
74
|
+
* Throws if there is no `stream` to write to available.
|
|
75
|
+
*/
|
|
76
|
+
write(data) {
|
|
77
|
+
if (this.outboundStream == null) {
|
|
78
|
+
logger.error("No writable connection to " + this.peerId.toString());
|
|
79
|
+
throw new Error("No writable connection to " + this.peerId.toString());
|
|
80
|
+
}
|
|
81
|
+
this.outboundStream.push(data instanceof Uint8Array ? new Uint8ArrayList(data) : data);
|
|
82
|
+
}
|
|
83
|
+
async waitForWrite(bytes) {
|
|
84
|
+
if (!this.isWritable) {
|
|
85
|
+
// Catch the event where the outbound stream is attach, but also abort if we shut down
|
|
86
|
+
const outboundPromise = new Promise((rs, rj) => {
|
|
87
|
+
const resolve = () => {
|
|
88
|
+
this.removeEventListener("stream:outbound", listener);
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
rs();
|
|
91
|
+
};
|
|
92
|
+
const reject = (err) => {
|
|
93
|
+
this.removeEventListener("stream:outbound", listener);
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
rj(err);
|
|
96
|
+
};
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
reject(new Error("Timed out"));
|
|
99
|
+
}, 3 * 1000); // TODO if this timeout > 10s we run into issues in the tests when running in CI
|
|
100
|
+
const abortHandler = () => {
|
|
101
|
+
this.removeEventListener("close", abortHandler);
|
|
102
|
+
reject(new Error("Aborted"));
|
|
103
|
+
};
|
|
104
|
+
this.addEventListener("close", abortHandler);
|
|
105
|
+
const listener = () => {
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
this.addEventListener("stream:outbound", listener);
|
|
109
|
+
if (this.isWritable) {
|
|
110
|
+
resolve();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await outboundPromise
|
|
114
|
+
.then(() => {
|
|
115
|
+
this.write(bytes);
|
|
116
|
+
})
|
|
117
|
+
.catch((error) => {
|
|
118
|
+
logger.error("Failed to send to stream: " +
|
|
119
|
+
this.peerId +
|
|
120
|
+
". " +
|
|
121
|
+
(error?.message || error?.toString()));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.write(bytes);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Attach a raw inbound stream and setup a read stream
|
|
130
|
+
*/
|
|
131
|
+
attachInboundStream(stream) {
|
|
132
|
+
// Create and attach a new inbound stream
|
|
133
|
+
// The inbound stream is:
|
|
134
|
+
// - abortable, set to only return on abort, rather than throw
|
|
135
|
+
// - transformed with length-prefix transform
|
|
136
|
+
this._rawInboundStream = stream;
|
|
137
|
+
this.inboundStream = abortableSource(pipe(this._rawInboundStream, (source) => lp.decode(source, { maxDataLength: 100001000 })), this.inboundAbortController.signal, {
|
|
138
|
+
returnOnAbort: true,
|
|
139
|
+
onReturnError: (err) => {
|
|
140
|
+
logger.error("Inbound stream error", err?.message);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
this.dispatchEvent(new CustomEvent("stream:inbound"));
|
|
144
|
+
return this.inboundStream;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Attach a raw outbound stream and setup a write stream
|
|
148
|
+
*/
|
|
149
|
+
async attachOutboundStream(stream) {
|
|
150
|
+
// If an outbound stream already exists, gently close it
|
|
151
|
+
const _prevStream = this.outboundStream;
|
|
152
|
+
this._rawOutboundStream = stream;
|
|
153
|
+
this.pingJob?.abort();
|
|
154
|
+
this.outboundStream = pushable({
|
|
155
|
+
objectMode: true,
|
|
156
|
+
onEnd: () => {
|
|
157
|
+
stream.close();
|
|
158
|
+
if (this._rawOutboundStream === stream) {
|
|
159
|
+
this.dispatchEvent(new CustomEvent("close"));
|
|
160
|
+
this._rawOutboundStream = undefined;
|
|
161
|
+
this.outboundStream = undefined;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
pipe(this.outboundStream, (source) => lp.encode(source), this._rawOutboundStream).catch((err) => {
|
|
166
|
+
logger.error(err);
|
|
167
|
+
});
|
|
168
|
+
// Emit if the connection is new
|
|
169
|
+
this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
170
|
+
if (_prevStream != null) {
|
|
171
|
+
// End the stream without emitting a close event
|
|
172
|
+
await _prevStream.end();
|
|
173
|
+
}
|
|
174
|
+
return this.outboundStream;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Closes the open connection to peer
|
|
178
|
+
*/
|
|
179
|
+
close() {
|
|
180
|
+
if (this.closed) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.closed = true;
|
|
184
|
+
// End the outbound stream
|
|
185
|
+
if (this.outboundStream != null) {
|
|
186
|
+
this.outboundStream.return();
|
|
187
|
+
this._rawOutboundStream?.close();
|
|
188
|
+
}
|
|
189
|
+
// End the inbound stream
|
|
190
|
+
if (this.inboundStream != null) {
|
|
191
|
+
this.inboundAbortController.abort();
|
|
192
|
+
this._rawInboundStream?.close();
|
|
193
|
+
}
|
|
194
|
+
this.pingJob?.abort();
|
|
195
|
+
this.pingLatency = undefined;
|
|
196
|
+
//this.dispatchEvent(new CustomEvent('close'))
|
|
197
|
+
this._rawOutboundStream = undefined;
|
|
198
|
+
this.outboundStream = undefined;
|
|
199
|
+
this._rawInboundStream = undefined;
|
|
200
|
+
this.inboundStream = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export class DirectStream extends EventEmitter {
|
|
204
|
+
components;
|
|
205
|
+
peerId;
|
|
206
|
+
peerIdStr;
|
|
207
|
+
publicKey;
|
|
208
|
+
publicKeyHash;
|
|
209
|
+
sign;
|
|
210
|
+
started;
|
|
211
|
+
/**
|
|
212
|
+
* Map of peer streams
|
|
213
|
+
*/
|
|
214
|
+
peers;
|
|
215
|
+
peerKeyHashToPublicKey;
|
|
216
|
+
peerIdToPublicKey;
|
|
217
|
+
routes;
|
|
218
|
+
/**
|
|
219
|
+
* If router can relay received messages, even if not subscribed
|
|
220
|
+
*/
|
|
221
|
+
canRelayMessage;
|
|
222
|
+
/**
|
|
223
|
+
* if publish should emit to self, if subscribed
|
|
224
|
+
*/
|
|
225
|
+
signaturePolicy;
|
|
226
|
+
emitSelf;
|
|
227
|
+
queue;
|
|
228
|
+
multicodecs;
|
|
229
|
+
seenCache;
|
|
230
|
+
earlyGoodbyes;
|
|
231
|
+
helloMap; // key is hash of publicKey, value is map whey key is hash of signature bytes, and value is latest Hello
|
|
232
|
+
multiaddrsMap;
|
|
233
|
+
_registrarTopologyIds;
|
|
234
|
+
maxInboundStreams;
|
|
235
|
+
maxOutboundStreams;
|
|
236
|
+
topology;
|
|
237
|
+
pingJobPromise;
|
|
238
|
+
pingJob;
|
|
239
|
+
pingInterval;
|
|
240
|
+
connectionManagerOptions;
|
|
241
|
+
recentDials;
|
|
242
|
+
constructor(components, multicodecs, options) {
|
|
243
|
+
super();
|
|
244
|
+
this.components = components;
|
|
245
|
+
const { canRelayMessage = false, emitSelf = false, messageProcessingConcurrency = 10, pingInterval = 10 * 1000, maxInboundStreams, maxOutboundStreams, signaturePolicy = "StictSign", connectionManager = { autoDial: true }, } = options || {};
|
|
246
|
+
const signKey = getKeypairFromPeerId(components.peerId);
|
|
247
|
+
this.sign = signKey.sign.bind(signKey);
|
|
248
|
+
this.peerId = components.peerId;
|
|
249
|
+
this.peerIdStr = components.peerId.toString();
|
|
250
|
+
this.publicKey = signKey.publicKey;
|
|
251
|
+
this.publicKeyHash = signKey.publicKey.hashcode();
|
|
252
|
+
this.multicodecs = multicodecs;
|
|
253
|
+
this.started = false;
|
|
254
|
+
this.peers = new Map();
|
|
255
|
+
this.helloMap = new Map();
|
|
256
|
+
this.multiaddrsMap = new Map();
|
|
257
|
+
this.routes = new Routes(this.publicKeyHash);
|
|
258
|
+
this.canRelayMessage = canRelayMessage;
|
|
259
|
+
this.emitSelf = emitSelf;
|
|
260
|
+
this.queue = new Queue({ concurrency: messageProcessingConcurrency });
|
|
261
|
+
this.earlyGoodbyes = new Map();
|
|
262
|
+
this.maxInboundStreams = maxInboundStreams;
|
|
263
|
+
this.maxOutboundStreams = maxOutboundStreams;
|
|
264
|
+
this.seenCache = new Cache({ max: 1e3, ttl: 10 * 60 * 1e3 });
|
|
265
|
+
this.peerKeyHashToPublicKey = new Map();
|
|
266
|
+
this.peerIdToPublicKey = new Map();
|
|
267
|
+
this.pingInterval = pingInterval;
|
|
268
|
+
this._onIncomingStream = this._onIncomingStream.bind(this);
|
|
269
|
+
this.onPeerConnected = this.onPeerConnected.bind(this);
|
|
270
|
+
this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
|
|
271
|
+
this.signaturePolicy = signaturePolicy;
|
|
272
|
+
this.connectionManagerOptions = connectionManager;
|
|
273
|
+
this.recentDials = new Cache({
|
|
274
|
+
ttl: connectionManager.retryDelay || 60 * 1000,
|
|
275
|
+
max: 1e3,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async start() {
|
|
279
|
+
if (this.started) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
logger.debug("starting");
|
|
283
|
+
this.topology = createTopology({
|
|
284
|
+
onConnect: this.onPeerConnected.bind(this),
|
|
285
|
+
onDisconnect: this.onPeerDisconnected.bind(this),
|
|
286
|
+
});
|
|
287
|
+
// register protocol with topology
|
|
288
|
+
// Topology callbacks called on connection manager changes
|
|
289
|
+
this._registrarTopologyIds = await Promise.all(this.multicodecs.map((multicodec) => this.components.registrar.register(multicodec, this.topology)));
|
|
290
|
+
// Incoming streams
|
|
291
|
+
// Called after a peer dials us
|
|
292
|
+
await Promise.all(this.multicodecs.map((multicodec) => this.components.registrar.handle(multicodec, this._onIncomingStream, {
|
|
293
|
+
maxInboundStreams: this.maxInboundStreams,
|
|
294
|
+
maxOutboundStreams: this.maxOutboundStreams,
|
|
295
|
+
})));
|
|
296
|
+
this.started = true;
|
|
297
|
+
// All existing connections are like new ones for us. To deduplication on remotes so we only resuse one connection for this protocol (we could be connected with many connections)
|
|
298
|
+
const peerToConnections = new Map();
|
|
299
|
+
const connections = this.components.connectionManager.getConnections();
|
|
300
|
+
for (const conn of connections) {
|
|
301
|
+
let arr = peerToConnections.get(conn.remotePeer.toString());
|
|
302
|
+
if (!arr) {
|
|
303
|
+
arr = [];
|
|
304
|
+
peerToConnections.set(conn.remotePeer.toString(), arr);
|
|
305
|
+
}
|
|
306
|
+
arr.push(conn);
|
|
307
|
+
}
|
|
308
|
+
for (const [_peer, arr] of peerToConnections) {
|
|
309
|
+
let conn = arr[0]; // TODO choose TCP when both websocket and tcp exist
|
|
310
|
+
for (const c of arr) {
|
|
311
|
+
if (!isWebsocketConnection(c)) {
|
|
312
|
+
// TODO what is correct connection prioritization?
|
|
313
|
+
conn = c; // always favor non websocket address
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await this.onPeerConnected(conn.remotePeer, conn, true);
|
|
318
|
+
}
|
|
319
|
+
const pingJob = async () => {
|
|
320
|
+
// TODO don't use setInterval but waitFor previous done to be done
|
|
321
|
+
await this.pingJobPromise;
|
|
322
|
+
const promises = [];
|
|
323
|
+
this.peers.forEach((peer) => {
|
|
324
|
+
promises.push(this.ping(peer).catch((e) => {
|
|
325
|
+
if (e instanceof TimeoutError) {
|
|
326
|
+
// Ignore
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
logger.error(e);
|
|
330
|
+
}
|
|
331
|
+
}));
|
|
332
|
+
});
|
|
333
|
+
promises.push(this.hello()); // Repetedly say hello to everyone to create traces in the network to measure latencies
|
|
334
|
+
this.pingJobPromise = Promise.all(promises)
|
|
335
|
+
.catch((e) => {
|
|
336
|
+
logger.error(e?.message);
|
|
337
|
+
})
|
|
338
|
+
.finally(() => {
|
|
339
|
+
if (!this.started) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.pingJob = setTimeout(pingJob, this.pingInterval);
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
pingJob();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Unregister the pubsub protocol and the streams with other peers will be closed.
|
|
349
|
+
*/
|
|
350
|
+
async stop() {
|
|
351
|
+
if (!this.started) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.started = false;
|
|
355
|
+
clearTimeout(this.pingJob);
|
|
356
|
+
await this.pingJobPromise;
|
|
357
|
+
await Promise.all(this.multicodecs.map((x) => this.components.registrar.unhandle(x)));
|
|
358
|
+
logger.debug("stopping");
|
|
359
|
+
for (const peerStreams of this.peers.values()) {
|
|
360
|
+
peerStreams.close();
|
|
361
|
+
}
|
|
362
|
+
// unregister protocol and handlers
|
|
363
|
+
if (this._registrarTopologyIds != null) {
|
|
364
|
+
this._registrarTopologyIds?.map((id) => this.components.registrar.unregister(id));
|
|
365
|
+
}
|
|
366
|
+
this.queue.clear();
|
|
367
|
+
this.helloMap.clear();
|
|
368
|
+
this.multiaddrsMap.clear();
|
|
369
|
+
this.earlyGoodbyes.clear();
|
|
370
|
+
this.peers.clear();
|
|
371
|
+
this.seenCache.clear();
|
|
372
|
+
this.routes.clear();
|
|
373
|
+
this.peerKeyHashToPublicKey.clear();
|
|
374
|
+
this.peerIdToPublicKey.clear();
|
|
375
|
+
logger.debug("stopped");
|
|
376
|
+
}
|
|
377
|
+
isStarted() {
|
|
378
|
+
return this.started;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* On an inbound stream opened
|
|
382
|
+
*/
|
|
383
|
+
async _onIncomingStream(data) {
|
|
384
|
+
if (!this.isStarted()) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const { stream, connection } = data;
|
|
388
|
+
const peerId = connection.remotePeer;
|
|
389
|
+
if (stream.stat.protocol == null) {
|
|
390
|
+
stream.abort(new Error("Stream was not multiplexed"));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const publicKey = getPublicKeyFromPeerId(peerId);
|
|
394
|
+
const peer = this.addPeer(peerId, publicKey, stream.stat.protocol, connection.id);
|
|
395
|
+
const inboundStream = peer.attachInboundStream(stream);
|
|
396
|
+
this.processMessages(peerId, inboundStream, peer).catch((err) => {
|
|
397
|
+
logger.error(err);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Registrar notifies an established connection with protocol
|
|
402
|
+
*/
|
|
403
|
+
async onPeerConnected(peerId, conn, fromExisting) {
|
|
404
|
+
if (!this.isStarted() || conn.stat.status !== "OPEN") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const peerKey = getPublicKeyFromPeerId(peerId);
|
|
409
|
+
const peerKeyHash = peerKey.hashcode();
|
|
410
|
+
for (const existingStreams of conn.streams) {
|
|
411
|
+
if (existingStreams.stat.protocol &&
|
|
412
|
+
this.multicodecs.includes(existingStreams.stat.protocol) &&
|
|
413
|
+
existingStreams.stat.direction === "outbound") {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// This condition seem to work better than the one above, for some reason.
|
|
418
|
+
// The reason we need this at all is because we will connect to existing connection and recieve connection that
|
|
419
|
+
// some times, yields a race connections where connection drop each other by reset
|
|
420
|
+
let stream = undefined; // TODO types
|
|
421
|
+
let tries = 0;
|
|
422
|
+
let peer = undefined;
|
|
423
|
+
while (tries <= 3) {
|
|
424
|
+
tries++;
|
|
425
|
+
if (!this.started) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
stream = await conn.newStream(this.multicodecs);
|
|
430
|
+
if (stream.stat.protocol == null) {
|
|
431
|
+
stream.abort(new Error("Stream was not multiplexed"));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
peer = this.addPeer(peerId, peerKey, stream.stat.protocol, conn.id); // TODO types
|
|
435
|
+
await peer.attachOutboundStream(stream);
|
|
436
|
+
// TODO do we want to do this?
|
|
437
|
+
/* if (!peer.inboundStream) {
|
|
438
|
+
const inboundStream = conn.streams.find(
|
|
439
|
+
(x) =>
|
|
440
|
+
x.stat.protocol &&
|
|
441
|
+
this.multicodecs.includes(x.stat.protocol) &&
|
|
442
|
+
x.stat.direction === "inbound"
|
|
443
|
+
);
|
|
444
|
+
if (inboundStream) {
|
|
445
|
+
this._onIncomingStream({
|
|
446
|
+
connection: conn,
|
|
447
|
+
stream: inboundStream,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} */
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (error.code === "ERR_UNSUPPORTED_PROTOCOL") {
|
|
454
|
+
await delay(100);
|
|
455
|
+
continue; // Retry
|
|
456
|
+
}
|
|
457
|
+
if (conn.stat.status !== "OPEN" ||
|
|
458
|
+
error?.message === "Muxer already closed" ||
|
|
459
|
+
error.code === "ERR_STREAM_RESET") {
|
|
460
|
+
return; // fail silenty
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
if (!stream) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (fromExisting) {
|
|
470
|
+
return; // we return here because we will enter this method once more once the protocol has been registered for the remote peer
|
|
471
|
+
}
|
|
472
|
+
// Add connection with assumed large latency
|
|
473
|
+
this.peerIdToPublicKey.set(peerId.toString(), peerKey);
|
|
474
|
+
const promises = [];
|
|
475
|
+
/* if (!existingStream) */ {
|
|
476
|
+
this.addRouteConnection(this.publicKey, peerKey, Number.MAX_SAFE_INTEGER);
|
|
477
|
+
// Get accurate latency
|
|
478
|
+
promises.push(this.ping(peer));
|
|
479
|
+
// Say hello
|
|
480
|
+
promises.push(this.publishMessage(this.components.peerId, await new Hello({
|
|
481
|
+
multiaddrs: this.components.addressManager
|
|
482
|
+
.getAddresses()
|
|
483
|
+
.map((x) => x.toString()),
|
|
484
|
+
}).sign(this.sign), [peer]));
|
|
485
|
+
// Send my goodbye early if I disconnect for some reason, (so my peer can say goodbye for me)
|
|
486
|
+
// TODO add custom condition fn for doing below
|
|
487
|
+
promises.push(this.publishMessage(this.components.peerId, await new Goodbye({ early: true }).sign(this.sign), [peer]));
|
|
488
|
+
// replay all hellos
|
|
489
|
+
for (const [sender, hellos] of this.helloMap) {
|
|
490
|
+
if (sender === peerKeyHash) {
|
|
491
|
+
// Don't say hellos from sender to same sender (uneccessary)
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
for (const [key, hello] of hellos) {
|
|
495
|
+
if (!hello.header.verify()) {
|
|
496
|
+
hellos.delete(key);
|
|
497
|
+
}
|
|
498
|
+
promises.push(this.publishMessage(this.components.peerId, hello, [peer]));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const resolved = await Promise.all(promises);
|
|
503
|
+
return resolved;
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
logger.error(err);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
addRouteConnection(from, to, latency) {
|
|
510
|
+
this.peerKeyHashToPublicKey.set(from.hashcode(), from);
|
|
511
|
+
this.peerKeyHashToPublicKey.set(to.hashcode(), to);
|
|
512
|
+
const links = this.routes.addLink(from.hashcode(), to.hashcode(), latency);
|
|
513
|
+
for (const added of links) {
|
|
514
|
+
const key = this.peerKeyHashToPublicKey.get(added);
|
|
515
|
+
if (key?.equals(this.publicKey) === false) {
|
|
516
|
+
this.onPeerReachable(key);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
removeRouteConnection(from, to) {
|
|
521
|
+
const has = this.routes.hasNode(to.hashcode());
|
|
522
|
+
if (!has) {
|
|
523
|
+
this.onPeerUnreachable(to);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const links = this.routes.deleteLink(from.hashcode(), to.hashcode());
|
|
527
|
+
for (const deleted of links) {
|
|
528
|
+
const key = this.peerKeyHashToPublicKey.get(deleted);
|
|
529
|
+
this.peerKeyHashToPublicKey.delete(deleted);
|
|
530
|
+
if (key?.equals(this.publicKey) === false) {
|
|
531
|
+
this.onPeerUnreachable(key);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Registrar notifies a closing connection with pubsub protocol
|
|
538
|
+
*/
|
|
539
|
+
async onPeerDisconnected(peerId, conn) {
|
|
540
|
+
// PeerId could be me, if so, it means that I am disconnecting
|
|
541
|
+
const peerKey = getPublicKeyFromPeerId(peerId);
|
|
542
|
+
const peerKeyHash = peerKey.hashcode();
|
|
543
|
+
const connections = this.components.connectionManager
|
|
544
|
+
.getConnectionsMap()
|
|
545
|
+
.get(peerId);
|
|
546
|
+
if (conn?.id &&
|
|
547
|
+
connections &&
|
|
548
|
+
connections.length > 0 &&
|
|
549
|
+
!this.components.connectionManager
|
|
550
|
+
.getConnectionsMap()
|
|
551
|
+
.get(peerId)
|
|
552
|
+
?.find((x) => x.id === conn.id) /* TODO this should work but does not? peer?.connId !== conn.id */) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (!this.publicKey.equals(peerKey)) {
|
|
556
|
+
this._removePeer(peerKey);
|
|
557
|
+
this.removeRouteConnection(this.publicKey, peerKey);
|
|
558
|
+
// Notify network
|
|
559
|
+
const earlyGoodBye = this.earlyGoodbyes.get(peerKeyHash);
|
|
560
|
+
if (earlyGoodBye) {
|
|
561
|
+
earlyGoodBye.early = false;
|
|
562
|
+
await earlyGoodBye.sign(this.sign);
|
|
563
|
+
await this.publishMessage(this.components.peerId, earlyGoodBye);
|
|
564
|
+
this.earlyGoodbyes.delete(peerKeyHash);
|
|
565
|
+
}
|
|
566
|
+
this.peerIdToPublicKey.delete(peerId.toString());
|
|
567
|
+
}
|
|
568
|
+
logger.debug("connection ended:" + peerKey.toString());
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* invoked when a new peer becomes reachable
|
|
572
|
+
* @param publicKeyHash
|
|
573
|
+
*/
|
|
574
|
+
onPeerReachable(publicKey) {
|
|
575
|
+
// override this fn
|
|
576
|
+
this.dispatchEvent(new CustomEvent("peer:reachable", { detail: publicKey }));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* invoked when a new peer becomes unreachable
|
|
580
|
+
* @param publicKeyHash
|
|
581
|
+
*/
|
|
582
|
+
onPeerUnreachable(publicKey) {
|
|
583
|
+
// override this fn
|
|
584
|
+
this.helloMap.delete(publicKey.hashcode());
|
|
585
|
+
this.multiaddrsMap.delete(publicKey.hashcode());
|
|
586
|
+
this.dispatchEvent(new CustomEvent("peer:unreachable", { detail: publicKey }));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Notifies the router that a peer has been connected
|
|
590
|
+
*/
|
|
591
|
+
addPeer(peerId, publicKey, protocol, connId) {
|
|
592
|
+
const publicKeyHash = publicKey.hashcode();
|
|
593
|
+
const existing = this.peers.get(publicKeyHash);
|
|
594
|
+
// If peer streams already exists, do nothing
|
|
595
|
+
if (existing != null) {
|
|
596
|
+
existing.counter += 1;
|
|
597
|
+
existing.connId = connId;
|
|
598
|
+
return existing;
|
|
599
|
+
}
|
|
600
|
+
// else create a new peer streams
|
|
601
|
+
const peerIdStr = peerId.toString();
|
|
602
|
+
logger.debug("new peer" + peerIdStr);
|
|
603
|
+
const peerStreams = new PeerStreams({
|
|
604
|
+
peerId,
|
|
605
|
+
publicKey,
|
|
606
|
+
protocol,
|
|
607
|
+
connId,
|
|
608
|
+
});
|
|
609
|
+
this.peers.set(publicKeyHash, peerStreams);
|
|
610
|
+
peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
|
|
611
|
+
once: true,
|
|
612
|
+
});
|
|
613
|
+
return peerStreams;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Notifies the router that a peer has been disconnected
|
|
617
|
+
*/
|
|
618
|
+
_removePeer(publicKey) {
|
|
619
|
+
const hash = publicKey.hashcode();
|
|
620
|
+
const peerStreams = this.peers.get(hash);
|
|
621
|
+
if (peerStreams == null) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// close peer streams
|
|
625
|
+
peerStreams.close();
|
|
626
|
+
// delete peer streams
|
|
627
|
+
logger.debug("delete peer" + publicKey.toString());
|
|
628
|
+
this.peers.delete(hash);
|
|
629
|
+
return peerStreams;
|
|
630
|
+
}
|
|
631
|
+
// MESSAGE METHODS
|
|
632
|
+
/**
|
|
633
|
+
* Responsible for processing each RPC message received by other peers.
|
|
634
|
+
*/
|
|
635
|
+
async processMessages(peerId, stream, peerStreams) {
|
|
636
|
+
try {
|
|
637
|
+
await pipe(stream, async (source) => {
|
|
638
|
+
for await (const data of source) {
|
|
639
|
+
const msgId = await getMsgId(data);
|
|
640
|
+
if (this.seenCache.has(msgId)) {
|
|
641
|
+
// we got message that WE sent?
|
|
642
|
+
/**
|
|
643
|
+
* Most propobable reason why we arrive here is a race condition/issue
|
|
644
|
+
|
|
645
|
+
┌─┐
|
|
646
|
+
│0│
|
|
647
|
+
└△┘
|
|
648
|
+
┌▽┐
|
|
649
|
+
│1│
|
|
650
|
+
└△┘
|
|
651
|
+
┌▽┐
|
|
652
|
+
│2│
|
|
653
|
+
└─┘
|
|
654
|
+
|
|
655
|
+
from 2s perspective,
|
|
656
|
+
|
|
657
|
+
if everyone conents to each other at the same time, then 0 will say hello to 1 and 1 will save that hello to resend to 2 if 2 ever connects
|
|
658
|
+
but two is already connected by onPeerConnected has not been invoked yet, so the hello message gets forwarded,
|
|
659
|
+
and later onPeerConnected gets invoked on 1, and the same message gets resent to 2
|
|
660
|
+
*/
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
this.seenCache.add(msgId);
|
|
664
|
+
this.processRpc(peerId, peerStreams, data).catch((err) => logger.warn(err));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
logger.error("error on processing messages to id: " +
|
|
670
|
+
peerStreams.peerId.toString() +
|
|
671
|
+
". " +
|
|
672
|
+
err?.message);
|
|
673
|
+
this.onPeerDisconnected(peerStreams.peerId);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Handles an rpc request from a peer
|
|
678
|
+
*/
|
|
679
|
+
async processRpc(from, peerStreams, message) {
|
|
680
|
+
if (!this.acceptFrom(from)) {
|
|
681
|
+
logger.debug("received message from unacceptable peer %p", from);
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
// logger.debug("rpc from " + from + ", " + this.peerIdStr);
|
|
685
|
+
if (message.length > 0) {
|
|
686
|
+
// logger.debug("messages from " + from);
|
|
687
|
+
await this.queue
|
|
688
|
+
.add(async () => {
|
|
689
|
+
try {
|
|
690
|
+
await this.processMessage(from, peerStreams, message);
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
logger.error(err);
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
.catch((err) => logger.warn(err));
|
|
697
|
+
}
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Handles a message from a peer
|
|
702
|
+
*/
|
|
703
|
+
async processMessage(from, peerStream, msg) {
|
|
704
|
+
if (!from.publicKey) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (this.components.peerId.equals(from) && !this.emitSelf) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// Ensure the message is valid before processing it
|
|
711
|
+
const message = Message.deserialize(msg);
|
|
712
|
+
this.dispatchEvent(new CustomEvent("message", {
|
|
713
|
+
detail: message,
|
|
714
|
+
}));
|
|
715
|
+
if (message instanceof DataMessage) {
|
|
716
|
+
await this.onDataMessage(from, peerStream, message);
|
|
717
|
+
}
|
|
718
|
+
else if (message instanceof Hello) {
|
|
719
|
+
await this.onHello(from, peerStream, message);
|
|
720
|
+
}
|
|
721
|
+
else if (message instanceof Goodbye) {
|
|
722
|
+
await this.onGoodbye(from, peerStream, message);
|
|
723
|
+
}
|
|
724
|
+
else if (message instanceof PingPong) {
|
|
725
|
+
await this.onPing(from, peerStream, message);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
throw new Error("Unsupported");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async onDataMessage(from, peerStream, message) {
|
|
732
|
+
const isFromSelf = this.components.peerId.equals(from);
|
|
733
|
+
if (!isFromSelf || this.emitSelf) {
|
|
734
|
+
const isForAll = message.to.length === 0;
|
|
735
|
+
const isForMe = !isForAll && message.to.find((x) => x === this.publicKeyHash);
|
|
736
|
+
if (isForAll || isForMe) {
|
|
737
|
+
if (this.signaturePolicy === "StictSign" &&
|
|
738
|
+
!(await message.verify(this.signaturePolicy === "StictSign"))) {
|
|
739
|
+
// we don't verify messages we don't dispatch because of the performance penalty // TODO add opts for this
|
|
740
|
+
logger.warn("Recieved message with invalid signature or timestamp");
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
this.dispatchEvent(new CustomEvent("data", {
|
|
744
|
+
detail: message,
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
if (isForMe && message.to.length === 1) {
|
|
748
|
+
// dont forward this message anymore because it was meant ONLY for me
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Forward
|
|
753
|
+
await this.relayMessage(from, message);
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
async onHello(from, peerStream, message) {
|
|
757
|
+
if (!(await message.verify(false))) {
|
|
758
|
+
const a = message.header.verify();
|
|
759
|
+
const b = message.networkInfo.pingLatencies.length ===
|
|
760
|
+
message.signatures.signatures.length - 1;
|
|
761
|
+
logger.warn(`Recieved hello message that did not verify. Header: ${a}, Ping info ${b}, Signatures ${a && b}`);
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
const sender = message.sender?.hashcode();
|
|
765
|
+
if (!sender) {
|
|
766
|
+
logger.warn("Recieved hello without sender");
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
const signatures = message.signatures;
|
|
770
|
+
for (let i = 0; i < signatures.signatures.length - 1; i++) {
|
|
771
|
+
this.addRouteConnection(signatures.signatures[i].publicKey, signatures.signatures[i + 1].publicKey, message.networkInfo.pingLatencies[i]);
|
|
772
|
+
}
|
|
773
|
+
message.networkInfo.pingLatencies.push(peerStream.pingLatency || 4294967295); // TODO don't propagate if latency is high?
|
|
774
|
+
await message.sign(this.sign); // sign it so othere peers can now I have seen it (and can build a network graph from trace info)
|
|
775
|
+
let hellos = this.helloMap.get(sender);
|
|
776
|
+
if (!hellos) {
|
|
777
|
+
hellos = new Map();
|
|
778
|
+
this.helloMap.set(sender, hellos);
|
|
779
|
+
}
|
|
780
|
+
this.multiaddrsMap.set(sender, message.multiaddrs);
|
|
781
|
+
const helloSignaturHash = await message.signatures.hashPublicKeys();
|
|
782
|
+
const existingHello = hellos.get(helloSignaturHash);
|
|
783
|
+
if (existingHello) {
|
|
784
|
+
if (existingHello.header.expires < message.header.expires) {
|
|
785
|
+
hellos.set(helloSignaturHash, message);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
hellos.set(helloSignaturHash, message);
|
|
790
|
+
}
|
|
791
|
+
// Forward
|
|
792
|
+
await this.relayMessage(from, message);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
async onGoodbye(from, peerStream, message) {
|
|
796
|
+
if (!(await message.verify(false))) {
|
|
797
|
+
logger.warn("Recieved message with invalid signature or timestamp");
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const sender = message.sender?.hashcode();
|
|
801
|
+
if (!sender) {
|
|
802
|
+
logger.warn("Recieved hello without sender");
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
const peerKey = getPublicKeyFromPeerId(from);
|
|
806
|
+
const peerKeyHash = peerKey.hashcode();
|
|
807
|
+
if (message.early) {
|
|
808
|
+
this.earlyGoodbyes.set(peerKeyHash, message);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
const signatures = message.signatures;
|
|
812
|
+
/* TODO Should we update routes on goodbye?
|
|
813
|
+
for (let i = 1; i < signatures.signatures.length - 1; i++) {
|
|
814
|
+
this.addRouteConnection(
|
|
815
|
+
signatures.signatures[i].publicKey,
|
|
816
|
+
signatures.signatures[i + 1].publicKey
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
*/
|
|
820
|
+
//let neighbour = message.trace[1] || this.peerIdStr;
|
|
821
|
+
this.removeRouteConnection(signatures.signatures[0].publicKey, signatures.signatures[1].publicKey || this.publicKey);
|
|
822
|
+
const relayToPeers = [];
|
|
823
|
+
for (const stream of this.peers.values()) {
|
|
824
|
+
if (stream.peerId.equals(from)) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
relayToPeers.push(stream);
|
|
828
|
+
}
|
|
829
|
+
await message.sign(this.sign); // sign it so othere peers can now I have seen it (and can build a network graph from trace info)
|
|
830
|
+
const hellos = this.helloMap.get(sender);
|
|
831
|
+
if (hellos) {
|
|
832
|
+
const helloSignaturHash = await message.signatures.hashPublicKeys();
|
|
833
|
+
hellos.delete(helloSignaturHash);
|
|
834
|
+
}
|
|
835
|
+
// Forward
|
|
836
|
+
await this.relayMessage(from, message);
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
async onPing(from, peerStream, message) {
|
|
841
|
+
if (message instanceof Ping) {
|
|
842
|
+
// respond with pong
|
|
843
|
+
await this.publishMessage(this.components.peerId, new Pong(message.pingBytes), [peerStream]);
|
|
844
|
+
}
|
|
845
|
+
else if (message instanceof Pong) {
|
|
846
|
+
// Let the (waiting) thread know that we have recieved the pong
|
|
847
|
+
peerStream.pingJob?.resolve();
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
throw new Error("Unsupported");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async ping(stream) {
|
|
854
|
+
return new Promise((resolve, reject) => {
|
|
855
|
+
stream.pingJob?.abort();
|
|
856
|
+
const ping = new Ping();
|
|
857
|
+
const start = +new Date();
|
|
858
|
+
const timeout = setTimeout(() => {
|
|
859
|
+
reject(new TimeoutError("Ping timed out"));
|
|
860
|
+
}, 10000);
|
|
861
|
+
const resolver = () => {
|
|
862
|
+
const end = +new Date();
|
|
863
|
+
clearTimeout(timeout);
|
|
864
|
+
// TODO what happens if a peer send a ping back then leaves? Any problems?
|
|
865
|
+
const latency = end - start;
|
|
866
|
+
stream.pingLatency = latency;
|
|
867
|
+
this.addRouteConnection(this.publicKey, stream.publicKey, latency);
|
|
868
|
+
resolve(undefined);
|
|
869
|
+
};
|
|
870
|
+
stream.pingJob = {
|
|
871
|
+
resolve: resolver,
|
|
872
|
+
abort: () => {
|
|
873
|
+
clearTimeout(timeout);
|
|
874
|
+
resolve(undefined);
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
this.publishMessage(this.components.peerId, ping, [stream]).catch((err) => {
|
|
878
|
+
clearTimeout(timeout);
|
|
879
|
+
reject(err);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Whether to accept a message from a peer
|
|
885
|
+
* Override to create a graylist
|
|
886
|
+
*/
|
|
887
|
+
acceptFrom(id) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Publishes messages to all peers
|
|
892
|
+
*/
|
|
893
|
+
async publish(data, options) {
|
|
894
|
+
if (!this.started) {
|
|
895
|
+
throw new Error("Not started");
|
|
896
|
+
}
|
|
897
|
+
// dispatch the event if we are interested
|
|
898
|
+
let toHashes;
|
|
899
|
+
if (options?.to) {
|
|
900
|
+
if (options.to instanceof Set) {
|
|
901
|
+
toHashes = new Array(options.to.size);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
toHashes = new Array(options.to.length);
|
|
905
|
+
}
|
|
906
|
+
let i = 0;
|
|
907
|
+
for (const to of options.to) {
|
|
908
|
+
toHashes[i++] =
|
|
909
|
+
to instanceof PublicSignKey
|
|
910
|
+
? to.hashcode()
|
|
911
|
+
: typeof to === "string"
|
|
912
|
+
? to
|
|
913
|
+
: getPublicKeyFromPeerId(to).hashcode();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
toHashes = [];
|
|
918
|
+
}
|
|
919
|
+
const message = new DataMessage({
|
|
920
|
+
data: data instanceof Uint8ArrayList ? data.subarray() : data,
|
|
921
|
+
to: toHashes,
|
|
922
|
+
});
|
|
923
|
+
if (this.signaturePolicy === "StictSign") {
|
|
924
|
+
await message.sign(this.sign);
|
|
925
|
+
}
|
|
926
|
+
if (this.emitSelf) {
|
|
927
|
+
super.dispatchEvent(new CustomEvent("data", {
|
|
928
|
+
detail: message,
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
// send to all the other peers
|
|
932
|
+
await this.publishMessage(this.components.peerId, message, undefined);
|
|
933
|
+
return message.id;
|
|
934
|
+
}
|
|
935
|
+
async hello(data) {
|
|
936
|
+
if (!this.started) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
// send to all the other peers
|
|
940
|
+
await this.publishMessage(this.components.peerId, await new Hello({
|
|
941
|
+
multiaddrs: this.components.addressManager
|
|
942
|
+
.getAddresses()
|
|
943
|
+
.map((x) => x.toString()),
|
|
944
|
+
data,
|
|
945
|
+
}).sign(this.sign.bind(this)));
|
|
946
|
+
}
|
|
947
|
+
async relayMessage(from, message, to) {
|
|
948
|
+
if (this.canRelayMessage) {
|
|
949
|
+
return this.publishMessage(from, message, to, true);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
logger.debug("received message we didn't subscribe to. Dropping.");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async publishMessage(from, message, to, relayed) {
|
|
956
|
+
if (message instanceof DataMessage && !to) {
|
|
957
|
+
// message.to can be distant peers, but "to" are neighbours
|
|
958
|
+
const fanoutMap = new Map();
|
|
959
|
+
// Message to > 0
|
|
960
|
+
if (message.to.length > 0) {
|
|
961
|
+
const missingPathsFor = [];
|
|
962
|
+
for (const to of message.to) {
|
|
963
|
+
const fromKey = this.peerIdToPublicKey
|
|
964
|
+
.get(from.toString())
|
|
965
|
+
?.hashcode();
|
|
966
|
+
if (to === this.publicKeyHash || fromKey === to) {
|
|
967
|
+
continue; // don't send to me or backwards
|
|
968
|
+
}
|
|
969
|
+
const directStream = this.peers.get(to);
|
|
970
|
+
if (directStream) {
|
|
971
|
+
// always favor direct stream, even path seems longer
|
|
972
|
+
const fanout = fanoutMap.get(to);
|
|
973
|
+
if (!fanout) {
|
|
974
|
+
fanoutMap.set(to, [to]);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
fanout.push(to);
|
|
978
|
+
}
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
const fromMe = from.equals(this.components.peerId);
|
|
983
|
+
const block = !fromMe ? fromKey : undefined;
|
|
984
|
+
const path = this.routes.getPath(this.publicKeyHash, to, {
|
|
985
|
+
block, // prevent send message backwards
|
|
986
|
+
});
|
|
987
|
+
if (path && path.length > 0) {
|
|
988
|
+
const fanout = fanoutMap.get(path[1]);
|
|
989
|
+
if (!fanout) {
|
|
990
|
+
fanoutMap.set(path[1], [to]);
|
|
991
|
+
if (this.connectionManagerOptions.autoDial &&
|
|
992
|
+
path.length >= 3) {
|
|
993
|
+
await this.maybeConnectDirectly(path).catch((e) => {
|
|
994
|
+
logger.error("Failed to request direct connection: " + e.message);
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
fanout.push(to);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
missingPathsFor.push(to);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// we can't find path, send message to all peers
|
|
1009
|
+
fanoutMap.clear();
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
// update to's
|
|
1013
|
+
let sentOnce = false;
|
|
1014
|
+
if (missingPathsFor.length === 0) {
|
|
1015
|
+
if (fanoutMap.size > 0) {
|
|
1016
|
+
for (const [neighbour, distantPeers] of fanoutMap) {
|
|
1017
|
+
message.to = distantPeers;
|
|
1018
|
+
const bytes = message.serialize();
|
|
1019
|
+
if (!sentOnce) {
|
|
1020
|
+
// if relayed = true, we have already added it to seenCache
|
|
1021
|
+
if (!relayed) {
|
|
1022
|
+
this.seenCache.add(await getMsgId(bytes));
|
|
1023
|
+
}
|
|
1024
|
+
sentOnce = true;
|
|
1025
|
+
}
|
|
1026
|
+
const stream = this.peers.get(neighbour);
|
|
1027
|
+
await stream.waitForWrite(bytes);
|
|
1028
|
+
}
|
|
1029
|
+
return; // we are done sending the message in all direction with updates 'to' lists
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
// else send to all (fallthrough to code below)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// We fils to send the message directly, instead fallback to floodsub
|
|
1037
|
+
const peers = to || this.peers;
|
|
1038
|
+
if (peers == null ||
|
|
1039
|
+
(Array.isArray(peers) && peers.length === 0) ||
|
|
1040
|
+
(peers instanceof Map && peers.size === 0)) {
|
|
1041
|
+
logger.debug("no peers are subscribed");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const bytes = message.serialize();
|
|
1045
|
+
let sentOnce = false;
|
|
1046
|
+
for (const stream of peers.values()) {
|
|
1047
|
+
const id = stream;
|
|
1048
|
+
if (id.peerId.equals(from)) {
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (!sentOnce) {
|
|
1052
|
+
sentOnce = true;
|
|
1053
|
+
if (!relayed) {
|
|
1054
|
+
// if relayed = true, we have already added it to seenCache
|
|
1055
|
+
const msgId = await getMsgId(bytes);
|
|
1056
|
+
this.seenCache.add(msgId);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
await id.waitForWrite(bytes);
|
|
1060
|
+
}
|
|
1061
|
+
if (!sentOnce && !relayed) {
|
|
1062
|
+
throw new Error("Message did not have any valid recievers. ");
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async maybeConnectDirectly(path) {
|
|
1066
|
+
if (path.length < 3) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const toHash = path[path.length - 1];
|
|
1070
|
+
if (this.peers.has(toHash)) {
|
|
1071
|
+
return; // TODO, is this expected, or are we to dial more addresses?
|
|
1072
|
+
}
|
|
1073
|
+
// Try to either connect directly
|
|
1074
|
+
if (!this.recentDials.has(toHash)) {
|
|
1075
|
+
this.recentDials.add(toHash);
|
|
1076
|
+
const addrs = this.multiaddrsMap.get(toHash);
|
|
1077
|
+
try {
|
|
1078
|
+
if (addrs && addrs.length > 0) {
|
|
1079
|
+
await this.components.connectionManager.openConnection(addrs.map((x) => multiaddr(x)));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
// continue regardless of error
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// Connect through a closer relay that maybe does holepunch for us
|
|
1088
|
+
const nextToHash = path[path.length - 2];
|
|
1089
|
+
const routeKey = nextToHash + toHash;
|
|
1090
|
+
if (!this.recentDials.has(routeKey)) {
|
|
1091
|
+
this.recentDials.add(routeKey);
|
|
1092
|
+
const to = this.peerKeyHashToPublicKey.get(toHash);
|
|
1093
|
+
const toPeerId = await to.toPeerId();
|
|
1094
|
+
const addrs = this.multiaddrsMap.get(path[path.length - 2]);
|
|
1095
|
+
if (addrs && addrs.length > 0) {
|
|
1096
|
+
const addressesToDial = addrs.sort((a, b) => {
|
|
1097
|
+
if (a.includes("/wss/")) {
|
|
1098
|
+
if (b.includes("/wss/")) {
|
|
1099
|
+
return 0;
|
|
1100
|
+
}
|
|
1101
|
+
return -1;
|
|
1102
|
+
}
|
|
1103
|
+
if (a.includes("/ws/")) {
|
|
1104
|
+
if (b.includes("/ws/")) {
|
|
1105
|
+
return 0;
|
|
1106
|
+
}
|
|
1107
|
+
if (b.includes("/wss/")) {
|
|
1108
|
+
return 1;
|
|
1109
|
+
}
|
|
1110
|
+
return -1;
|
|
1111
|
+
}
|
|
1112
|
+
return 0;
|
|
1113
|
+
});
|
|
1114
|
+
for (const addr of addressesToDial) {
|
|
1115
|
+
const circuitAddress = multiaddr(addr + "/p2p-circuit/webrtc/p2p/" + toPeerId.toString());
|
|
1116
|
+
try {
|
|
1117
|
+
await this.components.connectionManager.openConnection(circuitAddress);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
logger.error("Failed to connect directly to: " +
|
|
1122
|
+
circuitAddress.toString() +
|
|
1123
|
+
". " +
|
|
1124
|
+
error?.message);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
async waitFor(peer) {
|
|
1131
|
+
const hash = (peer instanceof PublicSignKey ? peer : getPublicKeyFromPeerId(peer)).hashcode();
|
|
1132
|
+
try {
|
|
1133
|
+
await waitFor(() => {
|
|
1134
|
+
if (!this.peers.has(hash)) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
if (!this.routes.hasLink(this.publicKeyHash, hash)) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
return true;
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
throw new Error("Stream to " +
|
|
1145
|
+
hash +
|
|
1146
|
+
" does not exist. Connection exist: " +
|
|
1147
|
+
this.peers.has(hash) +
|
|
1148
|
+
". Route exist: " +
|
|
1149
|
+
this.routes.hasLink(this.publicKeyHash, hash));
|
|
1150
|
+
}
|
|
1151
|
+
const stream = this.peers.get(hash);
|
|
1152
|
+
try {
|
|
1153
|
+
await waitFor(() => stream.isReadable && stream.isWritable);
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
throw new Error("Stream to " +
|
|
1157
|
+
stream.publicKey.hashcode() +
|
|
1158
|
+
" not ready. Readable: " +
|
|
1159
|
+
stream.isReadable +
|
|
1160
|
+
". Writable " +
|
|
1161
|
+
stream.isWritable);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
export const waitForPeers = async (...libs) => {
|
|
1166
|
+
for (let i = 0; i < libs.length; i++) {
|
|
1167
|
+
for (let j = 0; j < libs.length; j++) {
|
|
1168
|
+
if (i === j) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
await libs[i].waitFor(libs[j].peerId);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
//# sourceMappingURL=index.js.map
|