@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,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
|
+
}
|