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

@tabledeck/game-room

v0.1.1

Published

Shared Durable Object infrastructure and React hooks for tabledeck.us multiplayer games

Readme

@tabledeck/game-room

Shared Durable Object infrastructure and React hooks for tabledeck.us multiplayer games.

Provides the complete WebSocket plumbing, player-join coordination, and persistent state management so each new game only needs to implement game-specific logic.


What's included

Server (@tabledeck/game-room/server) — Cloudflare Workers / Durable Objects:

  • BaseGameRoomDO — abstract DO base class with WebSocket hibernation, game lifecycle, and state persistence
  • readGuestCookie / makeGuestCookieHeader — parse and write the per-game guest identity cookie

Client (@tabledeck/game-room/client) — React / browser:

  • useGameWebSocket — manages the WS connection with automatic reconnect and stale-closure-safe message routing

Installation

npm install @tabledeck/game-room

Peer dependencies (must be in your project already):

npm install @cloudflare/workers-types react react-router

Quick start

1. Implement BaseGameRoomDO

// workers/game-room.ts
import { BaseGameRoomDO } from "@tabledeck/game-room/server";

interface MyState {
  players: Array<{ seat: number; name: string; score: number } | null>;
  phase: "waiting" | "playing" | "finished";
}

interface MySettings {
  maxPlayers: number;  // required by BaseSettings
  roundCount: number;
}

export class GameRoomDO extends BaseGameRoomDO<MyState, MySettings, Env> {
  protected initializeState(settings: MySettings): MyState {
    return {
      players: Array(settings.maxPlayers).fill(null),
      phase: "waiting",
    };
  }

  protected serializeState(state: MyState) {
    return {
      players: state.players.filter(Boolean),
      phase: state.phase,
    };
  }

  protected deserializeState(data: Record<string, unknown>): MyState {
    return data as MyState;
  }

  protected isPlayerSeated(state: MyState, seat: number) {
    return state.players[seat] != null;
  }

  protected getPlayerName(state: MyState, seat: number) {
    return state.players[seat]?.name ?? null;
  }

  protected seatPlayer(state: MyState, seat: number, name: string): MyState {
    const players = [...state.players];
    players[seat] = { seat, name, score: 0 };
    return { ...state, players };
  }

  protected getSeatedCount(state: MyState) {
    return state.players.filter(Boolean).length;
  }

  protected async onAllPlayersSeated() {
    // Deal cards, start first round, etc.
    this.gameState!.phase = "playing";
    await this.persistState();
    this.broadcast(JSON.stringify({ type: "game_started" }));
  }

  protected async onGameMessage(ws: WebSocket, rawMsg: unknown, seat: number) {
    const msg = rawMsg as { type: string };
    if (msg.type === "make_move") {
      // ... validate, apply, broadcast
    }
  }
}

2. Add the DO to wrangler.toml

[[durable_objects.bindings]]
name = "MY_GAME_ROOM"
class_name = "GameRoomDO"

[[migrations]]
tag = "v1"
new_classes = ["GameRoomDO"]

3. Create the game from your worker's fetch handler

// On POST /game (create new game):
const doId = env.MY_GAME_ROOM.idFromName(gameId);
const stub = env.MY_GAME_ROOM.get(doId);
await stub.fetch(new Request("http://internal/create", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ settings, gameId }),
}));

4. Handle joins in your HTTP action

// React Router action — when a player submits the name modal:
import { readGuestCookie, makeGuestCookieHeader } from "@tabledeck/game-room/server";

export async function action({ params, request, context }: Route.ActionArgs) {
  const { gameId } = params;
  const body = await request.json() as { guestName?: string };

  if (body.guestName) {
    // 1. Write to your DB and get the assigned seat
    const seat = await assignSeatInDB(gameId, body.guestName);

    // 2. Notify the DO so it can broadcast player_joined to connected clients
    const stub = getGameRoomStub(env, gameId);
    await stub.fetch(new Request("http://internal/join", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ seat, name: body.guestName }),
    }));

    // 3. Set the identity cookie so the player is recognised on reload
    return data(
      { seat, name: body.guestName },
      { headers: { "Set-Cookie": makeGuestCookieHeader(`game_${gameId}`, seat, body.guestName) } },
    );
  }
}

5. Connect on the client

// app/routes/game.$gameId.tsx
import { useGameWebSocket } from "@tabledeck/game-room/client";
import { useCallback } from "react";

export default function GamePage({ loaderData }) {
  const { gameId, initialSeat, initialName } = loaderData;
  const [mySeat, setMySeat] = useState(initialSeat);
  const [myName, setMyName] = useState(initialName);
  const [players, setPlayers] = useState(loaderData.players);

  const { send } = useGameWebSocket({
    gameId,
    seat: mySeat,
    name: myName,
    onMessage: useCallback((msg: unknown) => {
      const m = msg as { type: string };
      switch (m.type) {
        case "game_state":
          // hydrate all state from (m as any).state
          break;
        case "player_joined":
          setPlayers((prev) => [...prev, { seat: (m as any).seat, name: (m as any).name }]);
          break;
        // ... your game messages
      }
    }, []),
  });

  return <GameBoard send={send} players={players} />;
}

Abstract methods reference

| Method | Purpose | |---|---| | initializeState(settings) | Create a fresh game state from settings | | serializeState(state) | JSON-safe representation for storage and WebSocket transport (strip private data) | | deserializeState(data) | Reconstruct state from stored JSON | | isPlayerSeated(state, seat) | Check if a seat is occupied | | getPlayerName(state, seat) | Return the display name at a seat, or null | | seatPlayer(state, seat, name) | Return new state with the seat filled (do not mutate) | | getSeatedCount(state) | Count occupied seats | | onAllPlayersSeated() | Called once when the last seat fills — start the game | | onGameMessage(ws, msg, seat, playerName) | Route game-specific WebSocket messages |

Optional overrides

| Method | Default | Override when | |---|---|---| | getPrivateStateForSeat(seat) | {} | You need to send per-player hidden data (hand of cards, tile rack, …) alongside game_state | | onPlayerDisconnected(seat) | no-op | You want to mark a player as disconnected in your state |


Cookie utilities

import { readGuestCookie, makeGuestCookieHeader } from "@tabledeck/game-room/server";

// In a loader — re-identify a returning player:
const identity = readGuestCookie(request, `game_${gameId}`);
// → { seat: 2, name: "Alice" } | null

// In an action — set the cookie after a successful join:
const header = makeGuestCookieHeader(`game_${gameId}`, seat, name);
// → "game_abc123=2:Alice; Path=/; Max-Age=86400; SameSite=Lax"

Further reading