@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.
@@ -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 };