@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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
// STATUS: INTEGRATED — wired into ClawserPod lifecycle, proven via E2E testing
|
|
3
|
+
* clawser-mesh-transport.js -- Transport Abstraction Layer.
|
|
4
|
+
*
|
|
5
|
+
* Unified interface for mesh connections across transport types.
|
|
6
|
+
* Actual transport creation is pluggable via adapter factories passed
|
|
7
|
+
* to MeshTransportNegotiator. This keeps the core logic testable
|
|
8
|
+
* without real WebRTC/WebSocket/WebTransport connections.
|
|
9
|
+
*
|
|
10
|
+
* Run tests:
|
|
11
|
+
* node --import ./web/test/_setup-globals.mjs --test web/test/clawser-mesh-transport.test.mjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** @type {readonly string[]} */
|
|
19
|
+
const TRANSPORT_TYPES = Object.freeze(['webrtc', 'wsh-wt', 'wsh-ws']);
|
|
20
|
+
|
|
21
|
+
/** @type {readonly string[]} */
|
|
22
|
+
const TRANSPORT_STATES = Object.freeze([
|
|
23
|
+
'disconnected',
|
|
24
|
+
'connecting',
|
|
25
|
+
'connected',
|
|
26
|
+
'closing',
|
|
27
|
+
'closed',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// MeshTransport (abstract base)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Abstract transport interface.
|
|
36
|
+
* All mesh transports must extend this and implement connect() and send().
|
|
37
|
+
*/
|
|
38
|
+
export class MeshTransport {
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
#type;
|
|
41
|
+
|
|
42
|
+
/** @type {string} */
|
|
43
|
+
#state = 'disconnected';
|
|
44
|
+
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
#latency = 0;
|
|
47
|
+
|
|
48
|
+
/** @type {{ stream: Function[], close: Function[], error: Function[], message: Function[] }} */
|
|
49
|
+
#callbacks = { stream: [], close: [], error: [], message: [] };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} type - One of TRANSPORT_TYPES
|
|
53
|
+
*/
|
|
54
|
+
constructor(type) {
|
|
55
|
+
if (!TRANSPORT_TYPES.includes(type)) {
|
|
56
|
+
throw new Error(`Unknown transport type: ${type}`);
|
|
57
|
+
}
|
|
58
|
+
this.#type = type;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Transport type identifier. */
|
|
62
|
+
get type() {
|
|
63
|
+
return this.#type;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Current connection state. */
|
|
67
|
+
get state() {
|
|
68
|
+
return this.#state;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when transport is in 'connected' state. */
|
|
72
|
+
get connected() {
|
|
73
|
+
return this.#state === 'connected';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Last measured latency in ms. */
|
|
77
|
+
get latency() {
|
|
78
|
+
return this.#latency;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Transition to a new state. Fires 'close' when entering 'closed'.
|
|
83
|
+
* @protected
|
|
84
|
+
* @param {string} state
|
|
85
|
+
*/
|
|
86
|
+
_setState(state) {
|
|
87
|
+
this.#state = state;
|
|
88
|
+
if (state === 'closed') {
|
|
89
|
+
this._fire('close');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update the latency measurement.
|
|
95
|
+
* @protected
|
|
96
|
+
* @param {number} ms
|
|
97
|
+
*/
|
|
98
|
+
_setLatency(ms) {
|
|
99
|
+
this.#latency = ms;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Connect to a peer endpoint. Must be overridden by subclass.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} endpoint
|
|
106
|
+
* @param {object} [auth]
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
async connect(endpoint, auth) {
|
|
110
|
+
throw new Error('connect() must be implemented by subclass');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Close the transport gracefully.
|
|
115
|
+
*/
|
|
116
|
+
close() {
|
|
117
|
+
this._setState('closing');
|
|
118
|
+
this._setState('closed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Send a message over the transport. Must be overridden by subclass.
|
|
123
|
+
*
|
|
124
|
+
* @param {*} data
|
|
125
|
+
*/
|
|
126
|
+
send(data) {
|
|
127
|
+
throw new Error('send() must be implemented by subclass');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// -- Event registration -------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register callback for incoming byte streams.
|
|
134
|
+
* @param {Function} cb
|
|
135
|
+
*/
|
|
136
|
+
onStream(cb) {
|
|
137
|
+
this.#callbacks.stream.push(cb);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register callback for transport close.
|
|
142
|
+
* @param {Function} cb
|
|
143
|
+
*/
|
|
144
|
+
onClose(cb) {
|
|
145
|
+
this.#callbacks.close.push(cb);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Register callback for transport errors.
|
|
150
|
+
* @param {Function} cb
|
|
151
|
+
*/
|
|
152
|
+
onError(cb) {
|
|
153
|
+
this.#callbacks.error.push(cb);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register callback for incoming messages.
|
|
158
|
+
* @param {Function} cb
|
|
159
|
+
*/
|
|
160
|
+
onMessage(cb) {
|
|
161
|
+
this.#callbacks.message.push(cb);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fire all callbacks for a given event, swallowing listener errors.
|
|
166
|
+
* @protected
|
|
167
|
+
* @param {string} event
|
|
168
|
+
* @param {*} [data]
|
|
169
|
+
*/
|
|
170
|
+
_fire(event, data) {
|
|
171
|
+
for (const cb of this.#callbacks[event] || []) {
|
|
172
|
+
try {
|
|
173
|
+
cb(data);
|
|
174
|
+
} catch {
|
|
175
|
+
/* listener errors do not propagate */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Serialize to a JSON-safe object (no callbacks/handles).
|
|
182
|
+
* @returns {object}
|
|
183
|
+
*/
|
|
184
|
+
toJSON() {
|
|
185
|
+
return {
|
|
186
|
+
type: this.#type,
|
|
187
|
+
state: this.#state,
|
|
188
|
+
latency: this.#latency,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// MockMeshTransport
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* In-memory mock transport for testing and local peer simulation.
|
|
199
|
+
* Supports pairing two instances for bidirectional message delivery.
|
|
200
|
+
*/
|
|
201
|
+
export class MockMeshTransport extends MeshTransport {
|
|
202
|
+
/** @type {Array} */
|
|
203
|
+
#messages = [];
|
|
204
|
+
|
|
205
|
+
/** @type {MockMeshTransport|null} */
|
|
206
|
+
#partner = null;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} [type='wsh-ws'] - Transport type to emulate
|
|
210
|
+
*/
|
|
211
|
+
constructor(type = 'wsh-ws') {
|
|
212
|
+
super(type);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Simulate a connection handshake.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} _endpoint
|
|
219
|
+
* @param {object} [_auth]
|
|
220
|
+
* @returns {Promise<void>}
|
|
221
|
+
*/
|
|
222
|
+
async connect(_endpoint, _auth) {
|
|
223
|
+
this._setState('connecting');
|
|
224
|
+
this._setState('connected');
|
|
225
|
+
this._setLatency(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Send data. Throws if not connected. If paired, delivers to partner.
|
|
230
|
+
*
|
|
231
|
+
* @param {*} data
|
|
232
|
+
*/
|
|
233
|
+
send(data) {
|
|
234
|
+
if (!this.connected) {
|
|
235
|
+
throw new Error('Transport not connected');
|
|
236
|
+
}
|
|
237
|
+
this.#messages.push(data);
|
|
238
|
+
if (this.#partner) {
|
|
239
|
+
this.#partner._fire('message', data);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Link two mock transports for bidirectional communication.
|
|
245
|
+
*
|
|
246
|
+
* @param {MockMeshTransport} other
|
|
247
|
+
*/
|
|
248
|
+
pair(other) {
|
|
249
|
+
this.#partner = other;
|
|
250
|
+
other.#partner = this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Messages sent through this transport instance.
|
|
255
|
+
* @returns {Array}
|
|
256
|
+
*/
|
|
257
|
+
get sentMessages() {
|
|
258
|
+
return [...this.#messages];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Close transport and detach partner.
|
|
263
|
+
*/
|
|
264
|
+
close() {
|
|
265
|
+
this.#partner = null;
|
|
266
|
+
super.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// MeshTransportNegotiator
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Tries transport adapters in preference order and returns the first
|
|
276
|
+
* that successfully connects. Adapters are registered as async factory
|
|
277
|
+
* functions that produce a connected MeshTransport.
|
|
278
|
+
*/
|
|
279
|
+
export class MeshTransportNegotiator {
|
|
280
|
+
/** @type {Map<string, Function>} type -> async factory(endpoint, auth) => MeshTransport */
|
|
281
|
+
#adapters = new Map();
|
|
282
|
+
|
|
283
|
+
/** @type {string[]} */
|
|
284
|
+
#preferenceOrder = ['webrtc', 'wsh-wt', 'wsh-ws'];
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {object} [opts]
|
|
288
|
+
* @param {string[]} [opts.preferenceOrder] - Override default preference
|
|
289
|
+
*/
|
|
290
|
+
constructor(opts = {}) {
|
|
291
|
+
if (opts.preferenceOrder) {
|
|
292
|
+
this.#preferenceOrder = [...opts.preferenceOrder];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Register a transport adapter factory.
|
|
298
|
+
*
|
|
299
|
+
* The factory signature is: `(endpoint, auth) => Promise<MeshTransport>`
|
|
300
|
+
*
|
|
301
|
+
* @param {string} type - One of TRANSPORT_TYPES
|
|
302
|
+
* @param {Function} factory
|
|
303
|
+
*/
|
|
304
|
+
registerAdapter(type, factory) {
|
|
305
|
+
if (!TRANSPORT_TYPES.includes(type)) {
|
|
306
|
+
throw new Error(`Unknown transport type: ${type}`);
|
|
307
|
+
}
|
|
308
|
+
this.#adapters.set(type, factory);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Negotiate the best transport for a peer.
|
|
313
|
+
*
|
|
314
|
+
* Tries each type in preference order. Returns the first successfully
|
|
315
|
+
* created transport. Throws if all fail.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} endpoints - Map of type -> endpoint string
|
|
318
|
+
* @param {object} [auth] - Auth credentials to pass to adapters
|
|
319
|
+
* @returns {Promise<MeshTransport>}
|
|
320
|
+
*/
|
|
321
|
+
async negotiate(endpoints, auth) {
|
|
322
|
+
const errors = [];
|
|
323
|
+
for (const type of this.#preferenceOrder) {
|
|
324
|
+
const factory = this.#adapters.get(type);
|
|
325
|
+
if (!factory) continue;
|
|
326
|
+
const endpoint = endpoints[type];
|
|
327
|
+
if (!endpoint) continue;
|
|
328
|
+
try {
|
|
329
|
+
const transport = await factory(endpoint, auth);
|
|
330
|
+
return transport;
|
|
331
|
+
} catch (e) {
|
|
332
|
+
errors.push({ type, error: e.message });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
throw new Error(`All transports failed: ${JSON.stringify(errors)}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* List transport types that have a registered adapter.
|
|
340
|
+
*
|
|
341
|
+
* @returns {string[]}
|
|
342
|
+
*/
|
|
343
|
+
availableTypes() {
|
|
344
|
+
return [...this.#adapters.keys()];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Current preference order (copy).
|
|
349
|
+
*
|
|
350
|
+
* @returns {string[]}
|
|
351
|
+
*/
|
|
352
|
+
get preferenceOrder() {
|
|
353
|
+
return [...this.#preferenceOrder];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { TRANSPORT_TYPES, TRANSPORT_STATES };
|