Conformance report
TL;DR Every behaviour on this page was measured against a real socket.io server and then against smocket, from one test file. The list is generated from that run and is written only when both targets pass, so a case is here because it was compared rather than because someone claimed it.
This report records the cases that run against both targets. It is not a percentage of Socket.IO's complete API and does not make a 100% compatibility claim.
The dual run
A test file here never imports a server directly. It calls setupServer() from
src/setup-server.ts, which resolves to a real socket.io
server or to smocket depending on SMOCKET_TARGET, and the two targets are the two
vitest projects pnpm test:real and pnpm test:mock run. The test body is the same
either way.
The real target is the oracle, and the order matters. A case is written against it first, so what the case asserts is socket.io's behaviour and not the mock's. Running the identical file against smocket then asks one question, whether the same assertion still holds. That is why a red mock target is read as a divergence located rather than a test that needs adjusting.
No case asserts non-receipt by waiting. A timeout only says an event had not arrived
yet, and it buys that weak claim with a slow suite. The cases send a later event to the
same socket instead and assert the ordering: once the marker has arrived, anything that
was supposed to precede it would already be there. The helpers are in
src/test-events.ts.
Both targets run on every push and pull request, across three operating systems, so the comparison is continuous rather than something last confirmed by hand. The single CI badge on the README goes red if either target does.
Verified against real socket.io
Every case below ran against socket.io 4.8.3 first and against smocket second, from the same test file, and passed on both. Each links to the test that pins it.
Connection and identity
Pairing a client with its server socket, the id both sides see, and the first emit.
- both sides have a socket id once connected
- server connection state changes at the same lifecycle boundaries as socket.io
- a socket id is 20 characters of url-safe base64
- io.on('connection') fires with the connecting server socket
- io.on('connect') is a synonym for 'connection' on the server
- a client-to-server emit arrives
- a client-to-server ack comes back
- a server-to-client ack comes back
Rooms
Join and leave, and which members an emit to a room reaches.
- joining a room receives emits for that room
- a client that has not joined does not receive emits for that room
- after leaving, a client no longer receives emits for that room
- a socket in several rooms receives the emits of each room
- every client in the same room receives (fan-out)
Broadcast
The broadcast variants and the sockets each one targets or excludes.
- socket.broadcast.emit goes to everyone except the sender
- io.except(room) goes to everyone not in that room
- to() with an array delivers to the union of the rooms
- chaining to() delivers to the union of the rooms
- the array union delivers only once even when a socket is in several rooms
- the chained union delivers only once even when a socket is in several rooms
- in() is an alias for to()
- socket.except(room) excludes both the sender and that room
- io.to(socketId) delivers only to that socket (its own id room)
- socket.rooms is server-only and reflects its own id and join/leave
- socket.to(room) excludes the sender even when the sender is a member of that room
- io.emit() delivers to everyone connected
Broadcast chaining
Narrowing a broadcast further, and whether the order of the narrowings matters.
- io.to(room).except(id) sends to the room minus that socket
- the two orderings of to and except reach the same sockets
- chaining except twice excludes the union of both
- a chained call returns a new operator and leaves the original alone
- socket.to(room).except(id) keeps excluding the sender
- socket.broadcast.except(id) excludes the sender and that socket
- in() on the operator is an alias for to()
- io.of(nsp).to(room).except(id) narrows within that namespace
Local broadcast socket lookup
Fetching existing local server Sockets through canonical room, exclusion, sender, and namespace selection.
- io.fetchSockets returns the existing local sockets in connection order
- fetchSockets applies room union, deduplication, and exclusions
- a socket management operator excludes its sender and named rooms
- fetchSockets stays inside its namespace even when room names match
- dynamic parents keep Socket.IO fetchSockets boundaries
- lookup ignores timeout, volatile, and compression delivery modifiers
Local bulk broadcast membership
Joining and leaving rooms synchronously through canonical room, exclusion, sender, and namespace selection.
- io.socketsJoin synchronously joins every root Socket
- socketsJoin applies room union, deduplication, and exclusions
- socketsLeave snapshots the selected set before mutating its target room
- a Socket management operator excludes its sender from bulk membership
- bulk membership stays inside its namespace
- bulk membership ignores delivery modifiers
- dynamic parent bulk membership follows Socket.IO and does not reach children
Local bulk broadcast disconnect
Disconnecting selected namespace Sockets or their shared Manager groups through canonical management selection.
- io.disconnectSockets(false) synchronously closes every root Socket only
- disconnectSockets applies room union, exclusions, and snapshot selection
- a Socket management operator excludes its sender from bulk disconnect
- disconnectSockets(true) closes each selected Manager group exactly once
- disconnectSockets(true) cancels pending admission on a selected Manager
- bulk disconnect ignores delivery modifiers and stays namespace-local
- dynamic parent bulk disconnect follows Socket.IO and does not reach children
Aliases and compression modifiers
send, write, open, close, socket in, and compression chaining. Compression
packet effects stay outside the transport-free mock boundary.
- Server and Namespace send and write broadcast message and return their receiver
- a dynamic parent sends and writes directly while exposing a compress operator
- server and client Socket send aliases emit message once and return their socket
- a buffered client send observes outgoing before connect and named delivery
- server Socket in aliases to while preserving sender exclusion
- compress returns immutable broadcast operators and composes with narrowing
- broadcast compress preserves a pending acknowledgement timeout
- Socket compress stays fluent through timeout and volatile decorations
- client open and close delegate to the lifecycle and keep fluent identity
Namespaces
What a namespace isolates: connections, emits, rooms, and socket ids.
- io.of normalizes empty and bare static namespace names
- a registered static namespace admits the normalized connection name
- an unregistered static namespace is rejected without membership
- a client can retry after its static namespace is registered
- io.of(nsp).on('connection') fires only for connections on that namespace
- io.of(nsp).emit() goes only to clients in that namespace
- io.emit() on the default namespace does not reach other namespaces
- a room of the same name is separate per namespace
- a client attached to two namespaces has a different socket id per namespace
- socket.broadcast stays inside the namespace of the sender
Dynamic namespace parents
Parent admission, concrete child lifecycle, setup snapshots, and direct broadcasts. Narrowed operator construction is covered, while narrowed delivery remains unverified under 0029.
- emits new_namespace synchronously once for static namespaces but not root or parents
- admits RegExp children, caches them, and attaches manual children to the parent
- preserves stateful RegExp lastIndex across dynamic admission attempts
- does not re-evaluate a stateful RegExp parent when reading cached namespaces
- preserves sticky RegExp lastIndex across dynamic admission attempts
- resets caller-assigned RegExp lastIndex after a failed manual attachment match
- uses admission order but the latest duplicate RegExp parent for manual attachment
- does not attach a manually created namespace to a function parent
- reuses one child for concurrent admission and supports the of listener overload
- tries function matchers in order with normalized names and auth until one allows
- invokes a dynamic namespace matcher after the client factory returns
- does not invoke a dynamic matcher for a connection cancelled after return
- rejects an unmatched dynamic namespace as Invalid namespace
- retries dynamic admission after an earlier matcher rejection
- dynamic admission reads the current client.auth on a manual retry
- creates a child before middleware and snapshots parent setup at creation
- copies the parent connect synonym to a concrete child
- ignores duplicate client connect calls while async dynamic admission is pending
- cancels dynamic admission while callback-form auth is unresolved
- cancels unresolved dynamic matching with shared Manager disconnect(true)
- continues parent matching after a cancelled parent rejects late
- reuses one child for concurrent async same-name admissions
- broadcasts directly across children while child rooms and lifecycle stay isolated
- exposes narrowed parent operators without selecting their delivery result
- keeps shared Manager teardown connection-wide across dynamic children
- uses one concrete child for nextConnection, lookup, and Manager grouping
- creates a RegExp child when nextConnection observes it before a client connects
- keeps new_namespace available as an ordinary Socket payload event
Acknowledgements
The trailing callback and emitWithAck, in both directions.
- multi-argument ack resolves with the first value
- the trailing callback receives the sender-side ack
- a client timeout callback preserves every acknowledgement argument
- a server timeout callback preserves every acknowledgement argument
- timeout emitWithAck keeps the first-value Promise policy in both directions
- calling ack twice runs the sender callback only once
- discards retained acks in both directions after the client disconnects
- discards retained acks in both directions after the server Socket disconnects
- discards retained acks in both directions during server close
- emitWithAck stays pending when the peer never acks
- server-to-client emitWithAck works without a timeout
- emitWithAck buffers while disconnected and settles after reconnect
Acknowledgement lifecycle
Connection-owned teardown guards, broadcast partial responses, and direction-specific one-shot consumption when response encoding fails.
- drops a client-to-server ack invoked after the server Socket disconnects
- drops a server-to-client ack invoked after the client disconnects
- drops a server-to-client ack invoked after Server.close()
- keeps acknowledgements invoked before teardown in both directions
- times out a broadcast with only the connected recipient response
- consumes a client-generated ack when its first BigInt response cannot be encoded
- consumes a client-generated ack when its first circular value response cannot be encoded
- keeps a server-generated ack callable after a failed BigInt response
- keeps a server-generated ack callable after a failed circular value response
- does not collect a broadcast retry after a client BigInt response fails
- does not collect a broadcast retry after a client circular value response fails
Payload serialization
JSON results, snapshot timing, invalid data, and reference isolation.
- client-to-server payloads use JSON results and snapshot at emit
- server-to-client payloads snapshot at emit and decode fresh values
- client-to-server ack requests and responses cross independent snapshots
- server-to-client ack requests and responses cross independent snapshots
- a buffered client payload stays live until outgoing observation and flush
- direct outgoing listeners mutate the live source before the snapshot
- broadcast snapshots once before outgoing listeners and decodes per recipient
- room ack broadcasts snapshot requests and responses per recipient
- toJSON and enumerable own properties determine decoded object results
- a plain toJSON result keeps an original binary property out of the packet
- a broadcast encodes even when its room has no recipients
- circular and BigInt payloads fail before delivery in both directions
- only a client timeout survives a payload encoding failure
- timeout and connected volatile wrappers use the same payload boundary
Acknowledgement timeouts
timeout(ms) on a single emit, and what a late ack does.
- the timeout callback receives (null, response) when the ack wins
- works server-to-client with the same success shape
- returns the same socket and consumes a direct timeout once, on both sides
- keeps a recipient timeout pending across plain and ack-collecting broadcasts
- the callback gets a single timeout Error when the peer never acks
- times out the same way server-to-client
- drops a late ack that arrives after the timeout already fired
- timeout().emitWithAck resolves with the response when the ack wins
- timeout().emitWithAck rejects with the timeout Error on expiry
- keeps a timed callback buffered until reconnect and settles it normally
- does not revive a timed callback that expired while buffered
- does not deliver emitWithAck after its buffered timeout expires
- server timeout().emitWithAck resolves and rejects with the same one-shot decoration
- times out volatile server emits in either modifier order without delivering them
- a callback-less timeout emit still delivers and arms no timer
Broadcast acknowledgements
Collecting an ack from every recipient of a broadcast, and answering on expiry.
- collects every recipient ack and answers (null, responses)
- orders responses by ack arrival, not by join order
- answers (Error, partial responses) when a recipient never acks in time
- invokes the callback exactly once, dropping an ack that arrives after expiry
- answers (null, []) at once for a broadcast to a room with no recipients
- socket.broadcast.timeout(ms) collects from everyone except the sender
- a chained except drops that recipient from the collection, timeout set first
- a chained except drops that recipient from the collection, timeout set last
- socket.timeout(ms).to(room) collects from the room, timeout set first
- socket timeout transfers once to the to operator
- socket timeout transfers once to the except operator
- socket timeout transfers once to the broadcast operator
Broadcast Promise acknowledgements
Awaiting every selected recipient, including timeout errors, snapshots, and wrapper composition.
- broadcast emitWithAck resolves responses in acknowledgement arrival order
- a disconnected recipient cannot finish a broadcast acknowledgement collection
- untimed broadcast acknowledgement collection keeps the timer race and resolves [] for nobody
- untimed broadcast acknowledgement collection times out when a recipient never acknowledges
- timeout rejection exposes partial responses and late acknowledgements mutate that array once
- server, namespace, room, exclusion, and socket broadcast share Promise collection
- timeout-first and narrowing-first Promise broadcasts select the same responders
- Promise broadcast hides its collector ack and observes each selected socket once
- reserved Promise broadcasts reject without outgoing observation
- dynamic parent Promise acknowledgements resolve [] without reaching concrete children
- Promise broadcast snapshots one request and each acknowledgement response independently
Connection middleware
io.use: admitting a connection, rejecting one, and the order two run in.
- invokes namespace middleware after the client factory returns
- does not invoke namespace middleware for a connection cancelled after return
- a pass-through middleware admits the connection and fires connection
- the middleware reads the connecting socket handshake
- next(err) makes the client observe connect_error with the error message
- the rejecting error's data passes through to the client
- connect_error exposes a JSON snapshot instead of the server Error
- a rejected connection cleans temporary membership and stays out of the roster
- a cancelled connection attempt cannot be admitted by a late middleware callback
- ignores duplicate connect calls while static namespace middleware is pending
- io.of(nsp).use() runs only for connections on that namespace
- two middlewares run in registration order
- an error in the first middleware short-circuits the second
- completes once per synchronous next call with one server Socket
- completes again when a retained next runs after the first connection
- ignores a retained next released while the server closes
- ignores a retained middleware error after the client disconnects
- reports a later middleware error after connection and removes the server Socket
Server Socket packet middleware
Per-packet ordering, mutation, acknowledgements, rejection, and independent asynchronous continuation.
- registers per-socket middleware in order and returns the same socket
- runs incoming catch-alls before middleware and exposes packet mutation downstream
- keeps the acknowledgement callback in the mutable middleware packet
- snapshots middleware when each packet begins processing
- lets a later packet complete while earlier packet middleware is held
- short-circuits on next(error), emits that Error, and does not acknowledge
- does not dispatch a held packet after the socket disconnects
Handshake
The handshake fields a mock can source, and how auth and query reach them.
- the connection handshake carries the fields a mock can source
- handshake.auth defaults to an empty object when the client passes none
- handshake.auth carries the client-supplied auth values
- handshake.auth is a JSON snapshot of the CONNECT packet
- handshake.query stringifies the client-supplied query values
- handshake.auth accepts a function form, resolved via its callback
- invokes callback-form auth after the client factory returns
- disconnect cancels a static connection while callback auth is unresolved
- a reconnect replays the client-supplied auth on the fresh socket
- a reconnect reads a replacement object from client.auth
- a reconnect re-evaluates the current callback from client.auth
socket.data
The per-socket store, its isolation, and its lifetime.
- socket.data is an empty object at connection
- middleware writes to data and a connection handler reads it back
- each socket has its own data
- a reconnection gets a fresh, empty data rather than the previous socket store
Volatile emits
What volatile delivers in steady state, and the one window where it drops.
- a volatile emit is delivered on a connected socket (server to client)
- a volatile emit is delivered on a connected socket (client to server)
- io.volatile.to(room) routes to the room like a normal broadcast in steady state
- socket.volatile.broadcast reaches everyone except the sender in steady state
- io.to(room).volatile and io.volatile.to(room) preserve the same target
- namespace narrowing and volatile preserve each other in either order
- socket.to(room).volatile and socket.volatile.to(room) keep sender exclusion
- socket.broadcast.volatile and socket.volatile.broadcast keep sender exclusion
- volatile stays immutable and survives to, in, except, and timeout in either order
- a volatile emit still carries an ack, which round-trips when delivered
- volatile emitWithAck delivers and fires outgoing catch-alls in both directions
- a volatile emit to a recipient still in the pre-connect window is dropped
- a volatile emit from a client still in the pre-connect window is dropped
- consumes volatile once when the same server socket reference is reused
- keeps a recipient volatile flag pending across an unrelated broadcast
- transfers a server volatile flag once to the to operator
- transfers a server volatile flag once to the except operator
- transfers a server volatile flag once to the broadcast operator
Catch-all listeners
onAny / offAny on both sides, and the events they do not see.
- a server-side catch-all fires for every incoming event with the name and args
- a catch-all runs before the specific listener for the same event
- a catch-all does not fire for the reserved disconnect events
- offAny(listener) removes one catch-all, offAny() removes all
- the same catch-all registered twice fires once per registration
- offAny removes one occurrence of a doubly-registered catch-all
- a catch-all receives an ack callback as the last argument
- a client-side catch-all fires for a server emit
- a client catch-all runs before the specific listener for the same event
- a client catch-all does not fire for the reserved disconnect event
- client offAny(listener) removes one catch-all, offAny() removes all
- server prependAny listeners run newest-first before onAny listeners
- client prependAny listeners run newest-first before onAny listeners
- server listenersAny is live and offAny removes the first matching duplicate
- client listenersAny is live and offAny removes the first matching duplicate
- offAny replaces both sides backing arrays and detaches earlier lookups
- incoming catch-all dispatch snapshots listener mutations on both sides
- a client incoming catch-all receives the server ack callback
- empty listenersAny lookups are fresh and cannot install listeners on either side
- offAny on untouched sockets keeps empty lookups fresh and inert
- offAny detaches the old arrays and installs stable empty replacements
Outgoing catch-all listeners
onAnyOutgoing / offAnyOutgoing, and where in the send path they fire.
- a server-side outgoing catch-all fires for a direct emit with the event name and args
- a client-side outgoing catch-all fires for a client emit
- a client timeout survives an outgoing catch-all throw and is then consumed once
- a connected volatile emit fires the outgoing catch-all, on both sides
- the outgoing catch-all runs before the peer receives the event
- io.emit fires the outgoing catch-all on every recipient socket
- a broadcast fires the outgoing catch-all on the reached socket, but not the sender
- the outgoing catch-all does not fire for the disconnect lifecycle
- the ack callback is stripped from the outgoing catch-all args, for emit and emitWithAck
- offAnyOutgoing(listener) removes one, offAnyOutgoing() removes all
- the client side carries offAnyOutgoing too
- server prependAnyOutgoing listeners run newest-first before onAnyOutgoing listeners
- client prependAnyOutgoing listeners run newest-first before onAnyOutgoing listeners
- server listenersAnyOutgoing is live and removes the first matching duplicate
- client listenersAnyOutgoing is live and removes the first matching duplicate
- offAnyOutgoing replaces both sides backing arrays and detaches earlier lookups
- outgoing catch-all dispatch snapshots listener mutations on both sides
- the client outgoing catch-all omits ack callbacks for emit and emitWithAck
- empty listenersAnyOutgoing lookups are fresh and cannot install listeners on either side
- offAnyOutgoing on untouched sockets keeps empty lookups fresh and inert
- offAnyOutgoing detaches the old arrays and installs stable empty replacements
Reserved event names
Which public emit names throw before delivery or outgoing observation.
- server emit surfaces reject the six reserved names and accept application events
- client emit surfaces reject the six reserved names and accept application events
- client wrappers reject reserved names while the connection is still pending
- rejected server emits reach neither the peer nor outgoing catch-alls
- rejected client emits reach neither the peer nor outgoing catch-alls
- a rejected client event retains its timeout for the next completed emit
- emitWithAck rejects reserved names without firing outgoing catch-alls
- connection and new_namespace remain ordinary public payload event names
Listener removal
off and removeAllListeners, including the places the two sides disagree.
- off removes only the named registration
- the same callback registered twice is called once per registration
- off removes one occurrence of a doubly-registered callback, leaving the rest
- removeAllListeners(event) clears every listener for that event only
- removeAllListeners() clears listeners for every event
- removeAllListeners() clears a disconnect handler, but the socket still tears down
- off and removeAllListeners are no-ops for unknown listeners or events
- catch-all removal is a no-op for an unknown listener
- a listener removed during its own dispatch still runs for that dispatch
- off removes a once registration by its original listener
- off removes a once registration on the client side too
- server off removes the last match, leaving the earlier once registration
- server off removes the last match, leaving the earlier on registration
- client off removes the first match, leaving the later on registration
- client off removes the first match, leaving the later once registration
- client off removes only the named registration
- client removeAllListeners(event) clears that event only
- server off(event) without a listener throws
- client off(event) without a listener clears that event
- removeAllListeners() does not stop room cleanup
- removeAllListeners() leaves catch-all listeners in place
Listener introspection
Side-specific snapshots, live arrays, counts, names, and once wrappers.
- a fresh server socket exposes only its internal error listener
- server listeners are fresh snapshots with duplicates and unwrapped once callbacks
- server listenerCount filters direct and once registrations for string and symbol names
- server event names delete empty keys and reinsert them at the end
- client listeners expose the live array and component-emitter once wrapper
- client last-off and once exhaustion empty and detach the old backing array
- client introspection is available before connect for reserved events
Disconnect
Room cleanup, the reason each side reports, and what happens to a pending ack.
- drops direct sends from the old server Socket after a client disconnect
- drops direct sends from the old server Socket after a server disconnect
- a disconnected socket no longer receives emits for that room
- client connected and disconnected remain exact inverses across teardown
- a room disappears from the adapter when its last member disconnects
- whole-socket cleanup removes the sid from adapter membership
- a disconnected socket cannot join rooms again
- a reconnected socket does not automatically rejoin its previous rooms
- rooms are still present at disconnecting and empty at disconnect
- a pending client.emitWithAck rejects when the connection drops
- disconnect clears a pending client timeout before rejecting emitWithAck
- a disconnect from an outgoing observer clears the current emitWithAck timeout
- client disconnect settles a sent timed callback once and clears its timer
- server Socket disconnect settles a sent client timed callback once
- an outgoing observer disconnect settles the timed callback being sent
- a trailing-callback ack is silently discarded when the connection drops
- a pending server.emitWithAck stays pending when the client disconnects
- client.disconnect() reports io client disconnect to the client and client namespace disconnect to the server
- serverSocket.disconnect() reports io server disconnect to the client and server namespace disconnect to the server
- disconnecting carries the same reason and fires before disconnect
Queued delivery lifecycle
Dropping or draining already-queued packets according to the receiver and teardown path that owns the connection lifecycle.
- drops client packets queued behind a server Socket disconnect without packet middleware
- drops client packets queued behind a server Socket disconnect with packet middleware
- drops server packets queued behind a client disconnect
- drops client packets queued behind Server.close() without packet middleware
- drops client packets queued behind Server.close() with packet middleware
- drains client packets admitted before client.disconnect()
- drains server packets queued before Server.close()
- preserves FIFO in both directions while the connection remains active
Shared Manager disconnect
Namespace grouping, connection-wide teardown order, independent Managers, and reconnect cleanup.
- disconnect(false) closes only its namespace socket
- disconnect(true) is inert after that server socket disconnects with false
- disconnect(true) closes shared namespaces in connection order before returning
- disconnect(true) from a connection handler includes the initiator and isolates opt-outs
- disconnect(true) cancels pending namespace admission on the shared Manager
- reentrant client disconnects do not duplicate shared Manager teardown
- disconnect(true) leaves duplicate and opted-out Managers connected
- shared Manager teardown rejects client acks and permits explicit reconnect
- disconnect(true) from a stale server socket leaves the reconnect connected
Server close
Server-wide teardown, its reasons, and what happens to pending acknowledgements.
- close invokes its callback and reports when called again
- close rejects a connection started immediately before it
- close rejects a connection whose callback auth resolves after shutdown
- close rejects a connection whose namespace middleware resolves after shutdown
- close rejects dynamic admission allowed after shutdown
- close disconnects every namespace with the shutdown reasons
- close rejects a pending client emitWithAck
- close settles a sent client timed callback once and clears its timer
- close leaves a pending server emitWithAck pending
- close does not cancel an armed server acknowledgement timeout
Return values
What emit, listener, middleware, connect, and disconnect methods hand back, and which chain.
- the client emit returns the socket, so it chains
- a buffered emit returns the socket too, before the connection completes
- the server socket emit returns true rather than the socket
- the server emit returns true
- a namespace emit returns true
- a broadcast emit returns true
- a timed broadcast emit returns true
- a timed server socket emit returns true, where the client one chains
- a volatile emit follows its own side: true on the server, the socket on the client
- a dropped volatile emit still returns the emitter it was called on
- the client listener methods return the socket, so they chain
- the server socket listener methods return the socket, so they chain
- chained registrations both take effect
- a namespace on returns the namespace, so it chains
- server and namespace use return the object they register on
- client connect returns the socket while connected and when reconnecting
- client disconnect returns the socket whether or not it is connected
- server socket disconnect returns that socket
Inherited emitter compatibility
Node and component-emitter aliases, listener order, wrappers, removal, delegation, and max-listener state.
- Server listener methods delegate state and runtime identity to the root Namespace
- Namespace prepend methods order connection listeners and expose once wrappers
- ParentNamespace snapshots inherited listener ordering when each child is created
- server Socket inherited methods preserve Node ordering and raw listener identity
- named listener callbacks receive their Namespace or Socket receiver
- Node emitter aliases remove the last matching registration
- once wrapper identity properties remain specific to each emitter side
- max-listener state is receiver-local and Server delegates it to root
- Node receivers warn once when a listener count exceeds their local maximum
- Namespace removal and filtered counts follow Node EventEmitter
- Node eventNames uses property-key order for integers, strings, and symbols
- Namespace meta-events collide with Socket.IO reserved outgoing names
- Server delegates the newListener collision to the root Namespace
- bulk removal of the final removeListener observer follows the host and receiver
- server Socket meta-events collide before add and after once removal
- client source and declaration aliases share component-emitter identity
- client removeAllListeners with no event clears every ordinary listener
smocket only
These have no oracle to compare against: they cover the API smocket adds (differences.md §B) and the internals behind it, so they run the same under both targets. They are listed apart because nothing about socket.io follows from them.
Native acknowledgement teardown race
Discarding or retaining acknowledgements as appropriate when a Smocket outgoing observer tears down a connection mid-send.
- discards an ack retained after an outgoing observer disconnects the client
- skips a buffered timed promise settled during reconnect flush
- stops reconnect flushing when an outgoing observer disconnects
connect(url) and the origin registry
Resolving a url to a server, and what the url contributes to the handshake.
- connect(url) resolves to the server registered for that origin
- a missing-server socket exposes disconnected state and mutable auth
- handshake.url is the normalized origin the client connected to
- two spellings of one origin resolve to the same server
- a bare https origin resolves to the same server as its default port
- connect(url) caches one Manager per normalized origin unless opted out
- the url's query string lands on handshake.query
- connect(url, { auth }) puts the auth object on the handshake
- a function auth holds the pairing until its callback fires
- a function auth is re-evaluated on each connection, including a reconnect
- a completed reconnect resets client.recovered to false
- the url query wins wholesale over the options query when both are given
- the options query is used only when the url carries none
- the url's path selects the namespace
- connect(url) rejects an unregistered namespace without creating membership
- a relative url resolves against location.origin
- connect(url) to an unregistered origin fires connect_error, without throwing
- a failed client still rejects reserved names on every emit wrapper
- close unregisters the server so later connect(url) reports a missing server
- closing a replaced server does not unregister its replacement
- the socket from a failed connect still chains
- a failed client carries the complete catch-all listener surface
- a failed client carries component-emitter listener introspection
Binary passthrough guard
Keeping out-of-scope binary-containing packets on the existing in-memory path without an encoding claim.
- keeps binary-containing direct packets on the existing in-memory path in both directions
- keeps binary-containing acknowledgement payloads on the existing in-memory path
Adapter API
Registering an adapter that changes the routing decision.
- Adapter ignores deletion for membership it does not hold
- broadcast routing ignores stale sids returned by a custom adapter
- io.adapter registers a custom adapter that observes the routing decision
- a custom adapter can drop a socket from the target set, and per-socket order still holds
- registering a custom adapter preserves per-socket delivery order
- builds an independent registered adapter for each dynamic concrete child
Adapter lifecycle
Factory isolation, setup boundaries, and whole-socket cleanup.
- builds isolated adapters for root, existing, future static, and dynamic namespaces
- keeps every existing adapter unchanged when replacement construction fails
- rejects one adapter instance shared by multiple namespaces without partial replacement
- reports dynamic adapter construction failure and lets the client retry
- does not register a future static namespace when its adapter construction fails
- rejects reusing an existing adapter for a future namespace
- closes adapter registration at the first connection attempt, including rejection
- signals whole-socket removal once for the client teardown path
- signals whole-socket removal once for the server teardown path
- signals whole-socket removal once for the manager teardown path
- signals whole-socket removal once for the close teardown path
- signals cleanup once for rejected and cancelled admission without lifecycle events
TracingAdapter
Recording immutable final broadcast routing decisions without payloads.
- records one final decision for Server, Namespace, room, exclusion, and Socket entry points
- keeps root and named namespace history isolated
- records a dynamic parent broadcast once in each concrete namespace
- records empty and volatile final recipient sets plus callback and Promise ack broadcasts
- records before outgoing observation and delivery without changing FIFO
- excludes direct socket traffic and failed broadcast encoding
- returns caller-cleared immutable snapshots with no payload reference
- observes recipients after a wrapped custom adapter changes routing
- composes with DelayingAdapter scheduling and removal
Deterministic broadcast dropping
A Smocket-only final-recipient filter by sid, including acknowledgements, cleanup, namespace isolation, and adapter composition.
- drops io.emit by sid and restores delivery without changing membership
- preserves room union, exclusions, sender exclusion, and unaffected direct traffic
- removes dropped recipients from callback and Promise acknowledgement collection
- does not cancel a broadcast acknowledgement already selected before the drop
- skips outgoing observation for dropped delivery and preserves remaining FIFO
- cleans state on disconnect, gives reconnect a fresh sid, and isolates namespaces
- composes dropping before tracing with wrapped delayed FIFO delivery
- receives the ordered final ids after volatile filtering and cannot add or reorder
Broadcast management adapter boundary
Keeping local management selection on canonical Socket state instead of custom event routing and delivery filtering.
- management lookup ignores custom routing and delivery dropping
- bulk membership ignores custom routing and delivery dropping
- bulk disconnect ignores custom routing and delivery dropping
DelayingAdapter
Holding a socket's client-inbound stream so a race can be interleaved on purpose.
- an emit from a connection handler reaches the client, before the pairing completes
- a delayed socket is held on the timer while an undelayed one still arrives next tick
- does not delay the server side: a client emit is received on the next tick
- preserves order within a delayed socket's stream, and holds it until the delay elapses
- a lowered delay does not let a new event overtake one already queued
- a new delay applies only to deliveries scheduled after it is set
- gates order through the queue, not the timer: only the head is ever scheduled
- ignores a non-finite delay rather than storing NaN or Infinity
- keeps delay state when the socket leaves only its id room
- drains a queued stream during close without duplicating scheduled callbacks
- drains the remaining queue when the scheduled head triggers teardown
- does not carry an old sid delay into a reconnect
Native broadcast Promise policy
Applying Smocket-only pre-connect volatile selection before acknowledgement counting.
Socket id encoding
The encoder behind the id shape the dual run pins.
- encodes bytes the way base64url does, url-safe alphabet included
- strips the padding a length off a multiple of three produces
- an id is 15 random bytes run through that encoder
Public entry points
What the package exports, including the io name the substitution path needs.
- connecting pairs the client and server socket with the same id
- exports
ioas socket.io-client's name for connect, so a module swap works - exports the contract types, so the swap keeps an app annotations to use
- exports a server type that keeps the smocket-only members
- exports the tracing adapter and trace type
- exports the deterministic dropping adapter
Public direct connection API
Pairing direct clients with server sockets, namespace queue order, admission outcomes, and close settlement.
- connect and nextConnection expose both sides of one admitted socket
- pairs wait-before-connect and connect-before-wait in connection order
- pairs connect-before-wait on a registered named namespace
- pairs multiple waiting observers with clients in FIFO order
- returns multiple ready sockets in FIFO order
- normalizes namespace names while keeping their queues isolated
- keeps direct connections in the established Manager groups
- skips rejected admission and resolves the waiter with the next accepted socket
- skips cancelled admission and resolves the waiter with the next accepted socket
- offers a repeatedly completed Socket to the direct API only once
- does not leave a claimed Socket queued after a repeated middleware error
- close rejects pending static and dynamic namespace observers
- close discards unclaimed sockets and rejects later observers
- close preserves a ready socket claimed before teardown
SharedWorker host bridge
Validating port messages, generations, acknowledgements, ordering, errors, and explicit teardown around the existing in-process server.
- shared-worker protocol validates message shape, direction, and protocol version at both boundaries
- shared-worker host connects through the existing server and carries acknowledgements both ways
- shared-worker host keeps client events FIFO through an acknowledgement marker
- shared-worker host ends the old generation and suppresses its late events and acknowledgements
- shared-worker host reports malformed and unexpected messages without stopping the port
- shared-worker host reports host delivery failures and survives an undeliverable report
- shared-worker host releases a connection when its admission result cannot cross the port
- shared-worker host tolerates a port that cannot carry a bridge error
- shared-worker host reports connection rejection and releases the failed generation
- shared-worker host disconnects a generation replaced before its connection callback
- shared-worker host closes an active host once and preserves its shutdown reason
- shared-worker host disconnects explicitly with the page-supplied reason
SharedWorker client facade
Connecting through the narrow page API, listener behavior, acknowledgements, stale-generation suppression, and bridge errors.
- shared-worker client facade connects automatically, snapshots auth, buffers emits, and explicitly replaces the active generation
- shared-worker client facade matches the supported ordinary and incoming catch-all listener behavior
- shared-worker client facade carries callback, promise, send, and server acknowledgements exactly once
- shared-worker client facade drops stale generation traffic and retained acknowledgements before a later marker
- shared-worker client facade reports invalid and non-cloneable traffic without stopping later delivery
- shared-worker client facade ignores host messages that do not belong to the current local state
- shared-worker client facade reports a server-initiated disconnect while locally connected
- shared-worker client facade finishes an immediate disconnect after the initial admission
- shared-worker client facade stays disconnected when the initial port post fails
- shared-worker client facade stops flushing a buffered batch when delivery changes connection state
- shared-worker client facade drops a server acknowledgement after local connection state ends
- shared-worker client facade reports admission failure once and uses current auth on an explicit retry
- shared-worker client facade disconnects once on pagehide and releases page lifecycle ownership
SharedWorker lobby application handlers
Running the documented lobby handlers against real Socket.IO and Smocket, including duplicate-label identity, readiness, start, and disconnect.
- the documented lobby handlers preserve identity and lifecycle across both targets
- normalizes a blank label and rejects readiness after departure
How to add a case
The gaps above are the shortest route into this repository, because a contribution here is judged mechanically rather than by taste.
- Put it in the area file it belongs to under
src/. A new file also needs an entry in the area table inscripts/conformance-report.mjs, which fails the run rather than dropping an unlisted file from this page. - Run
pnpm test:realfirst. Red here means the case states something socket.io does not do, so the case is wrong and the mock is not involved yet. - Then run
pnpm test:mock. Green on the real target and red on the mock is a divergence found, not a mistake made, and it arrives with its reproduction already written. Open it as an issue or fix the mock to match. - Prove non-receipt with a marker, never with a timeout. See the dual run above.
- Run
pnpm conformanceand commit the regenerated page. CI runs the same generation and fails if this file no longer matches the suite.
Supported versions
Each row is answered by a CI job rather than by a claim, so the evidence is in
.github/workflows/ci.yml.
| Question | Answer | Job |
|---|---|---|
| Which Node runs the suite | 22 and 24 on Linux, current LTS on Windows and macOS | test |
| Which Node runs the published package | 20 and up, the floor engines.node declares | declared node floor |
| Which TypeScript consumes the types | 5.0.2 and up, under NodeNext and Bundler | package |
| Which socket.io the cases hold for | 4.7 and 4.8 | real target |
| Which browser the mock runs in | Chromium, mock target only | browser |
The socket.io row is what lets the report speak for more than one version. The cases
encode socket.io's behaviour, the real target job passes them on both 4.7 and 4.8, and
the ordinary dual run passes the same cases on smocket. A behaviour the two socket.io
versions disagreed on cannot become a shared case, and the compatibility typecheck requires
the contract to admit both measured declarations. The Server.close() return difference is
recorded in differences.md.
The browser row is narrower on purpose. A page cannot host a socket.io server, so there is no real target to compare against there, and the job asks only whether the mock behaves in a browser the way it behaves in Node.
What a version number promises
The number promises fidelity to socket.io, not the result your suite got last week. A correction that moves the mock toward measured real behaviour is therefore a minor release even when it turns a passing test red, because the diverging result was never what the version promised.
- A correction toward measured real behaviour: minor when it changes what is delivered, patch when nothing observable moves.
- Newly covered socket.io surface: minor.
- Removing or altering a deliberate divergence from differences.md §A: major. Adding one: no bump, since it documents what was already happening.
- A public type change: minor if existing call sites still compile, major otherwise.
- Raising
engines.node: major. Lowering it: minor.
Before 1.0.0 every rule applies one place to the right, the way npm reads a 0.x range.
A release that changes what is delivered also carries its own section in the notes, with
the before and after as results and a link to the case above that pins the new
behaviour. The reasoning is in
0019.