Skip to content

New Feature: XEP-0198 Stream management#867

Open
jcbrand wants to merge 8 commits into
masterfrom
stream-management
Open

New Feature: XEP-0198 Stream management#867
jcbrand wants to merge 8 commits into
masterfrom
stream-management

Conversation

@jcbrand

@jcbrand jcbrand commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Adds support to XEP-0198 to Strophe, together with shared worker improvements and the ability to also do stream management when the websocket connection is in a shared worker.

@jcbrand jcbrand force-pushed the stream-management branch from 3b6ad79 to 35a010b Compare July 6, 2026 13:23
jcbrand added 2 commits July 6, 2026 15:28
Stream management engine which can be used either from the page-bound
Connection or from inside a SharedWorker.

This will let us do SM inside a SharedWorker, solving the problem
of different connections using a shared Websocket connection
not keeping track of the same SM counts.
Groundwork for worker-resident XEP-0198 stream management: exactly one
tab (port) holds the 'primary' role, all others are secondaries that
attach to the shared session.
@jcbrand jcbrand force-pushed the stream-management branch from 35a010b to 8c5a401 Compare July 6, 2026 13:30
jcbrand added 4 commits July 7, 2026 13:19
Wires the SM engine into the websocket connect flow via a page-side
adapter. Opt in with the enableStreamManagement option (plus optional
streamManagement tuning).

sessionStorage is used automatically in browsers.

The engine is constructed for any non-worker transport, but SM is only
negotiated over websocket, decided per stream on the live transport:
embedders can swap _proto after construction (e.g. via XEP-0156
discovery, or a BOSH-websocket fallback on reconnect), so a
construction-time check would silently disable SM for exactly those
setups. Over BOSH the engine simply stays inert.
The worker is the only party that sees every tab's traffic (one socket,
all sends relayed through it), so it now owns the SM session: counting,
the unacked queue, <r/>-><a/> replies (no page round-trip), <a/>
reconciliation and resend-on-resume. A stanza sent from a secondary tab
survives resumption, and failover to another tab needs no reconnect.
…nection

A message or presence sent from one tab was invisible to the other tabs
of a shared (worker) connection. Inbound frames are broadcast, but
outbound frames were only relayed to the socket.

The worker now reflects outbound message/presence frames to every port
except the sender (including stanzas salvaged into the SM queue from a
send into a dead socket, since those are replayed on the next resume).

On the page they surface through the new overridable
Connection.onForeignStanzaSent(elem) hook

IQs are not reflected, since they're request/response traffic private
to the sending tab.
Found by compiling Converse.js against this package:

- Use relative internal imports. tsc emits bare specifiers like
  'types' verbatim into the .d.ts files, and a consumer with its own
  baseUrl resolves them against its own project root.
- Give NS a string index signature, since it is extended at runtime
  via Strophe.addNamespace.
- Add the missing log/error methods to the Strophe namespace type.
- Type the callback that attach() accepts in place of a JID as
  ConnectCallback instead of () => void.
- Expose the SM storage backends on the Strophe namespace and export
  ConnectionOptions and ConnectCallback from the package root.
@jcbrand jcbrand force-pushed the stream-management branch from 2abba73 to 22f8dc3 Compare July 7, 2026 11:27
jcbrand added 2 commits July 7, 2026 15:36
A silently dead socket (NAT timeout, sleep/wake) keeps reporting OPEN,
and under the shared worker no page-side recovery could reach it: a
reloading or reconnecting tab is idempotently joined onto the existing
session while the socket looks live, so the only way out was the OS's
TCP timeout, which can take many minutes.

The worker's sweep now sends an <r/> when an SM-enabled stream has been
quiet for a full PING_INTERVAL (doubling as a keepalive), and treats no
inbound traffic within PROBE_TIMEOUT as socket death: the zombie is
closed and the tabs are told, so they reconnect and the worker resumes
the session with the unacked queue replayed. A tab joining the
established session triggers an immediate probe, so a page reload
recovers within seconds.
The new liveness probe correctly declared the socket dead, but the
recovery afterwards wedged every tab, and a related mirror gap surfaced:

- The tab driving the reconnect never learned that the worker's new
  connection attempt failed: the worker broadcasts _onClose with a
  reason string, and the base Websocket._onClose ignores closes while
  not connected unless given a real CloseEvent with code 1006. The tab
  sat in CONNECTING forever. WorkerWebsocket now fails the attempt with
  CONNFAIL and disconnects, so the embedder's reconnect logic retries.
- A tab whose _connect join was parked on the failed handshake was
  answered with ATTACHFAIL, which a connect()-initiated page treats as
  terminal. Parked joins now remember how they joined: _attach joins
  keep the ATTACHFAIL contract, _connect joins are told the socket
  closed and retry like the driver.
- A tab joining after <enabled/> missed the _smEnabled broadcast, so
  its SM mirror reported no Stream Management. The worker now seeds the
  mirror when handing out ATTACHED.
@jcbrand jcbrand force-pushed the stream-management branch from 4c8981b to 259ddcf Compare July 7, 2026 13:39
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.

1 participant