Adapter registration
TL;DR
io.adapter(...)lets a test supply its own adapter, the component that decides which sockets a broadcast reaches. It exists so the routing layer is not pinned to one built-in implementation, and its worth is in the adapters built on it. Registration finishes during setup, before any connection attempt.
Where this comes from
smocket began because a hand-written mock could pair one client but not a second: it had no room, broadcast, or namespace delivery. smocket is that missing piece, a mock that routes events the way real socket.io does. That routing core is the body of the project.
Once routing exists, the next question is whether a test can change it. Two uses want exactly that:
- An adapter that records every broadcast's targets, so a test can assert who was reached.
- An adapter that drops a socket from the target set, so a test can check the app survives a message it never receives.
Both need one thing before either can exist: a way to register a custom adapter. That shared prerequisite is this feature, which is why it comes first rather than as one more standalone capability.
What you can and cannot swap
In real socket.io an adapter does two jobs: it holds room membership and it delivers the event. smocket splits them. The adapter here answers which sids a broadcast targets, and by default delivery stays in the core, so per-socket order holds no matter which routing adapter is registered (see 0010).
So a routing adapter can retarget a broadcast, observing or narrowing the set, without
touching delivery. One optional hook goes further: scheduleDelivery(sid, deliver) lets
an adapter take over when a socket's client-inbound deliveries fire, which is how the
shipped DelayingAdapter delays what a socket's client receives for race-condition tests
(see 0018). It may delay that stream
but never reorder within it: reordering is the one thing the order guarantee forbids, so a
scheduling adapter owes 0010 the obligation to keep each socket's stream in send order.
Instance and cleanup lifecycle
Call io.adapter(factory) before the first connection attempt. The factory receives the
root namespace and every static namespace already created. Later static namespaces and
admitted concrete dynamic namespaces call it when they are created. Every call must return
a fresh instance. If any existing-namespace call throws or reuses an instance, no adapter
is replaced. Adapter state and membership never migrate through late registration.
Stateful adapters may implement removeSocket(sid). Whole-socket cleanup first calls
del for each room and removes the sids entry, then calls this optional method once while
the namespace roster still contains the socket. Ordinary socket.leave(socket.id) does not
call it. This is a Smocket extension hook, not Socket.IO's delAll or lifecycle events.
DelayingAdapter uses the hook to drain every queued server-to-client delivery in FIFO
order and release the sid's delay state. Scheduled callbacks for that detached queue become
inert. A fresh sid after reconnect starts without the old delay.
Final routing traces
An adapter may implement traceBroadcast(trace) to observe one final routing decision.
The hook runs after payload encoding and recipient selection, including exclusions and
volatile filtering, but before acknowledgement counting, outgoing catch-all listeners, or
delivery. Empty-recipient broadcasts are included. Direct Socket emits are not.
TracingAdapter supplies a caller-cleared history of frozen BroadcastTrace objects.
Each record contains only the event, target and except rooms, resolved excluded sids,
final recipient sids, and the volatile flag. It retains no payload. Pass another adapter
to its constructor to keep that adapter's routing, scheduling, and removal behavior while
adding traces, including new TracingAdapter(new DelayingAdapter()).
Deterministic broadcast dropping
DroppingAdapter removes a known sid from the final broadcast recipients until it is
restored or disconnected. Call setDropped(sid) after connection and
setDropped(sid, false) to restore it. Unknown and disconnected ids are ignored.
The filter runs after ordinary room, exclusion, sender, and volatile selection but before
tracing, acknowledgement counting, outgoing catch-all listeners, and delivery. It affects
broadcast events only: direct server-Socket emits, client-to-server events, rooms, and
lifecycle are unchanged. Wrap another adapter to combine behaviors, for example
new TracingAdapter(new DroppingAdapter(new DelayingAdapter())).
Why now, and why smocket-only
The seam lands before v1.0.0 on purpose. That release freezes the public surface, and adding an extension point after the freeze is a breaking change, so landing it first is what keeps the routing layer from being pinned to a single in-memory implementation (0008).
A custom adapter written here does not run on real socket.io, whose adapter also delivers and needs a transport smocket has none of (0009). smocket guarantees its own delivery matches real socket.io; it does not promise your extension code is portable.
That boundary is listed in differences.md §B and 0031. Final routing observation is recorded in 0032. Deterministic final-recipient dropping is recorded in 0036.