Skip to main content

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.

info

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.

Rooms

Join and leave, and which members an emit to a room reaches.

Broadcast

The broadcast variants and the sockets each one targets or excludes.

Broadcast chaining

Narrowing a broadcast further, and whether the order of the narrowings matters.

Local broadcast socket lookup

Fetching existing local server Sockets through canonical room, exclusion, sender, and namespace selection.

Local bulk broadcast membership

Joining and leaving rooms synchronously through canonical room, exclusion, sender, and namespace selection.

Local bulk broadcast disconnect

Disconnecting selected namespace Sockets or their shared Manager groups through canonical management selection.

Aliases and compression modifiers

send, write, open, close, socket in, and compression chaining. Compression packet effects stay outside the transport-free mock boundary.

Namespaces

What a namespace isolates: connections, emits, rooms, and socket ids.

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.

Acknowledgements

The trailing callback and emitWithAck, in both directions.

Acknowledgement lifecycle

Connection-owned teardown guards, broadcast partial responses, and direction-specific one-shot consumption when response encoding fails.

Payload serialization

JSON results, snapshot timing, invalid data, and reference isolation.

Acknowledgement timeouts

timeout(ms) on a single emit, and what a late ack does.

Broadcast acknowledgements

Collecting an ack from every recipient of a broadcast, and answering on expiry.

Broadcast Promise acknowledgements

Awaiting every selected recipient, including timeout errors, snapshots, and wrapper composition.

Connection middleware

io.use: admitting a connection, rejecting one, and the order two run in.

Server Socket packet middleware

Per-packet ordering, mutation, acknowledgements, rejection, and independent asynchronous continuation.

Handshake

The handshake fields a mock can source, and how auth and query reach them.

socket.data

The per-socket store, its isolation, and its lifetime.

Volatile emits

What volatile delivers in steady state, and the one window where it drops.

Catch-all listeners

onAny / offAny on both sides, and the events they do not see.

Outgoing catch-all listeners

onAnyOutgoing / offAnyOutgoing, and where in the send path they fire.

Reserved event names

Which public emit names throw before delivery or outgoing observation.

Listener removal

off and removeAllListeners, including the places the two sides disagree.

Listener introspection

Side-specific snapshots, live arrays, counts, names, and once wrappers.

Disconnect

Room cleanup, the reason each side reports, and what happens to a pending ack.

Queued delivery lifecycle

Dropping or draining already-queued packets according to the receiver and teardown path that owns the connection lifecycle.

Shared Manager disconnect

Namespace grouping, connection-wide teardown order, independent Managers, and reconnect cleanup.

Server close

Server-wide teardown, its reasons, and what happens to pending acknowledgements.

Return values

What emit, listener, middleware, connect, and disconnect methods hand back, and which chain.

Inherited emitter compatibility

Node and component-emitter aliases, listener order, wrappers, removal, delegation, and max-listener state.

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.

connect(url) and the origin registry

Resolving a url to a server, and what the url contributes to the handshake.

Binary passthrough guard

Keeping out-of-scope binary-containing packets on the existing in-memory path without an encoding claim.

Adapter API

Registering an adapter that changes the routing decision.

Adapter lifecycle

Factory isolation, setup boundaries, and whole-socket cleanup.

TracingAdapter

Recording immutable final broadcast routing decisions without payloads.

Deterministic broadcast dropping

A Smocket-only final-recipient filter by sid, including acknowledgements, cleanup, namespace isolation, and adapter composition.

Broadcast management adapter boundary

Keeping local management selection on canonical Socket state instead of custom event routing and delivery filtering.

DelayingAdapter

Holding a socket's client-inbound stream so a race can be interleaved on purpose.

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.

Public entry points

What the package exports, including the io name the substitution path needs.

Public direct connection API

Pairing direct clients with server sockets, namespace queue order, admission outcomes, and close settlement.

SharedWorker host bridge

Validating port messages, generations, acknowledgements, ordering, errors, and explicit teardown around the existing in-process server.

SharedWorker client facade

Connecting through the narrow page API, listener behavior, acknowledgements, stale-generation suppression, and bridge errors.

SharedWorker lobby application handlers

Running the documented lobby handlers against real Socket.IO and Smocket, including duplicate-label identity, readiness, start, and disconnect.

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.

  1. Put it in the area file it belongs to under src/. A new file also needs an entry in the area table in scripts/conformance-report.mjs, which fails the run rather than dropping an unlisted file from this page.
  2. Run pnpm test:real first. Red here means the case states something socket.io does not do, so the case is wrong and the mock is not involved yet.
  3. 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.
  4. Prove non-receipt with a marker, never with a timeout. See the dual run above.
  5. Run pnpm conformance and 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.

QuestionAnswerJob
Which Node runs the suite22 and 24 on Linux, current LTS on Windows and macOStest
Which Node runs the published package20 and up, the floor engines.node declaresdeclared node floor
Which TypeScript consumes the types5.0.2 and up, under NodeNext and Bundlerpackage
Which socket.io the cases hold for4.7 and 4.8real target
Which browser the mock runs inChromium, mock target onlybrowser

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.