npm.io
0.5.0 • Published 6d ago

@bytesocket/server

Licence
MIT
Version
0.5.0
Deps
1
Size
1.8 MB
Vulns
0
Weekly
11
Stars
7

@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.

npm version MIT node-current GitHub GitHub stars Socket Badge

Features

  • Transport-agnostic message dispatch - MessageHandler handles decoding, middleware execution, authentication, room join/leave, and event routing. Your adaptor only needs to forward raw message/open/close events 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 Serializer from @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/server

This package is not meant to be used directly. Choose an adaptor that matches your WebSocket library:


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 (the next()-driven global middleware chain), runHooks (the next()-driven chain used for room join/leave guards), and runSyncHooks (plain fan-out for void lifecycle listeners like open/close/upgrade).
  • LifecycleServer - implements ILifecycleServer; the typed on/once/off namespace for upgrade, open, auth_success, auth_error, message, close, and error.
  • RoomsServer - implements IRoomsServer; the server-wide io.rooms namespace (typed emit, on/off/once, publishRaw, and bulk operations).
  • SocketAuthManager - drives the per-connection authentication handshake (timeout handling, success/failure dispatch, broadcast-room auto-join).
  • SocketRooms / SocketRoomsBulk - the per-connection socket.rooms namespace.
  • 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 getters
  • serverHeartbeatTest - empty-binary ping/pong, automatic keep-alive
  • serverAuthTest - authentication flow (success / failure / timeout)
  • serverLifecycleTest - lifecycle hook ordering and errors
  • serverMessagingTest - message send / receive, serialization
  • serverRoomsSingleTest - single-room join/leave/emit
  • serverRoomsBulkTest - bulk room operations

Each factory function receives:

  1. The Vitest instance (import * as vitest from 'vitest')
  2. A createByteSocket function
  3. A createByteSocketServer function
  4. A destroyByteSocketServer function

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:

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

Keywords