@dxos/teleport-extension-gossip 0.10.0 → 0.11.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/lib/index.mjs +360 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -14
- package/dist/lib/browser/index.mjs +0 -326
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -328
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { Event, Trigger, scheduleTask, scheduleTaskInterval } from "@dxos/async";
|
|
2
|
+
import { Context, Resource } from "@dxos/context";
|
|
3
|
+
import { PublicKey } from "@dxos/keys";
|
|
4
|
+
import { log } from "@dxos/log";
|
|
5
|
+
import { RpcClosedError, TimeoutError } from "@dxos/protocols";
|
|
6
|
+
import { ComplexMap, ComplexSet } from "@dxos/util";
|
|
7
|
+
import { invariant } from "@dxos/invariant";
|
|
8
|
+
import { schema } from "@dxos/protocols/proto";
|
|
9
|
+
import { createProtoRpcPeer } from "@dxos/rpc";
|
|
10
|
+
//#region src/gossip-extension.ts
|
|
11
|
+
var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-gossip/src/gossip-extension.ts";
|
|
12
|
+
/**
|
|
13
|
+
* Sends announces between two peers for a single teleport session.
|
|
14
|
+
*/
|
|
15
|
+
var GossipExtension = class {
|
|
16
|
+
_callbacks;
|
|
17
|
+
_opened = new Trigger();
|
|
18
|
+
_closed = false;
|
|
19
|
+
_rpc;
|
|
20
|
+
constructor(_callbacks = {}) {
|
|
21
|
+
this._callbacks = _callbacks;
|
|
22
|
+
}
|
|
23
|
+
async onOpen(context) {
|
|
24
|
+
log("onOpen", {
|
|
25
|
+
localPeerId: context.localPeerId,
|
|
26
|
+
remotePeerId: context.remotePeerId
|
|
27
|
+
}, {
|
|
28
|
+
"~LogMeta": "~LogMeta",
|
|
29
|
+
F: __dxlog_file$2,
|
|
30
|
+
L: 37,
|
|
31
|
+
S: this
|
|
32
|
+
});
|
|
33
|
+
this._rpc = createProtoRpcPeer({
|
|
34
|
+
requested: { GossipService: schema.getService("dxos.mesh.teleport.gossip.GossipService") },
|
|
35
|
+
exposed: { GossipService: schema.getService("dxos.mesh.teleport.gossip.GossipService") },
|
|
36
|
+
handlers: { GossipService: { announce: async (message) => {
|
|
37
|
+
log("received announce", {
|
|
38
|
+
localPeerId: context.localPeerId,
|
|
39
|
+
remotePeerId: context.remotePeerId,
|
|
40
|
+
message
|
|
41
|
+
}, {
|
|
42
|
+
"~LogMeta": "~LogMeta",
|
|
43
|
+
F: __dxlog_file$2,
|
|
44
|
+
L: 49,
|
|
45
|
+
S: this
|
|
46
|
+
});
|
|
47
|
+
await this._callbacks.onAnnounce?.(message);
|
|
48
|
+
} } },
|
|
49
|
+
port: await context.createPort("rpc", { contentType: "application/x-protobuf; messageType=\"dxos.rpc.Message\"" })
|
|
50
|
+
});
|
|
51
|
+
await this._rpc.open();
|
|
52
|
+
this._opened.wake();
|
|
53
|
+
}
|
|
54
|
+
async onClose(err) {
|
|
55
|
+
log("close", { err }, {
|
|
56
|
+
"~LogMeta": "~LogMeta",
|
|
57
|
+
F: __dxlog_file$2,
|
|
58
|
+
L: 63,
|
|
59
|
+
S: this
|
|
60
|
+
});
|
|
61
|
+
await this._rpc?.close();
|
|
62
|
+
await this._callbacks.onClose?.(err);
|
|
63
|
+
this._closed = true;
|
|
64
|
+
}
|
|
65
|
+
async onAbort(err) {
|
|
66
|
+
log("abort", { err }, {
|
|
67
|
+
"~LogMeta": "~LogMeta",
|
|
68
|
+
F: __dxlog_file$2,
|
|
69
|
+
L: 70,
|
|
70
|
+
S: this
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await this._rpc?.abort();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.catch(err, void 0, {
|
|
76
|
+
"~LogMeta": "~LogMeta",
|
|
77
|
+
F: __dxlog_file$2,
|
|
78
|
+
L: 74,
|
|
79
|
+
S: this
|
|
80
|
+
});
|
|
81
|
+
} finally {
|
|
82
|
+
await this._callbacks.onClose?.(err);
|
|
83
|
+
}
|
|
84
|
+
this._closed = true;
|
|
85
|
+
}
|
|
86
|
+
async sendAnnounce(message) {
|
|
87
|
+
if (this._closed) return;
|
|
88
|
+
await this._opened.wait();
|
|
89
|
+
invariant(this._rpc, "RPC not initialized", {
|
|
90
|
+
"~LogMeta": "~LogMeta",
|
|
91
|
+
F: __dxlog_file$2,
|
|
92
|
+
L: 86,
|
|
93
|
+
S: this,
|
|
94
|
+
A: ["this._rpc", "'RPC not initialized'"]
|
|
95
|
+
});
|
|
96
|
+
await this._rpc.rpc.GossipService.announce(message);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/gossip.ts
|
|
101
|
+
var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-gossip/src/gossip.ts";
|
|
102
|
+
var RECEIVED_MESSAGES_GC_INTERVAL = 12e4;
|
|
103
|
+
var MAX_CTX_TASKS = 50;
|
|
104
|
+
/**
|
|
105
|
+
* Gossip extensions manager.
|
|
106
|
+
* Keeps track of all peers that are connected to the local peer.
|
|
107
|
+
* Routes received announces to all connected peers.
|
|
108
|
+
* Exposes API send announce to everybody and subscribe to .
|
|
109
|
+
*/
|
|
110
|
+
var Gossip = class {
|
|
111
|
+
_params;
|
|
112
|
+
_ctx = new Context({ onError: (err) => {
|
|
113
|
+
log.catch(err, void 0, {
|
|
114
|
+
"~LogMeta": "~LogMeta",
|
|
115
|
+
F: __dxlog_file$1,
|
|
116
|
+
L: 32,
|
|
117
|
+
S: this
|
|
118
|
+
});
|
|
119
|
+
} }, {
|
|
120
|
+
"~LogMeta": "~LogMeta",
|
|
121
|
+
F: __dxlog_file$1,
|
|
122
|
+
L: 30
|
|
123
|
+
});
|
|
124
|
+
_listeners = /* @__PURE__ */ new Map();
|
|
125
|
+
_receivedMessages = new ComplexSet(PublicKey.hash);
|
|
126
|
+
/**
|
|
127
|
+
* Keys scheduled to be cleared from _receivedMessages on the next iteration.
|
|
128
|
+
*/
|
|
129
|
+
_toClear = new ComplexSet(PublicKey.hash);
|
|
130
|
+
_connections = new ComplexMap(PublicKey.hash);
|
|
131
|
+
connectionClosed = new Event();
|
|
132
|
+
constructor(_params) {
|
|
133
|
+
this._params = _params;
|
|
134
|
+
}
|
|
135
|
+
get localPeerId() {
|
|
136
|
+
return this._params.localPeerId;
|
|
137
|
+
}
|
|
138
|
+
async open() {
|
|
139
|
+
scheduleTaskInterval(this._ctx, async () => {
|
|
140
|
+
this._performGc();
|
|
141
|
+
}, RECEIVED_MESSAGES_GC_INTERVAL);
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
await this._ctx.dispose();
|
|
145
|
+
}
|
|
146
|
+
getConnections() {
|
|
147
|
+
return Array.from(this._connections.keys());
|
|
148
|
+
}
|
|
149
|
+
createExtension({ remotePeerId }) {
|
|
150
|
+
const extension = new GossipExtension({
|
|
151
|
+
onAnnounce: async (message) => {
|
|
152
|
+
if (this._receivedMessages.has(message.messageId)) return;
|
|
153
|
+
this._receivedMessages.add(message.messageId);
|
|
154
|
+
this._callListeners(message);
|
|
155
|
+
if (this._ctx.disposeCallbacksLength > MAX_CTX_TASKS) {
|
|
156
|
+
log(`skipping propagating gossip message due to exessive tasks (${MAX_CTX_TASKS})`, void 0, {
|
|
157
|
+
"~LogMeta": "~LogMeta",
|
|
158
|
+
F: __dxlog_file$1,
|
|
159
|
+
L: 84,
|
|
160
|
+
S: this
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
scheduleTask(this._ctx, async () => {
|
|
165
|
+
await this._propagateAnnounce(message);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
onClose: async (err) => {
|
|
169
|
+
if (this._connections.has(remotePeerId)) this._connections.delete(remotePeerId);
|
|
170
|
+
this.connectionClosed.emit(remotePeerId);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
this._connections.set(remotePeerId, extension);
|
|
174
|
+
return extension;
|
|
175
|
+
}
|
|
176
|
+
postMessage(channel, payload) {
|
|
177
|
+
for (const extension of this._connections.values()) this._sendAnnounceWithTimeoutTracking(extension, {
|
|
178
|
+
peerId: this._params.localPeerId,
|
|
179
|
+
messageId: PublicKey.random(),
|
|
180
|
+
channelId: channel,
|
|
181
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
182
|
+
payload
|
|
183
|
+
}).catch(async (err) => {
|
|
184
|
+
if (err instanceof RpcClosedError) log("sendAnnounce failed because of RpcClosedError", { err }, {
|
|
185
|
+
"~LogMeta": "~LogMeta",
|
|
186
|
+
F: __dxlog_file$1,
|
|
187
|
+
L: 113,
|
|
188
|
+
S: this
|
|
189
|
+
});
|
|
190
|
+
else if (err instanceof TimeoutError || err.constructor.name === "TimeoutError" || err.message.startsWith("Timeout")) log("sendAnnounce failed because of TimeoutError", { err }, {
|
|
191
|
+
"~LogMeta": "~LogMeta",
|
|
192
|
+
F: __dxlog_file$1,
|
|
193
|
+
L: 119,
|
|
194
|
+
S: this
|
|
195
|
+
});
|
|
196
|
+
else log.catch(err, void 0, {
|
|
197
|
+
"~LogMeta": "~LogMeta",
|
|
198
|
+
F: __dxlog_file$1,
|
|
199
|
+
L: 121,
|
|
200
|
+
S: this
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
listen(channel, callback) {
|
|
205
|
+
if (!this._listeners.has(channel)) this._listeners.set(channel, /* @__PURE__ */ new Set());
|
|
206
|
+
this._listeners.get(channel).add(callback);
|
|
207
|
+
return { unsubscribe: () => {
|
|
208
|
+
this._listeners.get(channel).delete(callback);
|
|
209
|
+
} };
|
|
210
|
+
}
|
|
211
|
+
_callListeners(message) {
|
|
212
|
+
if (this._listeners.has(message.channelId)) this._listeners.get(message.channelId).forEach((callback) => {
|
|
213
|
+
callback(message);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
_propagateAnnounce(message) {
|
|
217
|
+
return Promise.all([...this._connections.entries()].map(async ([remotePeerId, extension]) => {
|
|
218
|
+
if (this._params.localPeerId.equals(message.peerId) || remotePeerId.equals(message.peerId)) return;
|
|
219
|
+
return this._sendAnnounceWithTimeoutTracking(extension, message).catch((err) => log(err, void 0, {
|
|
220
|
+
"~LogMeta": "~LogMeta",
|
|
221
|
+
F: __dxlog_file$1,
|
|
222
|
+
L: 154,
|
|
223
|
+
S: this
|
|
224
|
+
}));
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
_performGc() {
|
|
228
|
+
const start = performance.now();
|
|
229
|
+
for (const key of this._toClear.keys()) this._receivedMessages.delete(key);
|
|
230
|
+
this._toClear.clear();
|
|
231
|
+
for (const key of this._receivedMessages.keys()) this._toClear.add(key);
|
|
232
|
+
const elapsed = performance.now() - start;
|
|
233
|
+
if (elapsed > 100) log.warn("GC took too long", { elapsed }, {
|
|
234
|
+
"~LogMeta": "~LogMeta",
|
|
235
|
+
F: __dxlog_file$1,
|
|
236
|
+
L: 172,
|
|
237
|
+
S: this
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
_sendAnnounceWithTimeoutTracking(extension, message) {
|
|
241
|
+
return extension.sendAnnounce(message).catch((err) => {});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/presence.ts
|
|
246
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/core/mesh/teleport-extension-gossip/src/presence.ts";
|
|
247
|
+
var PRESENCE_CHANNEL_ID = "dxos.mesh.presence.Presence";
|
|
248
|
+
/**
|
|
249
|
+
* Presence manager.
|
|
250
|
+
* Keeps track of all peers that are connected to the local peer.
|
|
251
|
+
* Routes received presence announces to all connected peers.
|
|
252
|
+
* Exposes API to get the list of peers that are online.
|
|
253
|
+
*/
|
|
254
|
+
var Presence = class extends Resource {
|
|
255
|
+
_params;
|
|
256
|
+
updated = new Event();
|
|
257
|
+
newPeer = new Event();
|
|
258
|
+
_peerStates = new ComplexMap(PublicKey.hash);
|
|
259
|
+
_peersByIdentityKey = new ComplexMap(PublicKey.hash);
|
|
260
|
+
constructor(_params) {
|
|
261
|
+
super();
|
|
262
|
+
this._params = _params;
|
|
263
|
+
invariant(this._params.announceInterval < this._params.offlineTimeout, "Announce interval should be less than offline timeout.", {
|
|
264
|
+
"~LogMeta": "~LogMeta",
|
|
265
|
+
F: __dxlog_file,
|
|
266
|
+
L: 55,
|
|
267
|
+
S: this,
|
|
268
|
+
A: ["this._params.announceInterval < this._params.offlineTimeout", "'Announce interval should be less than offline timeout.'"]
|
|
269
|
+
});
|
|
270
|
+
this._params.gossip.listen(PRESENCE_CHANNEL_ID, (message) => {
|
|
271
|
+
this._receiveAnnounces(message);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async _open() {
|
|
275
|
+
scheduleTaskInterval(this._ctx, async () => {
|
|
276
|
+
const peerState = {
|
|
277
|
+
"@type": "dxos.mesh.presence.PeerState",
|
|
278
|
+
"identityKey": this._params.identityKey,
|
|
279
|
+
"connections": this._params.gossip.getConnections()
|
|
280
|
+
};
|
|
281
|
+
this._params.gossip.postMessage(PRESENCE_CHANNEL_ID, peerState);
|
|
282
|
+
}, this._params.announceInterval);
|
|
283
|
+
scheduleTaskInterval(this._ctx, async () => {
|
|
284
|
+
this.updated.emit();
|
|
285
|
+
}, this._params.offlineTimeout);
|
|
286
|
+
this._params.gossip.connectionClosed.on(this._ctx, (peerId) => {
|
|
287
|
+
const peerState = this._peerStates.get(peerId);
|
|
288
|
+
if (peerState != null) {
|
|
289
|
+
this._peerStates.delete(peerId);
|
|
290
|
+
this._removePeerFromIdentityKeyIndex(peerState);
|
|
291
|
+
this.updated.emit();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async _catch(err) {
|
|
296
|
+
log.catch(err, void 0, {
|
|
297
|
+
"~LogMeta": "~LogMeta",
|
|
298
|
+
F: __dxlog_file,
|
|
299
|
+
L: 101,
|
|
300
|
+
S: this
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
getPeers() {
|
|
304
|
+
return Array.from(this._peerStates.values()).map((message) => message.payload);
|
|
305
|
+
}
|
|
306
|
+
getPeersByIdentityKey(key) {
|
|
307
|
+
return (this._peersByIdentityKey.get(key) ?? []).filter(this._isOnline).map((m) => m.payload);
|
|
308
|
+
}
|
|
309
|
+
getPeersOnline() {
|
|
310
|
+
return Array.from(this._peerStates.values()).filter(this._isOnline).map((message) => message.payload);
|
|
311
|
+
}
|
|
312
|
+
_isOnline = (message) => {
|
|
313
|
+
return message.timestamp.getTime() > Date.now() - this._params.offlineTimeout;
|
|
314
|
+
};
|
|
315
|
+
getLocalState() {
|
|
316
|
+
return {
|
|
317
|
+
identityKey: this._params.identityKey,
|
|
318
|
+
connections: this._params.gossip.getConnections(),
|
|
319
|
+
peerId: this._params.gossip.localPeerId
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
_receiveAnnounces(message) {
|
|
323
|
+
invariant(message.channelId === PRESENCE_CHANNEL_ID, `Invalid channel ID: ${message.channelId}`, {
|
|
324
|
+
"~LogMeta": "~LogMeta",
|
|
325
|
+
F: __dxlog_file,
|
|
326
|
+
L: 131,
|
|
327
|
+
S: this,
|
|
328
|
+
A: ["message.channelId === PRESENCE_CHANNEL_ID", "`Invalid channel ID: ${message.channelId}`"]
|
|
329
|
+
});
|
|
330
|
+
const oldPeerState = this._peerStates.get(message.peerId);
|
|
331
|
+
if (!oldPeerState || oldPeerState.timestamp.getTime() < message.timestamp.getTime()) {
|
|
332
|
+
message.payload.peerId = message.peerId;
|
|
333
|
+
this._peerStates.set(message.peerId, message);
|
|
334
|
+
this._updatePeerInIdentityKeyIndex(message);
|
|
335
|
+
this.updated.emit();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
_removePeerFromIdentityKeyIndex(peerState) {
|
|
339
|
+
const identityPeerList = this._peersByIdentityKey.get(peerState.payload.identityKey) ?? [];
|
|
340
|
+
const peerIdIndex = identityPeerList.findIndex((id) => id.peerId?.equals(peerState.peerId));
|
|
341
|
+
if (peerIdIndex >= 0) identityPeerList.splice(peerIdIndex, 1);
|
|
342
|
+
}
|
|
343
|
+
_updatePeerInIdentityKeyIndex(newState) {
|
|
344
|
+
const identityKey = newState.payload.identityKey;
|
|
345
|
+
const identityKeyPeers = this._peersByIdentityKey.get(identityKey) ?? [];
|
|
346
|
+
const existingIndex = identityKeyPeers.findIndex((p) => p.peerId && newState.peerId?.equals(p.peerId));
|
|
347
|
+
if (existingIndex >= 0) {
|
|
348
|
+
const oldState = identityKeyPeers.splice(existingIndex, 1, newState)[0];
|
|
349
|
+
if (!this._isOnline(oldState)) this.newPeer.emit(newState.payload);
|
|
350
|
+
} else {
|
|
351
|
+
this._peersByIdentityKey.set(identityKey, identityKeyPeers);
|
|
352
|
+
identityKeyPeers.push(newState);
|
|
353
|
+
this.newPeer.emit(newState.payload);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
//#endregion
|
|
358
|
+
export { Gossip, GossipExtension, Presence };
|
|
359
|
+
|
|
360
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/gossip-extension.ts","../../src/gossip.ts","../../src/presence.ts"],"sourcesContent":["//\n// Copyright 2022 DXOS.org\n//\n\nimport { Trigger } from '@dxos/async';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\nimport { schema } from '@dxos/protocols/proto';\nimport { type GossipMessage, type GossipService } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';\nimport { type ProtoRpcPeer, createProtoRpcPeer } from '@dxos/rpc';\nimport { type ExtensionContext, type TeleportExtension } from '@dxos/teleport';\n\nexport type GossipCallbacks = {\n /**\n * Callback to be called when a new announce is received.\n */\n onAnnounce?: (message: GossipMessage) => Promise<void>;\n\n /**\n * Callback to be called when the extension is closed.\n */\n onClose?: (err?: Error) => Promise<void>;\n};\n\n/**\n * Sends announces between two peers for a single teleport session.\n */\nexport class GossipExtension implements TeleportExtension {\n private readonly _opened = new Trigger();\n private _closed = false;\n\n private _rpc?: ProtoRpcPeer<ServiceBundle>;\n\n constructor(private readonly _callbacks: GossipCallbacks = {}) {}\n\n async onOpen(context: ExtensionContext): Promise<void> {\n log('onOpen', { localPeerId: context.localPeerId, remotePeerId: context.remotePeerId });\n\n this._rpc = createProtoRpcPeer<ServiceBundle, ServiceBundle>({\n requested: {\n GossipService: schema.getService('dxos.mesh.teleport.gossip.GossipService'),\n },\n exposed: {\n GossipService: schema.getService('dxos.mesh.teleport.gossip.GossipService'),\n },\n handlers: {\n GossipService: {\n announce: async (message: GossipMessage) => {\n log('received announce', { localPeerId: context.localPeerId, remotePeerId: context.remotePeerId, message });\n await this._callbacks.onAnnounce?.(message);\n },\n },\n },\n port: await context.createPort('rpc', {\n contentType: 'application/x-protobuf; messageType=\"dxos.rpc.Message\"',\n }),\n });\n await this._rpc.open();\n this._opened.wake();\n }\n\n async onClose(err?: Error): Promise<void> {\n log('close', { err });\n await this._rpc?.close();\n await this._callbacks.onClose?.(err);\n this._closed = true;\n }\n\n async onAbort(err?: Error): Promise<void> {\n log('abort', { err });\n try {\n await this._rpc?.abort();\n } catch (err) {\n log.catch(err);\n } finally {\n await this._callbacks.onClose?.(err);\n }\n this._closed = true;\n }\n\n async sendAnnounce(message: GossipMessage): Promise<void> {\n if (this._closed) {\n return;\n }\n await this._opened.wait();\n invariant(this._rpc, 'RPC not initialized');\n await this._rpc.rpc.GossipService.announce(message);\n }\n}\n\ntype ServiceBundle = {\n GossipService: GossipService;\n};\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport { Event, scheduleTask, scheduleTaskInterval } from '@dxos/async';\nimport { Context } from '@dxos/context';\nimport { PublicKey } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport { RpcClosedError, TimeoutError } from '@dxos/protocols';\nimport { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';\nimport { ComplexMap, ComplexSet } from '@dxos/util';\n\nimport { GossipExtension } from './gossip-extension';\n\nexport type GossipProps = {\n localPeerId: PublicKey;\n};\n\nconst RECEIVED_MESSAGES_GC_INTERVAL = 120_000;\n\nconst MAX_CTX_TASKS = 50;\n\n/**\n * Gossip extensions manager.\n * Keeps track of all peers that are connected to the local peer.\n * Routes received announces to all connected peers.\n * Exposes API send announce to everybody and subscribe to .\n */\nexport class Gossip {\n private readonly _ctx = new Context({\n onError: (err) => {\n log.catch(err);\n },\n });\n\n private readonly _listeners = new Map<string, Set<(message: GossipMessage) => void>>();\n\n private readonly _receivedMessages = new ComplexSet<PublicKey>(PublicKey.hash);\n\n /**\n * Keys scheduled to be cleared from _receivedMessages on the next iteration.\n */\n private readonly _toClear = new ComplexSet<PublicKey>(PublicKey.hash);\n\n // remotePeerId -> PresenceExtension\n private readonly _connections = new ComplexMap<PublicKey, GossipExtension>(PublicKey.hash);\n\n public readonly connectionClosed = new Event<PublicKey>();\n\n constructor(private readonly _params: GossipProps) {}\n\n get localPeerId() {\n return this._params.localPeerId;\n }\n\n async open(): Promise<void> {\n // Clear the map periodically.\n scheduleTaskInterval(\n this._ctx,\n async () => {\n this._performGc();\n },\n RECEIVED_MESSAGES_GC_INTERVAL,\n );\n }\n\n async close(): Promise<void> {\n await this._ctx.dispose();\n }\n\n getConnections() {\n return Array.from(this._connections.keys());\n }\n\n createExtension({ remotePeerId }: { remotePeerId: PublicKey }): GossipExtension {\n const extension = new GossipExtension({\n onAnnounce: async (message) => {\n if (this._receivedMessages.has(message.messageId)) {\n return;\n }\n this._receivedMessages.add(message.messageId);\n this._callListeners(message);\n if (this._ctx.disposeCallbacksLength > MAX_CTX_TASKS) {\n log(`skipping propagating gossip message due to exessive tasks (${MAX_CTX_TASKS})`);\n return;\n }\n scheduleTask(this._ctx, async () => {\n await this._propagateAnnounce(message);\n });\n },\n onClose: async (err) => {\n if (this._connections.has(remotePeerId)) {\n this._connections.delete(remotePeerId);\n }\n this.connectionClosed.emit(remotePeerId);\n },\n });\n this._connections.set(remotePeerId, extension);\n\n return extension;\n }\n\n postMessage(channel: string, payload: any): void {\n for (const extension of this._connections.values()) {\n this._sendAnnounceWithTimeoutTracking(extension, {\n peerId: this._params.localPeerId,\n messageId: PublicKey.random(),\n channelId: channel,\n timestamp: new Date(),\n payload,\n }).catch(async (err) => {\n if (err instanceof RpcClosedError) {\n log('sendAnnounce failed because of RpcClosedError', { err });\n } else if (\n err instanceof TimeoutError ||\n err.constructor.name === 'TimeoutError' ||\n err.message.startsWith('Timeout')\n ) {\n log('sendAnnounce failed because of TimeoutError', { err });\n } else {\n log.catch(err);\n }\n });\n }\n }\n\n listen(channel: string, callback: (message: GossipMessage) => void): { unsubscribe: () => void } {\n if (!this._listeners.has(channel)) {\n this._listeners.set(channel, new Set());\n }\n this._listeners.get(channel)!.add(callback);\n\n return {\n unsubscribe: () => {\n this._listeners.get(channel)!.delete(callback);\n },\n };\n }\n\n private _callListeners(message: GossipMessage): void {\n if (this._listeners.has(message.channelId)) {\n this._listeners.get(message.channelId)!.forEach((callback) => {\n callback(message);\n });\n }\n }\n\n private _propagateAnnounce(message: GossipMessage): Promise<void[]> {\n return Promise.all(\n [...this._connections.entries()].map(async ([remotePeerId, extension]) => {\n if (this._params.localPeerId.equals(message.peerId) || remotePeerId.equals(message.peerId)) {\n return;\n }\n return this._sendAnnounceWithTimeoutTracking(extension, message).catch((err) => log(err));\n }),\n );\n }\n\n private _performGc(): void {\n const start = performance.now();\n\n for (const key of this._toClear.keys()) {\n this._receivedMessages.delete(key);\n }\n this._toClear.clear();\n for (const key of this._receivedMessages.keys()) {\n this._toClear.add(key);\n }\n\n const elapsed = performance.now() - start;\n if (elapsed > 100) {\n log.warn('GC took too long', { elapsed });\n }\n }\n\n private _sendAnnounceWithTimeoutTracking(extension: GossipExtension, message: GossipMessage): Promise<void> {\n return extension.sendAnnounce(message).catch((err) => {\n // Noop?\n });\n }\n}\n","//\n// Copyright 2022 DXOS.org\n//\n\nimport { Event, scheduleTaskInterval } from '@dxos/async';\nimport { type WithTypeUrl } from '@dxos/codec-protobuf';\nimport { Resource } from '@dxos/context';\nimport { invariant } from '@dxos/invariant';\nimport { PublicKey } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';\nimport { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';\nimport { ComplexMap } from '@dxos/util';\n\nimport { type Gossip } from './gossip';\n\nexport type PresenceProps = {\n /**\n * Interval between presence announces.\n */\n announceInterval: number;\n /**\n * Timeout after which a peer is considered offline.\n * Should be greater than announceInterval.\n */\n offlineTimeout: number;\n\n /**\n * Identity key of the local peer.\n */\n identityKey: PublicKey; // TODO(mykola): Remove once IdentityKey can be obtained from DeviceKey.\n\n gossip: Gossip;\n};\n\nconst PRESENCE_CHANNEL_ID = 'dxos.mesh.presence.Presence';\n\n/**\n * Presence manager.\n * Keeps track of all peers that are connected to the local peer.\n * Routes received presence announces to all connected peers.\n * Exposes API to get the list of peers that are online.\n */\nexport class Presence extends Resource {\n public readonly updated = new Event<void>();\n public readonly newPeer = new Event<PeerState>();\n\n private readonly _peerStates = new ComplexMap<PublicKey, GossipMessage>(PublicKey.hash);\n private readonly _peersByIdentityKey = new ComplexMap<PublicKey, GossipMessage[]>(PublicKey.hash);\n\n // remotePeerId -> PresenceExtension\n\n constructor(private readonly _params: PresenceProps) {\n super();\n invariant(\n this._params.announceInterval < this._params.offlineTimeout,\n 'Announce interval should be less than offline timeout.',\n );\n\n this._params.gossip.listen(PRESENCE_CHANNEL_ID, (message) => {\n this._receiveAnnounces(message);\n });\n }\n\n protected override async _open(): Promise<void> {\n // Send announce to all connected peers.\n scheduleTaskInterval(\n this._ctx,\n async () => {\n const peerState: WithTypeUrl<PeerState> = {\n '@type': 'dxos.mesh.presence.PeerState',\n 'identityKey': this._params.identityKey,\n 'connections': this._params.gossip.getConnections(),\n };\n this._params.gossip.postMessage(PRESENCE_CHANNEL_ID, peerState);\n },\n this._params.announceInterval,\n );\n\n // Emit updated event in case some peers went offline.\n scheduleTaskInterval(\n this._ctx,\n async () => {\n this.updated.emit();\n },\n this._params.offlineTimeout,\n );\n\n // Remove peer state when connection is closed.\n this._params.gossip.connectionClosed.on(this._ctx, (peerId) => {\n const peerState = this._peerStates.get(peerId);\n if (peerState != null) {\n this._peerStates.delete(peerId);\n this._removePeerFromIdentityKeyIndex(peerState);\n this.updated.emit();\n }\n });\n }\n\n protected override async _catch(err: Error): Promise<void> {\n log.catch(err);\n }\n\n getPeers(): PeerState[] {\n return Array.from(this._peerStates.values()).map((message) => message.payload);\n }\n\n getPeersByIdentityKey(key: PublicKey): PeerState[] {\n return (this._peersByIdentityKey.get(key) ?? []).filter(this._isOnline).map((m) => m.payload);\n }\n\n getPeersOnline(): PeerState[] {\n return Array.from(this._peerStates.values())\n .filter(this._isOnline)\n .map((message) => message.payload);\n }\n\n private _isOnline = (message: GossipMessage): boolean => {\n return message.timestamp.getTime() > Date.now() - this._params.offlineTimeout;\n };\n\n getLocalState(): PeerState {\n return {\n identityKey: this._params.identityKey,\n connections: this._params.gossip.getConnections(),\n peerId: this._params.gossip.localPeerId,\n };\n }\n\n private _receiveAnnounces(message: GossipMessage): void {\n invariant(message.channelId === PRESENCE_CHANNEL_ID, `Invalid channel ID: ${message.channelId}`);\n const oldPeerState = this._peerStates.get(message.peerId);\n if (!oldPeerState || oldPeerState.timestamp.getTime() < message.timestamp.getTime()) {\n // Assign peer id to payload.\n (message.payload as PeerState).peerId = message.peerId;\n\n this._peerStates.set(message.peerId, message);\n this._updatePeerInIdentityKeyIndex(message);\n this.updated.emit();\n }\n }\n\n private _removePeerFromIdentityKeyIndex(peerState: GossipMessage): void {\n const identityPeerList = this._peersByIdentityKey.get((peerState.payload as PeerState).identityKey) ?? [];\n const peerIdIndex = identityPeerList.findIndex((id) => id.peerId?.equals(peerState.peerId));\n if (peerIdIndex >= 0) {\n identityPeerList.splice(peerIdIndex, 1);\n }\n }\n\n private _updatePeerInIdentityKeyIndex(newState: GossipMessage): void {\n const identityKey = (newState.payload as PeerState).identityKey;\n const identityKeyPeers = this._peersByIdentityKey.get(identityKey) ?? [];\n const existingIndex = identityKeyPeers.findIndex((p) => p.peerId && newState.peerId?.equals(p.peerId));\n if (existingIndex >= 0) {\n const oldState = identityKeyPeers.splice(existingIndex, 1, newState)[0];\n if (!this._isOnline(oldState)) {\n this.newPeer.emit(newState.payload);\n }\n } else {\n this._peersByIdentityKey.set(identityKey, identityKeyPeers);\n identityKeyPeers.push(newState);\n this.newPeer.emit(newState.payload);\n }\n }\n}\n"],"mappings":"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"}
|