codehost 0.23.0 → 0.23.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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.23.1](https://github.com/snomiao/codehost/compare/v0.23.0...v0.23.1) (2026-06-16)
2
+
3
+
4
+ ### Performance Improvements
5
+
6
+ * **signal:** exponential, unbounded backoff on the room sweep alarm ([fde1c43](https://github.com/snomiao/codehost/commit/fde1c43aa0dbc9274abb645ae43fc7cbd6712ee0))
7
+
1
8
  # [0.23.0](https://github.com/snomiao/codehost/compare/v0.22.1...v0.23.0) (2026-06-15)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehost",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/worker/room.ts CHANGED
@@ -16,16 +16,33 @@ interface Attachment {
16
16
  lastSeen: number;
17
17
  }
18
18
 
19
- /** How often the room scans for dead sockets, and how long a socket may go
20
- * silent before eviction. Clients heartbeat every ~25s (HEARTBEAT_MS in
21
- * signaling-client.ts); allow ~2 misses, so a crashed peer drops out within
22
- * ~65-85s. Every sweep alarm and every ping is a billable DO request, so both
23
- * cadences are deliberately slow — a hidden Chrome tab's throttled timers
24
- * (1/min) must still beat STALE_MS, or background tabs churn evict/reconnect
25
- * cycles all day. */
26
- const SWEEP_MS = 20_000;
19
+ /** How long a socket may go silent before eviction. Clients heartbeat every
20
+ * ~25s (HEARTBEAT_MS in signaling-client.ts); allow ~2 misses, so a crashed
21
+ * peer drops out within ~65s+. A hidden Chrome tab's throttled timers (1/min)
22
+ * must still beat STALE_MS, or background tabs churn evict/reconnect all day. */
27
23
  const STALE_MS = 65_000;
28
24
 
25
+ /** The sweep alarm scans for dead sockets, and every firing is a billable DO
26
+ * request. A room with one always-on daemon never goes idle, so a fixed-cadence
27
+ * sweep would wake the DO forever — the dominant signaling cost. Instead the
28
+ * interval backs off exponentially with NO upper bound: it starts at
29
+ * SWEEP_MIN_MS so a just-changed room evicts promptly, doubles after every no-op
30
+ * sweep, and resets to the floor whenever a peer joins or a stale socket is
31
+ * evicted. So a long-stable room's sweep cost trends to zero — past
32
+ * SWEEP_STOP_MS it stops arming the alarm entirely. The trade-off: a
33
+ * hard-killed peer can then linger until the edge notices the dead socket or
34
+ * someone new joins the room (cosmetic — a graceful close still evicts
35
+ * immediately via webSocketClose). The interval is persisted (DO storage) so it
36
+ * survives hibernation between alarms. */
37
+ const SWEEP_MIN_MS = 20_000;
38
+ const SWEEP_MAX_MS = Infinity;
39
+ /** Overflow/sanity guard for the unbounded backoff: once the doubled interval
40
+ * passes a day, stop arming the sweep rather than hand setAlarm an ever-growing
41
+ * (eventually non-finite) timestamp. A peer join revives sweeping at the floor
42
+ * via ensureSweep, so phantom cleanup resumes whenever the room is used again. */
43
+ const SWEEP_STOP_MS = 24 * 60 * 60 * 1000;
44
+ const SWEEP_KEY = "sweepMs";
45
+
29
46
  /**
30
47
  * One Durable Object instance per token-room. Holds the live WebSocket
31
48
  * connections, keeps a registry of who is present, and relays WebRTC signals
@@ -117,9 +134,17 @@ export class Room implements DurableObject {
117
134
  }
118
135
  }
119
136
  if (evicted) this.broadcastPeers();
120
- // Keep sweeping while anyone is connected; let idle rooms go quiet.
121
- if (this.state.getWebSockets().length > 0) {
122
- await this.state.storage.setAlarm(now + SWEEP_MS);
137
+ // Idle room: stop sweeping and let the DO hibernate.
138
+ if (this.state.getWebSockets().length === 0) return;
139
+ // Stable sweep -> double the interval (capped); an eviction means the room is
140
+ // changing, so reset to the floor and stay vigilant.
141
+ const prev = (await this.state.storage.get<number>(SWEEP_KEY)) ?? SWEEP_MIN_MS;
142
+ const next = evicted ? SWEEP_MIN_MS : Math.min(prev * 2, SWEEP_MAX_MS);
143
+ await this.state.storage.put(SWEEP_KEY, next);
144
+ // Unbounded backoff: past the stop horizon, leave the alarm unset — a long
145
+ // stable room sweeps no more. A join (ensureSweep) or eviction revives it.
146
+ if (next <= SWEEP_STOP_MS) {
147
+ await this.state.storage.setAlarm(now + next);
123
148
  }
124
149
  }
125
150
 
@@ -151,11 +176,12 @@ export class Room implements DurableObject {
151
176
  return att;
152
177
  }
153
178
 
154
- /** Arm the sweep alarm if one isn't already pending. */
179
+ /** (Re)arm the sweep at the floor cadence. Called when a peer joins: a roster
180
+ * change should be re-checked promptly, so reset the backoff and pull the
181
+ * alarm in even if a (backed-off) one is already pending. */
155
182
  private async ensureSweep(): Promise<void> {
156
- if ((await this.state.storage.getAlarm()) == null) {
157
- await this.state.storage.setAlarm(Date.now() + SWEEP_MS);
158
- }
183
+ await this.state.storage.put(SWEEP_KEY, SWEEP_MIN_MS);
184
+ await this.state.storage.setAlarm(Date.now() + SWEEP_MIN_MS);
159
185
  }
160
186
 
161
187
  private findByPeerId(peerId: string): WebSocket | null {