Retry a stream we cannot read, and cover the framer reset on attach

A framing or codec error tears the link down rather than the session, so
the loop retries it on a fresh socket with a fresh framer — which is what
a desynced stream needs. Removing that reset failed nothing before; the
reconnect test now leaves half a PDU on the dying link, and does.

`disconnected` counts failed links rather than outages, which the README
now says, and the transport wires its socket as it is built.
This commit is contained in:
2026-08-31 22:27:16 +02:00
parent b2d121b4f3
commit b1c790b9a0
7 changed files with 62 additions and 18 deletions
+18 -4
View File
@@ -274,10 +274,10 @@ exactly 140.
`shutdownTimeout` bounds the drain alone, and `0` waits forever like every other timeout here;
`unbind()` then waits `responseTimeout` for its own response, and sends that PDU through
`request()` past both the window and the drain gate because it must go out either way. A stream
the framer or the codec cannot read takes `end()` instead, and so does `close({ signal })` on an
aborted signal and a peer's own `unbind` — nothing on a dead link can answer, an abort means stop
now, and a peer that has declared itself finished will not answer what it still owes us, so
draining any of the three would only hold a socket open for the timeout. `SmppServer.close()`
the framer or the codec cannot read takes `teardown()` instead, and `close({ signal })` on an
aborted signal and a peer's own `unbind` take `end()` — nothing on a dead link can answer, an abort
means stop now, and a peer that has declared itself finished will not answer what it still owes us,
so draining any of the three would only hold a socket open for the timeout. `SmppServer.close()`
reports each session's unfinished drain through `serverError`, because its own result says
nothing but that the listener stopped. `shutdownTimeout` stays a session option rather than a
`close()` argument: `server()` builds sessions on the caller's behalf, so the option
@@ -296,6 +296,20 @@ exactly 140.
that loop before tearing down, so every deliberate shutdown emits `close`. Without the split an
application that opens a replacement client on `close` ends up holding two binds on one account.
- **A stream this library cannot read is a dead link, not a dead session.** Maintainer's call,
2026-08-31: a framing or codec error tears the link down through `teardown()`, so the reconnect
loop retries it on a fresh socket with a fresh framer — which is what a desynced stream needs, and
the common cause. `sessionError` still carries every failure, so a peer that only ever sends
garbage is visible in the log rather than silent, and the backoff caps the retries at one per
`maxDelay`. Ending the session outright was inherited from when reconnect was opt-in, where the
distinction could not arise.
- **`disconnected` counts failed links, not outages.** A retry that opens a socket and then loses its
bind re-enters `attach()` and so emits again, which makes it deliberately not one-to-one with
`reconnected`: each emission is a link that went down, and suppressing the later ones would leave a
failed rebind with nothing but a log line. The README says so, because the pairing is what a reader
would otherwise assume.
- **`session.sock` is a getter over `PduTransport`, and stays public.** Maintainer's call, 2026-08-31:
`session.ts` had reached its line cap, so the socket-to-PDU seam todo.md named was opened —
`PduTransport` owns the socket, the framer and the parse, and hands the session raw bytes, framed