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

@table-games-engine/nakama-adapter

v0.1.3

Published

Nakama runtime match handler for table-games-engine games.

Readme

@table-games-engine/nakama-adapter

Nakama runtime bindings for @table-games-engine/core: wraps the engine (and @table-games-engine/persistence) in a Nakama TypeScript-runtime match handler so players can connect over WebSocket and play your game.

Most game authors don't install this directly: it comes with @table-games-engine/game-kit, whose preview command and the Logigra store host your game for you. Install it to run your own Nakama server.

Install

npm install @table-games-engine/nakama-adapter

What you get

  • createMatchHandler(opts) returns a Nakama MatchHandler implementing matchInit, matchJoinAttempt, matchJoin, matchLeave, matchLoop, matchSignal and matchTerminate.
  • GameRegistry resolves gameId to { manifest, handlers, setup } at matchInit, so one runtime can host many games (registerPlugin registers a GamePlugin).
  • IdempotencyCache absorbs client retries.
  • JSON wire format (parseInbound, serializeOutbound, OPCODES), per-player filtered broadcasts (broadcastViews), action timeouts (checkTimeouts), reconnection-aware joins.
  • Persistence wiring: events are appended per transition and snapshots follow a SnapshotPolicy.
  • NakamaPlatformPlugin hooks for analytics, audit and similar.

Wiring into Nakama

import { createMatchHandler, registerPlugin, GameRegistry, IdempotencyCache } from '@table-games-engine/nakama-adapter';
import { SqliteAdapter, everyNTransitions } from '@table-games-engine/persistence';

const registry = new GameRegistry();
registerPlugin(registry, myGamePlugin);   // a GamePlugin; or registry.register({ gameId, manifest, handlers, setup }) by hand

const handler = createMatchHandler({
  registry,
  idempotencyCache: new IdempotencyCache(),
  adapter: new SqliteAdapter('/data/engine.db'),
  policy: everyNTransitions(50),
  ownerNode: 'node-A',
  leaseSeconds: 60,
  timeout: { timeoutMs: 30_000 },
  plugins: [],
});

function InitModule(_ctx: nkruntime.Context, logger: nkruntime.Logger, _nk: nkruntime.Nakama, initializer: nkruntime.Initializer): void {
  initializer.registerMatch('tabletop_match', handler);
}

Create a match from your own server code:

nk.matchCreate('tabletop_match', {
  gameId: 'my-game',
  playerIds: 'alice,bob',                 // comma-separated; passed to setup()
  config: JSON.stringify({ /* game options */ }),
});

The adapter reads no environment variables; plumb configuration into the options yourself.

Store options (0.1.2)

Match params (matchCreate; strings only):

| Param | Format | Meaning | |---|---|---| | gameId | string | registry key (= slug) | | playerIds | comma-joined user ids | fixed seats (matchmade). Absent → open seats | | players | decimal string | seat count for open seats | | config | JSON string | preset config passed to setup | | seed | string | setup seed. Absent → today's seed (preview only) | | lobbyTimeoutMs | decimal string | per-match lobby deadline; overrides the option | | meta | JSON string | opaque; returned to hooks as match.meta |

params.matchId is ignored (the match id is always Nakama's).

Handler options:

| Option | Behaviour | |---|---| | tickRate | returned from matchInit | | reconnectGraceMs | a seated player whose last presence left keeps the seat this long, then is ejected (forfeit) | | lobbyTimeoutMs | a match still in lobby at the deadline ends: no setup is played, hooks see nothing, clients get NOTICE lobby-expired, the loop returns null | | turnTimeoutMs | a priority holder with no accepted input for this long is ejected (forfeit). The clock resets on every accepted input and starts when the match starts | | maxGameErrors | ends the match as game_error once this many game-code errors are counted. Unset → never | | requireLegalMove | an input whose canonical {type, data} differs from every entry of legalMoves(state, seat) is rejected (ILLEGAL_MOVE). Concede is exempt | | endLingerMs | after match-ended the match keeps answering joins with the final view for this long, then the loop returns null. A post-end input acks MATCH_ENDED | | hooks.onMatchStarted(env, match) | synchronous; once, when the match leaves lobby | | hooks.onMatchEnded(env, match, endReason, result) | synchronous; once per match. endReason is completed, forfeit or game_error |

@table-games-engine/core now exports winResult for building a win GameResult.

Lifecycle:

lobby ──(all seats joined)──▶ active ──(result)──▶ ended ──(endLingerMs)──▶ freed (loop → null)
  │                             │
  └─(lobbyTimeoutMs)──▶ freed   └─(terminating flag: plays on until result or Nakama's deadline)

terminating: after matchTerminate the match plays on, clients get the server-restart notice, grace and turn timeouts are suspended, and it ends normally or Nakama stops it at its deadline.

  • NOTICE (opcode 6): { notice } to one or all clients, the adapter sends server-restart and lobby-expired (hosts derive opponent-left from presence events); send with sendNotice.
  • match-ended (opcode 4) carries no result when the game failed (game_error); client-sdk subscribers then receive undefined.

Persistence and reconnection

With adapter set, matchInit stores snapshot v0 (and takes an ownership lease if ownerNode is set), each transition appends its events and may snapshot, and matchTerminate releases the lease without closing the adapter. When the same user rejoins with a new session, the handler swaps the presence, sends a fresh filtered state to that player only, and keeps the idempotency cache.

Plugins

import type { NakamaPlatformPlugin } from '@table-games-engine/nakama-adapter';

const audit: NakamaPlatformPlugin = {
  name: 'audit',
  async onMatchEnded(ctx, result) { /* ... */ },
};

Hooks: onMatchCreated, onMatchStarted, onPlayerJoined, onPlayerLeft, onPlayerAction, onStateTransition, onMatchEnded. All are optional and async. They are fire-and-forget: the match loop never waits for them, and a failing plugin is logged but never crashes the match.

Licence

FSL-1.1-ALv2 (Functional Source License, converts to Apache-2.0 after two years). See LICENSE.md in the package.