OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-svc commits / 953993b

client: detect UDS disconnect (event-driven) so consumers reconcile instead of reusing a dead client
The UDS client's 'close' handler only _failAll'd pending calls but never set `_closed`, and there was no 'end' handler. When the peer daemon restarts (OTA / pgh app restart of hpp, pgv3, sidecar), a long-lived consumer's cached client keeps `.closed === false`, so it hands back the DEAD client — calls then reject "connection closed" or hang with nothing to trigger a reconnect. This is what repeatedly broke favela-web posting/sign-in after every hpp restart. Add `_down()` (idempotent): on close/end/error, set `_closed`, fail in-flight calls, destroy the socket. 'end' is load-bearing under bare-net (allowHalfOpen: peer-end fires 'end' not 'close') — same posture as the server-side half-open fix (0.1.3). Now `.closed` is truthy after a disconnect, so consumers reconnect a fresh Client on their next call (fleet reconcile contract). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
953993ba04e6c465c570b109a58221e979c2d7a7
by plankatron <fleeky@gmail.com> · 2026-07-17T14:20:48.000Z

Changed files (2) +24 -6

lib/client.js +23 -5
……@@ -146,13 +146,22 @@ export class Client {
146146 this._decoder.push(chunk)
147147 } catch (err) {
148148 this._failAll(err)
149 this._sock.destroy()
149 this._down()
150150 }
151151 })
152 this._sock.on('close', () => {
153 this._failAll(new Error('connection closed'))
154 })
155 this._sock.on('error', () => { /* close handler runs */ })
152 // EVENT-DRIVEN disconnect detection (fleet reconciliation contract). When the peer daemon
153 // restarts (the common case: an OTA/`pgh app restart` of hpp/pgv3/etc.), the cached client's
154 // socket dies. Previously 'close' only `_failAll`'d the pending map but NEVER set `_closed`,
155 // and there was no 'end' handler at all — so `client.closed` stayed FALSE and long-lived
156 // consumers (favela-web Writer, pgh sidecar lookups) kept handing back a DEAD client, whose
157 // calls hang or reject "connection closed" forever with nothing to trigger a reconnect. Mark
158 // the client CLOSED on close/end/error so `.closed` is truthy → consumers reconcile (reconnect
159 // a fresh Client) on their next call. 'end' is load-bearing under bare-net (allowHalfOpen: on
160 // peer-end it fires 'end' but not 'close'), so we destroy to force teardown — same posture as
161 // the server-side half-open fix (hyper-svc 0.1.3).
162 this._sock.on('close', () => this._down())
163 this._sock.on('end', () => this._down())
164 this._sock.on('error', () => this._down())
156165 }
157166 }
158167
……@@ -207,6 +216,15 @@ export class Client {
207216 this._pending.clear()
208217 }
209218
219 // The socket died (peer restarted / errored / half-open end). Mark the client CLOSED so any
220 // consumer's `.closed` check reconnects, fail every in-flight call, and force teardown. Idempotent.
221 _down () {
222 if (this._closed) return
223 this._closed = true
224 this._failAll(new Error('connection closed'))
225 if (this._sock) { try { this._sock.destroy() } catch {} }
226 }
227
210228 async _callHttp (method, params) {
211229 const url = new URL(`${this._base}/rpc/${encodeURIComponent(method)}`)
212230 return new Promise((resolve, reject) => {
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-svc",
3 "version": "0.1.6",
3 "version": "0.1.7",
44 "description": "Reusable per-app daemon library \u2014 UDS + HTTP for local RPC, Hyperswarm for peer mirroring",
55 "type": "module",
66 "main": "index.js",