Skip to content

Releases: electrohyun/smocket

v1.0.1

Choose a tag to compare

@electrohyun electrohyun released this 11 Sep 12:24
1f15ec4

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

Smocket 1.0.1 is a maintenance release for the synchronized smocket and smocket-client packages. The published delivery rules, public TypeScript contracts, package entry points, and Node.js >=20 requirement are unchanged from 1.0.0.

The repository toolchain now uses pnpm 12 and Vitest 5. A dedicated Vitest 4 suite continues to exercise the built packages on Node 20, and the Playwright version is aligned across the maintained examples and package consumers. Renovate now keeps unrelated pnpm and Vitest major updates in separate pull requests.

The maintained browser examples now respect reduced motion, keep the drawing canvas usable in narrow viewports, and cover SharedWorker pages restored from the browser back and forward cache. These changes affect the examples and release validation rather than the package runtime.

Install the exact pair together:

npm install -D smocket@1.0.1 smocket-client@1.0.1

Published packages

The release workflow installed and exercised the exact published pair outside the repository checkout:

Full Changelog: v1.0.0...v1.0.1

v1.0.0

Choose a tag to compare

@electrohyun electrohyun released this 27 Aug 08:51
91a9479
exec-6ae94e84-7368-4e12-ab00-f94a00b95db0

πŸŽ‰smocket 1.0: a stable Socket.IO mock

smocket 1.0.0 is the first stable release of a Socket.IO mock built for frontend development and testing without a network server.

This release stabilizes the documented in-memory delivery and routing subset, its public TypeScript contracts, and the exact-version substitution path formed by smocket and smocket-client. The same application event handlers and framework-independent domain logic can be registered with smocket during frontend development and with a real Socket.IO server when the backend is available. The bootstrap changes; the supported application event layer can stay the same.

What you can build with smocket

Applications can exercise supported connection handlers, rooms, namespaces, targeted and broadcast delivery, acknowledgements, middleware, adapters, and socket lifecycle in the same JavaScript environment as the frontend.

The React drawing game demonstrates one application running against either a real Node.js HTTP and Socket.IO server or an in-browser smocket server hosted by a SharedWorker. Its three-page flow covers distinct socket identities, sender-excluding drawing broadcasts, chat, answer acknowledgements, one shared round result, page closure, refresh, and connection cleanup.

For multi-tab development, pages with the same origin, browser profile, worker URL, and worker name can share one caller-owned in-browser server while retaining separate socket identities. Worker termination or restart loses that in-memory state.

Stable public surface and runtimes

The supported public entry points are:

  • smocket for the in-process server, adapters, and server-side contracts;
  • smocket-client for the application-facing client facade;
  • smocket/shared-worker for the worker-host bridge;
  • smocket-client/shared-worker for the browser page bridge.

The root package is self-contained and has no runtime, optional, peer, or bundled dependencies. The client package carries one exact-version smocket peer and no duplicate implementation. Both packages provide supported ESM and CommonJS paths with self-contained declarations.

The published Node.js runtime floor is >=20. Desktop Chromium is the maintained automated SharedWorker target. Release checks exercise the packed artifacts outside the repository across the documented module, type, browser, test-runner, and SharedWorker entry paths.

Quick Start

Install the synchronized package pair at the same exact version:

npm install -D smocket@1.0.0 smocket-client@1.0.0

smocket owns the in-process server and registry. smocket-client provides the application-facing replacement for the supported socket.io-client surface.

import { Server } from 'smocket';
import { connect } from 'smocket-client';

async function main() {
  const URL = 'http://localhost:3000';
  const io = new Server(URL);

  io.on('connection', (socket) => {
    socket.on('join', async (room: string, done: () => void) => {
      await socket.join(room);
      done();
    });

    socket.on('say', (room: string, text: string) => {
      socket.to(room).emit('said', text);
    });
  });

  const alice = connect(URL);
  const bob = connect(URL);

  const joinLobby = (client: ReturnType<typeof connect>) =>
    new Promise<void>((done) => client.emit('join', 'lobby', done));

  try {
    await Promise.all([joinLobby(alice), joinLobby(bob)]);

    const bobHeard = new Promise<string>((done) => bob.once('said', done));
    alice.emit('say', 'lobby', 'hello');

    console.log(`Bob heard: ${await bobHeard}`);
  } finally {
    await io.close();
  }
}

void main();

The example prints Bob heard: hello and exits after close() disconnects both clients and releases the in-process server. No test runner is required. Existing applications can keep their socket.io-client imports and map that package name to smocket-client in the environment that uses the mock.

Support boundary and differences from Socket.IO

smocket 1.0.0 stabilizes its documented in-memory delivery and routing boundary; it does not claim complete Socket.IO compatibility or replace a production backend. Network transport, transport fallback, heartbeat, automatic network reconnection, persistence, authentication infrastructure, cross-device communication, binary encoding, and multi-server scaling remain outside that boundary.

The dual-run conformance suite compares maintained cases against real Socket.IO first and smocket second. Chromium checks cover the browser and SharedWorker paths, the drawing game covers one application workflow across real and in-memory bootstraps, and clean-consumer checks install candidate tarballs outside the workspace. An API that exists in Socket.IO but is absent from the documented boundary should not be assumed to work in smocket.

See the scope, differences, and conformance report for the exact boundary.

Upgrade from 0.5.1

Keep smocket and smocket-client on the same exact version and load both through the same module format. Applications using the documented 0.5.1 entry points should not need to rewrite their event handlers, but the following stabilized contracts may require TypeScript or edge-case updates:

  • The free connect(url, options) lookup derives its namespace from the URL. namespace is no longer part of its public ConnectOptions.
  • The callable smocket-client lookup follows the Socket.IO client shape without accepting event-map type arguments directly. Use the exported generic Socket<ListenEvents, EmitEvents> type when an explicit client annotation is needed.
  • Connection auth and middleware errors now cross a payload snapshot boundary. Code that depended on later mutation of the original object will observe the captured value instead.
  • Timed acknowledgements preserve all response arguments rather than only the first.
  • Tests that retained callbacks across disconnect or server close should expect those callbacks to be invalidated.
  • Queued delivery, reentrant middleware, and outgoing observation now follow the measured lifecycle boundaries when teardown occurs during an admitted emit.

The Node.js requirement remains >=20. Repository development uses a newer Node.js toolchain because of the pinned package manager, but that does not change the published runtime floor.

Release certification

The immutable 1.0.0 candidate passed the authoritative CI run for the exact main commit below. The publication workflow promoted that exact candidate, published both packages, and then installed and exercised the exact registry pair successfully. The v1.0.0 tag and this GitHub Release point to the certified commit.

  • Certified commit: 91a9479416d7d84eaf19119171497cc20098ead6
  • Exact-SHA CI run
  • Publication and registry verification run
  • Registry certification result: success β€” the exact published pair was installed and exercised outside the repository checkout.
  • smocket-1.0.0.tgz β€” 183,367 bytes β€” SHA-256 aba80de8ae080f34540fb06f248fa4ff87b6e59ede9425be4f108c045bfa3841
  • smocket-client-1.0.0.tgz β€” 3,362 bytes β€” SHA-256 3d1b609b7c925553276e6be6495b0ce7a653fa2f4e558615562f49663807c4e5

Packages and documentation

The synchronized published packages are:

The publication workflow certified this exact pair outside the repository checkout.

Thank you

smocket grew out of what I learned while developing Socket.IO services, and the wish to keep frontend work moving when the backend was not yet ready. After countless conformance cases, bug fixes, reviews, tests, documentation, community contributions, and unexpected lifecycle boundaries, smocket has reached 1.0.0.

To everyone who tested smocket, reported a problem, reviewed a change, contributed code or documentation, or followed the project on its way to its first stable release: thank you, sincerely, from the bottom of my heart.

Full Changelog: v0.5.1...v1.0.0

v0.5.1

Choose a tag to compare

@electrohyun electrohyun released this 23 Aug 15:00

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

Smocket 0.5.1 adds an explicit SharedWorker path for frontend development across several browser tabs.

Pages using the same origin, browser profile, module worker URL, and worker name can share one caller-owned, in-browser Smocket server while retaining separate socket identities.

The synchronized packages add two public entry points:

  • attachSharedWorker from smocket/shared-worker
  • connectSharedWorker from smocket-client/shared-worker

Install the exact pair together:

npm install -D smocket@0.5.1 smocket-client@0.5.1

Multi-tab frontend workflow

The new SharedWorker guide and lobby example demonstrate connection admission, rooms, broadcasts, acknowledgements, ordered delivery, explicit disconnect, page close, and participant cleanup across three Chromium pages.

The Runnable Drawing Game runs the same React game in three pages. Its framework-independent application handler is registered with either:

  • one in-browser Smocket server hosted by a SharedWorker; or
  • a real Node.js HTTP and Socket.IO server.

Both modes cover distinct socket ids, countdown, sender-excluded strokes, chat, guess acknowledgements, one shared round result, page close, refresh, and a repeated clean run. The browser UI, event types, game state, and application handler stay the same; only the worker/page or Socket.IO server/client bootstrap changes (#393, #395).

Corrected observable results

These corrections match results exercised against Socket.IO 4.7.5 and 4.8.3:

  • Connection admission: admission hooks could begin before connect() returned. Callback auth, dynamic namespace matching, and namespace middleware now begin after the client factory returns while queued attempts remain cancellable (cases, #362).
  • Repeated middleware completion: repeated calls to the same continuation were previously collapsed. Repeated acceptance now completes the lifecycle repeatedly on one server Socket, while acceptance followed by rejection preserves the observed teardown and client-error sequence (cases, #363).
  • Acknowledgement teardown: retained acknowledgement callbacks could outlive a disconnected socket or closed server. Teardown now invalidates those callbacks while already queued synchronous delivery may finish during the drain (cases, #364).
  • Timed callbacks: delivered timed callbacks could remain pending across disconnect paths. They now settle exactly once with the Socket.IO disconnected error, and expired buffered packets are removed instead of accumulating or being delivered later (disconnect cases, timeout cases, #365).
  • Client modifiers: an outgoing observer or payload-encoding failure could consume a pending client modifier too early. The modifier now remains available until an emit is dropped as volatile, buffered, or successfully encoded and scheduled (cases, #366).

Upgrade and scope

Keep smocket and smocket-client on the same exact version. The Node.js requirement remains >=20, the existing Socket.IO compatibility promises remain in place, and public 0.5.0 TypeScript consumers compile against the 0.5.1 declarations.

The applied ADR 0019 rows are:

  • A change to a smocket-only API (section B) for the compatible SharedWorker entry points, using ordinary semantic versioning.
  • A correction toward measured real behaviour with an observable change for the delivery and lifecycle corrections above.

The repository's 0.x version mapping places both classifications in 0.5.1.

The SharedWorker facade is deliberately narrow. It supports in-memory frontend development within one browser profile; it does not provide production transport, authentication, persistence, automatic reconnection, database access, cross-device communication, scaling, or the complete Socket.IO Client API. Worker termination or restart loses its state.

A deployed application replaces the page bootstrap with socket.io-client and verifies its real backend separately while retaining the supported event types and application handler flow.

Published packages

The release workflow independently installed and exercised the exact published pair outside the repository checkout:

The canonical published consumer now follows the same synchronized pair.

Full Changelog: v0.5.0...v0.5.1

v0.5.0

Choose a tag to compare

@electrohyun electrohyun released this 14 Aug 06:04

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

v0.5.0 is expected to be the last minor release before v1.0.0. The remaining stabilization work may produce one or two v0.5.x patch releases, but the public surface intended for 1.0.0 is now substantially in place.

This is the first synchronized release of smocket and smocket-client. The root smocket package owns the in-memory server and connection registry. The new smocket-client package preserves Socket.IO Client's package shapeβ€”default, io, connect, callable CommonJS, and the client Socket typeβ€”while delegating every lookup to an exact-version smocket peer. Publishing both as 0.5.0 means a test's client import and its in-memory server resolve through the same implementation and registry instead of duplicating connection state (#235, #276).

Install both packages at the same version:

npm install -D smocket smocket-client

An application can keep its existing import:

// src/chat.ts β€” unchanged application code
import { io } from 'socket.io-client';

export const socket = io('http://localhost:3000');

In tests, substitute that client package with smocket-client and provide the in-memory server:

// chat.test.ts
import { Server } from 'smocket';

const io = new Server('http://localhost:3000');
io.on('connection', (socket) => {
  socket.on('message', (text) => socket.broadcast.emit('message', text));
});

Runner-specific substitution examples live in the test-runner integration guide.

New public surface

The package boundary now matches the server/client split applications already use. smocket exports the server-side Socket type, while smocket-client exports the client-side Socket type with the correct event-map direction. The facade works through ESM, callable CommonJS, Node16 resolution, bundler resolution, and Chromium (#178, #235, #276).

Sockets gained the observable state and extension points used by real applications: connected, disconnected, recovered, mutable client auth, per-packet server middleware, complete catch-all and listener-inspection methods, and the accepted inherited emitter surface. A direct Server.connect() plus nextConnection() pair is also available for tests that already hold the server instance (#266, #267, #268, #274, #275, #277).

Namespace and broadcast coverage grew together. Dynamic namespace parents and new_namespace are supported; broadcast operators gained Promise acknowledgements, aliases, compression modifiers, local socket lookup, bulk room membership, and bulk disconnect. The built-in Adapter lifecycle is observable through its live rooms map, and deterministic tracing or dropping adapters can inspect or alter local delivery without pretending to implement multi-server scaling (#238, #261, #262, #269, #278, #322, #323, #324).

Non-binary payloads now cross the same JSON snapshot boundary measured on Socket.IO's default parser, and disconnect(true) closes every namespace socket sharing the client Manager rather than only the selected namespace (#237, #250, #254).

Corrected observable results

These corrections move the mock to the results measured on Socket.IO 4.7.5 and 4.8.3:

  • Static namespaces: 0.4.2 could admit an unregistered namespace and treated equivalent namespace spellings as different keys. 0.5.0 normalizes static names and rejects an unregistered namespace with Invalid namespace before a connection handler runs (case, #228).
  • Abandoned connections: rejected middleware could leave temporary rooms behind, and a client disconnected during pending middleware could still connect later. Rejection and cancellation now remove the attempt and a late middleware continuation cannot admit it (case, #229).
  • Disconnected membership: teardown could leave an empty sid entry, and a disconnected server socket could join rooms again. The sid is now removed and later joins cannot recreate membership (case, #230).
  • Reserved events: application code could emit Socket.IO lifecycle names through public emitters. Those emits now throw before peer delivery or outgoing catch-all observation (case, #232).
  • Volatile and timeout modifiers: socket modifiers used reusable wrapper objects and could leak into an unrelated broadcast. They now preserve Socket identity, apply to one operation, and stay isolated from recipient sockets; narrowed broadcast operators expose volatile as an operator again (volatile cases, timeout cases, #231, #259).
  • Fluent APIs: server and namespace middleware registration plus client/server connect or disconnect methods returned void. They now return the same receiver measured on Socket.IO, preserving chaining and event maps (cases, #233).
  • Payload isolation: JSON-compatible payloads could cross the in-memory boundary by reference, so mutation on one side was visible on the other. They now cross a serialization snapshot while binary values retain their supported pass-through behavior (cases, #237, #250).
  • Manager teardown: disconnect(true) could leave sibling namespace sockets from the same client alive. It now closes the shared Manager group and reports each namespace lifecycle once (cases, #236, #254).
  • Emitter removal: the final removeListener observer now follows the maintained Node and Socket.IO host behavior instead of a locally fixed bulk-removal rule (cases, #309).

Upgrade notes

The two packages must stay on the same exact version. Load both through ESM or both through CommonJS so they share one in-process registry; mixed formats can create separate root module instances.

This release includes public declaration changes that can reject TypeScript call sites accepted by 0.4.2. Code that assigned to socket.volatile, treated fluent APIs as explicitly void, or passed names outside a typed event map to listener-removal methods may need adjustment. The runtime changes generally expose the Socket.IO-compatible receiver or restriction, but helpers and type assertions written around the older declarations should be checked during the upgrade.

The Node.js floor remains >=20, and the packed root package still has zero runtime dependencies.

The governing ADR 0019 row is β€œa public type change that still compiles at existing call sites, else major.” Existing call sites no longer compile in the cases above, so the change is major under the stable-version rule. Before 1.0.0 that rule applies one place to the right, making 0.5.0 the required minor release. Newly covered Socket.IO surface and observable corrections also ship here, with the corrected results called out above rather than hidden in the version number.

Package adoption and release reliability

The documented Vitest and Jest substitutions now run as clean consumers outside the checkout. Candidate checks install the exact two tarballs through ESM, CommonJS, Node16, bundler, browser, and application fixtures. Release candidates also record each package's name, version, size, and SHA-256 digest so every package-level check consumes the same artifact set (#280, #281, #310).

The supported Socket.IO declarations are independently inventoried, package tarballs reject runtime dependencies, non-receipt assertions use completion markers instead of wall-clock waits, and exact published versions have a bounded registry verifier and remediation path (#273, #282, #283, #284, #310, #314, #316, #318).

What's Changed

Read more

v0.4.2

Choose a tag to compare

@electrohyun electrohyun released this 10 Aug 12:59

What matters in this one

A server can now finish the lifecycle it started. io.close(callback?) closes sockets in every namespace, reproduces the server and client disconnect reasons measured on Socket.IO 4.7 and 4.8, rejects a connection that was still being admitted, and releases smocket's origin registration so the next test starts from a real empty boundary (#193). Repeated close paths share their first completion, while an older server cannot unregister the replacement that took its URL.

The substitution seam now keeps application types. Socket.IO's four server generic slots cross the server, namespace, socket, broadcast, timeout, volatile, middleware, adapter, and test-harness contracts; event direction, acknowledgement values, reserved disconnect types, and socket.data stay intact (#171). SmocketServer also gives applications one public type that includes the smocket-only adapter and nextConnection members without widening the Socket.IO-compatible ServerContract (#188).

Emitter return values now match the real packages instead of disappearing. Client emitters and listener registration methods return the client socket for chaining, while server-side emitters return true; the timed and volatile forms preserve the same split (#189). The correction changes an observable result, but not which sockets receive an event.

The compatibility matrix now typechecks against both supported Socket.IO minors, the namespace-middleware isolation case joined the dual-run report, and the Korean contribution guide documents the path into the project (#201, #209, #211).

This release lands on three rows in ADR 0019: newly covered Socket.IO surface, public type changes that still compile at existing call sites, and a correction toward measured real behavior with an observable change. Each is minor after 1.0.0; before 1.0.0 the rules apply one place to the right, so they ship together as the 0.4.2 patch.

What's Changed

Full Changelog: v0.4.1...v0.4.2

v0.4.1

Choose a tag to compare

@electrohyun electrohyun released this 07 Aug 14:12

What matters in this one

The contract types are exported. An application that swaps socket.io-client for smocket in a test already ran, because the value side of the substitution resolved and only the type side was missing. There was no socket type to annotate with. ClientSocketContract, ServerSocketContract, ServerContract, NamespaceContract, and Handshake now leave the package under their own names, along with the nine types those five reach through their own members, so the substitution path keeps its types instead of dropping to any at the seam (#178).

The README is rewritten (#114). It opens on the problem a hand-written socket mock runs into, carries a quick start that is a real vitest file, and points at the test-runner integration guide, the conformance report, and the runnable chat room example.

Nothing else moved. dist/index.js and dist/index.cjs are byte-identical to 0.4.0 and only the declaration files grow, so there is no delivery behaviour in this upgrade to read for.

This is the first release the version rule decided rather than described. Adding an export is a public type change that still compiles at every existing call site, which is the row 0019 reads as a minor. Before 1.0.0 the rules apply one place to the right, so it ships as a patch and ^0.4.0 picks it up.

What's Changed

New Contributors

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@electrohyun electrohyun released this 05 Aug 15:43

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

smocket did not run in a browser before this release. Every connect() threw, because newId reached for node:crypto and a bundler's Buffer shim has no base64url, so the library was unusable in the environment it exists for. It is fixed, the package now imports nothing external at all, and CI runs the suite in Chromium so the same class of break cannot pass unnoticed again (#139, #105).

The public API is close to complete. Connection middleware, acknowledgement timeouts in both the single and broadcast forms, volatile, catch-all listeners in both directions, socket.data, off with removeAllListeners, and except chaining all landed. That is the surface v1.0.0 freezes, so this is the release to break against if something is missing.

Packaging is verified rather than assumed now. publint and arethetypeswrong run on the built package, the suite runs on Node 22 and 24 and against socket.io 4.7 and 4.8, and coverage gates the run instead of only reporting it.

What's Changed

Full Changelog: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@electrohyun electrohyun released this 03 Aug 08:15

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

An application's own code runs against smocket now. io.on('connection') and connect(url) are socket.io's and socket.io-client's own entry points, and io is exported under the name socket.io-client uses, so a test that swaps the import runs the same handlers the application already has.

Before this, reaching a server-side socket meant a helper with no counterpart in socket.io, which every reader met on the first line. That helper stays for tests that drive a connection directly, and it is no longer the way in.

connect(url, { auth, query }) carries both onto socket.handshake, including the function form of auth, so code that authenticates at connection time is exercised rather than skipped.

What's Changed

  • feat: app-facing entry points (io.on('connection'), connect(url), io alias) by @electrohyun in #93
  • feat: accept auth and query on connect(url), populating socket.handshake by @electrohyun in #96
  • feat: accept a function-form auth on connect(url) options by @electrohyun in #97
  • chore: bump version to 0.3.0 by @electrohyun in #100

Full Changelog: v0.2.1...v0.3.0

v0.2.1

Choose a tag to compare

@electrohyun electrohyun released this 30 Jul 07:51

What matters in this one

The usage example on the front page did not run. It is fixed here, and the release exists for that.

Nothing else about the library changed. The README also states the pre-1.0 status plainly, so a reader deciding whether to try it is told the API can still move before 1.0.0.

What's Changed

Full Changelog: v0.2.0...v0.2.1

v0.2.0

Choose a tag to compare

@electrohyun electrohyun released this 30 Jul 07:23

smocket. Test socket.io without a server. Sweet setup, rocket speed.

What matters in this one

The reasoning got written down. Ten decision records landed covering the principles, the public API shape, connection and delivery semantics, and the cross-cutting rules, along with a glossary, the scope boundary, the documentation conventions and the first differences.md. Before this release the answer to "why is it built this way" lived in review threads.

That is what makes the rest of the project reviewable by someone who was not there, and it is why a later change can be told it conflicts with a decision rather than argued from scratch.

The public adapter API also lands here, with a working example, so the routing decision can be replaced without touching the core. Two behaviours were corrected against measured socket.io: emitWithAck now buffers until the connection completes instead of reaching a dead socket, and a disconnect carries its reason, with socket.disconnect() available on the server side.

What's Changed

  • docs: establish documentation conventions (AGENTS.md, CONTRIBUTING-docs) by @electrohyun in #69
  • test: translate test descriptions to English by @electrohyun in #72
  • docs: add glossary and scope reference by @electrohyun in #71
  • docs: record the decisions settled in the core work (0010-0013) by @electrohyun in #73
  • docs: decision records for principles and the public API by @electrohyun in #75
  • docs: record cross-cutting decisions (0007-0009) by @electrohyun in #74
  • docs: decision records for connection and delivery semantics (0004-0006) by @electrohyun in #76
  • docs: add the differences list and the documentation index by @electrohyun in #77
  • feat: public adapter registration API with a working example by @electrohyun in #79
  • fix: buffer emitWithAck until (re)connect by @electrohyun in #80
  • feat: report disconnect reason and add server-side socket.disconnect by @electrohyun in #82
  • docs: note the injectable disconnect reason in scope.md heartbeat item by @electrohyun in #84
  • test: add a connectClients multi-client helper by @electrohyun in #85
  • chore: bump version to 0.2.0 by @electrohyun in #87

Full Changelog: v0.1.0...v0.2.0