( opts: CreateHelperClientOptions, )
| 129 | * helper reports a different protocol version, so B-3 can trigger a re-install. |
| 130 | */ |
| 131 | export function createHelperClient( |
| 132 | opts: CreateHelperClientOptions, |
| 133 | ): HelperClient { |
| 134 | const { socketPath, secret, onDisconnect } = opts |
| 135 | const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS |
| 136 | const closeTimeoutMs = opts.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS |
| 137 | |
| 138 | // FIFO queue of in-flight requests awaiting their response frame. |
| 139 | interface Pending { |
| 140 | resolve: (res: HelperResponse) => void |
| 141 | reject: (err: Error) => void |
| 142 | timer: ReturnType<typeof setTimeout> |
| 143 | } |
| 144 | const pending: Pending[] = [] |
| 145 | let buffer = '' |
| 146 | /** Set once the socket dies so later requests fail fast instead of hanging. */ |
| 147 | let fatal: Error | undefined |
| 148 | /** Set when WE end the socket so the close/error isn't read as a crash. */ |
| 149 | let closing = false |
| 150 | /** Guards onDisconnect to fire at most once (error + close can both arrive). */ |
| 151 | let disconnected = false |
| 152 | |
| 153 | /** |
| 154 | * Notify the owner of an UNEXPECTED drop exactly once. A deliberate `close()` |
| 155 | * sets `closing` first, so the resulting close/error is never misreported as a |
| 156 | * helper crash. No-op when no callback was supplied (sidecar callers). |
| 157 | */ |
| 158 | function reportDisconnect(err?: Error): void { |
| 159 | if (closing || disconnected) return |
| 160 | disconnected = true |
| 161 | onDisconnect?.(err) |
| 162 | } |
| 163 | |
| 164 | const socket: Socket = connect(socketPath) |
| 165 | socket.setEncoding('utf8') |
| 166 | |
| 167 | socket.on('data', (chunk: string) => { |
| 168 | buffer += chunk |
| 169 | const { messages, rest } = parseMessages<HelperResponse>(buffer) |
| 170 | buffer = rest |
| 171 | for (const res of messages) { |
| 172 | const next = pending.shift() |
| 173 | if (!next) continue |
| 174 | clearTimeout(next.timer) |
| 175 | if (res.ok) { |
| 176 | next.resolve(res) |
| 177 | } else { |
| 178 | // Surface the server's failure — never swallow it. |
| 179 | next.reject( |
| 180 | new HelperRequestError( |
| 181 | (res as { error?: string }).error ?? 'helper: request failed', |
| 182 | res.type, |
| 183 | ), |
| 184 | ) |
| 185 | } |
| 186 | } |
| 187 | }) |
| 188 |
no test coverage detected