@peerbit/pubsub 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 +27 -0
- package/lib/esm/index.d.ts +86 -0
- package/lib/esm/index.js +477 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/package.json +65 -0
- package/src/index.ts +679 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import type { PeerId as Libp2pPeerId } from "@libp2p/interface-peer-id";
|
|
2
|
+
import { logger as logFn } from "@peerbit/logger";
|
|
3
|
+
|
|
4
|
+
import { DataMessage } from "@peerbit/stream-interface";
|
|
5
|
+
import {
|
|
6
|
+
DirectStream,
|
|
7
|
+
DirectStreamComponents,
|
|
8
|
+
DirectStreamOptions,
|
|
9
|
+
PeerStreams,
|
|
10
|
+
} from "@peerbit/stream";
|
|
11
|
+
|
|
12
|
+
import { CodeError } from "@libp2p/interfaces/errors";
|
|
13
|
+
import {
|
|
14
|
+
PubSubMessage,
|
|
15
|
+
Subscribe,
|
|
16
|
+
PubSubData,
|
|
17
|
+
toUint8Array,
|
|
18
|
+
Unsubscribe,
|
|
19
|
+
GetSubscribers,
|
|
20
|
+
Subscription,
|
|
21
|
+
UnsubcriptionEvent,
|
|
22
|
+
SubscriptionEvent,
|
|
23
|
+
PubSub,
|
|
24
|
+
} from "@peerbit/pubsub-interface";
|
|
25
|
+
import { getPublicKeyFromPeerId, PublicSignKey } from "@peerbit/crypto";
|
|
26
|
+
import { CustomEvent } from "@libp2p/interfaces/events";
|
|
27
|
+
import { waitFor } from "@peerbit/time";
|
|
28
|
+
import { Connection } from "@libp2p/interface-connection";
|
|
29
|
+
|
|
30
|
+
import { equals, startsWith } from "@peerbit/uint8arrays";
|
|
31
|
+
import { PubSubEvents } from "@peerbit/pubsub-interface";
|
|
32
|
+
|
|
33
|
+
export const logger = logFn({ module: "direct-sub", level: "warn" });
|
|
34
|
+
|
|
35
|
+
export interface PeerStreamsInit {
|
|
36
|
+
id: Libp2pPeerId;
|
|
37
|
+
protocol: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DirectSubOptions = {
|
|
41
|
+
aggregate: boolean; // if true, we will collect topic/subscriber info for all traffic
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SubscriptionData = { timestamp: bigint; data?: Uint8Array };
|
|
45
|
+
|
|
46
|
+
export type DirectSubComponents = DirectStreamComponents;
|
|
47
|
+
|
|
48
|
+
export type PeerId = Libp2pPeerId | PublicSignKey;
|
|
49
|
+
|
|
50
|
+
export class DirectSub extends DirectStream<PubSubEvents> implements PubSub {
|
|
51
|
+
public topics: Map<string, Map<string, SubscriptionData>>; // topic -> peers --> Uint8Array subscription metadata (the latest recieved)
|
|
52
|
+
public peerToTopic: Map<string, Set<string>>; // peer -> topics
|
|
53
|
+
public topicsToPeers: Map<string, Set<string>>; // topic -> peers
|
|
54
|
+
public subscriptions: Map<string, { counter: number; data?: Uint8Array }>; // topic -> subscription ids
|
|
55
|
+
public lastSubscriptionMessages: Map<string, Map<string, DataMessage>> =
|
|
56
|
+
new Map();
|
|
57
|
+
|
|
58
|
+
constructor(components: DirectSubComponents, props?: DirectStreamOptions) {
|
|
59
|
+
super(components, ["pubsub/0.0.0"], props);
|
|
60
|
+
this.subscriptions = new Map();
|
|
61
|
+
this.topics = new Map();
|
|
62
|
+
this.topicsToPeers = new Map();
|
|
63
|
+
this.peerToTopic = new Map();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stop() {
|
|
67
|
+
this.subscriptions.clear();
|
|
68
|
+
this.topics.clear();
|
|
69
|
+
this.peerToTopic.clear();
|
|
70
|
+
this.topicsToPeers.clear();
|
|
71
|
+
return super.stop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async onPeerReachable(publicKey: PublicSignKey) {
|
|
75
|
+
// Aggregate subscribers for my topics through this new peer because if we don't do this we might end up with a situtation where
|
|
76
|
+
// we act as a relay and relay messages for a topic, but don't forward it to this new peer because we never learned about their subscriptions
|
|
77
|
+
await this.requestSubscribers([...this.topics.keys()], publicKey);
|
|
78
|
+
return super.onPeerReachable(publicKey);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async onPeerDisconnected(peerId: Libp2pPeerId, conn?: Connection) {
|
|
82
|
+
return super.onPeerDisconnected(peerId, conn);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private initializeTopic(topic: string) {
|
|
86
|
+
this.topics.get(topic) || this.topics.set(topic, new Map());
|
|
87
|
+
this.topicsToPeers.get(topic) || this.topicsToPeers.set(topic, new Set());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private initializePeer(publicKey: PublicSignKey) {
|
|
91
|
+
this.peerToTopic.get(publicKey.hashcode()) ||
|
|
92
|
+
this.peerToTopic.set(publicKey.hashcode(), new Set());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Subscribes to a given topic.
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* @param topic,
|
|
100
|
+
* @param data, metadata associated with the subscription, shared with peers
|
|
101
|
+
*/
|
|
102
|
+
async subscribe(topic: string | string[], options?: { data?: Uint8Array }) {
|
|
103
|
+
if (!this.started) {
|
|
104
|
+
throw new Error("Pubsub has not started");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
topic = typeof topic === "string" ? [topic] : topic;
|
|
108
|
+
|
|
109
|
+
const newTopicsForTopicData: string[] = [];
|
|
110
|
+
for (const t of topic) {
|
|
111
|
+
const prev = this.subscriptions.get(t);
|
|
112
|
+
if (prev) {
|
|
113
|
+
const difference =
|
|
114
|
+
!!prev.data != !!options?.data ||
|
|
115
|
+
(prev.data && options?.data && !equals(prev.data, options?.data));
|
|
116
|
+
prev.counter += 1;
|
|
117
|
+
|
|
118
|
+
if (difference) {
|
|
119
|
+
prev.data = options?.data;
|
|
120
|
+
newTopicsForTopicData.push(t);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
this.subscriptions.set(t, {
|
|
124
|
+
counter: 1,
|
|
125
|
+
data: options?.data,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
newTopicsForTopicData.push(t);
|
|
129
|
+
this.listenForSubscribers(t);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (newTopicsForTopicData.length > 0) {
|
|
134
|
+
const message = new DataMessage({
|
|
135
|
+
data: toUint8Array(
|
|
136
|
+
new Subscribe({
|
|
137
|
+
subscriptions: newTopicsForTopicData.map(
|
|
138
|
+
(x) => new Subscription(x, options?.data)
|
|
139
|
+
),
|
|
140
|
+
}).serialize()
|
|
141
|
+
),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await this.publishMessage(
|
|
145
|
+
this.components.peerId,
|
|
146
|
+
await message.sign(this.sign)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
*
|
|
153
|
+
* @param topic
|
|
154
|
+
* @param force
|
|
155
|
+
* @returns true unsubscribed completely
|
|
156
|
+
*/
|
|
157
|
+
async unsubscribe(
|
|
158
|
+
topic: string,
|
|
159
|
+
options?: { force: boolean; data: Uint8Array }
|
|
160
|
+
) {
|
|
161
|
+
if (!this.started) {
|
|
162
|
+
throw new Error("Pubsub is not started");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const subscriptions = this.subscriptions.get(topic);
|
|
166
|
+
|
|
167
|
+
logger.debug(
|
|
168
|
+
`unsubscribe from ${topic} - am subscribed with subscriptions ${subscriptions}`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (subscriptions?.counter && subscriptions?.counter >= 0) {
|
|
172
|
+
subscriptions.counter -= 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const peersOnTopic = this.topicsToPeers.get(topic);
|
|
176
|
+
if (peersOnTopic) {
|
|
177
|
+
for (const peer of peersOnTopic) {
|
|
178
|
+
this.lastSubscriptionMessages.delete(peer);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!subscriptions?.counter || options?.force) {
|
|
182
|
+
this.subscriptions.delete(topic);
|
|
183
|
+
this.topics.delete(topic);
|
|
184
|
+
this.topicsToPeers.delete(topic);
|
|
185
|
+
|
|
186
|
+
await this.publishMessage(
|
|
187
|
+
this.components.peerId,
|
|
188
|
+
await new DataMessage({
|
|
189
|
+
data: toUint8Array(new Unsubscribe({ topics: [topic] }).serialize()),
|
|
190
|
+
}).sign(this.sign)
|
|
191
|
+
);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getSubscribers(topic: string): Map<string, SubscriptionData> | undefined {
|
|
198
|
+
if (!this.started) {
|
|
199
|
+
throw new CodeError("not started yet", "ERR_NOT_STARTED_YET");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (topic == null) {
|
|
203
|
+
throw new CodeError("topic is required", "ERR_NOT_VALID_TOPIC");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this.topics.get(topic.toString());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getSubscribersWithData(
|
|
210
|
+
topic: string,
|
|
211
|
+
data: Uint8Array,
|
|
212
|
+
options?: { prefix: boolean }
|
|
213
|
+
): string[] | undefined {
|
|
214
|
+
const map = this.topics.get(topic);
|
|
215
|
+
if (map) {
|
|
216
|
+
const results: string[] = [];
|
|
217
|
+
for (const [peer, info] of map.entries()) {
|
|
218
|
+
if (!info.data) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (options?.prefix) {
|
|
222
|
+
if (!startsWith(info.data, data)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
if (!equals(info.data, data)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
results.push(peer);
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
listenForSubscribers(topic: string) {
|
|
238
|
+
this.initializeTopic(topic);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async requestSubscribers(
|
|
242
|
+
topic: string | string[],
|
|
243
|
+
from?: PublicSignKey
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
if (!this.started) {
|
|
246
|
+
throw new CodeError("not started yet", "ERR_NOT_STARTED_YET");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (topic == null) {
|
|
250
|
+
throw new CodeError("topic is required", "ERR_NOT_VALID_TOPIC");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (topic.length === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const topics = typeof topic === "string" ? [topic] : topic;
|
|
258
|
+
for (const topic of topics) {
|
|
259
|
+
this.listenForSubscribers(topic);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return this.publishMessage(
|
|
263
|
+
this.components.peerId,
|
|
264
|
+
await new DataMessage({
|
|
265
|
+
to: from ? [from.hashcode()] : [],
|
|
266
|
+
data: toUint8Array(new GetSubscribers({ topics }).serialize()),
|
|
267
|
+
}).sign(this.sign)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getPeersWithTopics(topics: string[], otherPeers?: string[]): Set<string> {
|
|
272
|
+
const peers: Set<string> = otherPeers ? new Set(otherPeers) : new Set();
|
|
273
|
+
if (topics?.length) {
|
|
274
|
+
for (const topic of topics) {
|
|
275
|
+
const peersOnTopic = this.topicsToPeers.get(topic.toString());
|
|
276
|
+
if (peersOnTopic) {
|
|
277
|
+
peersOnTopic.forEach((peer) => {
|
|
278
|
+
peers.add(peer);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return peers;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* getStreamsWithTopics(topics: string[], otherPeers?: string[]): PeerStreams[] {
|
|
287
|
+
const peers = this.getNeighboursWithTopics(topics, otherPeers);
|
|
288
|
+
return [...this.peers.values()].filter((s) =>
|
|
289
|
+
peers.has(s.publicKey.hashcode())
|
|
290
|
+
);
|
|
291
|
+
} */
|
|
292
|
+
|
|
293
|
+
async publish(
|
|
294
|
+
data: Uint8Array,
|
|
295
|
+
options:
|
|
296
|
+
| {
|
|
297
|
+
topics?: string[];
|
|
298
|
+
to?: (string | PeerId)[];
|
|
299
|
+
strict?: false;
|
|
300
|
+
}
|
|
301
|
+
| {
|
|
302
|
+
topics: string[];
|
|
303
|
+
to: (string | PeerId)[];
|
|
304
|
+
strict: true;
|
|
305
|
+
}
|
|
306
|
+
): Promise<Uint8Array> {
|
|
307
|
+
const topics =
|
|
308
|
+
(options as { topics: string[] }).topics?.map((x) => x.toString()) || [];
|
|
309
|
+
const tos =
|
|
310
|
+
options?.to?.map((x) =>
|
|
311
|
+
x instanceof PublicSignKey
|
|
312
|
+
? x.hashcode()
|
|
313
|
+
: typeof x === "string"
|
|
314
|
+
? x
|
|
315
|
+
: getPublicKeyFromPeerId(x).hashcode()
|
|
316
|
+
) || [];
|
|
317
|
+
// Embedd topic info before the data so that peers/relays can also use topic info to route messages efficiently
|
|
318
|
+
const message = new PubSubData({
|
|
319
|
+
topics: topics.map((x) => x.toString()),
|
|
320
|
+
data,
|
|
321
|
+
strict: options.strict,
|
|
322
|
+
});
|
|
323
|
+
const bytes = message.serialize();
|
|
324
|
+
return super.publish(
|
|
325
|
+
bytes instanceof Uint8Array ? bytes : bytes.subarray(),
|
|
326
|
+
{ to: options?.strict ? tos : this.getPeersWithTopics(topics, tos) }
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private deletePeerFromTopic(topic: string, publicKeyHash: string) {
|
|
331
|
+
const peers = this.topics.get(topic);
|
|
332
|
+
let change: SubscriptionData | undefined = undefined;
|
|
333
|
+
if (peers) {
|
|
334
|
+
change = peers.get(publicKeyHash);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.topics.get(topic)?.delete(publicKeyHash);
|
|
338
|
+
|
|
339
|
+
this.peerToTopic.get(publicKeyHash)?.delete(topic);
|
|
340
|
+
if (!this.peerToTopic.get(publicKeyHash)?.size) {
|
|
341
|
+
this.peerToTopic.delete(publicKeyHash);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.topicsToPeers.get(topic)?.delete(publicKeyHash);
|
|
345
|
+
|
|
346
|
+
return change;
|
|
347
|
+
}
|
|
348
|
+
public onPeerUnreachable(publicKey: PublicSignKey) {
|
|
349
|
+
super.onPeerUnreachable(publicKey);
|
|
350
|
+
|
|
351
|
+
const publicKeyHash = publicKey.hashcode();
|
|
352
|
+
const peerTopics = this.peerToTopic.get(publicKeyHash);
|
|
353
|
+
|
|
354
|
+
const changed: Subscription[] = [];
|
|
355
|
+
if (peerTopics) {
|
|
356
|
+
for (const topic of peerTopics) {
|
|
357
|
+
const change = this.deletePeerFromTopic(topic, publicKeyHash);
|
|
358
|
+
if (change) {
|
|
359
|
+
changed.push(new Subscription(topic, change.data));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
this.lastSubscriptionMessages.delete(publicKeyHash);
|
|
364
|
+
|
|
365
|
+
if (changed.length > 0) {
|
|
366
|
+
this.dispatchEvent(
|
|
367
|
+
new CustomEvent<UnsubcriptionEvent>("unsubscribe", {
|
|
368
|
+
detail: { from: publicKey, unsubscriptions: changed },
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private subscriptionMessageIsLatest(
|
|
375
|
+
message: DataMessage,
|
|
376
|
+
pubsubMessage: Subscribe | Unsubscribe
|
|
377
|
+
) {
|
|
378
|
+
const subscriber = message.signatures.signatures[0].publicKey!;
|
|
379
|
+
const subscriberKey = subscriber.hashcode(); // Assume first signature is the one who is signing
|
|
380
|
+
|
|
381
|
+
for (const topic of pubsubMessage.topics) {
|
|
382
|
+
const lastTimestamp = this.lastSubscriptionMessages
|
|
383
|
+
.get(subscriberKey)
|
|
384
|
+
?.get(topic)?.header.timetamp;
|
|
385
|
+
if (lastTimestamp != null && lastTimestamp > message.header.timetamp) {
|
|
386
|
+
return false; // message is old
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const topic of pubsubMessage.topics) {
|
|
391
|
+
if (!this.lastSubscriptionMessages.has(subscriberKey)) {
|
|
392
|
+
this.lastSubscriptionMessages.set(subscriberKey, new Map());
|
|
393
|
+
}
|
|
394
|
+
this.lastSubscriptionMessages.get(subscriberKey)?.set(topic, message);
|
|
395
|
+
}
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async onDataMessage(
|
|
400
|
+
from: Libp2pPeerId,
|
|
401
|
+
stream: PeerStreams,
|
|
402
|
+
message: DataMessage
|
|
403
|
+
) {
|
|
404
|
+
const pubsubMessage = PubSubMessage.deserialize(message.data);
|
|
405
|
+
if (pubsubMessage instanceof PubSubData) {
|
|
406
|
+
/**
|
|
407
|
+
* See if we know more subscribers of the message topics. If so, add aditional end recievers of the message
|
|
408
|
+
*/
|
|
409
|
+
let verified: boolean | undefined = undefined;
|
|
410
|
+
|
|
411
|
+
const isFromSelf = this.components.peerId.equals(from);
|
|
412
|
+
if (!isFromSelf || this.emitSelf) {
|
|
413
|
+
let isForMe: boolean;
|
|
414
|
+
if (pubsubMessage.strict) {
|
|
415
|
+
isForMe =
|
|
416
|
+
!!pubsubMessage.topics.find((topic) =>
|
|
417
|
+
this.subscriptions.has(topic)
|
|
418
|
+
) && !!message.to.find((x) => this.publicKeyHash === x);
|
|
419
|
+
} else {
|
|
420
|
+
isForMe =
|
|
421
|
+
!!pubsubMessage.topics.find((topic) =>
|
|
422
|
+
this.subscriptions.has(topic)
|
|
423
|
+
) ||
|
|
424
|
+
(pubsubMessage.topics.length === 0 &&
|
|
425
|
+
!!message.to.find((x) => this.publicKeyHash === x));
|
|
426
|
+
}
|
|
427
|
+
if (isForMe) {
|
|
428
|
+
if (verified === undefined) {
|
|
429
|
+
verified = await message.verify(
|
|
430
|
+
this.signaturePolicy === "StictSign" ? true : false
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (!verified) {
|
|
434
|
+
logger.warn("Recieved message that did not verify PubSubData");
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
this.dispatchEvent(
|
|
438
|
+
new CustomEvent("data", {
|
|
439
|
+
detail: { data: pubsubMessage, message },
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Forward
|
|
446
|
+
if (!pubsubMessage.strict) {
|
|
447
|
+
const newTos = this.getPeersWithTopics(
|
|
448
|
+
pubsubMessage.topics,
|
|
449
|
+
message.to
|
|
450
|
+
);
|
|
451
|
+
newTos.delete(this.publicKeyHash);
|
|
452
|
+
message.to = [...newTos];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only relay if we got additional recievers
|
|
456
|
+
// or we are NOT subscribing ourselves (if we are not subscribing ourselves we are)
|
|
457
|
+
// If we are not subscribing ourselves, then we don't have enough information to "stop" message propagation here
|
|
458
|
+
if (
|
|
459
|
+
message.to.length > 0 ||
|
|
460
|
+
!pubsubMessage.topics.find((topic) => this.topics.has(topic))
|
|
461
|
+
) {
|
|
462
|
+
await this.relayMessage(from, message);
|
|
463
|
+
}
|
|
464
|
+
} else if (pubsubMessage instanceof Subscribe) {
|
|
465
|
+
if (!(await message.verify(true))) {
|
|
466
|
+
logger.warn("Recieved message that did not verify Subscribe");
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (message.signatures.signatures.length === 0) {
|
|
471
|
+
logger.warn("Recieved subscription message with no signers");
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (pubsubMessage.subscriptions.length === 0) {
|
|
476
|
+
logger.info("Recieved subscription message with no topics");
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!this.subscriptionMessageIsLatest(message, pubsubMessage)) {
|
|
481
|
+
logger.trace("Recieved old subscription message");
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const subscriber = message.signatures.signatures[0].publicKey!;
|
|
486
|
+
const subscriberKey = subscriber.hashcode(); // Assume first signature is the one who is signing
|
|
487
|
+
|
|
488
|
+
this.initializePeer(subscriber);
|
|
489
|
+
|
|
490
|
+
const changed: Subscription[] = [];
|
|
491
|
+
pubsubMessage.subscriptions.forEach((subscription) => {
|
|
492
|
+
const peers = this.topics.get(subscription.topic);
|
|
493
|
+
if (peers == null) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// if no subscription data, or new subscription has data (and is newer) then overwrite it.
|
|
498
|
+
// subscription where data is undefined is not intended to replace existing data
|
|
499
|
+
const existingSubscription = peers.get(subscriberKey);
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
!existingSubscription ||
|
|
503
|
+
(existingSubscription.timestamp < message.header.timetamp &&
|
|
504
|
+
subscription.data)
|
|
505
|
+
) {
|
|
506
|
+
peers.set(subscriberKey, {
|
|
507
|
+
timestamp: message.header.timetamp, // TODO update timestamps on all messages?
|
|
508
|
+
data: subscription.data,
|
|
509
|
+
});
|
|
510
|
+
if (
|
|
511
|
+
!existingSubscription?.data ||
|
|
512
|
+
!equals(existingSubscription.data, subscription.data)
|
|
513
|
+
) {
|
|
514
|
+
changed.push(subscription);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.topicsToPeers.get(subscription.topic)?.add(subscriberKey);
|
|
519
|
+
this.peerToTopic.get(subscriberKey)?.add(subscription.topic);
|
|
520
|
+
});
|
|
521
|
+
if (changed.length > 0) {
|
|
522
|
+
const subscriptionEvent: SubscriptionEvent = {
|
|
523
|
+
from: subscriber,
|
|
524
|
+
subscriptions: changed,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
this.dispatchEvent(
|
|
528
|
+
new CustomEvent<SubscriptionEvent>("subscribe", {
|
|
529
|
+
detail: subscriptionEvent,
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Forward
|
|
535
|
+
await this.relayMessage(from, message);
|
|
536
|
+
} else if (pubsubMessage instanceof Unsubscribe) {
|
|
537
|
+
if (!(await message.verify(true))) {
|
|
538
|
+
logger.warn("Recieved message that did not verify Unsubscribe");
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (message.signatures.signatures.length === 0) {
|
|
543
|
+
logger.warn("Recieved subscription message with no signers");
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!this.subscriptionMessageIsLatest(message, pubsubMessage)) {
|
|
548
|
+
logger.trace("Recieved old subscription message");
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const changed: Subscription[] = [];
|
|
553
|
+
const subscriber = message.signatures.signatures[0].publicKey!;
|
|
554
|
+
const subscriberKey = subscriber.hashcode(); // Assume first signature is the one who is signing
|
|
555
|
+
|
|
556
|
+
for (const unsubscription of pubsubMessage.unsubscriptions) {
|
|
557
|
+
const change = this.deletePeerFromTopic(
|
|
558
|
+
unsubscription.topic,
|
|
559
|
+
subscriberKey
|
|
560
|
+
);
|
|
561
|
+
if (change) {
|
|
562
|
+
changed.push(new Subscription(unsubscription.topic, change.data));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (changed.length > 0) {
|
|
567
|
+
this.dispatchEvent(
|
|
568
|
+
new CustomEvent<UnsubcriptionEvent>("unsubscribe", {
|
|
569
|
+
detail: { from: subscriber, unsubscriptions: changed },
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Forward
|
|
575
|
+
await this.relayMessage(from, message);
|
|
576
|
+
} else if (pubsubMessage instanceof GetSubscribers) {
|
|
577
|
+
if (!(await message.verify(true))) {
|
|
578
|
+
logger.warn("Recieved message that did not verify Unsubscribe");
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const subscriptionsToSend: Subscription[] = [];
|
|
583
|
+
for (const topic of pubsubMessage.topics) {
|
|
584
|
+
const subscription = this.subscriptions.get(topic);
|
|
585
|
+
if (subscription) {
|
|
586
|
+
subscriptionsToSend.push(new Subscription(topic, subscription.data));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (subscriptionsToSend.length > 0) {
|
|
591
|
+
// respond
|
|
592
|
+
if (!stream.isWritable) {
|
|
593
|
+
try {
|
|
594
|
+
await waitFor(() => stream.isWritable);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
logger.warn(
|
|
597
|
+
`Failed to respond to GetSubscribers request to ${from.toString()} stream is not writable`
|
|
598
|
+
);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
this.publishMessage(
|
|
603
|
+
this.components.peerId,
|
|
604
|
+
await new DataMessage({
|
|
605
|
+
data: toUint8Array(
|
|
606
|
+
new Subscribe({
|
|
607
|
+
subscriptions: subscriptionsToSend,
|
|
608
|
+
}).serialize()
|
|
609
|
+
),
|
|
610
|
+
}).sign(this.sign),
|
|
611
|
+
[stream]
|
|
612
|
+
); // send back to same stream
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Forward
|
|
616
|
+
await this.relayMessage(from, message);
|
|
617
|
+
}
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export const waitForSubscribers = async (
|
|
623
|
+
libp2p: { services: { pubsub: DirectSub } },
|
|
624
|
+
peersToWait:
|
|
625
|
+
| PeerId
|
|
626
|
+
| PeerId[]
|
|
627
|
+
| { peerId: Libp2pPeerId }
|
|
628
|
+
| { peerId: Libp2pPeerId }[]
|
|
629
|
+
| string
|
|
630
|
+
| string[],
|
|
631
|
+
topic: string
|
|
632
|
+
) => {
|
|
633
|
+
const peersToWaitArr = Array.isArray(peersToWait)
|
|
634
|
+
? peersToWait
|
|
635
|
+
: [peersToWait];
|
|
636
|
+
|
|
637
|
+
const peerIdsToWait: string[] = peersToWaitArr.map((peer) => {
|
|
638
|
+
if (typeof peer === "string") {
|
|
639
|
+
return peer;
|
|
640
|
+
}
|
|
641
|
+
const id: PublicSignKey | Libp2pPeerId = peer["peerId"] || peer;
|
|
642
|
+
if (typeof id === "string") {
|
|
643
|
+
return id;
|
|
644
|
+
}
|
|
645
|
+
return id instanceof PublicSignKey
|
|
646
|
+
? id.hashcode()
|
|
647
|
+
: getPublicKeyFromPeerId(id).hashcode();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await libp2p.services.pubsub.requestSubscribers(topic);
|
|
651
|
+
return new Promise<void>((resolve, reject) => {
|
|
652
|
+
let counter = 0;
|
|
653
|
+
const interval = setInterval(async () => {
|
|
654
|
+
counter += 1;
|
|
655
|
+
if (counter > 100) {
|
|
656
|
+
clearInterval(interval);
|
|
657
|
+
reject(
|
|
658
|
+
new Error("Failed to find expected subscribers for topic: " + topic)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const peers = libp2p.services.pubsub.getSubscribers(topic);
|
|
663
|
+
const hasAllPeers =
|
|
664
|
+
peerIdsToWait
|
|
665
|
+
.map((e) => peers && peers.has(e))
|
|
666
|
+
.filter((e) => e === false).length === 0;
|
|
667
|
+
|
|
668
|
+
// FIXME: Does not fail on timeout, not easily fixable
|
|
669
|
+
if (hasAllPeers) {
|
|
670
|
+
clearInterval(interval);
|
|
671
|
+
resolve();
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {
|
|
674
|
+
clearInterval(interval);
|
|
675
|
+
reject(e);
|
|
676
|
+
}
|
|
677
|
+
}, 200);
|
|
678
|
+
});
|
|
679
|
+
};
|