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

@serve-tools/signal-shared-websocket

v0.2.1

Published

SharedWorker WebSocket clients with subscription Signal state

Readme

@serve-tools/signal-shared-websocket

@serve-tools/signal-shared-websocket provides the shared WebSocket client together with explicit, read-only subscription Signal state. Use it when several tabs or windows share one physical WebSocket while each UI owns its own reactive view of a subscription.

The package re-exports the shared client runtime and the observe() adapter from @serve-tools/signal-websocket with types specialized for SharedWebSocketClient. It adds no independent subscription runtime.

Install

npm install @serve-tools/signal-effect @serve-tools/signal-shared-websocket

The WebSocket server must implement the binary request-and-subscription protocol used by @serve-tools/client-websocket. These packages provide the browser client and shared-worker bridge, not the server.

Usage: share live presence across tabs

1. Open the WebSocket in a shared worker

Call listen() once in the shared worker. It opens the physical WebSocket and serves the declared protocol to every connected page. Export the inferred protocol type so the page client stays in sync without duplicating the declaration.

// presence.worker.ts
import { listen } from "@serve-tools/signal-shared-websocket/scope/shared-worker";

export const presenceServer = listen<{
	requests: {
		getRoom(input: { room: string }): { title: string };
	};
	subscriptions: {
		presence(input: { room: string }): { online: number };
		announcements(): string;
	};
}>("wss://example.com/presence");

export type PresenceProtocol = listen.ProtocolType<typeof presenceServer>;

Request return types describe responses; subscription return types describe emitted values. These types are compile-time only, so validate server data at runtime.

2. Observe and render presence in the page

Each page connects to the worker, observes the latest presence value, and releases its own resources on pagehide.

<output id="presence">Connecting…</output>
<script type="module" src="./presence.js"></script>
// presence.ts
import { effect } from "@serve-tools/signal-effect";
import { connect, observe } from "@serve-tools/signal-shared-websocket";
import type { PresenceProtocol } from "./presence.worker.js";

const worker = new SharedWorker(new URL("./presence.worker.js", import.meta.url), {
	name: "presence",
	type: "module",
});
const client = connect<PresenceProtocol>(worker.port);
const presence = observe(client, "presence", { input: { room: "lobby" } });
const output = document.querySelector<HTMLOutputElement>("#presence");

if (!output) {
	throw new Error("Missing #presence output");
}

const stopRendering = effect(() => {
	const state = presence.get();

	switch (state.status) {
		case "pending":
			output.value = "Connecting…";
			break;
		case "ready":
			output.value = `${state.value.online} online`;
			break;
		case "complete":
			output.value = "Presence ended";
			break;
		case "error":
			output.value = `Presence failed: ${String(state.error)}`;
			break;
	}
});

addEventListener(
	"pagehide",
	() => {
		stopRendering();
		presence.dispose();
		client.close();
		worker.port.close();
	},
	{ once: true },
);

Same-origin pages that open the same worker URL and name share one worker and one physical WebSocket. Each page still owns its client, observation, and port.

effect() is illustrative; a Signal-aware renderer can read presence.get() directly.

Observation state

observe() subscribes immediately and returns an Observation<Value>. Calling get() returns one of four states:

type ObservationState<Value> =
	| { status: "pending" }
	| { status: "ready"; value: Value }
	| { status: "complete" }
	| { status: "error"; error: unknown };
  • pending is the initial state before the first event or terminal outcome.
  • ready contains the latest subscription event.
  • complete means the server completed the subscription normally.
  • error contains a remote, transport, setup, or cancellation failure.

Signal consumers may coalesce intermediate ready values. Use client.subscribe() directly when the application must process every event occurrence.

Inputs and cancellation

For a subscription with one input, put the typed input in the required input option:

const controller = new AbortController();

const presence = observe(client, "presence", {
	input: { room: "lobby" },
	signal: controller.signal,
});

For a subscription with no input, omit input:

const announcements = observe(client, "announcements", { signal: controller.signal });

Aborting the signal publishes an error state containing the signal's reason and cancels the underlying subscription. Requests remain Promise-based and are sent through client.request(); observe() is only for subscriptions.

Ownership and cleanup

The ownership chain is explicit:

  • the shared worker owns the physical WebSocket and the server returned by listen();
  • each page owns its logical client and SharedWorker.port;
  • each UI consumer owns the observation returned by observe().

Dispose the UI effect first, then the observation, then the page client, and finally the page's message port. presence.dispose() is idempotent, unsubscribes, and freezes the current observation state. It does not close the page client, message port, shared worker, or physical WebSocket.

Closing one page client leaves the worker's physical WebSocket available to other pages. Call presenceServer.close() inside the worker only when the application intends to stop all page connections and close the physical WebSocket.

The package does not reconnect, replay events, resume subscriptions, add backpressure, authenticate, or validate server values. Those policies belong in the application protocol.

Public API

  • connect(port) creates the page client; /scope/window exposes the same client-and-observation surface.
  • /scope/shared-worker re-exports listen() and its worker-owned server types.
  • observe(client, name, options?) eagerly observes one typed shared WebSocket subscription.
  • Observation<Value> describes the read-only computed Signal and its disposal lifecycle.
  • ObservationState<Value> describes the pending, ready, complete, and error states.
  • ObserveOptions describes cancellation with an optional AbortSignal.
  • Protocol, ProtocolType, SharedWebSocketClient, SubscribeOptions, and Subscription are re-exported types.

Compatibility

The page requires a modern browser with SharedWorker, MessagePort, structured clone, and the platform features required by @serve-tools/signal-websocket. The worker requires the platform features used by @serve-tools/client-shared-websocket, including WebSocket and Promise.withResolvers(). Explicit resource management is optional because observations also expose dispose() and clients expose close().

Agent Skill

The package includes an Agent Skill at skills/serve-tools-signal-shared-websocket. Install or link that directory into your agent's skill directory when you want package-specific guidance for shared transport ownership, reactive state, and cleanup.

Development

npm ci --ignore-scripts
npm run verify

The basic public import is compile-checked by test/signal-shared-websocket.recipes.ts.

License

MIT-0