@kyneta/webrtc-transport 1.3.1 → 1.4.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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Transport, GeneratedChannel, TransportFactory } from '@kyneta/transport';
1
+ import { GeneratedChannel, Transport, TransportFactory } from "@kyneta/transport";
2
2
 
3
+ //#region src/data-channel-like.d.ts
3
4
  /**
4
5
  * Minimal interface for a WebRTC-style data channel.
5
6
  *
@@ -27,61 +28,62 @@ import { Transport, GeneratedChannel, TransportFactory } from '@kyneta/transport
27
28
  * the WebRTC connection lifecycle independently.
28
29
  */
29
30
  interface DataChannelLike {
30
- /**
31
- * Current state of the data channel.
32
- *
33
- * The transport treats `"open"` as sendable; all other values
34
- * (including `"connecting"`, `"closing"`, `"closed"`) as not sendable.
35
- *
36
- * For native `RTCDataChannel`, this is one of:
37
- * `"connecting" | "open" | "closing" | "closed"`.
38
- *
39
- * Wrappers may return any string — the transport only checks `=== "open"`.
40
- */
41
- readonly readyState: string;
42
- /**
43
- * Binary type hint for incoming data.
44
- *
45
- * The transport writes `"arraybuffer"` on attach as a best-effort hint.
46
- * It does NOT depend on this being respected — the message handler
47
- * accepts both `ArrayBuffer` and `Uint8Array` data regardless.
48
- *
49
- * For native `RTCDataChannel`, this controls whether `MessageEvent.data`
50
- * is an `ArrayBuffer` or a `Blob`. For wrappers that ignore this
51
- * property (e.g. simple-peer bridges), the write is harmless.
52
- */
53
- binaryType: string;
54
- /**
55
- * Send binary data through the data channel.
56
- *
57
- * The transport always sends `Uint8Array` instances (CBOR-encoded
58
- * wire frames, optionally fragmented). Native `RTCDataChannel.send`
59
- * accepts `ArrayBufferView` (which `Uint8Array` satisfies), so
60
- * conformance is structural.
61
- */
62
- send(data: Uint8Array): void;
63
- /**
64
- * Register an event listener.
65
- *
66
- * The transport uses this for `"open"`, `"close"`, `"error"`, and
67
- * `"message"` events. For `"message"` events, the transport reads
68
- * `event.data` and handles both `ArrayBuffer` and `Uint8Array`.
69
- *
70
- * @param type - Event type string
71
- * @param listener - Callback. The `event` parameter is untyped to
72
- * avoid coupling to DOM `Event` / `MessageEvent` types.
73
- */
74
- addEventListener(type: string, listener: (event: any) => void): void;
75
- /**
76
- * Remove a previously registered event listener.
77
- *
78
- * Called during `detachDataChannel()` to clean up all four event
79
- * listeners. The transport always passes the same function reference
80
- * that was used in `addEventListener`.
81
- */
82
- removeEventListener(type: string, listener: (event: any) => void): void;
31
+ /**
32
+ * Current state of the data channel.
33
+ *
34
+ * The transport treats `"open"` as sendable; all other values
35
+ * (including `"connecting"`, `"closing"`, `"closed"`) as not sendable.
36
+ *
37
+ * For native `RTCDataChannel`, this is one of:
38
+ * `"connecting" | "open" | "closing" | "closed"`.
39
+ *
40
+ * Wrappers may return any string — the transport only checks `=== "open"`.
41
+ */
42
+ readonly readyState: string;
43
+ /**
44
+ * Binary type hint for incoming data.
45
+ *
46
+ * The transport writes `"arraybuffer"` on attach as a best-effort hint.
47
+ * It does NOT depend on this being respected — the message handler
48
+ * accepts both `ArrayBuffer` and `Uint8Array` data regardless.
49
+ *
50
+ * For native `RTCDataChannel`, this controls whether `MessageEvent.data`
51
+ * is an `ArrayBuffer` or a `Blob`. For wrappers that ignore this
52
+ * property (e.g. simple-peer bridges), the write is harmless.
53
+ */
54
+ binaryType: string;
55
+ /**
56
+ * Send binary data through the data channel.
57
+ *
58
+ * The transport always sends `Uint8Array` instances (CBOR-encoded
59
+ * wire frames, optionally fragmented). Native `RTCDataChannel.send`
60
+ * accepts `ArrayBufferView` (which `Uint8Array` satisfies), so
61
+ * conformance is structural.
62
+ */
63
+ send(data: Uint8Array): void;
64
+ /**
65
+ * Register an event listener.
66
+ *
67
+ * The transport uses this for `"open"`, `"close"`, `"error"`, and
68
+ * `"message"` events. For `"message"` events, the transport reads
69
+ * `event.data` and handles both `ArrayBuffer` and `Uint8Array`.
70
+ *
71
+ * @param type - Event type string
72
+ * @param listener - Callback. The `event` parameter is untyped to
73
+ * avoid coupling to DOM `Event` / `MessageEvent` types.
74
+ */
75
+ addEventListener(type: string, listener: (event: any) => void): void;
76
+ /**
77
+ * Remove a previously registered event listener.
78
+ *
79
+ * Called during `detachDataChannel()` to clean up all four event
80
+ * listeners. The transport always passes the same function reference
81
+ * that was used in `addEventListener`.
82
+ */
83
+ removeEventListener(type: string, listener: (event: any) => void): void;
83
84
  }
84
-
85
+ //#endregion
86
+ //#region src/webrtc-transport.d.ts
85
87
  /**
86
88
  * Default fragment threshold in bytes.
87
89
  *
@@ -96,20 +98,20 @@ declare const DEFAULT_FRAGMENT_THRESHOLD: number;
96
98
  * Configuration options for the WebRTC transport.
97
99
  */
98
100
  interface WebrtcTransportOptions {
99
- /**
100
- * Fragment threshold in bytes. Messages larger than this are fragmented
101
- * for SCTP compatibility. Set to 0 to disable fragmentation (not recommended).
102
- *
103
- * @default 204800 (200KB)
104
- */
105
- fragmentThreshold?: number;
101
+ /**
102
+ * Fragment threshold in bytes. Messages larger than this are fragmented
103
+ * for SCTP compatibility. Set to 0 to disable fragmentation (not recommended).
104
+ *
105
+ * @default 204800 (200KB)
106
+ */
107
+ fragmentThreshold?: number;
106
108
  }
107
109
  /**
108
110
  * Context for each attached data channel — stored per remotePeerId.
109
111
  */
110
112
  type DataChannelContext = {
111
- remotePeerId: string;
112
- channel: DataChannelLike;
113
+ remotePeerId: string;
114
+ channel: DataChannelLike;
113
115
  };
114
116
  /**
115
117
  * WebRTC data channel transport for @kyneta/exchange.
@@ -130,7 +132,7 @@ type DataChannelContext = {
130
132
  * const webrtcTransport = createWebrtcTransport()
131
133
  *
132
134
  * const exchange = new Exchange({
133
- * identity: { peerId: "alice", name: "Alice" },
135
+ * id: { peerId: "alice", name: "Alice" },
134
136
  * transports: [webrtcTransport],
135
137
  * })
136
138
  *
@@ -149,61 +151,61 @@ type DataChannelContext = {
149
151
  * WebRTC connection lifecycle independently.
150
152
  */
151
153
  declare class WebrtcTransport extends Transport<DataChannelContext> {
152
- #private;
153
- constructor(options?: WebrtcTransportOptions);
154
- /**
155
- * Generate a channel for a data channel context.
156
- *
157
- * Called internally by the `Transport` base class when `addChannel()` is
158
- * invoked. Users never call this directly — use `attachDataChannel()`.
159
- */
160
- protected generate(context: DataChannelContext): GeneratedChannel;
161
- /**
162
- * Called when the transport starts.
163
- *
164
- * No-op for WebRTC — channels are added dynamically via
165
- * `attachDataChannel()`, not at start time.
166
- */
167
- onStart(): Promise<void>;
168
- /**
169
- * Called when the transport stops.
170
- *
171
- * Detaches all attached data channels and cleans up resources.
172
- */
173
- onStop(): Promise<void>;
174
- /**
175
- * Attach a data channel for a remote peer.
176
- *
177
- * Creates an internal sync channel when the data channel is open
178
- * (or waits for the `"open"` event if still connecting). The sync
179
- * channel triggers the establishment handshake with the remote peer.
180
- *
181
- * If a data channel is already attached for this peer, the old one
182
- * is detached first.
183
- *
184
- * @param remotePeerId - The stable peer ID of the remote peer
185
- * @param channel - Any object satisfying `DataChannelLike`
186
- * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`
187
- */
188
- attachDataChannel(remotePeerId: string, channel: DataChannelLike): () => void;
189
- /**
190
- * Detach a data channel for a remote peer.
191
- *
192
- * Removes the sync channel, cleans up event listeners, and disposes
193
- * the reassembler. Does NOT close the data channel — the application
194
- * manages the WebRTC connection lifecycle.
195
- *
196
- * @param remotePeerId - The peer ID to detach
197
- */
198
- detachDataChannel(remotePeerId: string): void;
199
- /**
200
- * Check if a data channel is attached for a peer.
201
- */
202
- hasDataChannel(remotePeerId: string): boolean;
203
- /**
204
- * Get all peer IDs with attached data channels.
205
- */
206
- getAttachedPeerIds(): string[];
154
+ #private;
155
+ constructor(options?: WebrtcTransportOptions);
156
+ /**
157
+ * Generate a channel for a data channel context.
158
+ *
159
+ * Called internally by the `Transport` base class when `addChannel()` is
160
+ * invoked. Users never call this directly — use `attachDataChannel()`.
161
+ */
162
+ protected generate(context: DataChannelContext): GeneratedChannel;
163
+ /**
164
+ * Called when the transport starts.
165
+ *
166
+ * No-op for WebRTC — channels are added dynamically via
167
+ * `attachDataChannel()`, not at start time.
168
+ */
169
+ onStart(): Promise<void>;
170
+ /**
171
+ * Called when the transport stops.
172
+ *
173
+ * Detaches all attached data channels and cleans up resources.
174
+ */
175
+ onStop(): Promise<void>;
176
+ /**
177
+ * Attach a data channel for a remote peer.
178
+ *
179
+ * Creates an internal sync channel when the data channel is open
180
+ * (or waits for the `"open"` event if still connecting). The sync
181
+ * channel triggers the establishment handshake with the remote peer.
182
+ *
183
+ * If a data channel is already attached for this peer, the old one
184
+ * is detached first.
185
+ *
186
+ * @param remotePeerId - The stable peer ID of the remote peer
187
+ * @param channel - Any object satisfying `DataChannelLike`
188
+ * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`
189
+ */
190
+ attachDataChannel(remotePeerId: string, channel: DataChannelLike): () => void;
191
+ /**
192
+ * Detach a data channel for a remote peer.
193
+ *
194
+ * Removes the sync channel, cleans up event listeners, and disposes
195
+ * the reassembler. Does NOT close the data channel — the application
196
+ * manages the WebRTC connection lifecycle.
197
+ *
198
+ * @param remotePeerId - The peer ID to detach
199
+ */
200
+ detachDataChannel(remotePeerId: string): void;
201
+ /**
202
+ * Check if a data channel is attached for a peer.
203
+ */
204
+ hasDataChannel(remotePeerId: string): boolean;
205
+ /**
206
+ * Get all peer IDs with attached data channels.
207
+ */
208
+ getAttachedPeerIds(): string[];
207
209
  }
208
210
  /**
209
211
  * Create a WebRTC transport factory for use with `Exchange`.
@@ -222,11 +224,12 @@ declare class WebrtcTransport extends Transport<DataChannelContext> {
222
224
  * import { createWebrtcTransport } from "@kyneta/webrtc-transport"
223
225
  *
224
226
  * const exchange = new Exchange({
225
- * identity: { peerId: "alice", name: "Alice" },
227
+ * id: { peerId: "alice", name: "Alice" },
226
228
  * transports: [createWebrtcTransport()],
227
229
  * })
228
230
  * ```
229
231
  */
230
232
  declare function createWebrtcTransport(options?: WebrtcTransportOptions): TransportFactory;
231
-
233
+ //#endregion
232
234
  export { DEFAULT_FRAGMENT_THRESHOLD, type DataChannelLike, WebrtcTransport, type WebrtcTransportOptions, createWebrtcTransport };
235
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/data-channel-like.ts","../src/webrtc-transport.ts"],"mappings":";;;;;;AA6CA;;;;;;;;;;;;;;;;;;;;;;ACJA;UDIiB,eAAA;;;;ACKjB;;;;;AAQC;;;WDDU,UAAA;ECWT;;;;;AAwDF;;;;;;EDtDE,UAAA;EC2GiB;;;;;;;;EDjGjB,IAAA,CAAK,IAAA,EAAM,UAAA;;;;;;;;;;;;EAaX,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,KAAA;ECoHxC;;;;;;;ED3GF,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,KAAA;AAAA;;;AAzD/C;;;;;;;;;AAAA,cCJa,0BAAA;;;;UASI,sBAAA;EDoDK;;;;;;EC7CpB,iBAAA;AAAA;AAhBF;;;AAAA,KA0BK,kBAAA;EACH,YAAA;EACA,OAAA,EAAS,eAAA;AAAA;;;;AAXV;;;;;;;;;AAkED;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,eAAA,SAAwB,SAAA,CAAU,kBAAA;EAAA;cAWjC,OAAA,GAAU,sBAAA;EA0EpB;;;;;;EAAA,UA1DQ,QAAA,CAAS,OAAA,EAAS,kBAAA,GAAqB,gBAAA;EA+JjD;;;AAuHF;;;EA5PQ,OAAA,CAAA,GAAW,OAAA;EA6PP;;;;;EAtPJ,MAAA,CAAA,GAAU,OAAA;;;;;;;;;;;;;;;EAwBhB,iBAAA,CACE,YAAA,UACA,OAAA,EAAS,eAAA;;;;;;;;;;EAyEX,iBAAA,CAAkB,YAAA;;;;EAoBlB,cAAA,CAAe,YAAA;;;;EAOf,kBAAA,CAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;iBAuHc,qBAAA,CACd,OAAA,GAAU,sBAAA,GACT,gBAAA"}
package/dist/index.js CHANGED
@@ -1,228 +1,255 @@
1
- // src/webrtc-transport.ts
2
1
  import { Transport } from "@kyneta/transport";
3
- import {
4
- decodeBinaryMessages,
5
- encodeBinaryAndSend,
6
- FragmentReassembler
7
- } from "@kyneta/wire";
8
- var DEFAULT_FRAGMENT_THRESHOLD = 200 * 1024;
2
+ import { FragmentReassembler, decodeBinaryMessages, encodeBinaryAndSend } from "@kyneta/wire";
3
+ //#region src/webrtc-transport.ts
4
+ /**
5
+ * Default fragment threshold in bytes.
6
+ *
7
+ * SCTP (the underlying transport for WebRTC data channels) has a message
8
+ * size limit of approximately 256KB. 200KB provides a safe margin.
9
+ *
10
+ * This differs from the WebSocket transport's 100KB default, which
11
+ * targets AWS API Gateway's 128KB limit. WebRTC has no such gateway.
12
+ */
13
+ const DEFAULT_FRAGMENT_THRESHOLD = 200 * 1024;
14
+ /**
15
+ * WebRTC data channel transport for @kyneta/exchange.
16
+ *
17
+ * Follows a "Bring Your Own Data Channel" (BYODC) design — the application
18
+ * manages WebRTC connections and attaches data channels to this transport
19
+ * for kyneta document synchronization.
20
+ *
21
+ * Uses binary CBOR encoding with transport-level fragmentation via
22
+ * `@kyneta/wire` — the same pipeline as the WebSocket transport.
23
+ *
24
+ * ## Usage
25
+ *
26
+ * ```typescript
27
+ * import { Exchange } from "@kyneta/exchange"
28
+ * import { createWebrtcTransport } from "@kyneta/webrtc-transport"
29
+ *
30
+ * const webrtcTransport = createWebrtcTransport()
31
+ *
32
+ * const exchange = new Exchange({
33
+ * id: { peerId: "alice", name: "Alice" },
34
+ * transports: [webrtcTransport],
35
+ * })
36
+ *
37
+ * // When a WebRTC connection is established:
38
+ * const cleanup = transport.attachDataChannel(remotePeerId, dataChannel)
39
+ *
40
+ * // When done:
41
+ * cleanup() // or transport.detachDataChannel(remotePeerId)
42
+ * ```
43
+ *
44
+ * ## Ownership
45
+ *
46
+ * The transport does NOT own the data channel. `detachDataChannel()`
47
+ * removes the sync channel and event listeners but does not close the
48
+ * data channel or the peer connection. The application manages the
49
+ * WebRTC connection lifecycle independently.
50
+ */
9
51
  var WebrtcTransport = class extends Transport {
10
- /**
11
- * Map of remotePeerId → attached channel tracking.
12
- */
13
- #attachedChannels = /* @__PURE__ */ new Map();
14
- /**
15
- * Fragment threshold in bytes.
16
- */
17
- #fragmentThreshold;
18
- constructor(options) {
19
- super({ transportType: "webrtc-datachannel" });
20
- this.#fragmentThreshold = options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD;
21
- }
22
- // ==========================================================================
23
- // Transport abstract method implementations
24
- // ==========================================================================
25
- /**
26
- * Generate a channel for a data channel context.
27
- *
28
- * Called internally by the `Transport` base class when `addChannel()` is
29
- * invoked. Users never call this directly — use `attachDataChannel()`.
30
- */
31
- generate(context) {
32
- const { channel } = context;
33
- return {
34
- transportType: this.transportType,
35
- send: (msg) => {
36
- if (channel.readyState !== "open") {
37
- return;
38
- }
39
- encodeBinaryAndSend(
40
- msg,
41
- this.#fragmentThreshold,
42
- (data) => channel.send(data)
43
- );
44
- },
45
- stop: () => {
46
- }
47
- };
48
- }
49
- /**
50
- * Called when the transport starts.
51
- *
52
- * No-op for WebRTC channels are added dynamically via
53
- * `attachDataChannel()`, not at start time.
54
- */
55
- async onStart() {
56
- }
57
- /**
58
- * Called when the transport stops.
59
- *
60
- * Detaches all attached data channels and cleans up resources.
61
- */
62
- async onStop() {
63
- for (const remotePeerId of [...this.#attachedChannels.keys()]) {
64
- this.detachDataChannel(remotePeerId);
65
- }
66
- }
67
- // ==========================================================================
68
- // Public API — data channel management
69
- // ==========================================================================
70
- /**
71
- * Attach a data channel for a remote peer.
72
- *
73
- * Creates an internal sync channel when the data channel is open
74
- * (or waits for the `"open"` event if still connecting). The sync
75
- * channel triggers the establishment handshake with the remote peer.
76
- *
77
- * If a data channel is already attached for this peer, the old one
78
- * is detached first.
79
- *
80
- * @param remotePeerId - The stable peer ID of the remote peer
81
- * @param channel - Any object satisfying `DataChannelLike`
82
- * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`
83
- */
84
- attachDataChannel(remotePeerId, channel) {
85
- if (this.#attachedChannels.has(remotePeerId)) {
86
- this.detachDataChannel(remotePeerId);
87
- }
88
- channel.binaryType = "arraybuffer";
89
- const reassembler = new FragmentReassembler({ timeoutMs: 1e4 });
90
- const onOpen = () => {
91
- this.#createSyncChannel(remotePeerId);
92
- };
93
- const onClose = () => {
94
- this.#removeSyncChannel(remotePeerId);
95
- };
96
- const onError = () => {
97
- this.#removeSyncChannel(remotePeerId);
98
- };
99
- const onMessage = (event) => {
100
- this.#handleMessage(remotePeerId, event);
101
- };
102
- const cleanup = () => {
103
- channel.removeEventListener("open", onOpen);
104
- channel.removeEventListener("close", onClose);
105
- channel.removeEventListener("error", onError);
106
- channel.removeEventListener("message", onMessage);
107
- };
108
- channel.addEventListener("open", onOpen);
109
- channel.addEventListener("close", onClose);
110
- channel.addEventListener("error", onError);
111
- channel.addEventListener("message", onMessage);
112
- const attached = {
113
- remotePeerId,
114
- channel,
115
- channelId: null,
116
- reassembler,
117
- cleanup
118
- };
119
- this.#attachedChannels.set(remotePeerId, attached);
120
- if (channel.readyState === "open") {
121
- this.#createSyncChannel(remotePeerId);
122
- }
123
- return () => this.detachDataChannel(remotePeerId);
124
- }
125
- /**
126
- * Detach a data channel for a remote peer.
127
- *
128
- * Removes the sync channel, cleans up event listeners, and disposes
129
- * the reassembler. Does NOT close the data channel — the application
130
- * manages the WebRTC connection lifecycle.
131
- *
132
- * @param remotePeerId - The peer ID to detach
133
- */
134
- detachDataChannel(remotePeerId) {
135
- const attached = this.#attachedChannels.get(remotePeerId);
136
- if (!attached) return;
137
- this.#removeSyncChannel(remotePeerId);
138
- attached.reassembler.dispose();
139
- attached.cleanup();
140
- this.#attachedChannels.delete(remotePeerId);
141
- }
142
- /**
143
- * Check if a data channel is attached for a peer.
144
- */
145
- hasDataChannel(remotePeerId) {
146
- return this.#attachedChannels.has(remotePeerId);
147
- }
148
- /**
149
- * Get all peer IDs with attached data channels.
150
- */
151
- getAttachedPeerIds() {
152
- return [...this.#attachedChannels.keys()];
153
- }
154
- // ==========================================================================
155
- // Internal — sync channel lifecycle
156
- // ==========================================================================
157
- /**
158
- * Create an internal sync channel for an attached data channel.
159
- *
160
- * Called when the data channel's `"open"` event fires (or immediately
161
- * if already open on attach). The sync channel is registered with the
162
- * Transport base class, which triggers the establishment handshake.
163
- */
164
- #createSyncChannel(remotePeerId) {
165
- const attached = this.#attachedChannels.get(remotePeerId);
166
- if (!attached) return;
167
- if (attached.channelId !== null) return;
168
- const syncChannel = this.addChannel({
169
- remotePeerId,
170
- channel: attached.channel
171
- });
172
- attached.channelId = syncChannel.channelId;
173
- this.establishChannel(syncChannel.channelId);
174
- }
175
- /**
176
- * Remove the internal sync channel for a peer.
177
- */
178
- #removeSyncChannel(remotePeerId) {
179
- const attached = this.#attachedChannels.get(remotePeerId);
180
- if (!attached || attached.channelId === null) return;
181
- this.removeChannel(attached.channelId);
182
- attached.channelId = null;
183
- }
184
- // ==========================================================================
185
- // Internal — message handling
186
- // ==========================================================================
187
- /**
188
- * Handle an incoming message from a data channel.
189
- *
190
- * Extracts binary data from the event, feeding both `ArrayBuffer`
191
- * (native RTCDataChannel with binaryType "arraybuffer") and
192
- * `Uint8Array` (simple-peer and other wrappers) into the shared
193
- * decode pipeline.
194
- */
195
- #handleMessage(remotePeerId, event) {
196
- const attached = this.#attachedChannels.get(remotePeerId);
197
- if (!attached || attached.channelId === null) return;
198
- const syncChannel = this.channels.get(attached.channelId);
199
- if (!syncChannel) return;
200
- const raw = event.data;
201
- const bytes = raw instanceof ArrayBuffer ? new Uint8Array(raw) : raw instanceof Uint8Array ? raw : null;
202
- if (!bytes) {
203
- return;
204
- }
205
- try {
206
- const messages = decodeBinaryMessages(bytes, attached.reassembler);
207
- if (messages) {
208
- for (const msg of messages) {
209
- syncChannel.onReceive(msg);
210
- }
211
- }
212
- } catch (error) {
213
- console.error(
214
- `[webrtc-transport] Failed to decode message from peer ${remotePeerId}:`,
215
- error
216
- );
217
- }
218
- }
52
+ /**
53
+ * Map of remotePeerId → attached channel tracking.
54
+ */
55
+ #attachedChannels = /* @__PURE__ */ new Map();
56
+ /**
57
+ * Fragment threshold in bytes.
58
+ */
59
+ #fragmentThreshold;
60
+ constructor(options) {
61
+ super({ transportType: "webrtc-datachannel" });
62
+ this.#fragmentThreshold = options?.fragmentThreshold ?? 204800;
63
+ }
64
+ /**
65
+ * Generate a channel for a data channel context.
66
+ *
67
+ * Called internally by the `Transport` base class when `addChannel()` is
68
+ * invoked. Users never call this directly use `attachDataChannel()`.
69
+ */
70
+ generate(context) {
71
+ const { channel } = context;
72
+ return {
73
+ transportType: this.transportType,
74
+ send: (msg) => {
75
+ if (channel.readyState !== "open") return;
76
+ encodeBinaryAndSend(msg, this.#fragmentThreshold, (data) => channel.send(data));
77
+ },
78
+ stop: () => {}
79
+ };
80
+ }
81
+ /**
82
+ * Called when the transport starts.
83
+ *
84
+ * No-op for WebRTC — channels are added dynamically via
85
+ * `attachDataChannel()`, not at start time.
86
+ */
87
+ async onStart() {}
88
+ /**
89
+ * Called when the transport stops.
90
+ *
91
+ * Detaches all attached data channels and cleans up resources.
92
+ */
93
+ async onStop() {
94
+ for (const remotePeerId of [...this.#attachedChannels.keys()]) this.detachDataChannel(remotePeerId);
95
+ }
96
+ /**
97
+ * Attach a data channel for a remote peer.
98
+ *
99
+ * Creates an internal sync channel when the data channel is open
100
+ * (or waits for the `"open"` event if still connecting). The sync
101
+ * channel triggers the establishment handshake with the remote peer.
102
+ *
103
+ * If a data channel is already attached for this peer, the old one
104
+ * is detached first.
105
+ *
106
+ * @param remotePeerId - The stable peer ID of the remote peer
107
+ * @param channel - Any object satisfying `DataChannelLike`
108
+ * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`
109
+ */
110
+ attachDataChannel(remotePeerId, channel) {
111
+ if (this.#attachedChannels.has(remotePeerId)) this.detachDataChannel(remotePeerId);
112
+ channel.binaryType = "arraybuffer";
113
+ const reassembler = new FragmentReassembler({ timeoutMs: 1e4 });
114
+ const onOpen = () => {
115
+ this.#createSyncChannel(remotePeerId);
116
+ };
117
+ const onClose = () => {
118
+ this.#removeSyncChannel(remotePeerId);
119
+ };
120
+ const onError = () => {
121
+ this.#removeSyncChannel(remotePeerId);
122
+ };
123
+ const onMessage = (event) => {
124
+ this.#handleMessage(remotePeerId, event);
125
+ };
126
+ const cleanup = () => {
127
+ channel.removeEventListener("open", onOpen);
128
+ channel.removeEventListener("close", onClose);
129
+ channel.removeEventListener("error", onError);
130
+ channel.removeEventListener("message", onMessage);
131
+ };
132
+ channel.addEventListener("open", onOpen);
133
+ channel.addEventListener("close", onClose);
134
+ channel.addEventListener("error", onError);
135
+ channel.addEventListener("message", onMessage);
136
+ const attached = {
137
+ remotePeerId,
138
+ channel,
139
+ channelId: null,
140
+ reassembler,
141
+ cleanup
142
+ };
143
+ this.#attachedChannels.set(remotePeerId, attached);
144
+ if (channel.readyState === "open") this.#createSyncChannel(remotePeerId);
145
+ return () => this.detachDataChannel(remotePeerId);
146
+ }
147
+ /**
148
+ * Detach a data channel for a remote peer.
149
+ *
150
+ * Removes the sync channel, cleans up event listeners, and disposes
151
+ * the reassembler. Does NOT close the data channel — the application
152
+ * manages the WebRTC connection lifecycle.
153
+ *
154
+ * @param remotePeerId - The peer ID to detach
155
+ */
156
+ detachDataChannel(remotePeerId) {
157
+ const attached = this.#attachedChannels.get(remotePeerId);
158
+ if (!attached) return;
159
+ this.#removeSyncChannel(remotePeerId);
160
+ attached.reassembler.dispose();
161
+ attached.cleanup();
162
+ this.#attachedChannels.delete(remotePeerId);
163
+ }
164
+ /**
165
+ * Check if a data channel is attached for a peer.
166
+ */
167
+ hasDataChannel(remotePeerId) {
168
+ return this.#attachedChannels.has(remotePeerId);
169
+ }
170
+ /**
171
+ * Get all peer IDs with attached data channels.
172
+ */
173
+ getAttachedPeerIds() {
174
+ return [...this.#attachedChannels.keys()];
175
+ }
176
+ /**
177
+ * Create an internal sync channel for an attached data channel.
178
+ *
179
+ * Called when the data channel's `"open"` event fires (or immediately
180
+ * if already open on attach). The sync channel is registered with the
181
+ * Transport base class, which triggers the establishment handshake.
182
+ */
183
+ #createSyncChannel(remotePeerId) {
184
+ const attached = this.#attachedChannels.get(remotePeerId);
185
+ if (!attached) return;
186
+ if (attached.channelId !== null) return;
187
+ const syncChannel = this.addChannel({
188
+ remotePeerId,
189
+ channel: attached.channel
190
+ });
191
+ attached.channelId = syncChannel.channelId;
192
+ this.establishChannel(syncChannel.channelId);
193
+ }
194
+ /**
195
+ * Remove the internal sync channel for a peer.
196
+ */
197
+ #removeSyncChannel(remotePeerId) {
198
+ const attached = this.#attachedChannels.get(remotePeerId);
199
+ if (!attached || attached.channelId === null) return;
200
+ this.removeChannel(attached.channelId);
201
+ attached.channelId = null;
202
+ }
203
+ /**
204
+ * Handle an incoming message from a data channel.
205
+ *
206
+ * Extracts binary data from the event, feeding both `ArrayBuffer`
207
+ * (native RTCDataChannel with binaryType "arraybuffer") and
208
+ * `Uint8Array` (simple-peer and other wrappers) into the shared
209
+ * decode pipeline.
210
+ */
211
+ #handleMessage(remotePeerId, event) {
212
+ const attached = this.#attachedChannels.get(remotePeerId);
213
+ if (!attached || attached.channelId === null) return;
214
+ const syncChannel = this.channels.get(attached.channelId);
215
+ if (!syncChannel) return;
216
+ const raw = event.data;
217
+ const bytes = raw instanceof ArrayBuffer ? new Uint8Array(raw) : raw instanceof Uint8Array ? raw : null;
218
+ if (!bytes) return;
219
+ try {
220
+ const messages = decodeBinaryMessages(bytes, attached.reassembler);
221
+ if (messages) for (const msg of messages) syncChannel.onReceive(msg);
222
+ } catch (error) {
223
+ console.error(`[webrtc-transport] Failed to decode message from peer ${remotePeerId}:`, error);
224
+ }
225
+ }
219
226
  };
227
+ /**
228
+ * Create a WebRTC transport factory for use with `Exchange`.
229
+ *
230
+ * Returns a `TransportFactory` — pass directly to
231
+ * `Exchange({ transports: [...] })`. The returned transport instance
232
+ * exposes `attachDataChannel()` / `detachDataChannel()` for BYODC
233
+ * data channel management.
234
+ *
235
+ * To access the transport instance after creation, use
236
+ * `exchange.getTransport("webrtc-datachannel")`.
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * import { Exchange } from "@kyneta/exchange"
241
+ * import { createWebrtcTransport } from "@kyneta/webrtc-transport"
242
+ *
243
+ * const exchange = new Exchange({
244
+ * id: { peerId: "alice", name: "Alice" },
245
+ * transports: [createWebrtcTransport()],
246
+ * })
247
+ * ```
248
+ */
220
249
  function createWebrtcTransport(options) {
221
- return () => new WebrtcTransport(options);
250
+ return () => new WebrtcTransport(options);
222
251
  }
223
- export {
224
- DEFAULT_FRAGMENT_THRESHOLD,
225
- WebrtcTransport,
226
- createWebrtcTransport
227
- };
252
+ //#endregion
253
+ export { DEFAULT_FRAGMENT_THRESHOLD, WebrtcTransport, createWebrtcTransport };
254
+
228
255
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/webrtc-transport.ts"],"sourcesContent":["// webrtc-transport — BYODC WebRTC data channel transport for @kyneta/exchange.\n//\n// \"Bring Your Own Data Channel\" design: the application manages WebRTC\n// connections (signaling, ICE, media streams). This transport attaches\n// to already-established data channels for kyneta document sync.\n//\n// Uses the shared binary pipeline from @kyneta/wire (same as WebSocket):\n// encodeBinaryAndSend — outbound: encode → fragment → sendFn\n// decodeBinaryMessages — inbound: reassemble → decode → ChannelMsg[]\n//\n// The transport accepts any object satisfying `DataChannelLike` — a\n// 5-member interface that native RTCDataChannel satisfies structurally\n// and that libraries like simple-peer can conform to via a trivial bridge.\n\nimport type {\n ChannelId,\n ChannelMsg,\n GeneratedChannel,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n decodeBinaryMessages,\n encodeBinaryAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport type { DataChannelLike } from \"./data-channel-like.js\"\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default fragment threshold in bytes.\n *\n * SCTP (the underlying transport for WebRTC data channels) has a message\n * size limit of approximately 256KB. 200KB provides a safe margin.\n *\n * This differs from the WebSocket transport's 100KB default, which\n * targets AWS API Gateway's 128KB limit. WebRTC has no such gateway.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 200 * 1024\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration options for the WebRTC transport.\n */\nexport interface WebrtcTransportOptions {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented\n * for SCTP compatibility. Set to 0 to disable fragmentation (not recommended).\n *\n * @default 204800 (200KB)\n */\n fragmentThreshold?: number\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/**\n * Context for each attached data channel — stored per remotePeerId.\n */\ntype DataChannelContext = {\n remotePeerId: string\n channel: DataChannelLike\n}\n\n/**\n * Internal tracking for an attached data channel.\n */\ntype AttachedChannel = {\n remotePeerId: string\n channel: DataChannelLike\n channelId: ChannelId | null\n reassembler: FragmentReassembler\n cleanup: () => void\n}\n\n// ---------------------------------------------------------------------------\n// WebrtcTransport\n// ---------------------------------------------------------------------------\n\n/**\n * WebRTC data channel transport for @kyneta/exchange.\n *\n * Follows a \"Bring Your Own Data Channel\" (BYODC) design — the application\n * manages WebRTC connections and attaches data channels to this transport\n * for kyneta document synchronization.\n *\n * Uses binary CBOR encoding with transport-level fragmentation via\n * `@kyneta/wire` — the same pipeline as the WebSocket transport.\n *\n * ## Usage\n *\n * ```typescript\n * import { Exchange } from \"@kyneta/exchange\"\n * import { createWebrtcTransport } from \"@kyneta/webrtc-transport\"\n *\n * const webrtcTransport = createWebrtcTransport()\n *\n * const exchange = new Exchange({\n * identity: { peerId: \"alice\", name: \"Alice\" },\n * transports: [webrtcTransport],\n * })\n *\n * // When a WebRTC connection is established:\n * const cleanup = transport.attachDataChannel(remotePeerId, dataChannel)\n *\n * // When done:\n * cleanup() // or transport.detachDataChannel(remotePeerId)\n * ```\n *\n * ## Ownership\n *\n * The transport does NOT own the data channel. `detachDataChannel()`\n * removes the sync channel and event listeners but does not close the\n * data channel or the peer connection. The application manages the\n * WebRTC connection lifecycle independently.\n */\nexport class WebrtcTransport extends Transport<DataChannelContext> {\n /**\n * Map of remotePeerId → attached channel tracking.\n */\n readonly #attachedChannels = new Map<string, AttachedChannel>()\n\n /**\n * Fragment threshold in bytes.\n */\n readonly #fragmentThreshold: number\n\n constructor(options?: WebrtcTransportOptions) {\n super({ transportType: \"webrtc-datachannel\" })\n this.#fragmentThreshold =\n options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n }\n\n // ==========================================================================\n // Transport abstract method implementations\n // ==========================================================================\n\n /**\n * Generate a channel for a data channel context.\n *\n * Called internally by the `Transport` base class when `addChannel()` is\n * invoked. Users never call this directly — use `attachDataChannel()`.\n */\n protected generate(context: DataChannelContext): GeneratedChannel {\n const { channel } = context\n\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n if (channel.readyState !== \"open\") {\n return\n }\n encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>\n channel.send(data),\n )\n },\n stop: () => {\n // Cleanup is handled by detachDataChannel().\n // This callback fires when the internal channel is removed.\n },\n }\n }\n\n /**\n * Called when the transport starts.\n *\n * No-op for WebRTC — channels are added dynamically via\n * `attachDataChannel()`, not at start time.\n */\n async onStart(): Promise<void> {}\n\n /**\n * Called when the transport stops.\n *\n * Detaches all attached data channels and cleans up resources.\n */\n async onStop(): Promise<void> {\n for (const remotePeerId of [...this.#attachedChannels.keys()]) {\n this.detachDataChannel(remotePeerId)\n }\n }\n\n // ==========================================================================\n // Public API — data channel management\n // ==========================================================================\n\n /**\n * Attach a data channel for a remote peer.\n *\n * Creates an internal sync channel when the data channel is open\n * (or waits for the `\"open\"` event if still connecting). The sync\n * channel triggers the establishment handshake with the remote peer.\n *\n * If a data channel is already attached for this peer, the old one\n * is detached first.\n *\n * @param remotePeerId - The stable peer ID of the remote peer\n * @param channel - Any object satisfying `DataChannelLike`\n * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`\n */\n attachDataChannel(\n remotePeerId: string,\n channel: DataChannelLike,\n ): () => void {\n // Detach existing channel for this peer if any\n if (this.#attachedChannels.has(remotePeerId)) {\n this.detachDataChannel(remotePeerId)\n }\n\n // Best-effort: request arraybuffer mode for incoming data.\n // The message handler doesn't depend on this — it accepts both\n // ArrayBuffer and Uint8Array regardless.\n channel.binaryType = \"arraybuffer\"\n\n // Create reassembler for this data channel\n const reassembler = new FragmentReassembler({ timeoutMs: 10_000 })\n\n // Event handlers — stored as named functions for removeEventListener\n const onOpen = () => {\n this.#createSyncChannel(remotePeerId)\n }\n\n const onClose = () => {\n this.#removeSyncChannel(remotePeerId)\n }\n\n const onError = () => {\n this.#removeSyncChannel(remotePeerId)\n }\n\n const onMessage = (event: any) => {\n this.#handleMessage(remotePeerId, event)\n }\n\n // Cleanup function to remove all event listeners\n const cleanup = () => {\n channel.removeEventListener(\"open\", onOpen)\n channel.removeEventListener(\"close\", onClose)\n channel.removeEventListener(\"error\", onError)\n channel.removeEventListener(\"message\", onMessage)\n }\n\n // Register event listeners\n channel.addEventListener(\"open\", onOpen)\n channel.addEventListener(\"close\", onClose)\n channel.addEventListener(\"error\", onError)\n channel.addEventListener(\"message\", onMessage)\n\n // Track the attached channel\n const attached: AttachedChannel = {\n remotePeerId,\n channel,\n channelId: null,\n reassembler,\n cleanup,\n }\n this.#attachedChannels.set(remotePeerId, attached)\n\n // If the channel is already open, create the sync channel immediately\n if (channel.readyState === \"open\") {\n this.#createSyncChannel(remotePeerId)\n }\n\n return () => this.detachDataChannel(remotePeerId)\n }\n\n /**\n * Detach a data channel for a remote peer.\n *\n * Removes the sync channel, cleans up event listeners, and disposes\n * the reassembler. Does NOT close the data channel — the application\n * manages the WebRTC connection lifecycle.\n *\n * @param remotePeerId - The peer ID to detach\n */\n detachDataChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached) return\n\n // Remove the sync channel if it exists\n this.#removeSyncChannel(remotePeerId)\n\n // Dispose the reassembler to clean up timers\n attached.reassembler.dispose()\n\n // Remove event listeners from the data channel\n attached.cleanup()\n\n // Remove from tracking\n this.#attachedChannels.delete(remotePeerId)\n }\n\n /**\n * Check if a data channel is attached for a peer.\n */\n hasDataChannel(remotePeerId: string): boolean {\n return this.#attachedChannels.has(remotePeerId)\n }\n\n /**\n * Get all peer IDs with attached data channels.\n */\n getAttachedPeerIds(): string[] {\n return [...this.#attachedChannels.keys()]\n }\n\n // ==========================================================================\n // Internal — sync channel lifecycle\n // ==========================================================================\n\n /**\n * Create an internal sync channel for an attached data channel.\n *\n * Called when the data channel's `\"open\"` event fires (or immediately\n * if already open on attach). The sync channel is registered with the\n * Transport base class, which triggers the establishment handshake.\n */\n #createSyncChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached) return\n\n // Don't create if already exists\n if (attached.channelId !== null) return\n\n // addChannel() creates and registers the sync channel\n const syncChannel = this.addChannel({\n remotePeerId,\n channel: attached.channel,\n })\n attached.channelId = syncChannel.channelId\n\n // Start the establishment handshake\n this.establishChannel(syncChannel.channelId)\n }\n\n /**\n * Remove the internal sync channel for a peer.\n */\n #removeSyncChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached || attached.channelId === null) return\n\n this.removeChannel(attached.channelId)\n attached.channelId = null\n }\n\n // ==========================================================================\n // Internal — message handling\n // ==========================================================================\n\n /**\n * Handle an incoming message from a data channel.\n *\n * Extracts binary data from the event, feeding both `ArrayBuffer`\n * (native RTCDataChannel with binaryType \"arraybuffer\") and\n * `Uint8Array` (simple-peer and other wrappers) into the shared\n * decode pipeline.\n */\n #handleMessage(remotePeerId: string, event: any): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached || attached.channelId === null) return\n\n const syncChannel = this.channels.get(attached.channelId)\n if (!syncChannel) return\n\n // Extract bytes — robust to both ArrayBuffer and Uint8Array\n const raw = event.data\n const bytes =\n raw instanceof ArrayBuffer\n ? new Uint8Array(raw)\n : raw instanceof Uint8Array\n ? raw\n : null\n\n if (!bytes) {\n // Unexpected data type (e.g. string) — ignore silently\n return\n }\n\n try {\n const messages = decodeBinaryMessages(bytes, attached.reassembler)\n if (messages) {\n for (const msg of messages) {\n syncChannel.onReceive(msg)\n }\n }\n } catch (error) {\n console.error(\n `[webrtc-transport] Failed to decode message from peer ${remotePeerId}:`,\n error,\n )\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a WebRTC transport factory for use with `Exchange`.\n *\n * Returns a `TransportFactory` — pass directly to\n * `Exchange({ transports: [...] })`. The returned transport instance\n * exposes `attachDataChannel()` / `detachDataChannel()` for BYODC\n * data channel management.\n *\n * To access the transport instance after creation, use\n * `exchange.getTransport(\"webrtc-datachannel\")`.\n *\n * @example\n * ```typescript\n * import { Exchange } from \"@kyneta/exchange\"\n * import { createWebrtcTransport } from \"@kyneta/webrtc-transport\"\n *\n * const exchange = new Exchange({\n * identity: { peerId: \"alice\", name: \"Alice\" },\n * transports: [createWebrtcTransport()],\n * })\n * ```\n */\nexport function createWebrtcTransport(\n options?: WebrtcTransportOptions,\n): TransportFactory {\n return () => new WebrtcTransport(options)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"index.js","names":["#attachedChannels","#fragmentThreshold","#createSyncChannel","#removeSyncChannel","#handleMessage"],"sources":["../src/webrtc-transport.ts"],"sourcesContent":["// webrtc-transport — BYODC WebRTC data channel transport for @kyneta/exchange.\n//\n// \"Bring Your Own Data Channel\" design: the application manages WebRTC\n// connections (signaling, ICE, media streams). This transport attaches\n// to already-established data channels for kyneta document sync.\n//\n// Uses the shared binary pipeline from @kyneta/wire (same as WebSocket):\n// encodeBinaryAndSend — outbound: encode → fragment → sendFn\n// decodeBinaryMessages — inbound: reassemble → decode → ChannelMsg[]\n//\n// The transport accepts any object satisfying `DataChannelLike` — a\n// 5-member interface that native RTCDataChannel satisfies structurally\n// and that libraries like simple-peer can conform to via a trivial bridge.\n\nimport type {\n ChannelId,\n ChannelMsg,\n GeneratedChannel,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n decodeBinaryMessages,\n encodeBinaryAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport type { DataChannelLike } from \"./data-channel-like.js\"\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default fragment threshold in bytes.\n *\n * SCTP (the underlying transport for WebRTC data channels) has a message\n * size limit of approximately 256KB. 200KB provides a safe margin.\n *\n * This differs from the WebSocket transport's 100KB default, which\n * targets AWS API Gateway's 128KB limit. WebRTC has no such gateway.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 200 * 1024\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Configuration options for the WebRTC transport.\n */\nexport interface WebrtcTransportOptions {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented\n * for SCTP compatibility. Set to 0 to disable fragmentation (not recommended).\n *\n * @default 204800 (200KB)\n */\n fragmentThreshold?: number\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/**\n * Context for each attached data channel — stored per remotePeerId.\n */\ntype DataChannelContext = {\n remotePeerId: string\n channel: DataChannelLike\n}\n\n/**\n * Internal tracking for an attached data channel.\n */\ntype AttachedChannel = {\n remotePeerId: string\n channel: DataChannelLike\n channelId: ChannelId | null\n reassembler: FragmentReassembler\n cleanup: () => void\n}\n\n// ---------------------------------------------------------------------------\n// WebrtcTransport\n// ---------------------------------------------------------------------------\n\n/**\n * WebRTC data channel transport for @kyneta/exchange.\n *\n * Follows a \"Bring Your Own Data Channel\" (BYODC) design — the application\n * manages WebRTC connections and attaches data channels to this transport\n * for kyneta document synchronization.\n *\n * Uses binary CBOR encoding with transport-level fragmentation via\n * `@kyneta/wire` — the same pipeline as the WebSocket transport.\n *\n * ## Usage\n *\n * ```typescript\n * import { Exchange } from \"@kyneta/exchange\"\n * import { createWebrtcTransport } from \"@kyneta/webrtc-transport\"\n *\n * const webrtcTransport = createWebrtcTransport()\n *\n * const exchange = new Exchange({\n * id: { peerId: \"alice\", name: \"Alice\" },\n * transports: [webrtcTransport],\n * })\n *\n * // When a WebRTC connection is established:\n * const cleanup = transport.attachDataChannel(remotePeerId, dataChannel)\n *\n * // When done:\n * cleanup() // or transport.detachDataChannel(remotePeerId)\n * ```\n *\n * ## Ownership\n *\n * The transport does NOT own the data channel. `detachDataChannel()`\n * removes the sync channel and event listeners but does not close the\n * data channel or the peer connection. The application manages the\n * WebRTC connection lifecycle independently.\n */\nexport class WebrtcTransport extends Transport<DataChannelContext> {\n /**\n * Map of remotePeerId → attached channel tracking.\n */\n readonly #attachedChannels = new Map<string, AttachedChannel>()\n\n /**\n * Fragment threshold in bytes.\n */\n readonly #fragmentThreshold: number\n\n constructor(options?: WebrtcTransportOptions) {\n super({ transportType: \"webrtc-datachannel\" })\n this.#fragmentThreshold =\n options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n }\n\n // ==========================================================================\n // Transport abstract method implementations\n // ==========================================================================\n\n /**\n * Generate a channel for a data channel context.\n *\n * Called internally by the `Transport` base class when `addChannel()` is\n * invoked. Users never call this directly — use `attachDataChannel()`.\n */\n protected generate(context: DataChannelContext): GeneratedChannel {\n const { channel } = context\n\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n if (channel.readyState !== \"open\") {\n return\n }\n encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>\n channel.send(data),\n )\n },\n stop: () => {\n // Cleanup is handled by detachDataChannel().\n // This callback fires when the internal channel is removed.\n },\n }\n }\n\n /**\n * Called when the transport starts.\n *\n * No-op for WebRTC — channels are added dynamically via\n * `attachDataChannel()`, not at start time.\n */\n async onStart(): Promise<void> {}\n\n /**\n * Called when the transport stops.\n *\n * Detaches all attached data channels and cleans up resources.\n */\n async onStop(): Promise<void> {\n for (const remotePeerId of [...this.#attachedChannels.keys()]) {\n this.detachDataChannel(remotePeerId)\n }\n }\n\n // ==========================================================================\n // Public API — data channel management\n // ==========================================================================\n\n /**\n * Attach a data channel for a remote peer.\n *\n * Creates an internal sync channel when the data channel is open\n * (or waits for the `\"open\"` event if still connecting). The sync\n * channel triggers the establishment handshake with the remote peer.\n *\n * If a data channel is already attached for this peer, the old one\n * is detached first.\n *\n * @param remotePeerId - The stable peer ID of the remote peer\n * @param channel - Any object satisfying `DataChannelLike`\n * @returns A cleanup function that calls `detachDataChannel(remotePeerId)`\n */\n attachDataChannel(\n remotePeerId: string,\n channel: DataChannelLike,\n ): () => void {\n // Detach existing channel for this peer if any\n if (this.#attachedChannels.has(remotePeerId)) {\n this.detachDataChannel(remotePeerId)\n }\n\n // Best-effort: request arraybuffer mode for incoming data.\n // The message handler doesn't depend on this — it accepts both\n // ArrayBuffer and Uint8Array regardless.\n channel.binaryType = \"arraybuffer\"\n\n // Create reassembler for this data channel\n const reassembler = new FragmentReassembler({ timeoutMs: 10_000 })\n\n // Event handlers — stored as named functions for removeEventListener\n const onOpen = () => {\n this.#createSyncChannel(remotePeerId)\n }\n\n const onClose = () => {\n this.#removeSyncChannel(remotePeerId)\n }\n\n const onError = () => {\n this.#removeSyncChannel(remotePeerId)\n }\n\n const onMessage = (event: any) => {\n this.#handleMessage(remotePeerId, event)\n }\n\n // Cleanup function to remove all event listeners\n const cleanup = () => {\n channel.removeEventListener(\"open\", onOpen)\n channel.removeEventListener(\"close\", onClose)\n channel.removeEventListener(\"error\", onError)\n channel.removeEventListener(\"message\", onMessage)\n }\n\n // Register event listeners\n channel.addEventListener(\"open\", onOpen)\n channel.addEventListener(\"close\", onClose)\n channel.addEventListener(\"error\", onError)\n channel.addEventListener(\"message\", onMessage)\n\n // Track the attached channel\n const attached: AttachedChannel = {\n remotePeerId,\n channel,\n channelId: null,\n reassembler,\n cleanup,\n }\n this.#attachedChannels.set(remotePeerId, attached)\n\n // If the channel is already open, create the sync channel immediately\n if (channel.readyState === \"open\") {\n this.#createSyncChannel(remotePeerId)\n }\n\n return () => this.detachDataChannel(remotePeerId)\n }\n\n /**\n * Detach a data channel for a remote peer.\n *\n * Removes the sync channel, cleans up event listeners, and disposes\n * the reassembler. Does NOT close the data channel — the application\n * manages the WebRTC connection lifecycle.\n *\n * @param remotePeerId - The peer ID to detach\n */\n detachDataChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached) return\n\n // Remove the sync channel if it exists\n this.#removeSyncChannel(remotePeerId)\n\n // Dispose the reassembler to clean up timers\n attached.reassembler.dispose()\n\n // Remove event listeners from the data channel\n attached.cleanup()\n\n // Remove from tracking\n this.#attachedChannels.delete(remotePeerId)\n }\n\n /**\n * Check if a data channel is attached for a peer.\n */\n hasDataChannel(remotePeerId: string): boolean {\n return this.#attachedChannels.has(remotePeerId)\n }\n\n /**\n * Get all peer IDs with attached data channels.\n */\n getAttachedPeerIds(): string[] {\n return [...this.#attachedChannels.keys()]\n }\n\n // ==========================================================================\n // Internal — sync channel lifecycle\n // ==========================================================================\n\n /**\n * Create an internal sync channel for an attached data channel.\n *\n * Called when the data channel's `\"open\"` event fires (or immediately\n * if already open on attach). The sync channel is registered with the\n * Transport base class, which triggers the establishment handshake.\n */\n #createSyncChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached) return\n\n // Don't create if already exists\n if (attached.channelId !== null) return\n\n // addChannel() creates and registers the sync channel\n const syncChannel = this.addChannel({\n remotePeerId,\n channel: attached.channel,\n })\n attached.channelId = syncChannel.channelId\n\n // Start the establishment handshake\n this.establishChannel(syncChannel.channelId)\n }\n\n /**\n * Remove the internal sync channel for a peer.\n */\n #removeSyncChannel(remotePeerId: string): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached || attached.channelId === null) return\n\n this.removeChannel(attached.channelId)\n attached.channelId = null\n }\n\n // ==========================================================================\n // Internal — message handling\n // ==========================================================================\n\n /**\n * Handle an incoming message from a data channel.\n *\n * Extracts binary data from the event, feeding both `ArrayBuffer`\n * (native RTCDataChannel with binaryType \"arraybuffer\") and\n * `Uint8Array` (simple-peer and other wrappers) into the shared\n * decode pipeline.\n */\n #handleMessage(remotePeerId: string, event: any): void {\n const attached = this.#attachedChannels.get(remotePeerId)\n if (!attached || attached.channelId === null) return\n\n const syncChannel = this.channels.get(attached.channelId)\n if (!syncChannel) return\n\n // Extract bytes — robust to both ArrayBuffer and Uint8Array\n const raw = event.data\n const bytes =\n raw instanceof ArrayBuffer\n ? new Uint8Array(raw)\n : raw instanceof Uint8Array\n ? raw\n : null\n\n if (!bytes) {\n // Unexpected data type (e.g. string) — ignore silently\n return\n }\n\n try {\n const messages = decodeBinaryMessages(bytes, attached.reassembler)\n if (messages) {\n for (const msg of messages) {\n syncChannel.onReceive(msg)\n }\n }\n } catch (error) {\n console.error(\n `[webrtc-transport] Failed to decode message from peer ${remotePeerId}:`,\n error,\n )\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a WebRTC transport factory for use with `Exchange`.\n *\n * Returns a `TransportFactory` — pass directly to\n * `Exchange({ transports: [...] })`. The returned transport instance\n * exposes `attachDataChannel()` / `detachDataChannel()` for BYODC\n * data channel management.\n *\n * To access the transport instance after creation, use\n * `exchange.getTransport(\"webrtc-datachannel\")`.\n *\n * @example\n * ```typescript\n * import { Exchange } from \"@kyneta/exchange\"\n * import { createWebrtcTransport } from \"@kyneta/webrtc-transport\"\n *\n * const exchange = new Exchange({\n * id: { peerId: \"alice\", name: \"Alice\" },\n * transports: [createWebrtcTransport()],\n * })\n * ```\n */\nexport function createWebrtcTransport(\n options?: WebrtcTransportOptions,\n): TransportFactory {\n return () => new WebrtcTransport(options)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyneta/webrtc-transport",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "WebRTC data channel transport for @kyneta/exchange — BYODC (Bring Your Own Data Channel) with DataChannelLike interface",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
@@ -25,21 +25,21 @@
25
25
  "./src/*": "./src/*"
26
26
  },
27
27
  "peerDependencies": {
28
- "@kyneta/transport": "^1.3.1",
29
- "@kyneta/wire": "^1.3.1"
28
+ "@kyneta/transport": "^1.4.0",
29
+ "@kyneta/wire": "^1.4.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22",
33
- "tsup": "^8.5.0",
33
+ "tsdown": "^0.21.9",
34
34
  "typescript": "^5.9.2",
35
35
  "vitest": "^4.0.17",
36
- "@kyneta/exchange": "^1.3.1",
37
- "@kyneta/wire": "^1.3.1",
38
- "@kyneta/schema": "^1.3.1",
39
- "@kyneta/transport": "^1.3.1"
36
+ "@kyneta/exchange": "^1.4.0",
37
+ "@kyneta/wire": "^1.4.0",
38
+ "@kyneta/schema": "^1.4.0",
39
+ "@kyneta/transport": "^1.4.0"
40
40
  },
41
41
  "scripts": {
42
- "build": "tsup",
42
+ "build": "tsdown",
43
43
  "test": "verify logic",
44
44
  "verify": "verify"
45
45
  }
@@ -45,9 +45,9 @@ async function initializeTransport(
45
45
  return ctx
46
46
  }
47
47
 
48
- /** A minimal establish-request message for send/receive tests. */
48
+ /** A minimal establish message for send/receive tests. */
49
49
  const TEST_MSG: ChannelMsg = {
50
- type: "establish-request",
50
+ type: "establish",
51
51
  identity: { peerId: "remote", name: "R", type: "user" },
52
52
  }
53
53
 
@@ -210,7 +210,7 @@ describe("Receive", () => {
210
210
  if (!callArgs)
211
211
  throw new Error("expected onChannelReceive to have been called")
212
212
  const [, receivedMsg] = callArgs
213
- expect(receivedMsg.type).toBe("establish-request")
213
+ expect(receivedMsg.type).toBe("establish")
214
214
  expect((receivedMsg as any).identity.peerId).toBe("remote")
215
215
  })
216
216
 
@@ -229,7 +229,7 @@ describe("Receive", () => {
229
229
  if (!callArgs)
230
230
  throw new Error("expected onChannelReceive to have been called")
231
231
  const [, receivedMsg] = callArgs
232
- expect(receivedMsg.type).toBe("establish-request")
232
+ expect(receivedMsg.type).toBe("establish")
233
233
  expect((receivedMsg as any).identity.peerId).toBe("remote")
234
234
  })
235
235
 
@@ -303,7 +303,7 @@ describe("Fragmentation", () => {
303
303
  // binary frame may exceed it if the CBOR encoding + frame header is large enough.
304
304
  // Use a message with enough payload to guarantee fragmentation.
305
305
  const largeMsg: ChannelMsg = {
306
- type: "establish-request",
306
+ type: "establish",
307
307
  identity: {
308
308
  peerId: `a]very-long-peer-id-${"x".repeat(200)}`,
309
309
  name: `A Long Name ${"y".repeat(200)}`,
@@ -326,7 +326,7 @@ describe("Fragmentation", () => {
326
326
 
327
327
  // Build a message large enough to guarantee multiple fragments at chunk size 50
328
328
  const largeMsg: ChannelMsg = {
329
- type: "establish-request",
329
+ type: "establish",
330
330
  identity: {
331
331
  peerId: `peer-${"z".repeat(200)}`,
332
332
  name: `Name-${"w".repeat(200)}`,
@@ -364,7 +364,7 @@ describe("Fragmentation", () => {
364
364
  if (!callArgs)
365
365
  throw new Error("expected onChannelReceive to have been called")
366
366
  const [, receivedMsg] = callArgs
367
- expect(receivedMsg.type).toBe("establish-request")
367
+ expect(receivedMsg.type).toBe("establish")
368
368
  expect((receivedMsg as any).identity.peerId).toBe(`peer-${"z".repeat(200)}`)
369
369
  })
370
370
  })
@@ -104,7 +104,7 @@ type AttachedChannel = {
104
104
  * const webrtcTransport = createWebrtcTransport()
105
105
  *
106
106
  * const exchange = new Exchange({
107
- * identity: { peerId: "alice", name: "Alice" },
107
+ * id: { peerId: "alice", name: "Alice" },
108
108
  * transports: [webrtcTransport],
109
109
  * })
110
110
  *
@@ -422,7 +422,7 @@ export class WebrtcTransport extends Transport<DataChannelContext> {
422
422
  * import { createWebrtcTransport } from "@kyneta/webrtc-transport"
423
423
  *
424
424
  * const exchange = new Exchange({
425
- * identity: { peerId: "alice", name: "Alice" },
425
+ * id: { peerId: "alice", name: "Alice" },
426
426
  * transports: [createWebrtcTransport()],
427
427
  * })
428
428
  * ```