codehost 0.11.0 → 0.11.1
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/cli/run-server.ts +7 -1
- package/src/shared/signaling-client.ts +43 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.11.1](https://github.com/snomiao/codehost/compare/v0.11.0...v0.11.1) (2026-06-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **signaling:** harden reconnect backoff + log close code/duration ([a0aa1ce](https://github.com/snomiao/codehost/commit/a0aa1ce7aeae49815fc51dd272a8e08852ed55b2))
|
|
7
|
+
|
|
1
8
|
# [0.11.0](https://github.com/snomiao/codehost/compare/v0.10.0...v0.11.0) (2026-06-09)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/cli/run-server.ts
CHANGED
|
@@ -50,7 +50,13 @@ export async function runServer(opts: RunServerOptions): Promise<never> {
|
|
|
50
50
|
peerId,
|
|
51
51
|
meta: opts.meta,
|
|
52
52
|
onOpen: () => console.log(`[codehost] registered as "${opts.meta.name}" (${peerId.slice(0, 8)})`),
|
|
53
|
-
onClose: () =>
|
|
53
|
+
onClose: (info) => {
|
|
54
|
+
// Surface the close code + how long the socket lived: a near-instant drop
|
|
55
|
+
// (low ms) points at a middlebox killing the WebSocket after the upgrade,
|
|
56
|
+
// not the signaling server. Helps triage field reconnect storms.
|
|
57
|
+
const detail = info ? ` (code ${info.code}${info.reason ? ` "${info.reason}"` : ""}, up ${info.ms}ms)` : "";
|
|
58
|
+
console.log(`[codehost] disconnected from signaling${detail}, reconnecting…`);
|
|
59
|
+
},
|
|
54
60
|
onSignal: (from, data) => rtc.handleSignal(from, data),
|
|
55
61
|
});
|
|
56
62
|
|
|
@@ -17,9 +17,26 @@ export interface SignalingClientOptions {
|
|
|
17
17
|
onPeers?: (peers: PeerInfo[]) => void;
|
|
18
18
|
onSignal?: (from: string, data: unknown) => void;
|
|
19
19
|
onOpen?: () => void;
|
|
20
|
-
|
|
20
|
+
/** Called on every socket close. `info` carries the WebSocket close code,
|
|
21
|
+
* reason, and how long the socket stayed open (ms) — for diagnosing networks
|
|
22
|
+
* that complete the upgrade then drop the connection. */
|
|
23
|
+
onClose?: (info?: CloseInfo) => void;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
export interface CloseInfo {
|
|
27
|
+
code: number;
|
|
28
|
+
reason: string;
|
|
29
|
+
/** Milliseconds the socket was open before it closed. */
|
|
30
|
+
ms: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reset the reconnect backoff only after a socket has stayed open this long. A
|
|
34
|
+
* connection that completes the handshake then drops within seconds (a
|
|
35
|
+
* middlebox that accepts the WebSocket upgrade but kills the socket, seen on
|
|
36
|
+
* some field networks) must keep backing off — otherwise every reset-to-1s
|
|
37
|
+
* open/close cycle becomes a sub-second reconnect storm. */
|
|
38
|
+
const STABLE_MS = 10_000;
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
41
|
* Thin WebSocket client for the signaling room. Runs unchanged in the browser
|
|
25
42
|
* and in Bun (both expose a global `WebSocket`). Auto-reconnects with backoff
|
|
@@ -31,6 +48,10 @@ export class SignalingClient {
|
|
|
31
48
|
private closed = false;
|
|
32
49
|
private reconnectDelay = 1000;
|
|
33
50
|
private heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
/** Fires STABLE_MS after a socket opens; only then is the backoff reset. */
|
|
52
|
+
private stableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
/** Wall-clock ms when the current socket opened (0 if never/closed). */
|
|
54
|
+
private openedAt = 0;
|
|
34
55
|
|
|
35
56
|
constructor(private opts: SignalingClientOptions) {
|
|
36
57
|
this.peerId = opts.peerId ?? newPeerId();
|
|
@@ -51,7 +72,14 @@ export class SignalingClient {
|
|
|
51
72
|
this.ws = ws;
|
|
52
73
|
|
|
53
74
|
ws.onopen = () => {
|
|
54
|
-
this.
|
|
75
|
+
this.openedAt = Date.now();
|
|
76
|
+
// Don't reset the backoff yet — only once the socket proves stable (see
|
|
77
|
+
// STABLE_MS). A handshake-then-drop network never reaches this timer, so
|
|
78
|
+
// its backoff keeps growing instead of hammering at 1s.
|
|
79
|
+
this.clearStableTimer();
|
|
80
|
+
this.stableTimer = setTimeout(() => {
|
|
81
|
+
this.reconnectDelay = 1000;
|
|
82
|
+
}, STABLE_MS);
|
|
55
83
|
const hello: ClientMessage = {
|
|
56
84
|
type: "hello",
|
|
57
85
|
role: this.opts.role,
|
|
@@ -74,9 +102,12 @@ export class SignalingClient {
|
|
|
74
102
|
else if (msg.type === "signal") this.opts.onSignal?.(msg.from, msg.data);
|
|
75
103
|
};
|
|
76
104
|
|
|
77
|
-
ws.onclose = () => {
|
|
105
|
+
ws.onclose = (ev) => {
|
|
106
|
+
this.clearStableTimer();
|
|
78
107
|
this.stopHeartbeat();
|
|
79
|
-
this.
|
|
108
|
+
const ms = this.openedAt ? Date.now() - this.openedAt : 0;
|
|
109
|
+
this.openedAt = 0;
|
|
110
|
+
this.opts.onClose?.({ code: ev?.code ?? 0, reason: ev?.reason ?? "", ms });
|
|
80
111
|
if (!this.closed) this.scheduleReconnect();
|
|
81
112
|
};
|
|
82
113
|
|
|
@@ -110,6 +141,13 @@ export class SignalingClient {
|
|
|
110
141
|
}
|
|
111
142
|
}
|
|
112
143
|
|
|
144
|
+
private clearStableTimer(): void {
|
|
145
|
+
if (this.stableTimer != null) {
|
|
146
|
+
clearTimeout(this.stableTimer);
|
|
147
|
+
this.stableTimer = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
private scheduleReconnect(): void {
|
|
114
152
|
const delay = this.reconnectDelay;
|
|
115
153
|
this.reconnectDelay = Math.min(delay * 2, 15000);
|
|
@@ -126,6 +164,7 @@ export class SignalingClient {
|
|
|
126
164
|
close(): void {
|
|
127
165
|
this.closed = true;
|
|
128
166
|
this.stopHeartbeat();
|
|
167
|
+
this.clearStableTimer();
|
|
129
168
|
try {
|
|
130
169
|
this.ws?.close();
|
|
131
170
|
} catch {
|