@johnhenry/browsermesh-transport 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +31 -0
- package/src/channel-relay.mjs +225 -0
- package/src/cross-origin.mjs +543 -0
- package/src/gateway.mjs +627 -0
- package/src/index.mjs +12 -0
- package/src/relay.mjs +653 -0
- package/src/silent-catch.mjs +55 -0
- package/src/streams.mjs +627 -0
- package/src/transport.mjs +357 -0
- package/src/webrtc.mjs +773 -0
- package/src/websocket.mjs +1082 -0
- package/src/webtransport.mjs +216 -0
- package/src/wisp-client.mjs +747 -0
- package/src/wisp.mjs +348 -0
- package/src/wsh-bridge.mjs +242 -0
package/src/gateway.mjs
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
// STATUS: INTEGRATED — wired into ClawserPod lifecycle, proven via E2E testing
|
|
3
|
+
* clawser-mesh-gateway.js -- Gateway Node for BrowserMesh.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around relay functionality providing multi-hop routing,
|
|
6
|
+
* route advertisement, and gateway discovery for the mesh network.
|
|
7
|
+
*
|
|
8
|
+
* GatewayRoute represents a single route between two pods via a gateway.
|
|
9
|
+
* RouteTable manages a collection of routes with TTL expiration.
|
|
10
|
+
* GatewayNode orchestrates peer registration, route management, and relay.
|
|
11
|
+
* GatewayDiscovery tracks available gateways for destination selection.
|
|
12
|
+
*
|
|
13
|
+
* No browser-only imports at module level.
|
|
14
|
+
*
|
|
15
|
+
* Run tests:
|
|
16
|
+
* node --import ./web/test/_setup-globals.mjs --test web/test/clawser-mesh-gateway.test.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Wire Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Gateway announcement message type. */
|
|
24
|
+
const GATEWAY_ANNOUNCE = 0xA0;
|
|
25
|
+
|
|
26
|
+
/** Gateway route advertisement message type. */
|
|
27
|
+
const GATEWAY_ROUTE = 0xA1;
|
|
28
|
+
|
|
29
|
+
/** Gateway relay request message type. */
|
|
30
|
+
const GATEWAY_RELAY = 0xA2;
|
|
31
|
+
|
|
32
|
+
/** Gateway route withdrawal message type. */
|
|
33
|
+
const GATEWAY_WITHDRAW = 0xA3;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// GatewayRoute
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A single route between two pods via a gateway node.
|
|
41
|
+
*/
|
|
42
|
+
export class GatewayRoute {
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {string} opts.fromPodId - Source pod identifier
|
|
46
|
+
* @param {string} opts.toPodId - Destination pod identifier
|
|
47
|
+
* @param {string} opts.viaGateway - Gateway pod that forwarded this route
|
|
48
|
+
* @param {number} opts.hopCount - Number of hops along this route
|
|
49
|
+
* @param {number} [opts.latencyMs] - Estimated latency in milliseconds
|
|
50
|
+
* @param {number} [opts.createdAt] - Unix timestamp (ms) when route was created
|
|
51
|
+
* @param {number} [opts.ttl] - Time-to-live in milliseconds
|
|
52
|
+
*/
|
|
53
|
+
constructor({
|
|
54
|
+
fromPodId,
|
|
55
|
+
toPodId,
|
|
56
|
+
viaGateway,
|
|
57
|
+
hopCount,
|
|
58
|
+
latencyMs = null,
|
|
59
|
+
createdAt = Date.now(),
|
|
60
|
+
ttl = 60_000,
|
|
61
|
+
}) {
|
|
62
|
+
if (!fromPodId || typeof fromPodId !== 'string') {
|
|
63
|
+
throw new Error('fromPodId is required and must be a non-empty string');
|
|
64
|
+
}
|
|
65
|
+
if (!toPodId || typeof toPodId !== 'string') {
|
|
66
|
+
throw new Error('toPodId is required and must be a non-empty string');
|
|
67
|
+
}
|
|
68
|
+
if (!viaGateway || typeof viaGateway !== 'string') {
|
|
69
|
+
throw new Error('viaGateway is required and must be a non-empty string');
|
|
70
|
+
}
|
|
71
|
+
if (typeof hopCount !== 'number' || hopCount < 0) {
|
|
72
|
+
throw new Error('hopCount must be a non-negative number');
|
|
73
|
+
}
|
|
74
|
+
this.fromPodId = fromPodId;
|
|
75
|
+
this.toPodId = toPodId;
|
|
76
|
+
this.viaGateway = viaGateway;
|
|
77
|
+
this.hopCount = hopCount;
|
|
78
|
+
this.latencyMs = latencyMs;
|
|
79
|
+
this.createdAt = createdAt;
|
|
80
|
+
this.ttl = ttl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check whether this route has expired.
|
|
85
|
+
*
|
|
86
|
+
* @param {number} [now=Date.now()] - Current timestamp in ms
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
isExpired(now = Date.now()) {
|
|
90
|
+
return now >= this.createdAt + this.ttl;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Serialize to a JSON-safe object.
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
toJSON() {
|
|
98
|
+
return {
|
|
99
|
+
fromPodId: this.fromPodId,
|
|
100
|
+
toPodId: this.toPodId,
|
|
101
|
+
viaGateway: this.viaGateway,
|
|
102
|
+
hopCount: this.hopCount,
|
|
103
|
+
latencyMs: this.latencyMs,
|
|
104
|
+
createdAt: this.createdAt,
|
|
105
|
+
ttl: this.ttl,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-hydrate from a plain object.
|
|
111
|
+
* @param {object} data
|
|
112
|
+
* @returns {GatewayRoute}
|
|
113
|
+
*/
|
|
114
|
+
static fromJSON(data) {
|
|
115
|
+
return new GatewayRoute(data);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// RouteTable
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collection of gateway routes with TTL-based expiration.
|
|
125
|
+
*/
|
|
126
|
+
export class RouteTable {
|
|
127
|
+
/** @type {Map<string, GatewayRoute>} key -> route */
|
|
128
|
+
#routes = new Map();
|
|
129
|
+
|
|
130
|
+
/** @type {number} */
|
|
131
|
+
#maxRoutes;
|
|
132
|
+
|
|
133
|
+
/** @type {number} */
|
|
134
|
+
#ttlMs;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @param {number} [opts.maxRoutes=1000] - Maximum number of routes to store
|
|
139
|
+
* @param {number} [opts.ttlMs=60000] - Default TTL for routes in ms
|
|
140
|
+
*/
|
|
141
|
+
constructor(opts = {}) {
|
|
142
|
+
this.#maxRoutes = opts.maxRoutes ?? 1000;
|
|
143
|
+
this.#ttlMs = opts.ttlMs ?? 60_000;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate a composite key for a route.
|
|
148
|
+
* @param {string} fromPodId
|
|
149
|
+
* @param {string} toPodId
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
static #key(fromPodId, toPodId) {
|
|
153
|
+
return `${fromPodId}:${toPodId}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add or replace a route.
|
|
158
|
+
* If the table is full, the oldest route is evicted.
|
|
159
|
+
*
|
|
160
|
+
* @param {GatewayRoute} route
|
|
161
|
+
*/
|
|
162
|
+
addRoute(route) {
|
|
163
|
+
const key = RouteTable.#key(route.fromPodId, route.toPodId);
|
|
164
|
+
if (!this.#routes.has(key) && this.#routes.size >= this.#maxRoutes) {
|
|
165
|
+
// Evict the oldest route
|
|
166
|
+
const oldest = this.#routes.keys().next().value;
|
|
167
|
+
this.#routes.delete(oldest);
|
|
168
|
+
}
|
|
169
|
+
this.#routes.set(key, route);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Remove a specific route.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} fromPodId
|
|
176
|
+
* @param {string} toPodId
|
|
177
|
+
* @returns {boolean} true if the route existed
|
|
178
|
+
*/
|
|
179
|
+
removeRoute(fromPodId, toPodId) {
|
|
180
|
+
return this.#routes.delete(RouteTable.#key(fromPodId, toPodId));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find a specific route.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} fromPodId
|
|
187
|
+
* @param {string} toPodId
|
|
188
|
+
* @returns {GatewayRoute|null}
|
|
189
|
+
*/
|
|
190
|
+
findRoute(fromPodId, toPodId) {
|
|
191
|
+
return this.#routes.get(RouteTable.#key(fromPodId, toPodId)) ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Find all routes to a destination, sorted by hopCount ascending.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} toPodId
|
|
198
|
+
* @returns {GatewayRoute[]}
|
|
199
|
+
*/
|
|
200
|
+
findRoutes(toPodId) {
|
|
201
|
+
const matches = [];
|
|
202
|
+
for (const route of this.#routes.values()) {
|
|
203
|
+
if (route.toPodId === toPodId) {
|
|
204
|
+
matches.push(route);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
matches.sort((a, b) => a.hopCount - b.hopCount);
|
|
208
|
+
return matches;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove all expired routes.
|
|
213
|
+
*
|
|
214
|
+
* @returns {number} number of routes pruned
|
|
215
|
+
*/
|
|
216
|
+
pruneExpired() {
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
let count = 0;
|
|
219
|
+
for (const [key, route] of this.#routes) {
|
|
220
|
+
if (route.isExpired(now)) {
|
|
221
|
+
this.#routes.delete(key);
|
|
222
|
+
count++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @returns {number} current number of routes */
|
|
229
|
+
get size() {
|
|
230
|
+
return this.#routes.size;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** @returns {number} configured default TTL */
|
|
234
|
+
get defaultTtl() {
|
|
235
|
+
return this.#ttlMs;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* List all routes.
|
|
240
|
+
* @returns {GatewayRoute[]}
|
|
241
|
+
*/
|
|
242
|
+
listAll() {
|
|
243
|
+
return [...this.#routes.values()];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Serialize to a JSON-safe object.
|
|
248
|
+
* @returns {object}
|
|
249
|
+
*/
|
|
250
|
+
toJSON() {
|
|
251
|
+
return {
|
|
252
|
+
maxRoutes: this.#maxRoutes,
|
|
253
|
+
ttlMs: this.#ttlMs,
|
|
254
|
+
routes: [...this.#routes.values()].map(r => r.toJSON()),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Re-hydrate from serialized data.
|
|
260
|
+
* @param {object} data
|
|
261
|
+
* @returns {RouteTable}
|
|
262
|
+
*/
|
|
263
|
+
static fromJSON(data) {
|
|
264
|
+
const table = new RouteTable({
|
|
265
|
+
maxRoutes: data.maxRoutes,
|
|
266
|
+
ttlMs: data.ttlMs,
|
|
267
|
+
});
|
|
268
|
+
for (const r of data.routes || []) {
|
|
269
|
+
table.addRoute(GatewayRoute.fromJSON(r));
|
|
270
|
+
}
|
|
271
|
+
return table;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// GatewayNode
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Orchestrates peer registration, route management, and payload relay
|
|
281
|
+
* for a single gateway node in the mesh.
|
|
282
|
+
*/
|
|
283
|
+
export class GatewayNode {
|
|
284
|
+
/** @type {string} */
|
|
285
|
+
#localPodId;
|
|
286
|
+
|
|
287
|
+
/** @type {Set<string>} connected peer pod IDs */
|
|
288
|
+
#peers = new Set();
|
|
289
|
+
|
|
290
|
+
/** @type {RouteTable} */
|
|
291
|
+
#routeTable;
|
|
292
|
+
|
|
293
|
+
/** @type {number} */
|
|
294
|
+
#maxConnections;
|
|
295
|
+
|
|
296
|
+
/** @type {number} */
|
|
297
|
+
#maxHops;
|
|
298
|
+
|
|
299
|
+
/** @type {boolean} */
|
|
300
|
+
#allowRelay;
|
|
301
|
+
|
|
302
|
+
/** @type {number} total relay operations performed */
|
|
303
|
+
#relayCount = 0;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {string} localPodId - This gateway's pod identifier
|
|
307
|
+
* @param {object} [opts]
|
|
308
|
+
* @param {number} [opts.maxConnections=64] - Maximum peer connections
|
|
309
|
+
* @param {number} [opts.maxHops=8] - Maximum hop count for routes
|
|
310
|
+
* @param {boolean} [opts.allowRelay=true] - Whether relay is enabled
|
|
311
|
+
*/
|
|
312
|
+
constructor(localPodId, opts = {}) {
|
|
313
|
+
if (!localPodId || typeof localPodId !== 'string') {
|
|
314
|
+
throw new Error('localPodId is required and must be a non-empty string');
|
|
315
|
+
}
|
|
316
|
+
this.#localPodId = localPodId;
|
|
317
|
+
this.#maxConnections = opts.maxConnections ?? 64;
|
|
318
|
+
this.#maxHops = opts.maxHops ?? 8;
|
|
319
|
+
this.#allowRelay = opts.allowRelay !== undefined ? opts.allowRelay : true;
|
|
320
|
+
this.#routeTable = new RouteTable();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// -- Accessors ------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
/** This gateway's pod identifier. */
|
|
326
|
+
get localPodId() {
|
|
327
|
+
return this.#localPodId;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Whether relay is enabled. */
|
|
331
|
+
get isRelayEnabled() {
|
|
332
|
+
return this.#allowRelay;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Set of connected peer pod IDs. */
|
|
336
|
+
get connectedPeers() {
|
|
337
|
+
return new Set(this.#peers);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The underlying route table. */
|
|
341
|
+
get routeTable() {
|
|
342
|
+
return this.#routeTable;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// -- Peer Management ------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Register a directly connected peer.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} podId
|
|
351
|
+
*/
|
|
352
|
+
registerPeer(podId) {
|
|
353
|
+
if (!podId || typeof podId !== 'string') {
|
|
354
|
+
throw new Error('podId is required and must be a non-empty string');
|
|
355
|
+
}
|
|
356
|
+
if (this.#peers.size >= this.#maxConnections && !this.#peers.has(podId)) {
|
|
357
|
+
throw new Error('Maximum connections reached');
|
|
358
|
+
}
|
|
359
|
+
this.#peers.add(podId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Unregister a peer connection.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} podId
|
|
366
|
+
* @returns {boolean} true if the peer existed
|
|
367
|
+
*/
|
|
368
|
+
unregisterPeer(podId) {
|
|
369
|
+
return this.#peers.delete(podId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// -- Routing --------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if a route exists between two pods.
|
|
376
|
+
* A route exists if both pods are directly connected peers, or if
|
|
377
|
+
* an explicit route is registered in the route table.
|
|
378
|
+
*
|
|
379
|
+
* @param {string} fromPodId
|
|
380
|
+
* @param {string} toPodId
|
|
381
|
+
* @returns {boolean}
|
|
382
|
+
*/
|
|
383
|
+
canRoute(fromPodId, toPodId) {
|
|
384
|
+
// Direct connection: both peers are registered
|
|
385
|
+
if (this.#peers.has(fromPodId) && this.#peers.has(toPodId)) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
// Explicit route in the table
|
|
389
|
+
return this.#routeTable.findRoute(fromPodId, toPodId) !== null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Find the best route (lowest hop count) between two pods.
|
|
394
|
+
* Returns null if no route exists.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} fromPodId
|
|
397
|
+
* @param {string} toPodId
|
|
398
|
+
* @returns {GatewayRoute|null}
|
|
399
|
+
*/
|
|
400
|
+
findBestRoute(fromPodId, toPodId) {
|
|
401
|
+
// Check for direct route in table first
|
|
402
|
+
const direct = this.#routeTable.findRoute(fromPodId, toPodId);
|
|
403
|
+
if (direct) return direct;
|
|
404
|
+
|
|
405
|
+
// If both are directly connected peers, synthesize a 1-hop route
|
|
406
|
+
if (this.#peers.has(fromPodId) && this.#peers.has(toPodId)) {
|
|
407
|
+
return new GatewayRoute({
|
|
408
|
+
fromPodId,
|
|
409
|
+
toPodId,
|
|
410
|
+
viaGateway: this.#localPodId,
|
|
411
|
+
hopCount: 1,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Advertise a route through this gateway.
|
|
420
|
+
* Rejects routes exceeding maxHops.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} fromPodId
|
|
423
|
+
* @param {string} toPodId
|
|
424
|
+
* @param {number} hopCount
|
|
425
|
+
* @returns {GatewayRoute}
|
|
426
|
+
*/
|
|
427
|
+
advertiseRoute(fromPodId, toPodId, hopCount) {
|
|
428
|
+
if (hopCount > this.#maxHops) {
|
|
429
|
+
throw new Error(`hopCount ${hopCount} exceeds maxHops ${this.#maxHops}`);
|
|
430
|
+
}
|
|
431
|
+
const route = new GatewayRoute({
|
|
432
|
+
fromPodId,
|
|
433
|
+
toPodId,
|
|
434
|
+
viaGateway: this.#localPodId,
|
|
435
|
+
hopCount,
|
|
436
|
+
ttl: this.#routeTable.defaultTtl,
|
|
437
|
+
});
|
|
438
|
+
this.#routeTable.addRoute(route);
|
|
439
|
+
return route;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Revoke (remove) an advertised route.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} fromPodId
|
|
446
|
+
* @param {string} toPodId
|
|
447
|
+
* @returns {boolean} true if a route was removed
|
|
448
|
+
*/
|
|
449
|
+
revokeRoute(fromPodId, toPodId) {
|
|
450
|
+
return this.#routeTable.removeRoute(fromPodId, toPodId);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Relay a payload from one pod to another.
|
|
455
|
+
* Returns a result object indicating success or failure.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} fromPodId
|
|
458
|
+
* @param {string} toPodId
|
|
459
|
+
* @param {*} payload
|
|
460
|
+
* @returns {{ relayed: boolean, route?: GatewayRoute, error?: string }}
|
|
461
|
+
*/
|
|
462
|
+
relay(fromPodId, toPodId, payload) {
|
|
463
|
+
if (!this.#allowRelay) {
|
|
464
|
+
return { relayed: false, error: 'Relay is disabled on this gateway' };
|
|
465
|
+
}
|
|
466
|
+
const route = this.findBestRoute(fromPodId, toPodId);
|
|
467
|
+
if (!route) {
|
|
468
|
+
return { relayed: false, error: `No route from ${fromPodId} to ${toPodId}` };
|
|
469
|
+
}
|
|
470
|
+
this.#relayCount++;
|
|
471
|
+
return { relayed: true, route };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// -- Statistics ------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get aggregate gateway statistics.
|
|
478
|
+
* @returns {{ routeCount: number, peerCount: number, relayCount: number }}
|
|
479
|
+
*/
|
|
480
|
+
get stats() {
|
|
481
|
+
return {
|
|
482
|
+
routeCount: this.#routeTable.size,
|
|
483
|
+
peerCount: this.#peers.size,
|
|
484
|
+
relayCount: this.#relayCount,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// -- Serialization --------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Serialize to a JSON-safe object.
|
|
492
|
+
* @returns {object}
|
|
493
|
+
*/
|
|
494
|
+
toJSON() {
|
|
495
|
+
return {
|
|
496
|
+
localPodId: this.#localPodId,
|
|
497
|
+
maxConnections: this.#maxConnections,
|
|
498
|
+
maxHops: this.#maxHops,
|
|
499
|
+
allowRelay: this.#allowRelay,
|
|
500
|
+
peers: [...this.#peers],
|
|
501
|
+
routeTable: this.#routeTable.toJSON(),
|
|
502
|
+
relayCount: this.#relayCount,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Re-hydrate from serialized data.
|
|
508
|
+
* @param {object} data
|
|
509
|
+
* @returns {GatewayNode}
|
|
510
|
+
*/
|
|
511
|
+
static fromJSON(data) {
|
|
512
|
+
const node = new GatewayNode(data.localPodId, {
|
|
513
|
+
maxConnections: data.maxConnections,
|
|
514
|
+
maxHops: data.maxHops,
|
|
515
|
+
allowRelay: data.allowRelay,
|
|
516
|
+
});
|
|
517
|
+
for (const peerId of data.peers || []) {
|
|
518
|
+
node.registerPeer(peerId);
|
|
519
|
+
}
|
|
520
|
+
node.#routeTable = RouteTable.fromJSON(data.routeTable);
|
|
521
|
+
node.#relayCount = data.relayCount || 0;
|
|
522
|
+
return node;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// GatewayDiscovery
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Tracks available gateways in the mesh for destination selection.
|
|
532
|
+
*/
|
|
533
|
+
export class GatewayDiscovery {
|
|
534
|
+
/** @type {string} */
|
|
535
|
+
#localPodId;
|
|
536
|
+
|
|
537
|
+
/** @type {Map<string, { podId: string, capabilities: string[], addedAt: number }>} */
|
|
538
|
+
#gateways = new Map();
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* @param {string} localPodId - This node's pod identifier
|
|
542
|
+
*/
|
|
543
|
+
constructor(localPodId) {
|
|
544
|
+
if (!localPodId || typeof localPodId !== 'string') {
|
|
545
|
+
throw new Error('localPodId is required and must be a non-empty string');
|
|
546
|
+
}
|
|
547
|
+
this.#localPodId = localPodId;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Register a gateway node.
|
|
552
|
+
*
|
|
553
|
+
* @param {string} podId
|
|
554
|
+
* @param {string[]} [capabilities=[]]
|
|
555
|
+
*/
|
|
556
|
+
addGateway(podId, capabilities = []) {
|
|
557
|
+
if (!podId || typeof podId !== 'string') {
|
|
558
|
+
throw new Error('podId is required and must be a non-empty string');
|
|
559
|
+
}
|
|
560
|
+
this.#gateways.set(podId, {
|
|
561
|
+
podId,
|
|
562
|
+
capabilities: [...capabilities],
|
|
563
|
+
addedAt: Date.now(),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Remove a gateway.
|
|
569
|
+
*
|
|
570
|
+
* @param {string} podId
|
|
571
|
+
* @returns {boolean} true if the gateway existed
|
|
572
|
+
*/
|
|
573
|
+
removeGateway(podId) {
|
|
574
|
+
return this.#gateways.delete(podId);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* List all known gateways.
|
|
579
|
+
*
|
|
580
|
+
* @returns {Array<{ podId: string, capabilities: string[], addedAt: number }>}
|
|
581
|
+
*/
|
|
582
|
+
listGateways() {
|
|
583
|
+
return [...this.#gateways.values()].map(g => ({
|
|
584
|
+
podId: g.podId,
|
|
585
|
+
capabilities: [...g.capabilities],
|
|
586
|
+
addedAt: g.addedAt,
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Select the best gateway for reaching a destination.
|
|
592
|
+
* Returns the first gateway that isn't the local node and isn't the
|
|
593
|
+
* destination itself. Returns null if no suitable gateway exists.
|
|
594
|
+
*
|
|
595
|
+
* @param {string} toPodId
|
|
596
|
+
* @param {object} [opts]
|
|
597
|
+
* @param {string} [opts.requiredCapability] - Only consider gateways with this capability
|
|
598
|
+
* @returns {string|null} pod ID of the selected gateway, or null
|
|
599
|
+
*/
|
|
600
|
+
selectGateway(toPodId, opts = {}) {
|
|
601
|
+
for (const gw of this.#gateways.values()) {
|
|
602
|
+
if (gw.podId === this.#localPodId) continue;
|
|
603
|
+
if (gw.podId === toPodId) continue;
|
|
604
|
+
if (opts.requiredCapability && !gw.capabilities.includes(opts.requiredCapability)) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
return gw.podId;
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** @returns {number} number of known gateways */
|
|
613
|
+
get size() {
|
|
614
|
+
return this.#gateways.size;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Exports
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
export {
|
|
623
|
+
GATEWAY_ANNOUNCE,
|
|
624
|
+
GATEWAY_ROUTE,
|
|
625
|
+
GATEWAY_RELAY,
|
|
626
|
+
GATEWAY_WITHDRAW,
|
|
627
|
+
};
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// browsermesh-transport — Transport layer
|
|
2
|
+
export * from './transport.mjs';
|
|
3
|
+
export * from './websocket.mjs';
|
|
4
|
+
export * from './webrtc.mjs';
|
|
5
|
+
export * from './webtransport.mjs';
|
|
6
|
+
export * from './relay.mjs';
|
|
7
|
+
export * from './gateway.mjs';
|
|
8
|
+
export * from './streams.mjs';
|
|
9
|
+
export * from './cross-origin.mjs';
|
|
10
|
+
export * from './wsh-bridge.mjs';
|
|
11
|
+
export * from './wisp.mjs';
|
|
12
|
+
export * from './channel-relay.mjs';
|