Skip to main content

0026. Payloads cross a JSON snapshot boundary

Status: Accepted · 2026-08-12 · #237 Governed by: 0000, 0010

TL;DR Non-binary event and acknowledgement data uses Socket.IO's default-parser JSON result and snapshot timing, without parser extensibility, binary encoding, or wire frames.

Decision

Fresh Socket.IO 4.7.5 and 4.8.3 installs both resolved socket.io-parser 4.2.7 and produced the same result in Node and Chromium. Connected client and server emits encode synchronously: mutation after emit cannot change what arrives. Ack requests follow that rule, and ack responses snapshot when the receiver calls the ack. A client emit buffered before connection is the exception: its source stays live until the buffer flushes.

The decoded data follows JSON conversion. A Date becomes an ISO string, undefined object properties disappear, unsupported array positions and non-finite numbers become null, toJSON is honored, and custom prototypes become plain objects. Map, Set, RegExp, and Error retain only enumerable own properties, and repeated references decode as distinct objects. Circular data and BigInt fail when encoding is attempted: immediately for a connected send and at flush for a buffered client packet.

A broadcast encodes one packet at the call, including when it has no recipients, and each recipient decodes an independent object graph. Neither later mutation of the source nor mutation by one recipient reaches another recipient. Smocket will preserve these results through direct emits, acknowledgements, broadcasts, timeout wrappers, connected volatile emits, and buffered delivery in both directions.

Sending-side onAnyOutgoing still observes live source values. It precedes a connected direct emit's snapshot; a buffered client notifies and snapshots only at flush. Its mutation therefore changes what is encoded. A broadcast snapshots first; each reached server socket's listener sees the shared source but cannot change the encoded value.

Client delivery modifiers are consumed only after outgoing observation and packet encoding complete. A reserved-event rejection, an outgoing listener that throws, or a supported payload encoding failure therefore leaves the modifier armed for the next completed emit, which consumes it once. Server Socket modifiers keep their existing earlier consumption boundary, including when their payload encoding fails.

The compatibility contract is the successful decoded value, reference isolation, and snapshot or failure timing. It does not promise Socket.IO's internal traversal algorithm, the number of getter or toJSON calls, or a native exception's exact class and message. Those vary below the application result and copying them would couple Smocket to parser internals rather than the stable boundary.

This decision covers default-parser packets only when they contain no binary value. Binary encoding and Engine.IO framing remain outside scope, as do custom parser options. The implementation must use host-neutral primitives, add no Socket.IO runtime dependency, pass Node and browser gates, and land separately in [#250].

Under 0019, this newly covered Socket.IO surface is minor after 1.0 and patch before 1.0. Until #250 lands, direct same-process references remain a known gap rather than an intentional divergence.

Alternatives rejected

  • Normalize only Date. Date exposed the defect but not its boundary; it would leave prototype loss, aliases, mutation timing, invalid values, and recipient isolation wrong.
  • Keep direct references deliberately. This changes application-visible values and lets a test pass through reference sharing that cannot occur with real Socket.IO.
  • Use structuredClone. It preserves Date, Map, Set, and graph aliases that the default parser does not, while accepting and rejecting a different set of inputs.
  • Depend on socket.io-parser. It adds binary and parser machinery outside the selected surface and couples a logic-layer mock to upstream transport representation.