@kyneta/webrtc-transport 1.3.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,517 @@
1
+ // webrtc-transport.test — unit tests for WebrtcTransport.
2
+ //
3
+ // Tests the BYODC (Bring Your Own Data Channel) WebRTC transport
4
+ // lifecycle, send/receive pipelines, fragmentation, and multi-peer
5
+ // channel management.
6
+
7
+ import type { ChannelMsg } from "@kyneta/transport"
8
+ import {
9
+ cborCodec,
10
+ encodeComplete,
11
+ fragmentPayload,
12
+ wrapCompleteMessage,
13
+ } from "@kyneta/wire"
14
+ import { beforeEach, describe, expect, it, vi } from "vitest"
15
+ import { WebrtcTransport } from "../webrtc-transport.js"
16
+ import { MockDataChannel } from "./mock-data-channel.js"
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Initialize and start a WebrtcTransport for testing.
24
+ *
25
+ * Wires up the full Transport lifecycle (construct → initialize → start)
26
+ * with stubbed callbacks. Returns the spies so tests can assert on
27
+ * channel lifecycle events and message delivery.
28
+ */
29
+ function createContext() {
30
+ return {
31
+ identity: { peerId: "local-peer", name: "Local", type: "user" as const },
32
+ onChannelReceive: vi.fn(),
33
+ onChannelAdded: vi.fn(),
34
+ onChannelRemoved: vi.fn(),
35
+ onChannelEstablish: vi.fn(),
36
+ }
37
+ }
38
+
39
+ async function initializeTransport(
40
+ transport: WebrtcTransport,
41
+ ctx = createContext(),
42
+ ) {
43
+ transport._initialize(ctx)
44
+ await transport._start()
45
+ return ctx
46
+ }
47
+
48
+ /** A minimal establish-request message for send/receive tests. */
49
+ const TEST_MSG: ChannelMsg = {
50
+ type: "establish-request",
51
+ identity: { peerId: "remote", name: "R", type: "user" },
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // 1. Lifecycle
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe("Lifecycle", () => {
59
+ let transport: WebrtcTransport
60
+
61
+ beforeEach(async () => {
62
+ transport = new WebrtcTransport()
63
+ await initializeTransport(transport)
64
+ })
65
+
66
+ it("attach creates internal channel when data channel is already open", () => {
67
+ const dc = new MockDataChannel("open")
68
+ transport.attachDataChannel("peer-1", dc)
69
+
70
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
71
+ expect(transport.getAttachedPeerIds()).toContain("peer-1")
72
+ })
73
+
74
+ it("attach waits for open event when connecting", () => {
75
+ const dc = new MockDataChannel("connecting")
76
+ transport.attachDataChannel("peer-1", dc)
77
+
78
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
79
+ // Sync channel not yet created — still connecting
80
+ expect(transport.channels.size).toBe(0)
81
+
82
+ dc.open()
83
+ // Now the sync channel should exist
84
+ expect(transport.channels.size).toBe(1)
85
+ })
86
+
87
+ it("detach removes the attached channel", () => {
88
+ const dc = new MockDataChannel("open")
89
+ transport.attachDataChannel("peer-1", dc)
90
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
91
+
92
+ transport.detachDataChannel("peer-1")
93
+ expect(transport.hasDataChannel("peer-1")).toBe(false)
94
+ expect(transport.getAttachedPeerIds()).toEqual([])
95
+ })
96
+
97
+ it("detach cleans up event listeners", () => {
98
+ const dc = new MockDataChannel("open")
99
+ transport.attachDataChannel("peer-1", dc)
100
+ expect(dc.hasListeners()).toBe(true)
101
+
102
+ transport.detachDataChannel("peer-1")
103
+ expect(dc.hasListeners()).toBe(false)
104
+ })
105
+
106
+ it("double-attach detaches old channel first", () => {
107
+ const dc1 = new MockDataChannel("open")
108
+ const dc2 = new MockDataChannel("open")
109
+
110
+ transport.attachDataChannel("peer-1", dc1)
111
+ transport.attachDataChannel("peer-1", dc2)
112
+
113
+ // Old channel's listeners should be cleaned up
114
+ expect(dc1.hasListeners()).toBe(false)
115
+ // New channel should be attached
116
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
117
+ expect(dc2.hasListeners()).toBe(true)
118
+ })
119
+
120
+ it("onStop detaches all channels", async () => {
121
+ const dc1 = new MockDataChannel("open")
122
+ const dc2 = new MockDataChannel("open")
123
+
124
+ transport.attachDataChannel("peer-a", dc1)
125
+ transport.attachDataChannel("peer-b", dc2)
126
+ expect(transport.getAttachedPeerIds()).toHaveLength(2)
127
+
128
+ await transport._stop()
129
+
130
+ expect(transport.hasDataChannel("peer-a")).toBe(false)
131
+ expect(transport.hasDataChannel("peer-b")).toBe(false)
132
+ expect(dc1.hasListeners()).toBe(false)
133
+ expect(dc2.hasListeners()).toBe(false)
134
+ })
135
+
136
+ it("cleanup function returned by attach calls detach", () => {
137
+ const dc = new MockDataChannel("open")
138
+ const cleanup = transport.attachDataChannel("peer-1", dc)
139
+
140
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
141
+ cleanup()
142
+ expect(transport.hasDataChannel("peer-1")).toBe(false)
143
+ })
144
+
145
+ it("detach is idempotent for unknown peer", () => {
146
+ // Should not throw
147
+ transport.detachDataChannel("nonexistent")
148
+ })
149
+ })
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // 2. Send
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe("Send", () => {
156
+ let transport: WebrtcTransport
157
+
158
+ beforeEach(async () => {
159
+ transport = new WebrtcTransport()
160
+ await initializeTransport(transport)
161
+ })
162
+
163
+ it("send encodes and delivers binary data via dc.send", () => {
164
+ const dc = new MockDataChannel("open")
165
+ transport.attachDataChannel("peer-1", dc)
166
+
167
+ const syncChannel = [...transport.channels].find(
168
+ ch => ch.type === "connected",
169
+ )
170
+ if (!syncChannel) throw new Error("expected syncChannel to be defined")
171
+
172
+ syncChannel.send(TEST_MSG)
173
+
174
+ expect(dc.send).toHaveBeenCalledOnce()
175
+ expect(dc.send.mock.calls.at(0)?.at(0)).toBeInstanceOf(Uint8Array)
176
+ })
177
+ })
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // 3. Receive
181
+ // ---------------------------------------------------------------------------
182
+
183
+ describe("Receive", () => {
184
+ let transport: WebrtcTransport
185
+ let ctx: ReturnType<typeof createContext>
186
+
187
+ beforeEach(async () => {
188
+ transport = new WebrtcTransport()
189
+ ctx = await initializeTransport(transport)
190
+ })
191
+
192
+ it("receive with ArrayBuffer data", () => {
193
+ const dc = new MockDataChannel("open")
194
+ transport.attachDataChannel("peer-1", dc)
195
+
196
+ // Encode a test message through the wire pipeline
197
+ const encoded = encodeComplete(cborCodec, TEST_MSG)
198
+ const wrapped = wrapCompleteMessage(encoded)
199
+
200
+ // Convert to ArrayBuffer (as native RTCDataChannel with binaryType "arraybuffer" would deliver)
201
+ const ab = wrapped.buffer.slice(
202
+ wrapped.byteOffset,
203
+ wrapped.byteOffset + wrapped.byteLength,
204
+ )
205
+
206
+ dc.emit("message", { data: ab })
207
+
208
+ expect(ctx.onChannelReceive).toHaveBeenCalled()
209
+ const callArgs = ctx.onChannelReceive.mock.calls.at(0)
210
+ if (!callArgs)
211
+ throw new Error("expected onChannelReceive to have been called")
212
+ const [, receivedMsg] = callArgs
213
+ expect(receivedMsg.type).toBe("establish-request")
214
+ expect((receivedMsg as any).identity.peerId).toBe("remote")
215
+ })
216
+
217
+ it("receive with Uint8Array data", () => {
218
+ const dc = new MockDataChannel("open")
219
+ transport.attachDataChannel("peer-1", dc)
220
+
221
+ const encoded = encodeComplete(cborCodec, TEST_MSG)
222
+ const wrapped = wrapCompleteMessage(encoded)
223
+
224
+ // Pass Uint8Array directly (as simple-peer and other wrappers may deliver)
225
+ dc.emit("message", { data: wrapped })
226
+
227
+ expect(ctx.onChannelReceive).toHaveBeenCalled()
228
+ const callArgs = ctx.onChannelReceive.mock.calls.at(0)
229
+ if (!callArgs)
230
+ throw new Error("expected onChannelReceive to have been called")
231
+ const [, receivedMsg] = callArgs
232
+ expect(receivedMsg.type).toBe("establish-request")
233
+ expect((receivedMsg as any).identity.peerId).toBe("remote")
234
+ })
235
+
236
+ it("receive ignores string data", () => {
237
+ const dc = new MockDataChannel("open")
238
+ transport.attachDataChannel("peer-1", dc)
239
+
240
+ dc.emit("message", { data: "unexpected string" })
241
+
242
+ expect(ctx.onChannelReceive).not.toHaveBeenCalled()
243
+ })
244
+ })
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // 4. readyState
248
+ // ---------------------------------------------------------------------------
249
+
250
+ describe("readyState", () => {
251
+ let transport: WebrtcTransport
252
+
253
+ beforeEach(async () => {
254
+ transport = new WebrtcTransport()
255
+ await initializeTransport(transport)
256
+ })
257
+
258
+ it("sets binaryType on attach", () => {
259
+ const dc = new MockDataChannel("open")
260
+ expect(dc.binaryType).toBe("blob")
261
+
262
+ transport.attachDataChannel("peer-1", dc)
263
+ expect(dc.binaryType).toBe("arraybuffer")
264
+ })
265
+
266
+ it("send is no-op when readyState is not open", () => {
267
+ const dc = new MockDataChannel("connecting")
268
+ transport.attachDataChannel("peer-1", dc)
269
+ dc.open()
270
+
271
+ const syncChannel = [...transport.channels].find(
272
+ ch => ch.type === "connected",
273
+ )
274
+ if (!syncChannel) throw new Error("expected syncChannel to be defined")
275
+
276
+ // Simulate the channel transitioning to closing
277
+ dc.readyState = "closing"
278
+
279
+ syncChannel.send(TEST_MSG)
280
+ expect(dc.send).not.toHaveBeenCalled()
281
+ })
282
+ })
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // 5. Fragmentation
286
+ // ---------------------------------------------------------------------------
287
+
288
+ describe("Fragmentation", () => {
289
+ it("fragments large messages", async () => {
290
+ // Tiny threshold to force fragmentation on any message
291
+ const transport = new WebrtcTransport({ fragmentThreshold: 200 })
292
+ await initializeTransport(transport)
293
+
294
+ const dc = new MockDataChannel("open")
295
+ transport.attachDataChannel("peer-1", dc)
296
+
297
+ const syncChannel = [...transport.channels].find(
298
+ ch => ch.type === "connected",
299
+ )
300
+ if (!syncChannel) throw new Error("expected syncChannel to be defined")
301
+
302
+ // Send a message — with a 200-byte threshold, even a small message's
303
+ // binary frame may exceed it if the CBOR encoding + frame header is large enough.
304
+ // Use a message with enough payload to guarantee fragmentation.
305
+ const largeMsg: ChannelMsg = {
306
+ type: "establish-request",
307
+ identity: {
308
+ peerId: `a]very-long-peer-id-${"x".repeat(200)}`,
309
+ name: `A Long Name ${"y".repeat(200)}`,
310
+ type: "user",
311
+ },
312
+ }
313
+
314
+ syncChannel.send(largeMsg)
315
+
316
+ // Should have been called more than once (multiple fragments)
317
+ expect(dc.send.mock.calls.length).toBeGreaterThan(1)
318
+ })
319
+
320
+ it("reassembles fragmented incoming messages across multiple events", async () => {
321
+ const transport = new WebrtcTransport()
322
+ const ctx = await initializeTransport(transport)
323
+
324
+ const dc = new MockDataChannel("open")
325
+ transport.attachDataChannel("peer-1", dc)
326
+
327
+ // Build a message large enough to guarantee multiple fragments at chunk size 50
328
+ const largeMsg: ChannelMsg = {
329
+ type: "establish-request",
330
+ identity: {
331
+ peerId: `peer-${"z".repeat(200)}`,
332
+ name: `Name-${"w".repeat(200)}`,
333
+ type: "user",
334
+ },
335
+ }
336
+
337
+ const encoded = encodeComplete(cborCodec, largeMsg)
338
+ const fragments = fragmentPayload(encoded, 50)
339
+ expect(fragments.length).toBeGreaterThan(1)
340
+
341
+ // Emit all but the last fragment — should NOT trigger receive yet
342
+ for (let i = 0; i < fragments.length - 1; i++) {
343
+ const frag = fragments.at(i)
344
+ if (!frag) throw new Error(`expected fragment at index ${i}`)
345
+ const ab = frag.buffer.slice(
346
+ frag.byteOffset,
347
+ frag.byteOffset + frag.byteLength,
348
+ )
349
+ dc.emit("message", { data: ab })
350
+ }
351
+ expect(ctx.onChannelReceive).not.toHaveBeenCalled()
352
+
353
+ // Emit the last fragment — should complete reassembly
354
+ const lastFrag = fragments.at(-1)
355
+ if (!lastFrag) throw new Error("expected last fragment to exist")
356
+ const ab = lastFrag.buffer.slice(
357
+ lastFrag.byteOffset,
358
+ lastFrag.byteOffset + lastFrag.byteLength,
359
+ )
360
+ dc.emit("message", { data: ab })
361
+
362
+ expect(ctx.onChannelReceive).toHaveBeenCalledTimes(1)
363
+ const callArgs = ctx.onChannelReceive.mock.calls.at(0)
364
+ if (!callArgs)
365
+ throw new Error("expected onChannelReceive to have been called")
366
+ const [, receivedMsg] = callArgs
367
+ expect(receivedMsg.type).toBe("establish-request")
368
+ expect((receivedMsg as any).identity.peerId).toBe(`peer-${"z".repeat(200)}`)
369
+ })
370
+ })
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // 6. Multi-peer
374
+ // ---------------------------------------------------------------------------
375
+
376
+ describe("Multi-peer", () => {
377
+ let transport: WebrtcTransport
378
+
379
+ beforeEach(async () => {
380
+ transport = new WebrtcTransport()
381
+ await initializeTransport(transport)
382
+ })
383
+
384
+ it("independent channels for different peers", () => {
385
+ const dc1 = new MockDataChannel("open")
386
+ const dc2 = new MockDataChannel("open")
387
+
388
+ transport.attachDataChannel("peer-a", dc1)
389
+ transport.attachDataChannel("peer-b", dc2)
390
+
391
+ expect(transport.getAttachedPeerIds()).toContain("peer-a")
392
+ expect(transport.getAttachedPeerIds()).toContain("peer-b")
393
+ expect(transport.channels.size).toBe(2)
394
+
395
+ const channels = [...transport.channels]
396
+ const ch0 = channels.at(0)
397
+ const ch1 = channels.at(1)
398
+ if (!ch0) throw new Error("expected channel at index 0")
399
+ if (!ch1) throw new Error("expected channel at index 1")
400
+ ch0.send(TEST_MSG)
401
+ ch1.send(TEST_MSG)
402
+
403
+ // Each data channel received its own send call
404
+ expect(dc1.send).toHaveBeenCalledOnce()
405
+ expect(dc2.send).toHaveBeenCalledOnce()
406
+
407
+ // Payloads are distinct Uint8Array instances (not shared references)
408
+ const bytes1 = dc1.send.mock.calls.at(0)?.at(0) as Uint8Array
409
+ const bytes2 = dc2.send.mock.calls.at(0)?.at(0) as Uint8Array
410
+ expect(bytes1).not.toBe(bytes2)
411
+ })
412
+ })
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // 7. DataChannelLike conformance
416
+ // ---------------------------------------------------------------------------
417
+
418
+ describe("DataChannelLike conformance", () => {
419
+ let transport: WebrtcTransport
420
+
421
+ beforeEach(async () => {
422
+ transport = new WebrtcTransport()
423
+ await initializeTransport(transport)
424
+ })
425
+
426
+ it("works with a plain object literal", () => {
427
+ const dc = {
428
+ readyState: "open" as string,
429
+ binaryType: "blob",
430
+ send: vi.fn(),
431
+ addEventListener: vi.fn(),
432
+ removeEventListener: vi.fn(),
433
+ }
434
+
435
+ // Should not throw
436
+ transport.attachDataChannel("peer-1", dc)
437
+ expect(transport.hasDataChannel("peer-1")).toBe(true)
438
+
439
+ // binaryType should have been set
440
+ expect(dc.binaryType).toBe("arraybuffer")
441
+
442
+ // addEventListener should have been called for the 4 event types
443
+ const eventTypes = dc.addEventListener.mock.calls.map(
444
+ (call: any[]) => call[0],
445
+ )
446
+ expect(eventTypes).toContain("open")
447
+ expect(eventTypes).toContain("close")
448
+ expect(eventTypes).toContain("error")
449
+ expect(eventTypes).toContain("message")
450
+ })
451
+ })
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // 8. Channel close event
455
+ // ---------------------------------------------------------------------------
456
+
457
+ describe("Message before open (race condition)", () => {
458
+ it("silently drops messages received before sync channel is created", async () => {
459
+ const transport = new WebrtcTransport()
460
+ const ctx = await initializeTransport(transport)
461
+
462
+ const dc = new MockDataChannel("connecting")
463
+ transport.attachDataChannel("peer-1", dc)
464
+
465
+ // Data channel is still connecting — no sync channel exists yet
466
+ expect(transport.channels.size).toBe(0)
467
+
468
+ // Simulate a message arriving before the open event
469
+ const encoded = encodeComplete(cborCodec, TEST_MSG)
470
+ const wrapped = wrapCompleteMessage(encoded)
471
+ dc.emit("message", { data: wrapped })
472
+
473
+ // Message should be silently dropped — no delivery, no error
474
+ expect(ctx.onChannelReceive).not.toHaveBeenCalled()
475
+
476
+ // Now open the channel — subsequent messages should work
477
+ dc.open()
478
+ expect(transport.channels.size).toBe(1)
479
+
480
+ dc.emit("message", { data: wrapped })
481
+ expect(ctx.onChannelReceive).toHaveBeenCalledOnce()
482
+ })
483
+ })
484
+
485
+ describe("Channel close event", () => {
486
+ let transport: WebrtcTransport
487
+ let ctx: ReturnType<typeof createContext>
488
+
489
+ beforeEach(async () => {
490
+ transport = new WebrtcTransport()
491
+ ctx = await initializeTransport(transport)
492
+ })
493
+
494
+ it("removes sync channel when data channel fires close", () => {
495
+ const dc = new MockDataChannel("open")
496
+ transport.attachDataChannel("peer-1", dc)
497
+ expect(transport.channels.size).toBe(1)
498
+
499
+ dc.close()
500
+
501
+ // The sync channel should be removed, but the attachment tracking remains
502
+ // (the transport removes the internal sync channel but doesn't auto-detach)
503
+ expect(transport.channels.size).toBe(0)
504
+ expect(ctx.onChannelRemoved).toHaveBeenCalled()
505
+ })
506
+
507
+ it("removes sync channel when data channel fires error", () => {
508
+ const dc = new MockDataChannel("open")
509
+ transport.attachDataChannel("peer-1", dc)
510
+ expect(transport.channels.size).toBe(1)
511
+
512
+ dc.emit("error", new Error("connection failed"))
513
+
514
+ expect(transport.channels.size).toBe(0)
515
+ expect(ctx.onChannelRemoved).toHaveBeenCalled()
516
+ })
517
+ })
@@ -0,0 +1,104 @@
1
+ // data-channel-like — minimal interface for WebRTC-style data channels.
2
+ //
3
+ // Native `RTCDataChannel` satisfies this structurally — no wrapper needed.
4
+ // Libraries like simple-peer can conform via a trivial bridge function.
5
+ //
6
+ // This interface captures the exact surface the transport uses:
7
+ // - readyState (read)
8
+ // - binaryType (write, best-effort hint)
9
+ // - send (call)
10
+ // - addEventListener / removeEventListener (4 event types)
11
+ //
12
+ // It does NOT import any DOM types. The `event: any` parameter avoids
13
+ // coupling to `MessageEvent`, `Event`, etc. — the transport inspects
14
+ // `event.data` at runtime.
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // DataChannelLike — the BYODC contract
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Minimal interface for a WebRTC-style data channel.
22
+ *
23
+ * Native `RTCDataChannel` satisfies this structurally (no wrapper needed).
24
+ * Libraries like simple-peer can conform via a ~20-line bridge function
25
+ * that maps EventEmitter events to addEventListener calls.
26
+ *
27
+ * The transport uses exactly these members — nothing else. This is
28
+ * intentional: the narrower the interface, the easier it is to bridge
29
+ * from any WebRTC library.
30
+ *
31
+ * ## Event types used
32
+ *
33
+ * The transport registers listeners for exactly four event types:
34
+ * - `"open"` — data channel became ready for sending
35
+ * - `"close"` — data channel was closed
36
+ * - `"error"` — data channel encountered an error
37
+ * - `"message"` — data arrived; the transport reads `event.data`
38
+ *
39
+ * ## Ownership contract
40
+ *
41
+ * The transport does NOT own the data channel. Calling
42
+ * `detachDataChannel()` removes the sync channel but does not close
43
+ * the data channel or the peer connection. The application manages
44
+ * the WebRTC connection lifecycle independently.
45
+ */
46
+ export interface DataChannelLike {
47
+ /**
48
+ * Current state of the data channel.
49
+ *
50
+ * The transport treats `"open"` as sendable; all other values
51
+ * (including `"connecting"`, `"closing"`, `"closed"`) as not sendable.
52
+ *
53
+ * For native `RTCDataChannel`, this is one of:
54
+ * `"connecting" | "open" | "closing" | "closed"`.
55
+ *
56
+ * Wrappers may return any string — the transport only checks `=== "open"`.
57
+ */
58
+ readonly readyState: string
59
+
60
+ /**
61
+ * Binary type hint for incoming data.
62
+ *
63
+ * The transport writes `"arraybuffer"` on attach as a best-effort hint.
64
+ * It does NOT depend on this being respected — the message handler
65
+ * accepts both `ArrayBuffer` and `Uint8Array` data regardless.
66
+ *
67
+ * For native `RTCDataChannel`, this controls whether `MessageEvent.data`
68
+ * is an `ArrayBuffer` or a `Blob`. For wrappers that ignore this
69
+ * property (e.g. simple-peer bridges), the write is harmless.
70
+ */
71
+ binaryType: string
72
+
73
+ /**
74
+ * Send binary data through the data channel.
75
+ *
76
+ * The transport always sends `Uint8Array` instances (CBOR-encoded
77
+ * wire frames, optionally fragmented). Native `RTCDataChannel.send`
78
+ * accepts `ArrayBufferView` (which `Uint8Array` satisfies), so
79
+ * conformance is structural.
80
+ */
81
+ send(data: Uint8Array): void
82
+
83
+ /**
84
+ * Register an event listener.
85
+ *
86
+ * The transport uses this for `"open"`, `"close"`, `"error"`, and
87
+ * `"message"` events. For `"message"` events, the transport reads
88
+ * `event.data` and handles both `ArrayBuffer` and `Uint8Array`.
89
+ *
90
+ * @param type - Event type string
91
+ * @param listener - Callback. The `event` parameter is untyped to
92
+ * avoid coupling to DOM `Event` / `MessageEvent` types.
93
+ */
94
+ addEventListener(type: string, listener: (event: any) => void): void
95
+
96
+ /**
97
+ * Remove a previously registered event listener.
98
+ *
99
+ * Called during `detachDataChannel()` to clean up all four event
100
+ * listeners. The transport always passes the same function reference
101
+ * that was used in `addEventListener`.
102
+ */
103
+ removeEventListener(type: string, listener: (event: any) => void): void
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ // @kyneta/webrtc-transport — BYODC WebRTC data channel transport.
2
+ //
3
+ // Bring Your Own Data Channel: the application manages WebRTC connections
4
+ // (signaling, ICE, media streams). This transport attaches to data channels
5
+ // for kyneta document synchronization.
6
+ //
7
+ // Native RTCDataChannel satisfies DataChannelLike structurally — no wrapper
8
+ // needed. Libraries like simple-peer can conform via a trivial bridge function.
9
+
10
+ export type { DataChannelLike } from "./data-channel-like.js"
11
+ export {
12
+ createWebrtcTransport,
13
+ DEFAULT_FRAGMENT_THRESHOLD,
14
+ WebrtcTransport,
15
+ type WebrtcTransportOptions,
16
+ } from "./webrtc-transport.js"