codehost 0.20.4 → 0.20.5
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/shared/rtc.ts +8 -0
- package/src/web/conn-broker.ts +5 -4
- package/src/web/discovery.tsx +2 -2
- package/src/web/room-client.ts +1 -1
- package/src/web/rtc-client.ts +20 -2
- package/src/web/tunnel-client.ts +31 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.20.5](https://github.com/snomiao/codehost/compare/v0.20.4...v0.20.5) (2026-06-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Performance Improvements
|
|
5
|
+
|
|
6
|
+
* **tunnel:** dedicated bulk lane — HTTP bodies ride a second data channel so they can't HOL-block typing ([b14797b](https://github.com/snomiao/codehost/commit/b14797b5e4efb4e3f299ba9c3b78bdf360a9a064))
|
|
7
|
+
|
|
1
8
|
## [0.20.4](https://github.com/snomiao/codehost/compare/v0.20.3...v0.20.4) (2026-06-12)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/shared/rtc.ts
CHANGED
|
@@ -24,3 +24,11 @@ export type RtcSignal = SdpSignal | CandidateSignal;
|
|
|
24
24
|
|
|
25
25
|
/** Label used for the control/tunnel data channel. */
|
|
26
26
|
export const CHANNEL_LABEL = "codehost";
|
|
27
|
+
/**
|
|
28
|
+
* Second data channel for bulk HTTP bodies. Separate channel = separate SCTP
|
|
29
|
+
* stream, so multi-MB asset downloads no longer head-of-line block the
|
|
30
|
+
* interactive WS traffic (VS Code remote protocol, terminal) on
|
|
31
|
+
* CHANNEL_LABEL. The daemon spins up one Tunnel per incoming channel, so old
|
|
32
|
+
* daemons handle this unmodified; old browsers simply never open it.
|
|
33
|
+
*/
|
|
34
|
+
export const BULK_CHANNEL_LABEL = "codehost-bulk";
|
package/src/web/conn-broker.ts
CHANGED
|
@@ -15,9 +15,10 @@ import { TunnelClient, type TunnelLike, type TunnelWsHandlers, type TunnelWsHand
|
|
|
15
15
|
|
|
16
16
|
type AnyMsg = Record<string, any>;
|
|
17
17
|
|
|
18
|
-
/** Creates the RTCPeerConnection for a peer and resolves with its open
|
|
18
|
+
/** Creates the RTCPeerConnection for a peer and resolves with its open
|
|
19
|
+
* interactive channel plus the (possibly still-connecting) bulk lane.
|
|
19
20
|
* Provided by the UI (discovery.tsx) and invoked only when this tab is owner. */
|
|
20
|
-
export type Establish = () => Promise<RTCDataChannel>;
|
|
21
|
+
export type Establish = () => Promise<{ channel: RTCDataChannel; bulk: RTCDataChannel | null }>;
|
|
21
22
|
|
|
22
23
|
class ConnBroker {
|
|
23
24
|
private port: MessagePort | null = null;
|
|
@@ -110,8 +111,8 @@ class ConnBroker {
|
|
|
110
111
|
if (!establish) return;
|
|
111
112
|
this.establishing.add(peerId);
|
|
112
113
|
try {
|
|
113
|
-
const channel = await establish();
|
|
114
|
-
this.locals.set(peerId, new TunnelClient(channel));
|
|
114
|
+
const { channel, bulk } = await establish();
|
|
115
|
+
this.locals.set(peerId, new TunnelClient(channel, bulk));
|
|
115
116
|
this.post({ t: "ready", peerId });
|
|
116
117
|
this.resolveReady(peerId);
|
|
117
118
|
} catch (err) {
|
package/src/web/discovery.tsx
CHANGED
|
@@ -431,7 +431,7 @@ export function Discovery() {
|
|
|
431
431
|
// only invoked when we're the owner (or get promoted on failover); other
|
|
432
432
|
// tabs reuse the owner's channel via a proxy, so they never open WebRTC.
|
|
433
433
|
const establish = () =>
|
|
434
|
-
new Promise<RTCDataChannel>((resolve, reject) => {
|
|
434
|
+
new Promise<{ channel: RTCDataChannel; bulk: RTCDataChannel | null }>((resolve, reject) => {
|
|
435
435
|
const rtc = new RtcClient({
|
|
436
436
|
sendSignal: (data: RtcSignal) => send(server.peerId, data),
|
|
437
437
|
onState: (state) => {
|
|
@@ -439,7 +439,7 @@ export function Discovery() {
|
|
|
439
439
|
},
|
|
440
440
|
onOpen: (channel) => {
|
|
441
441
|
clearTimeout(timer);
|
|
442
|
-
resolve(channel);
|
|
442
|
+
resolve({ channel, bulk: rtc.bulkChannel });
|
|
443
443
|
},
|
|
444
444
|
onClose: () => setConnState((s) => (s === "connected" ? "idle" : s)),
|
|
445
445
|
});
|
package/src/web/room-client.ts
CHANGED
package/src/web/rtc-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CHANNEL_LABEL, ICE_SERVERS, type RtcSignal } from "../shared/rtc";
|
|
1
|
+
import { BULK_CHANNEL_LABEL, CHANNEL_LABEL, ICE_SERVERS, type RtcSignal } from "../shared/rtc";
|
|
2
2
|
|
|
3
3
|
export interface RtcClientOptions {
|
|
4
4
|
/** Relay a signal to the server peer via the signaling channel. */
|
|
@@ -15,6 +15,7 @@ export interface RtcClientOptions {
|
|
|
15
15
|
export class RtcClient {
|
|
16
16
|
private pc: RTCPeerConnection;
|
|
17
17
|
private channel: RTCDataChannel | null = null;
|
|
18
|
+
private bulk: RTCDataChannel | null = null;
|
|
18
19
|
|
|
19
20
|
constructor(private opts: RtcClientOptions) {
|
|
20
21
|
this.pc = new RTCPeerConnection({
|
|
@@ -36,7 +37,7 @@ export class RtcClient {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** Create the data
|
|
40
|
+
/** Create the data channels + offer and kick off the handshake. */
|
|
40
41
|
async start(): Promise<void> {
|
|
41
42
|
const channel = this.pc.createDataChannel(CHANNEL_LABEL, { ordered: true });
|
|
42
43
|
channel.binaryType = "arraybuffer";
|
|
@@ -44,6 +45,13 @@ export class RtcClient {
|
|
|
44
45
|
channel.onopen = () => this.opts.onOpen?.(channel);
|
|
45
46
|
channel.onclose = () => this.opts.onClose?.();
|
|
46
47
|
|
|
48
|
+
// Bulk lane for HTTP bodies (its own SCTP stream — no HOL with the
|
|
49
|
+
// interactive channel above). Senders fall back to the interactive channel
|
|
50
|
+
// until it opens, so nothing waits on it.
|
|
51
|
+
const bulk = this.pc.createDataChannel(BULK_CHANNEL_LABEL, { ordered: true });
|
|
52
|
+
bulk.binaryType = "arraybuffer";
|
|
53
|
+
this.bulk = bulk;
|
|
54
|
+
|
|
47
55
|
const offer = await this.pc.createOffer();
|
|
48
56
|
await this.pc.setLocalDescription(offer);
|
|
49
57
|
this.opts.sendSignal({ kind: "offer", type: "offer", sdp: offer.sdp ?? "" });
|
|
@@ -69,6 +77,11 @@ export class RtcClient {
|
|
|
69
77
|
return this.channel;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/** The bulk lane (may still be CONNECTING when the interactive one opens). */
|
|
81
|
+
get bulkChannel(): RTCDataChannel | null {
|
|
82
|
+
return this.bulk;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
/**
|
|
73
86
|
* Which ICE path the nominated candidate pair uses: "lan" when both ends
|
|
74
87
|
* are host candidates (same network — traffic never leaves it), "p2p" for a
|
|
@@ -111,6 +124,11 @@ export class RtcClient {
|
|
|
111
124
|
} catch {
|
|
112
125
|
// ignore
|
|
113
126
|
}
|
|
127
|
+
try {
|
|
128
|
+
this.bulk?.close();
|
|
129
|
+
} catch {
|
|
130
|
+
// ignore
|
|
131
|
+
}
|
|
114
132
|
try {
|
|
115
133
|
this.pc.close();
|
|
116
134
|
} catch {
|
package/src/web/tunnel-client.ts
CHANGED
|
@@ -49,9 +49,24 @@ export class TunnelClient {
|
|
|
49
49
|
private wsRx = new WsReassembler(); // reassembles daemon -> browser WS messages
|
|
50
50
|
private textEncoder = new TextEncoder();
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `channel` carries the interactive traffic (WebSocket frames — VS Code's
|
|
54
|
+
* remote protocol, terminals); `bulk`, when provided and open, carries HTTP
|
|
55
|
+
* request/response streams on its own SCTP stream so multi-MB asset bodies
|
|
56
|
+
* never head-of-line block a keystroke. The daemon runs one Tunnel per
|
|
57
|
+
* channel, so each lane answers on itself and no demuxing is needed beyond
|
|
58
|
+
* listening on both.
|
|
59
|
+
*/
|
|
60
|
+
constructor(
|
|
61
|
+
private channel: RTCDataChannel,
|
|
62
|
+
private bulk: RTCDataChannel | null = null,
|
|
63
|
+
) {
|
|
53
64
|
channel.binaryType = "arraybuffer";
|
|
54
65
|
channel.addEventListener("message", (ev) => this.onFrame(ev.data));
|
|
66
|
+
if (bulk) {
|
|
67
|
+
bulk.binaryType = "arraybuffer";
|
|
68
|
+
bulk.addEventListener("message", (ev) => this.onFrame(ev.data));
|
|
69
|
+
}
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
private allocId(): number {
|
|
@@ -171,11 +186,16 @@ export class TunnelClient {
|
|
|
171
186
|
},
|
|
172
187
|
});
|
|
173
188
|
|
|
174
|
-
|
|
189
|
+
// HTTP rides the bulk lane. Pinned per request: every frame of this
|
|
190
|
+
// stream must hit the SAME channel — the daemon runs one Tunnel per
|
|
191
|
+
// channel, so a mid-request switch (e.g. bulk finishing its handshake)
|
|
192
|
+
// would strand the stream across two Tunnels.
|
|
193
|
+
const lane = this.bulk?.readyState === "open" ? this.bulk : this.channel;
|
|
194
|
+
this.sendOn(lane, encodeJson(Op.HttpReq, streamId, { method, path, headers: reqHeaders }));
|
|
175
195
|
if (body && body.byteLength) {
|
|
176
|
-
for (const part of chunk(body)) this.
|
|
196
|
+
for (const part of chunk(body)) this.sendOn(lane, encodeFrame(Op.HttpReqBody, streamId, part));
|
|
177
197
|
}
|
|
178
|
-
this.
|
|
198
|
+
this.sendOn(lane, encodeFrame(Op.HttpReqEnd, streamId));
|
|
179
199
|
});
|
|
180
200
|
}
|
|
181
201
|
|
|
@@ -198,12 +218,17 @@ export class TunnelClient {
|
|
|
198
218
|
};
|
|
199
219
|
}
|
|
200
220
|
|
|
221
|
+
/** Interactive-channel send (WS frames, control). */
|
|
201
222
|
private send(frame: Uint8Array): void {
|
|
202
|
-
|
|
223
|
+
this.sendOn(this.channel, frame);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private sendOn(ch: RTCDataChannel, frame: Uint8Array): void {
|
|
227
|
+
if (ch.readyState === "open") {
|
|
203
228
|
// Copy into a fresh ArrayBuffer-backed view to satisfy send()'s typing.
|
|
204
229
|
const copy = new Uint8Array(frame.byteLength);
|
|
205
230
|
copy.set(frame);
|
|
206
|
-
|
|
231
|
+
ch.send(copy.buffer);
|
|
207
232
|
}
|
|
208
233
|
}
|
|
209
234
|
|