npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@firtoz/socka

v3.0.3

Published

Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects

Readme

@firtoz/socka

npm version npm downloads license

TypeScript WebSocket Standard Schema

Socka — WebSocket RPC, Standard Schema

Typed WebSocket RPC and pushes for TypeScript. One defineSocka contract gives you session.send.* for RPCs and session.subscribe for typed server pushes—validated, correlated, same schema on client and server.

Validation is Standard Schema v1, not Zod-specific: any compliant library works (e.g. Zod, Valibot, or others). Examples below use Zod for familiarity.

npm: @firtoz/socka. Socka is the project name in prose; install and import paths always use @firtoz/socka or @firtoz/socka/.... The published artifact is compiled ESM + .d.ts in dist/ (see package.json exports).

Call output shapes (at a glance)

| Goal | In defineSocka calls | |------|--------------------------| | Fire-and-forget (cursors, live drafts, high frequency) | Omit output | | Request/response await after the handler runs | Normal output schema | | Correlated ack with no payload | output: z.void() |

Details: Client — Fire-and-forget · Reference — Optional output. For output-less calls, void send.foo(...).catch(...) does not observe serverError—use reportError on SockaSession / useSockaSession (see Client — Fire-and-forget observability).

React + Cloudflare Durable ObjectsReact + Durable Objects (shared contract, SockaWebSocketDO, useSockaSession, no casts). Canvas / whiteboard-style contract sketch: Collaborative realtime.

Hand-written types next to Zod under exactOptionalPropertyTypes can break inference—see Reference — TypeScript and exact optional properties.

Minimal example: multi-room chat (Bun)

Join/leave and live messages use pushes; persisted lines use listHistory; who is connected uses listPresence; clearHistory wipes stored messages and historyCleared notifies the room (examples also show presence in the UI). This snippet keeps history in memory so it stays short—see chatroom-bun for SQLite, chatroom-hono for file JSON, and chatroom-do for Durable Object SQLite.

contract.ts (shared):

import { defineSocka } from "@firtoz/socka/core";
import * as z from "zod";

export const messageRow = z.object({
	id: z.string(),
	ts: z.number(),
	userId: z.string(),
	displayName: z.string(),
	text: z.string(),
});

export type ChatMessageRow = z.infer<typeof messageRow>;

const onlineUser = z.object({
	userId: z.string(),
	displayName: z.string(),
});

export const chatContract = defineSocka({
	calls: {
		listHistory: {
			input: z.object({ limit: z.number().int().min(1).max(500).optional() }),
			output: z.object({ messages: z.array(messageRow) }),
		},
		listPresence: {
			input: z.object({}).optional(),
			output: z.object({
				selfUserId: z.string(),
				users: z.array(onlineUser),
			}),
		},
		sendMessage: {
			input: z.object({ text: z.string().min(1) }),
			output: z.object({ ok: z.literal(true) }),
		},
		clearHistory: {
			input: z.object({}).optional(),
			output: z.object({ ok: z.literal(true) }),
		},
	},
	pushes: {
		userJoined: z.object({ userId: z.string(), displayName: z.string() }),
		userLeft: z.object({
			userId: z.string(),
			displayName: z.string(),
		}),
		roomMessage: messageRow,
		historyCleared: z.object({
			ts: z.number(),
			clearedByUserId: z.string(),
			clearedByDisplayName: z.string(),
		}),
	},
});

Fire-and-forget vs output: z.void() — Omit output on a call when you want one-way success semantics: the server does not send a serverResponse, and await session.send.* resolves after the frame is sent (it does not wait for server processing). Server failures still return a correlated serverError; use reportError on SockaSession / useSockaSession to observe those when using output-less calls. Use output: z.void() when you still want a normal request/response await that completes only after the server acknowledges.

server.tscreateSockaRoomRegistry gives each room its own sessionMap and config. By default createData receives SockaStrictWebSocketInit: init.request is the upgrade Request (Bun/Hono/adapters pass it through; see Server — Strict upgrade request). Set strictUpgradeRequest: false when you have no Request. session.listPeers() returns session.data for other sockets in the same room—use it to implement listPresence-style calls (see Presence).

import type { ServerWebSocket } from "bun";
import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
import {
	createSockaRoomRegistry,
	type SockaWebSocketSessionConfig,
} from "@firtoz/socka/server";
import { type ChatMessageRow, chatContract } from "./contract";

type SessionData = { roomId: string; userId: string; displayName: string };

/** In-memory demo store — swap for SQLite / files / DO in real apps. */
const history = new Map<string, ChatMessageRow[]>();

const registry = createSockaRoomRegistry(
	(roomId): SockaWebSocketSessionConfig<typeof chatContract, SessionData> => ({
		contract: chatContract,
		createData: (init) => {
			const u = new URL(init.request.url);
			const displayName = u.searchParams.get("name")?.trim() || "anon";
			return { roomId, userId: crypto.randomUUID(), displayName };
		},
		onAttached: async (session) => {
			await session.broadcastPush(
				"userJoined",
				{ userId: session.data.userId, displayName: session.data.displayName },
				true,
			);
		},
		handlers: {
			listHistory: async (input, session) => {
				const lim = input.limit ?? 200;
				const rows = history.get(session.data.roomId) ?? [];
				return { messages: rows.slice(-lim) };
			},
			listPresence: async (_input, session) => {
				const users = session.listPeers().map((d) => ({
					userId: d.userId,
					displayName: d.displayName,
				}));
				users.sort((a, b) => a.displayName.localeCompare(b.displayName));
				return { selfUserId: session.data.userId, users };
			},
			sendMessage: async (input, session) => {
				const row = {
					id: crypto.randomUUID(),
					ts: Date.now(),
					userId: session.data.userId,
					displayName: session.data.displayName,
					text: input.text,
				};
				const list = history.get(session.data.roomId) ?? [];
				list.push(row);
				history.set(session.data.roomId, list);
				await session.broadcastPush("roomMessage", row);
				return { ok: true as const };
			},
			clearHistory: async (_input, session) => {
				history.set(session.data.roomId, []);
				const ts = Date.now();
				await session.broadcastPush("historyCleared", {
					ts,
					clearedByUserId: session.data.userId,
					clearedByDisplayName: session.data.displayName,
				});
				return { ok: true as const };
			},
		},
		handleClose: async (session) => {
			await session.broadcastPush(
				"userLeft",
				{ userId: session.data.userId, displayName: session.data.displayName },
				true,
			);
		},
	}),
);

type BunWsData = { roomId: string; request: Request };

const { websocket } = createSockaBunWebSocketHandlers({
	resolveScope(ws: ServerWebSocket<BunWsData>) {
		const { roomId } = ws.data;
		const room = registry.get(roomId);
		return { sessionMap: room.sessionMap, config: room.config };
	},
});

Bun.serve<BunWsData>({
	port: 3450,
	fetch(req, server) {
		const url = new URL(req.url);
		if (url.pathname.startsWith("/ws/")) {
			const roomId = decodeURIComponent(url.pathname.slice(4)) || "default";
			if (server.upgrade(req, { data: { roomId, request: req } })) return undefined;
			return new Response("WebSocket upgrade failed", { status: 400 });
		}
		return new Response("OK");
	},
	websocket,
});

Ports: this minimal snippet listens on 3450; the full-stack examples use 3461–3466.

client.ts (browser or Bun):

import { SockaSession } from "@firtoz/socka/client";
import { chatContract } from "./contract";

const session = new SockaSession({
	contract: chatContract,
	url: "ws://localhost:3450/ws/lobby?name=Ada",
});

session.subscribe.on("userJoined", (p) => console.log("joined", p));
session.subscribe.on("userLeft", (p) => console.log("left", p.displayName));
session.subscribe.on("roomMessage", (m) => console.log(`${m.displayName}: ${m.text}`));
session.subscribe.on("historyCleared", (p) =>
	console.log("history cleared by", p.clearedByDisplayName, "at", p.ts),
);

const { messages } = await session.send.listHistory({});
console.log("history", messages);
const { selfUserId, users } = await session.send.listPresence({});
console.log("online", selfUserId, users);
await session.send.sendMessage({ text: "hello room" });
await session.send.clearHistory({});

Run bun run server.ts, then point the client at the same ws://…/ws/<room>?name=… path you upgrade in fetch.

More examples: chatroom-bun (SQLite + multi-room UI) · chatroom-hono · chatroom-do · tic-tac-toe Bun · Hono + Node · Cloudflare DO.

Install

Always install @firtoz/socka, then add only what your imports need (npm / pnpm / bun add as you prefer):

| You are building… | Install | |-------------------|---------| | Browser / Vite SPA (client only) | npm install @firtoz/socka | | React (@firtoz/socka/react) | npm install @firtoz/socka react — add @types/react as a dev dependency if TypeScript asks | | Bun (Bun.serve, @firtoz/socka/bun) | npm install @firtoz/socka — add bun-types as a dev dependency if you type-check Bun APIs | | Node + Hono + @hono/node-ws | npm install @firtoz/socka hono @hono/node-ws @hono/node-server ws — add @types/ws as a dev dependency when you use ws on Node | | Cloudflare Workers + Hono (@firtoz/socka/hono/cloudflare) | npm install @firtoz/socka hono | | Cloudflare Durable Objects (@firtoz/socka/do) | npm install @firtoz/socka hono @firtoz/websocket-do |

For Cloudflare TypeScript types, prefer wrangler types (or your app’s typegen) so globals and bindings match your Worker — see Cloudflare’s TypeScript guide. More detail: Peers.

Other runtimes

Pick how the socket is upgraded, then use the matching import path and guide:

| Runtime | Import path | Quick start | |---------|-------------|-------------| | Node + ws (or any standard WebSocket after upgrade) | @firtoz/socka/server | attachSockaWebSocket | | Bun (Bun.serve / ServerWebSocket) | @firtoz/socka/bun | createSockaBunWebSocketHandlers | | Hono on Node (@hono/node-ws) | @firtoz/socka/hono | sockaHonoNodeWs | | Hono on Cloudflare Workers | @firtoz/socka/hono/cloudflare | sockaHonoCloudflare | | Cloudflare Durable Objects | @firtoz/socka/do | Durable Objects |

Why not socket.io, tRPC, or DIY?

  • Schema-first RPC + push — one contract; no parallel “event” protocol for server pushes.
  • Correlated envelopes — request/response IDs and validation hooks are built in.
  • Same contract across Bun, Hono, Node ws, and Durable Objects (see Comparison for socket.io, tRPC, and custom WebSocket stacks).
  • Room registry + presence helperscreateSockaRoomRegistry for per-room sessionMap / config; session.listPeers() to list other peers in the room (see Presence).
  • Strict upgrade typing + optional reconnect — by default createData sees init.request on the upgrade; SockaWebSocketClient / SockaSession can reconnect with exponential backoff (see Reconnection).

Documentation

Hub: docs/README.md (getting started, peers, lifecycle, multi-room, reference). React + Cloudflare DO: docs/react-durable-objects.md · Collaborative / canvas contracts: docs/collaborative-realtime.md.

Roadmap: deferred ideas and future work. Agent skills: skills/.

Full-stack examples

| Topic | Stack | Folder | Port | |-------|--------|--------|------| | Chat + history | Bun + SQLite | chatroom-bun | 3464 | | Chat + history | Hono + Node + JSON files | chatroom-hono | 3465 | | Chat + history | Cloudflare DO + Drizzle SQLite | chatroom-do | 3466 | | Tic-tac-toe | Bun | tic-tac-toe-bun | 3461 | | Tic-tac-toe | Hono + Node | tic-tac-toe-hono | 3462 | | Tic-tac-toe | Cloudflare DO | tic-tac-toe-do | 3463 |

Chat apps: bun run dev (or wrangler dev for chatroom-do). Tic-tac-toe: same.