@bytesocket/server
Shared server logic for ByteSocket WebSocket server implementations. This package provides the composable building blocks -- event registries, a serializer, a middleware runner, and a message dispatcher -- that transport‑specific adaptors (@bytesocket/node, @bytesocket/uws) wire together to create a fully‑typed, real‑time server.
You do not need to install this package directly; it is a dependency of the main packages.
Features
- Transport-agnostic message dispatch -
MessageHandlerhandles decoding, middleware execution, authentication, room join/leave, and event routing. Your adaptor only needs to forward rawmessage/open/closeevents from your transport library into it. - Type-safe event system - full TypeScript generics for emit/listen maps, room events, socket data, and middleware callbacks.
- Pluggable serialization - JSON (text) or MessagePack (binary) encoding, selectable per-server instance or per-message, via the shared
Serializerfrom@bytesocket/core. - Shared test utilities - common test factories (
/test-utils) let you run the exact same test suite against every adaptor.
Installation
npm install @bytesocket/serverThis package is not meant to be used directly. Choose an adaptor that matches your WebSocket library:
@bytesocket/node- for Node.jsws@bytesocket/uws- for uWebSockets.js
Exports
Main entry (@bytesocket/server)
MessageHandler- decodes incoming WebSocket messages and dispatches them to auth, room, and event handlers.MiddlewareRunner- registers global middleware (use()) and exposes the three dispatch primitives every adaptor needs:runGlobal(thenext()-driven global middleware chain),runHooks(thenext()-driven chain used for room join/leave guards), andrunSyncHooks(plain fan-out for void lifecycle listeners likeopen/close/upgrade).LifecycleServer- implementsILifecycleServer; the typedon/once/offnamespace forupgrade,open,auth_success,auth_error,message,close, anderror.RoomsServer- implementsIRoomsServer; the server-wideio.roomsnamespace (typedemit,on/off/once,publishRaw, andbulkoperations).SocketAuthManager- drives the per-connection authentication handshake (timeout handling, success/failure dispatch, broadcast-room auto-join).SocketRooms/SocketRoomsBulk- the per-connectionsocket.roomsnamespace.IByteSocket/ISocket/ISocketServer/ISocketRooms/IRoomsServer/ILifecycleServer- public API interfaces.ByteSocketOptionsBase- configuration options shared by every adaptor.ServerIncomingData/ServerOutgoingData- type aliases for raw WebSocket data.Middleware,EventCallback,RoomEventMiddleware,AuthFunction,MiddlewareNext- callback types.SocketData- the user data shape attached to every socket.- Everything from
@bytesocket/core(CallbackRegistry,ScopedRegistry,Serializer,LifecycleTypes, type guards, etc.) is re-exported as well.
Test utilities (@bytesocket/server/test-utils)
Factory functions that create a server + test client for running integration tests across adaptors:
import { serverConnectionTest } from "@bytesocket/server/test-utils";Available test suites:
serverConnectionTest- connection open/close, origin checks, header gettersserverHeartbeatTest- empty-binary ping/pong, automatic keep-aliveserverAuthTest- authentication flow (success / failure / timeout)serverLifecycleTest- lifecycle hook ordering and errorsserverMessagingTest- message send / receive, serializationserverRoomsSingleTest- single-room join/leave/emitserverRoomsBulkTest- bulk room operations
Each factory function receives:
- The Vitest instance (
import * as vitest from 'vitest') - A
createByteSocketfunction - A
createByteSocketServerfunction - A
destroyByteSocketServerfunction
See the adaptor packages for concrete examples.
API overview
Building an adaptor
There's no base class to extend -- each adaptor implements the IByteSocket<TEvents, SD, UpgradeCallback> interface directly and composes the shared building blocks in its constructor:
import {
ByteSocketOptionsBase,
CallbackRegistry,
LifecycleServer,
LifecycleTypes,
MessageHandler,
MiddlewareRunner,
RoomsServer,
ScopedRegistry,
Serializer,
type IByteSocket,
type ISocketServer,
type SocketEvents,
} from "@bytesocket/server";
class MyByteSocket<TEvents extends SocketEvents> implements IByteSocket<TEvents> {
#globalEvents: CallbackRegistry;
#lifecycleEvents: CallbackRegistry<LifecycleTypes>;
#roomsEvents: ScopedRegistry<string>;
#serializer: Serializer;
#middlewareRunner: MiddlewareRunner<TEvents, SocketData>;
#messageHandler: MessageHandler<TEvents, SocketData>;
#destroyed = false;
readonly lifecycle: LifecycleServer<TEvents, SocketData, () => void>;
readonly rooms: RoomsServer<TEvents, SocketData>;
constructor(private readonly options: ByteSocketOptionsBase<TEvents> /* + defaults applied */) {
this.#globalEvents = new CallbackRegistry(options.debug ?? false);
this.#lifecycleEvents = new CallbackRegistry(options.debug ?? false);
this.#roomsEvents = new ScopedRegistry(options.debug ?? false);
this.#serializer = new Serializer(options.msgpackrOptions, options.serialization);
this.#middlewareRunner = new MiddlewareRunner(this.#lifecycleEvents, options);
this.#messageHandler = new MessageHandler(
this.#lifecycleEvents,
this.#globalEvents,
this.#roomsEvents,
this.#serializer,
this.#middlewareRunner,
options,
);
this.lifecycle = new LifecycleServer(this.#lifecycleEvents);
this.rooms = new RoomsServer(this.#lifecycleEvents, this.#roomsEvents, this.#serializer, this.#publishRaw.bind(this), () => this.#destroyed);
}
// attach(server, path), publishRaw(...), and per-connection wiring are transport-specific.
#publishRaw(room: string, message: unknown) {
/* publish via your transport */
}
}From your transport's event handlers, forward into the shared dispatcher:
// on "message"
this.#messageHandler.handle(socket, rawData, isBinary);
// on "upgrade" / "open" / "close" — plain void listeners, no next() involved
this.#middlewareRunner.runSyncHooks(this.#lifecycleEvents.listeners.get(LifecycleTypes.open), [socket], (error) => {
if (error != null) this.#lifecycleEvents.trigger(LifecycleTypes.error, socket, { phase: "onOpen", error });
});Per-connection sockets
Each connection gets its own auth handshake and room membership, via SocketAuthManager and SocketRooms:
import { SocketAuthManager, SocketRooms, type ISocketServer } from "@bytesocket/server";
class MySocket<TEvents extends SocketEvents> implements ISocketServer<TEvents> {
readonly rooms: SocketRooms<TEvents>;
#authManager: SocketAuthManager<TEvents, SocketData>;
#closed = false;
constructor(/* ws, serializer, userData, roomManager, options */) {
this.rooms = new SocketRooms();
/* publishRaw, joinRoom, leaveRoom, getRoomList, serializer, getCanSend, getClosed */
this.#authManager = new SocketAuthManager(
this,
/* sendUnchecked, joinRoom, getClosed, wsClose, options, startHeartbeat? */
);
}
_handleAuth(parsed, next) {
return this.#authManager.handle(parsed, next);
}
}ISocket is the public, user-facing interface (what your io.on/io.lifecycle.onOpen handlers receive). ISocketServer extends it with the internal _handleAuth method that only the adaptor itself calls during the upgrade/open flow.
Common options (ByteSocketOptionsBase)
| Option | Type | Default | Description |
|---|---|---|---|
debug |
boolean |
false |
Enable debug logging |
serialization |
"json" | "binary" |
"binary" |
Payload encoding format |
broadcastRoom |
string |
"__bytesocket_broadcast__" |
Internal room used for global broadcasts |
authTimeout |
number |
0 |
Max milliseconds to wait for an auth response |
middlewareTimeout |
number |
0 |
Timeout for global middleware |
roomMiddlewareTimeout |
number |
0 |
Timeout for room middleware |
idleTimeout |
number |
120000 |
Milliseconds before an idle connection is closed |
sendPingsAutomatically |
boolean |
true |
Send WebSocket pings to keep the connection alive |
origins |
string[] |
- | Allowed origin list (empty = all allowed) |
onMiddlewareError |
"ignore" | "close" | function |
"ignore" |
Action when global middleware errors |
onMiddlewareTimeout |
"ignore" | "close" | function |
"ignore" |
Action when global middleware times out |
msgpackrOptions |
object |
- | Options forwarded to the msgpackr Packr instance |
auth |
AuthFunction |
- | User‑supplied authentication handler |
Usage example (via an adaptor)
import { ByteSocket } from '@bytesocket/node'; // or '@bytesocket/uws'
import { SocketEvents } from '@bytesocket/core';
type MyEvents = SocketEvents<{
"chat:message": { text: string };
"user:joined": { userId: string };
}>;
const io = new ByteSocket<MyEvents>({ debug: true });
io.on('chat:message', (socket, data) => {
console.log(\`${socket.id} says: ${data.text}\`);
});
io.broadcast('user:joined', { userId: 'server' });
// attach to an HTTP server or uWS app
io.attach(server, '/ws');Adaptors
Transport‑specific implementations:
- @bytesocket/node - Node.js
wslibrary - @bytesocket/uws - uWebSockets.js
Both expose a concrete ByteSocket class that you instantiate directly and that implements IByteSocket.
Testing with shared utilities
// packages/node/tests/connection.test.ts
import * as vitest from "vitest";
import { serverConnectionTest } from "@bytesocket/server/test-utils";
import { createByteSocket, createByteSocketServer, destroyByteSocketServer } from "./factory";
describe("ByteSocket node: Connection", () => {
serverConnectionTest(vitest, createByteSocket, createByteSocketServer, destroyByteSocketServer);
});Your factory file provides the three functions that wrap your specific transport setup, returning instances typed as IByteSocket<TestEvents>. The shared test suite handles the rest.
License
MIT 2026 Ahmed Ouda
- GitHub: @a7med3ouda