Skip to content

fix(messagebox): keep live sockets and room subscriptions across reconnects, push unpaid HTTP sends - #739

Open
kjartan221 wants to merge 28 commits into
bsv-blockchain:mainfrom
kjartan221:fix/messagebox-live-socket
Open

kjartan221 wants to merge 28 commits into
bsv-blockchain:mainfrom
kjartan221:fix/messagebox-live-socket

Conversation

@kjartan221

@kjartan221 kjartan221 commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

MessageBoxClient discarded a socket at the moment Socket.IO began reconnecting
it. The orphan kept reconnecting, re-sent authenticated down whichever socket
had replaced it, and cleared that healthy socket on its own next drop. Room
membership was recorded as intent and never reconciled, so a replacement socket
inherited a claim it had never made and was pushed nothing. Separately, the
server pushed a stored message to a recipient's room only when the sender had
used a socket, so a message sent over HTTP reached nobody live.

Program and scope

  • Tracker or issue: none. Found while integrating @bsv/group-messaging, which
    carried ~300 lines of transport machinery working around this behaviour.
  • Program gate(s) advanced: none.
  • Why this change is needed: a live subscriber could be connected,
    authenticated and certain it was subscribed while receiving nothing, with no
    supported way to detect or recover from it.
  • Explicitly out of scope: making a client-initiated disconnect recoverable; pointing
    infra/message-box-server at the workspace client instead of the published
    one; the POSIX-separator fix to scripts/package-documentation.mjs, held
    back for maintainers since it is repo tooling rather than package code.
  • Exact head SHA reviewed: 240a614649a0e4bc91caa0f7b5c73fffc7c5699f

Client

  • Every handler is bound to the socket it belongs to, so a replaced socket
    cannot act on its successor.
  • Rooms the consumer asked for are tracked apart from rooms joined on the
    current socket, and re-emitted after each authentication. Membership is
    cleared on a drop because it belongs to the server-side socket.
  • joinedRoom and joinFailed are read. The record stays optimistic on emit,
    so a server that answers nothing behaves as before; an answer corrects it.
  • One live-message listener per room and one acknowledgement listener per room,
    in place of listeners accumulated per subscription and per send that the
    socket wrapper has no off to remove.
  • io server disconnect triggers a rebuild: immediate, then doubling from 1s
    to a 30s cap. reconnection: false is honoured by disposing.
  • leaveRoom releases its room whether or not the socket is up.
  • onLiveStatus(listener) / liveStatus report what the socket is doing:
    connecting, live with the rooms joined on it, reconnecting, or
    closed. A refused join reports a new status, so its room leaves rooms.
  • Room joins are paced: at most 16 await the server's answer per socket, freed
    by joinedRoom, joinFailed or a 5 s timeout, the rest queued. The server's
    authsocket transport drops a socket with more than 32 signed messages in
    process, so a client on more than 32 boxes used to be dropped after every
    authentication. Leaving a room frees its slot, so joining it again asks again.
  • sendLiveMessage falls back to HTTP when the socket cannot be established,
    authenticated or joined (for example right after a server restart) instead
    of throwing. A conflicting WebSocket identity pin still throws.

Server

  • A stored message is announced on sendMessage-<roomId> from the HTTP send
    route, as the socket route already did.
  • A send carrying a payment is never announced. The announcement carries
    the request body, which holds no payment, while the stored row holds
    { message, payment }; a recipient acknowledging on the announcement would
    delete the row before its wallet internalized the output, and only
    listMessages internalizes.
  • joinFailed names the refused room and carries a code.
  • The announcement fan-out is bounded by the limit the send route already
    applies to notifications over the same recipient list.

Impact

  • No public package source or manifest changed
  • Public package source or manifest changed; affected packages are listed below
  • Infrastructure source, dependency, image, or deployment configuration changed
  • Public API, exports, types, runtime targets, or browser/mobile behavior changed
  • Security-sensitive boundary changed
  • Documentation or examples changed

New public API: onLiveStatus(), the liveStatus getter and the
LiveStatus / LiveConnectionState types, all additive. The third box is
also ticked for observable behaviour: a live message is delivered a tick after arrival rather than
synchronously (already so whenever it was encrypted); getJoinedRooms()
reports rather than acts, so mutating the returned set no longer cancels a
rejoin; a dropped socket is kept while Socket.IO reconnects. All three are in
the package CHANGELOG and the release note.

The fifth is ticked for two boundaries: recipient-fee payment handling on the
new push path, and the room-ownership refusal the client now acts on.

Affected packages/services and intended patch versions:

Verification

  • Local commands and results:
    • pnpm --filter @bsv/message-box-client run test — 434 passed, 16 suites (after merging main, including fix(message-box-client): keep list-API payments the wallet did not store #629's tests)
    • pnpm --filter @bsv/message-box-client run test:coverage — see below
    • infra/message-box-server: npm test — 237 passed, 29 suites;
      npm run typecheck and npm run lint — clean
    • workspace typecheck (recursive, excluding the root) — exit 0
    • pnpm exec oxlint on the changed files with --deny-warnings — clean
    • pnpm exec prettier --check on the changed files — clean
    • pnpm install --frozen-lockfile --ignore-scripts after rebasing — clean
    • Mutation checks: every fix was reverted one at a time and the suite re-run.
      12 of 13 client mutations and 4 of 7 server mutations turn a test red. The
      three surviving server mutations are the redundant status === 200 and
      status === 'success' guards, which the results-array check already
      covers, and passing the configured socket cap, which is covered where it
      lives in webSocketConnections.test.ts. The surviving client mutation is
      disconnectWebSocket detaching before it disconnects, which reaches the
      same state either way under faithful wrapper semantics. The fixes added
      after the live stress test (join pacing, refused-join status, HTTP
      fallback, pin rethrow, leave-then-rejoin) were each written test-first and
      mutation-checked the same way.
  • Live stress test against a real MessageBox server: a private harness ran this
    branch's client and a server image built from this branch's
    infra/message-box-server, then the published 2.6.0 client and 1.1.53 image
    for comparison, on a clean database:
    • live-socket suite (outage the server survives, docker restart with
      io server disconnect, leave/rejoin, HTTP announce single and batch, paid
      send not announced, refused join and the 128-room limit, a 10-subscriber
      soak through kill/restart/kill, 40 and 100 boxes, send right after a
      restart): 10/10 on this branch, failing on the published builds.
    • 50 live clients through a server restart: 50/50 old clients received
      again, all back in 7.4 s, 452 sent and 452 stored, 0 lost. Published
      2.6.0: 0/50 old clients ever received again.
    • 100 boxes on one client: all joins answered in 843 ms, 0 drops (before the
      pacing fix: dropped in a loop, 0 rooms held).
    • Existing live-load and happy-path suites: pass on both.
      The run surfaced three client defects, all fixed in this PR before merge:
      joins outrunning the server's in-flight limit, a refused room left in
      liveStatus, and sendLiveMessage throwing after a restart.
  • Hosted CI run: passed on 3f3bef8a0; pending on 240a61464.
  • Conformance evidence: there is no AuthSocket vector directory in the corpus
    (docs/specs/authsocket.md). specs/messaging/authsocket-asyncapi.yaml and
    specs/messaging/message-box-http.yaml are updated and parse; both take a
    minor bump, and every change is additive.
  • Coverage delta (measured before the stress-test fixes; CI re-measures):
    @bsv/message-box-client all files 93.14% statements /
    89.89% branches / 94.64% lines; MessageBoxClient.ts 92.06% / 87.72% /
    93.86%, up from 90.72% / 87.15% / 92.30% before this work.
  • Lint/typecheck delta: zero warnings and zero errors in both packages,
    unchanged from base.
  • Browser/mobile/packed-consumer evidence: not run. pack:check fails on
    the authoring host (Windows) at spawn pnpm ENOENT inside
    scripts/check-package-artifact.mjs before it reaches the artifact; the
    build itself succeeds. test:browser was not run. Both need hosted CI.
  • Performance or bundle-size delta: not measured. No dependency change and no
    new import; MessageBoxClient.ts and types.ts together are +675 / −212.
  • I self-reviewed the complete diff for correctness, security,
    compatibility, public API, artifacts, dependencies, docs, and operations
  • All applicable checks are terminal and successful on the exact head; any
    scope-based skip is expected and validated by the merge gate

Security and dependencies

  • No dependency or lockfile change
  • Changelog, runtime relevance, peer compatibility, transitive graph, and
    audit results were reviewed
  • CodeQL/negative tests cover any changed trust boundary
  • The exact-head CodeQL analysis has no new alert
  • The exact-head repository quality gate reports zero new Sonar findings
    (including accepted or false-positive issue states) and zero unreviewed hotspots;
    Sonar's aggregate Quality Gate passed verdict alone is not merge evidence
  • No new override, advisory dismissal, quality suppression, or skipped test
  • Any temporary exception is registered with owner, evidence, review date,
    and removal condition
  • Workflow permissions and lifecycle-script behavior remain least privilege

No manifest, lockfile or workflow is touched by this branch, so the second box
is ticked as reviewed and not applicable. Negative tests cover both changed
boundaries: a paid send is not announced, a refused send is not announced, a
room owned by another identity is refused and named, and a permanent refusal
stops being requested. No temporary exception is added. CodeQL and Sonar need
hosted CI.

Dependency evidence

Not applicable: no manifest, lockfile, container base, action or generator
dependency changed.

Release and operations

  • No npm publication was performed from a workstation or from this PR
  • Required npm patch bumps are included or intentionally deferred by the
    controlling program
  • Image/SBOM/provenance/deployment/rollback impact is documented
  • Documentation, changelog, migration, and operational guidance are current

Release order: the client depends on @bsv/authsocket-client through
workspace:^, which publishes as ^2.1.9 from main (#1243). npm has
2.1.8, so @bsv/authsocket-client 2.1.9 must be published before or with
@bsv/message-box-client 2.7.0.

Deployment: the client and the server can ship in either order. Reading the
join answer only corrects the client's record against a server that names the
refused room, which is in this PR; against any other server the client records
joins exactly as it always did. The HTTP announcement starts only once this
server image is deployed, and it is best effort — it reaches only sockets held
by the process that served the request, so listMessages polling remains
required for correctness. Rollback is a redeploy of the previous image with no
data or wire migration; the recipient falls back to polling, which is current
behaviour.

Documentation updated in this PR: specs/messaging/authsocket-asyncapi.yaml,
specs/messaging/message-box-http.yaml,
packages/messaging/message-box-client/README.md and its CHANGELOG,
infra/message-box-server/README.md,
docs/packages/messaging/message-box-client.md,
governance/package-release-notes.json, the regenerated
docs/reference/package-api-migrations.md, and JSDoc on joinRoom,
listenForLiveMessages, leaveRoom and getJoinedRooms.

Completion evidence

  • The linked tracker is updated only for work fully proved by merged code,
    passing checks, resolved alerts, measurements, or an approved exception
  • Review conversations are resolved
  • Documentation, changelog, migration notes, release notes, and operator
    guidance are current or concretely not applicable
  • No pending, failed, stale, cancelled, or unexpectedly skipped check is
    being handed to another contributor as "complete"
  • One qualified maintainer approval is sufficient; no last-pusher
    restriction is assumed

Known gaps, stated rather than left to be found

  • infra/message-box-server devDepends on the published
    @bsv/message-box-client@^2.6.0, as every infra/ package pins published
    first-party versions, so no test in this repository exercises this client
    against this server. The live stress test above did, outside the repository.
    The opt-in live suite (MESSAGE_BOX_RUN_INTEGRATION=true) has not been run.
  • The rebuild backoff resets once a socket holds its authentication for the
    length of the next delay, so a relay dropping sockets just outside that
    window still gets an immediate rebuild each time. The backoff bites when
    drops come faster than the current delay.
  • Re-subscribing with a different callback after a drop can still deliver
    twice.
  • A client-initiated disconnect remains terminal by design.

…today

Characterisation tests, not fixes. Each asserts what the client does now and is
expected to fail when the defect it names is fixed; that failure is the signal.

A room is never rejoined after disconnectWebSocket, because the set that records
membership is not cleared and the joinRoom guard then matches on the next
listen. The socket is live, the client believes it is subscribed, and the server
never put it in the room. No relay restart or network is involved.

A dropped socket is orphaned rather than disposed: the reference is cleared and
disconnect() is never called on it, so Socket.IO keeps reconnecting an object
nothing is watching.

The mock collects every handler per event rather than the last, because the
client registers more than one disconnect listener and firing only the latest
exercises the wrong one.
off() now actually detaches, and every handler for an event is fired rather than
the last registered. Storing only the latest meant the test called an auth-phase
disconnect handler that flips a flag and never touches the socket, so it
simulated no disconnect at all and reported the defect absent.

While fixing it: three disconnect listeners are attached after authentication
and none are detached. Worth knowing for the fix work, since the socket these
accumulate on is the one that gets replaced.
…embership

A disconnect cleared this.socket without stopping the socket. Socket.IO
reconnects by default, so that object kept reconnecting unwatched, and its
handlers still read this.socket: once a replacement existed, the orphan
authenticated on it and its next disconnect cleared it.

joinedRooms outlived the socket that joined, so the guard in joinRoom matched
and a new socket never emitted a join. Connected, believed subscribed, and never
put in the room by the server. disconnectWebSocket left the same stale claim,
which reproduces it without a relay restart: listen, disconnect, listen.

leaveRoom dropped the room after its no-socket guard, so leaving while
disconnected left the claim in place. It now drops first.

Rejoining rooms on a reconnect is deliberately not here: eight call sites treat
a null socket as no connection, so keeping the socket across a transient drop is
its own change.
…cription

An unrequested drop keeps the socket object; the connect handler
re-authenticates and rejoins the rooms the consumer asked for, tracked in
requestedRooms separately from the per-socket joinedRooms. A reconnect
registers in connectionInitPromise so concurrent callers await it.
disconnect() is back to disconnectWebSocket() only. The auth wait is settled
by the socket's long-lived handlers, so no listeners accumulate.
Live-message handlers are held per room and re-attached when a socket is
rebuilt after a terminal disconnect, so a restored room is not silent. A wait
that times out with the socket still down marks it stale and the next caller
rebuilds, covering host configs that disable or exhaust Socket.IO reconnection.
Tests now hand out a distinct production-shaped mock (no off) per construction.
…, honour reconnection:false

Socket.IO never retries io server disconnect and a listener makes no calls to
trigger the lazy rebuild, so the client rebuilds once on its own, only for a
socket that had authenticated. With managerOptions.reconnection false a drop
disposes the socket and the next call rebuilds; nothing rebuilds proactively.
listenForLiveMessages no longer attaches a callback a rebuild already restored.
The WebSocket sendMessage handler announced a stored message to the
recipient's joined sockets; the HTTP route never did, so an HTTP send
reached recipients only through polling. Move the announcement into
deliverLiveMessage and call it from both paths. HTTP reaches the socket
registry through a LiveDelivery handle on the context, null until sockets
are attached or after they close.
Require ctx.liveDelivery when mounting the send route instead of silently
skipping delivery, refuse a second attach on one context, and only clear the
handle when it still points at the closing registry. Tests now wait out a
quiet period after the first push so a double emit is counted, for both the
HTTP and WebSocket send paths, and the disabled case runs against a live
joined recipient.
A recipient fee is stored on the message row as { message, payment }, but
the live push carries the request body, which has no payment in it. The
live handler only decrypts; internalization happens in listMessages. So a
consumer that acknowledges on the push deletes the row before the
recipient wallet ever sees the output, and the fee is lost.

Paid sends now stay poll-only, which is what they were before this branch
started pushing HTTP sends at all. The socket send path never forwards a
payment, so it cannot reach a paying recipient either way.
Reverting each fix one at a time left four passing: the handler identity
guards, the connected check before reusing a socket, leaveRoom's delete
ahead of its socket guard, and the two liveDelivery status guards. The
first of those is the orphaned socket, which is the defect this branch
exists for, so it was shipping on faith.

The mock now fires `disconnect` from `disconnect()` as Socket.IO does,
which is what the orphan test needs to replace a socket and then have the
old one report on its own handlers.

Still unkillable, and left alone: disconnectWebSocket detaching before it
disconnects, which under faithful semantics reaches the same state either
way, and liveDelivery's status/success checks, which the results-array
check already covers.
The branch changes @bsv/message-box-client's published bytes, so the
governance entry and the package changelog have to say what changed and
what it means for a consumer. The migration decision is: nothing to do,
with two behaviour notes for anyone who worked around the old defects —
getJoinedRooms() is no longer a lever, and a dropped socket is kept.

The entry also claimed the pending release had no source change, which
stopped being true here.

The ledger is regenerated from those notes. Note that regenerating it on
Windows needs a separate one-line fix to the generator, which is not this
branch's business and travels on its own.
Nothing runs the branch client against a real server: infra pins the
published @bsv/message-box-client, as every infra package pins published
first-party versions, and switching that one to workspace:* for test
convenience would break a convention the whole directory keeps.

The opt-in live suite is the seam the repo already provides for this, so
the rejoin-after-disconnect case goes there.

Unrun here: it needs MESSAGE_BOX_RUN_INTEGRATION=true, a host and a
json-api wallet. The deterministic twin is in the defect suite and is
mutation-verified; this one is the acceptance check against a real relay.
The server has always answered joinRoom with joinedRoom or joinFailed and
the client has never listened, so a refused join left it certain it was
subscribed to a room it had never been given. joinFailed now also names
the room: a client can have several joins in flight, and without the room
the event cannot be attributed to any of them.

The record stays optimistic on emit, so a server that answers nothing
behaves exactly as before. An answer only corrects it: a refusal drops
the claim, and one that re-emitting can never clear — a room owned by
another identity, an invalid room — also stops being asked for on every
reconnect. The room's message handler stays attached either way, since
nothing is pushed to a room we are not in and dropping it would let a
later listen attach a second one.

The HTTP announce fanned out with Promise.all over the recipient list.
One send reaches up to MAX_RECIPIENTS recipients on up to
WEBSOCKET_MAX_RECIPIENT_CONNECTIONS sockets each, so a standard profile
could issue 2500 emits off one request while the notification fan-out
over the same list, in the same function, was bounded to four. It now
uses that same bound.
Two client defects left from the review.

A room's listener could never be taken off the socket, because the
wrapper has no `off`. leaveRoom dropped its record and a later listen
attached a second listener, so every message after that arrived twice for
the life of the socket. There is now one listener per room per socket
that reads its subscribers when a message arrives, which makes
subscribing and unsubscribing list operations. It also reads the body
once rather than once per subscriber, which matters because reading it
rewrites `message.body` in place.

That does change one thing for a caller: delivery is now always a tick
after arrival. It already was for an encrypted message, since decrypting
awaited; the unencrypted path happened to run straight through. The
existing listen test asserted synchronously and now waits a tick — same
assertion, one await.

The rebuild after `io server disconnect` had no delay, so a relay that
authenticated a socket and dropped it got one rebuild per authentication
for as long as it kept doing that. The first rebuild is still immediate
and the rest double to a thirty-second cap, resetting once a socket has
held its authentication that long. A deliberate close cancels one in
flight, which it previously could not.
…the notes

Subscribers share one room listener since the last commit, so a throwing
one took the rest of the room's message with it and left an unhandled
rejection. 2.5.4 isolated them by giving each its own listener, so this
was a regression; each is now called in its own try/catch.

The acknowledgement path had the leak the room path just lost. Every
sendLiveMessage attached a `sendMessageAck-<room>` listener and removed
it through `typeof socketAny?.off === 'function'`, which is always false
because AuthSocketClient has no `off`: five sends left five listeners,
and every acknowledgement walked all of them. One listener per room now,
with sends queued behind it. A refusal carries no messageId, so the
oldest send in flight still takes the acknowledgement, which is what the
per-send listeners did — the settled ones returned at once and the first
unsettled one took it.

The hardening test waited for a new listener per send, which was the leak
restated as an expectation. It now waits for the send to be queued; both
assertions are unchanged.

The live WebSocket test never awaited its listen, so it read the joined
rooms before the join had happened. It also claimed to be a reconnect
when a deliberate close is terminal by design, so it is named for what it
does prove: a socket built after a close joins its room for itself.

Release text: the two rebuild descriptions are now one and state the
reset rule as the code implements it, including that a relay dropping
sockets just outside the window still gets an immediate rebuild. Reading
the join answer only corrects anything against a server that names the
room, which is said where it was previously implied by omission, and
"nothing else changes for a caller" is gone.
The AsyncAPI spec described joinFailed as reason-only and said the room
broadcast followed a socket send. Both are now wrong: the refusal names
the room it refused and carries a code, and the broadcast also follows an
HTTP store. Where it does not follow one is the part worth writing down —
only this process's sockets, never a paid send, never a refused send —
so that goes on the channel and on the HTTP route that now triggers it.
Both specs take a minor bump; every change is additive.

The client README said reconnection settings "remain supported" without
saying what honouring reconnection: false costs a pure subscriber, which
is that nothing brings it back. It now says so, next to a section on what
the socket does on its own and the two things that follow for a caller:
keep polling, and getJoinedRooms is reporting rather than a lever.

The server README describes the join answer and the announcement on the
same terms, and names the concurrency limit the fan-out borrows.

JSDoc on joinRoom, listenForLiveMessages and leaveRoom: resolving a join
means the request went out, not that it was granted; subscribers to a
room share one listener, so the body is read once and delivery is a tick
late; leaving releases the room whether or not the socket is up.
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.38202% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ssaging/message-box-client/src/MessageBoxClient.ts 94.38% 1 Missing and 14 partials ⚠️

📢 Thoughts on this report? Let us know!

Sonar put initializeConnection at cognitive complexity 17 against a limit
of 15, and oxlint had it at cyclomatic 23 against 20. It had grown a
socket construction, seven inline handlers and their shared closure state
inside one `if (this.socket == null)` block.

Construction moves to openSocket, wiring to attachSocketHandlers, and
each handler to a method of its own. The guard that each repeats —
return if this is no longer the client's socket — reads as the rule it
is rather than as seven copies of a line inside one function.

The `wasAuthenticated` closure that authenticationSuccess shared with
disconnect becomes an authenticatedSockets WeakSet, since the two
handlers no longer share a scope. Same rule: only a socket that
authenticated is rebuilt.

No behaviour change. All twelve client mutations still turn a test red,
including the two this touches most — the handler identity guards and
rebuilding only an authenticated socket.
Codecov puts the patch at 86.74% against a 90% target. These are the
reachable misses in the client: a join the server confirms, a body whose
ciphertext is not a string, and a socket-level error event. Each is a
behaviour worth pinning rather than a line worth touching — a
confirmation restores a room a refusal had dropped, an unreadable body
reaches the subscriber as an error instead of vanishing, and an error
event does not take the socket with it.

This file's patch coverage goes from 97.50% to 97.93%. It will not move
the Codecov verdict on its own: the aggregate lane collects
`packages/*/coverage/lcov.info` only, so every line added under `infra/`
is counted as a miss no matter how well the server is tested.
onLiveStatus(listener) reports connecting, live with the rooms joined on
the socket, reconnecting when something will restore it, or closed when
nothing will. It calls the listener once with the current status, returns
a function that unsubscribes, and `liveStatus` reads the same value
without subscribing. Listeners are isolated from each other, same as the
message subscribers.

Until now a consumer could only learn the socket's state by sending and
watching what happened, which is why group-messaging kept a
self-addressed ping: not for the ping, but to manufacture the traffic the
answer was hidden in. This is the cheap half of that — no wire change, no
server work. It does not replace the echo idea, which is parked: status
says what this client knows, not that the room still routes.

A new public method makes this a minor, so 2.5.5 becomes 2.6.0 and the
governance entry moves from patch to minor.

Also covers nine of the eighteen partial branches Codecov counted against
the patch — two rooms on one socket, an orphan's room traffic, a missing
identity key at connect and at initialize, and a conflicting identity pin.
The bump moved package.json and the release ledger and left three behind:
the repository-health baselines, the docs page frontmatter, and the
generated stack facts. `calendar expiry is advisory in source CI and
enforced by the maintenance audit` is the test that catches it, and the
whole repository-health suite passes now.

stack-facts.md is regenerated, not edited. I had run prettier across it
with the rest of the changed files, which rewrote 75 lines the generator
owns; upstream's own copy is not prettier-clean either, so that was my
mistake rather than a latent one. Both generated artifacts verify with
--check.

The infra package-lock files still name the published 2.5.4/2.5.5, which
is correct: they consume the registry, not this working tree.
Picks up the mutation-scope classifier fix (0b7dff8). No overlap with
this branch: it touches scripts/ci-affected-scope.mjs, its test, and
docs/reference/ci-performance.md.
Resolve the release-metadata conflicts with bsv-blockchain#629 (published as 2.6.0)
and bsv-blockchain#1243 (unpublished 2.6.1, SDK4 peer range): release the live-socket
work as 2.7.0, a minor from published 2.6.0, keep both CHANGELOG sections
and both release-note passages, and regenerate the package API migrations
and stack facts.
…ce pin conflicts

Cap room joins awaiting the server's answer at 16 per socket and queue the
rest, freed by joinedRoom, joinFailed or a 5 s timeout. The server's
authsocket transport drops a socket with more than 32 signed messages in
process, so a client on more than 32 boxes re-emitted every join after each
authentication and was dropped in a loop without ever holding a room.

Report a new live status when a join is refused, so liveStatus.rooms no
longer lists a room that getJoinedRooms() has dropped.

sendLiveMessage falls back to HTTP when the socket cannot be established,
authenticated or joined, but rethrows a conflicting WebSocket identity pin,
which is a misconfiguration the fallback would hide.

Also tidy docs, types and tests after review: restore PeerMessage's JSDoc,
define "joined" once, align the AsyncAPI and server README with the code,
and replace real sleeps in the live-socket tests with fake timers.
@kjartan221
kjartan221 marked this pull request as ready for review October 21, 2026 15:40
@kjartan221
kjartan221 requested a review from sirdeggen as a code owner October 21, 2026 15:40
sirdeggen and others added 2 commits October 21, 2026 20:30
Resolve docs/reference/package-api-migrations.md by regenerating it with
scripts/package-documentation.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve docs/reference/package-api-migrations.md by regenerating it with
scripts/package-documentation.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants