@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,216 @@
1
+ /**
2
+ // STATUS: INTEGRATED — wired into ClawserPod lifecycle, proven via E2E testing
3
+ * clawser-mesh-webtransport.js -- WebTransport transport bridge.
4
+ *
5
+ * Extends MeshTransport with WebTransport API support. Provides
6
+ * datagram and bidirectional stream communication. Falls back gracefully
7
+ * when WebTransport is unavailable.
8
+ *
9
+ * No browser-only imports at module level.
10
+ *
11
+ * Run tests:
12
+ * node --import ./web/test/_setup-globals.mjs --test web/test/clawser-mesh-webtransport.test.mjs
13
+ */
14
+
15
+ import { MeshTransport } from './transport.mjs'
16
+ import { silentCatch } from './silent-catch.mjs'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Feature detection
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Check if the WebTransport API is available.
24
+ * @returns {boolean}
25
+ */
26
+ export function supportsWebTransport() {
27
+ return typeof WebTransport !== 'undefined'
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // WebTransportBridge
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * WebTransport-based mesh transport.
36
+ * Uses datagrams for small messages and bidirectional streams for larger data.
37
+ */
38
+ export class WebTransportBridge extends MeshTransport {
39
+ /** @type {WebTransport|null} */
40
+ #transport = null
41
+
42
+ /** @type {WritableStreamDefaultWriter|null} */
43
+ #writer = null
44
+
45
+ /** @type {Map<string, { readable: ReadableStream, writable: WritableStream }>} */
46
+ #streams = new Map()
47
+
48
+ /** @type {string|null} */
49
+ #url = null
50
+
51
+ /** @type {boolean} */
52
+ #closed = false
53
+
54
+ constructor() {
55
+ super('wsh-wt')
56
+ }
57
+
58
+ /**
59
+ * Connect to a WebTransport server.
60
+ * @param {string} url - wss:// or https:// URL
61
+ * @param {object} [opts]
62
+ * @returns {Promise<void>}
63
+ */
64
+ async connect(url, opts = {}) {
65
+ if (this.connected) throw new Error('Already connected')
66
+ this.#url = url
67
+ this._setState('connecting')
68
+
69
+ try {
70
+ this.#transport = new WebTransport(url)
71
+ await this.#transport.ready
72
+ this.#writer = this.#transport.datagrams.writable.getWriter()
73
+ this._setState('connected')
74
+
75
+ // Read incoming datagrams
76
+ this.#readDatagrams()
77
+
78
+ // Handle incoming bidirectional streams
79
+ this.#acceptStreams()
80
+
81
+ // Handle close
82
+ this.#transport.closed.then(() => {
83
+ if (!this.#closed) this.close()
84
+ }).catch(() => {
85
+ if (!this.#closed) {
86
+ this._fire('error', new Error('Transport closed unexpectedly'))
87
+ this.close()
88
+ }
89
+ })
90
+ } catch (err) {
91
+ this._setState('disconnected')
92
+ throw err
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Send data via datagram.
98
+ * @param {*} data - Will be encoded as UTF-8 if string, or sent as-is if Uint8Array
99
+ */
100
+ send(data) {
101
+ if (!this.connected) throw new Error('Not connected')
102
+ const bytes = typeof data === 'string'
103
+ ? new TextEncoder().encode(data)
104
+ : (data instanceof Uint8Array ? data : new TextEncoder().encode(JSON.stringify(data)))
105
+ this.#writer.write(bytes)
106
+ }
107
+
108
+ /**
109
+ * Open a named bidirectional stream.
110
+ * @param {string} id - Stream identifier
111
+ * @returns {Promise<{ readable: ReadableStream, writable: WritableStream }>}
112
+ */
113
+ async openStream(id) {
114
+ if (!this.connected) throw new Error('Not connected')
115
+ const bidi = await this.#transport.createBidirectionalStream()
116
+ this.#streams.set(id, { readable: bidi.readable, writable: bidi.writable })
117
+ this._fire('stream', { id, readable: bidi.readable, writable: bidi.writable })
118
+ return { readable: bidi.readable, writable: bidi.writable }
119
+ }
120
+
121
+ /**
122
+ * Close the transport and all streams.
123
+ */
124
+ close() {
125
+ if (this.#closed) return
126
+ this.#closed = true
127
+ this._setState('closing')
128
+ this.#streams.clear()
129
+ if (this.#writer) {
130
+ try { this.#writer.close() } catch (e) { silentCatch('clawser-mesh-webtransport', 'this', e) }
131
+ this.#writer = null
132
+ }
133
+ if (this.#transport) {
134
+ try { this.#transport.close() } catch (e) { silentCatch('clawser-mesh-webtransport', 'this', e) }
135
+ this.#transport = null
136
+ }
137
+ this._setState('closed')
138
+ }
139
+
140
+ /** Number of currently tracked streams. */
141
+ get streamCount() { return this.#streams.size }
142
+
143
+ /** The URL this transport is connected to. */
144
+ get url() { return this.#url }
145
+
146
+ // -- Private helpers ------------------------------------------------------
147
+
148
+ /**
149
+ * Read datagrams loop. Runs until the transport is closed or the
150
+ * datagram readable stream ends.
151
+ */
152
+ async #readDatagrams() {
153
+ try {
154
+ const reader = this.#transport.datagrams.readable.getReader()
155
+ while (true) {
156
+ const { value, done } = await reader.read()
157
+ if (done || this.#closed) break
158
+ const decoded = new TextDecoder().decode(value)
159
+ try {
160
+ this._fire('message', JSON.parse(decoded))
161
+ } catch {
162
+ this._fire('message', decoded)
163
+ }
164
+ }
165
+ } catch {
166
+ // Transport closed or errored — no action needed
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Accept incoming bidirectional streams and fire 'stream' events.
172
+ */
173
+ async #acceptStreams() {
174
+ try {
175
+ const reader = this.#transport.incomingBidirectionalStreams.getReader()
176
+ while (true) {
177
+ const { value, done } = await reader.read()
178
+ if (done || this.#closed) break
179
+ const id = `incoming_${this.#streams.size}`
180
+ this.#streams.set(id, { readable: value.readable, writable: value.writable })
181
+ this._fire('stream', { id, readable: value.readable, writable: value.writable })
182
+ }
183
+ } catch {
184
+ // Transport closed or errored — no action needed
185
+ }
186
+ }
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // WebTransportAdapterFactory
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Factory for creating WebTransport transports.
195
+ * Can be registered with MeshTransportNegotiator.
196
+ */
197
+ export class WebTransportAdapterFactory {
198
+ /**
199
+ * Whether this factory handles the given transport type.
200
+ * @param {string} type
201
+ * @returns {boolean}
202
+ */
203
+ canCreate(type) { return type === 'wsh-wt' }
204
+
205
+ /**
206
+ * Create a new WebTransportBridge (not yet connected).
207
+ * Caller must call bridge.connect(url, opts).
208
+ * @param {string} url
209
+ * @param {object} [opts]
210
+ * @returns {WebTransportBridge}
211
+ */
212
+ create(url, opts) {
213
+ const bridge = new WebTransportBridge()
214
+ return bridge
215
+ }
216
+ }