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

@yingyeothon/gamebase-all-together

v2.0.1

Published

Wait/running stage game loop plugin for @yingyeothon/lambda-gamebase (formerly do-game-all-together).

Readme

@yingyeothon/gamebase-all-together

A ready-made gameMain for @yingyeothon/lambda-gamebase for games everyone plays together: a wait stage that processes enter/leave messages until enough users are connected (or the waiting time runs out), a running stage that drives your game controller until the game is over (or the running time runs out), and an end stage that reports the outcome and drops every remaining connection.

This package decides when things happen — a tick, a snapshot, a stage change, the end of the game. What reaches the clients, and in what shape, is up to you: every outbound message goes through a hook. Leave the hooks unset and it keeps sending the messages it always has ({ type: "stage", payload: { stage, age } } once per second, and { type: "enter", payload: { memberId } } on entrance); set one and it is replaced entirely.

This package was formerly published as @yingyeothon/do-game-all-together.

Install

npm install @yingyeothon/gamebase-all-together @yingyeothon/lambda-gamebase

Usage

ESM:

import { runGameAllTogether } from "@yingyeothon/gamebase-all-together";
import {
  broadcast,
  createGamebaseContext,
  gamebaseOptionsFromEnv,
  handleActor,
  reply,
  type BaseGameRequest,
  type GameActorStartEvent,
} from "@yingyeothon/lambda-gamebase";

type MoveMessage = { type: "move"; connectionId: string; x: number };
type GameMessage = BaseGameRequest | MoveMessage;

// One context per Lambda container: owns the Redis connection and the
// API Gateway management client.
const context = createGamebaseContext(gamebaseOptionsFromEnv());

export async function actor(event: GameActorStartEvent) {
  await handleActor<GameMessage>({
    event,
    context,
    eventKeyPrefix: "game:event:",
    awaiterKeyPrefix: "game:awaiter:",
    queueKeyPrefix: "game:queue:",
    lockKeyPrefix: "game:lock:",
    lifetimeSeconds: 300,
    gameMain: (options) =>
      runGameAllTogether<GameMessage>({
        ...options,
        network: { context },
        gameWaitingSeconds: 30,
        gameRunningSeconds: 180,
        pollIntervalMillis: 100,
        // Real time: the simulation advances whether or not anyone acts.
        tick: { mode: "fixed", intervalMillis: 50 },
        snapshotIntervalMillis: 100,
        minPlayers: 3,
        isGameOver: ({ context }) =>
          Object.keys(context.connectedUsers).length === 0,
        processMessage: async ({ context, message }) => {
          // Apply a game message to your own state.
        },
        updateTimeDelta: async ({ context, delta }) => {
          // Advance monsters, damage over time, cooldowns.
        },
        onSnapshot: async ({ context }) => {
          // Your own protocol; this package defines no snapshot format.
          await broadcast(
            Object.keys(context.connectedUsers),
            buildSnapshot(),
            {
              context,
            },
          );
        },
        onMemberEntered: async ({ connectionId }) => {
          // Also fires on a reconnect: the place to resynchronize.
          await reply(connectionId, buildSnapshot(), { context });
        },
        onGameEnd: async ({ context, reason }) => {
          // Connections are still open here, so the result still gets out.
          await broadcast(
            Object.keys(context.connectedUsers),
            { type: "result", payload: { reason } },
            { context },
          );
        },
      }),
  });
}

CJS:

const {
  GameStage,
  runGameAllTogether,
} = require("@yingyeothon/gamebase-all-together");
console.log(GameStage.Wait, GameStage.Running, GameStage.End);

Tick policy

The running stage advances the simulation one of two ways.

tick: { mode: "perMessage" }                       // default
tick: { mode: "fixed", intervalMillis: 50, maxCatchUpSteps: 5 }
  • perMessage calls updateTimeDelta once per processed message with the wall-clock time since the previous call. Turn-based games want this: nothing moves unless somebody acts.
  • fixed accumulates elapsed time and runs whole intervalMillis steps, whether or not messages arrived, with a constant delta. Real-time games need it — otherwise monster AI, damage over time, and cooldowns freeze while the party stands still and then jump by seconds at once. The constant delta also makes the simulation deterministic and replayable. Leftover time stays owed instead of drifting; a backlog beyond maxCatchUpSteps (default 5) is dropped with a warn, so a loop that cannot keep up stays responsive rather than falling further behind.

pollIntervalMillis paces the queue polling; the fixed policy ignores it and paces itself by intervalMillis.

Public API

  • runGameAllTogether / RunGameAllTogetherOptions (type) — the wait → running → end game loop; a drop-in gameMain for handleActor. Pass network: { context } (or an explicit client or transport) so broadcasts and connection drops reach the clients; logger defaults to nullLogger. endRepeatCount repeats the end stage and the drops, which a gateway transport needs — see below
  • GameStage — enum Wait = "wait", Running = "running", End = "end"
  • GameTickPolicy (type) — { mode: "perMessage" } or { mode: "fixed", intervalMillis, maxCatchUpSteps? }
  • GameEndReason (type) — "cleared" | "timeout" | "notEnoughPlayers" | "error", decided by this package and handed to onGameEnd
  • GameHooks (type) — onStageChanged, onMemberEntered, onSnapshot, onGameEnd, with StageChangedOptions, MemberEnteredOptions, SnapshotOptions, GameEndOptions (types). onSnapshot is a client-facing broadcast scheduler, not durable state: it is rate-limited by snapshotIntervalMillis and nothing it produces is persisted. Game state lives in the actor's heap and is gone when the invocation ends
  • GameController (type) — isGameOver, processMessage, optional updateTimeDelta
  • GameMessageBase (type) — { type: string; connectionId: string }, the constraint for the message type parameter M
  • doInStageWait / DoInStageWaitOptions (type) — the wait stage; it ends early only when every user is connected, and otherwise runs the full gameWaitingSeconds and then resolves true if at least minPlayers made it (default: every user)
  • doInStageRunning / DoInStageRunningOptions (type) — the running stage loop
  • broadcastStage — the default onStageChanged; broadcasts { type: "stage", payload: { stage, age } } and reports per-connection delivery
  • broadcastMemberEntered — the default onMemberEntered; broadcasts { type: "enter", payload: { memberId } }
  • createStageAnnouncer / StageAnnouncerOptions (type) — resolves a stage announcement to the caller's hook or the default broadcast
  • pruneUndeliveredUsers / PruneUndeliveredUsersOptions (type) — unbinds the connections a broadcast could not reach, so a client that vanished without a $disconnect stops holding the game open. Enabled for the default announcement by dropUndeliveredConnections; call it yourself from a custom hook
  • processEnterLeave, processEnter, processLeave (+ ProcessEnterLeaveOptions, ProcessEnterOptions, ProcessLeaveOptions types) — enter/leave bookkeeping on the base game context

Behavior changes

  • Connections are dropped endDropDelayMillis (default 1 s) after the end stage. API Gateway can lose a frame posted immediately before DeleteConnection, so dropping right after the result broadcast sometimes swallowed the result; pass endDropDelayMillis: 0 for the old behavior.

  • A superseded connection is closed, not merely unbound. A member entering on a second connection used to leave the first socket open forever, receiving nothing, with its eventual leave a no-op.

  • updateTimeDelta moved out of the message loop. It used to be called from inside the per-message loop, so a game with no traffic did not simulate at all. It is now driven by the tick policy above; perMessage reproduces the old cadence.

  • loopIntervalpollIntervalMillis, matching the repository's *Millis naming.

  • Outbound messages moved behind hooks. The stage and entrance messages are unchanged unless onStageChanged / onMemberEntered is set, in which case the hook replaces them rather than adding to them.

  • BroadcastStageOptionsStageChangedOptions, and broadcastStage now resolves with the RespondResult of its broadcast.

  • The wait stage can start short-handed via minPlayers; the default is still every user. It relaxes the verdict, not the schedule — latecomers still get the whole waiting window.

Ending a game over a gateway transport

endRepeatCount (default 1) repeats the end-stage announcement and the drops, spaced by endRepeatIntervalMillis (default 200). With createRedisPubSubTransport both are published exactly once and pub/sub has no redelivery, so a subscriber gap either shows the party no result or leaves their sockets open forever — and unlike a tick snapshot, nothing later heals it. Set it to 2 or more there; both operations are idempotent.

Every announcement goes out before any drop, so a repeat is never addressed to a socket the previous round already closed. Leave it at 1 for the API Gateway transport, where each repeat is one PostToConnection per player against a connection that is already gone.

Migrating from the legacy package

  • The npm package was renamed: @yingyeothon/do-game-all-together@yingyeothon/gamebase-all-together.
  • Options types follow the monorepo *Options convention: RunGameAllTogetherArgsRunGameAllTogetherOptions, and every exported function's options object now has a named, exported *Options type (DoInStageWaitOptions, DoInStageRunningOptions, StageChangedOptions, ProcessEnterOptions, ProcessLeaveOptions, ProcessEnterLeaveOptions). Function names are unchanged.
  • @yingyeothon/lambda-gamebase no longer keeps module-singleton network clients. Pass network: { context } (a GamebaseContext from createGamebaseContext) or network: { client } in RunGameAllTogetherOptions — it is threaded to every broadcast/dropConnection call. Without it, network calls fall back to environment-independent defaults and will fail outside tests.
  • Logging: logger now defaults to nullLogger (previously a debug-level console logger); pass createConsoleLogger("debug") from @yingyeothon/logger to restore the old behavior. Log calls use the message-first style logger.info("Game end", { gameId }) instead of the pino-style context-first form.
  • All exports are named; there are no default exports. GameController moved to a root export (it was a deep import from services/doInStageRunning before), and the stage helpers (doInStageWait, doInStageRunning, broadcastStage, processEnter*) are now part of the public surface.
  • The message type parameter M is now constrained by GameMessageBase ({ type: string; connectionId: string }) instead of BaseGameRequest. The legacy constraint made it impossible to type game-specific messages (the whole point of processMessage); every legacy-valid M still satisfies the new constraint. Runtime dispatch is unchanged: enter/leave go to enter/leave processing, everything else to processMessage.
  • See Behavior changes above for what moved since the first v2 pass. Note that the underlying @yingyeothon/lambda-gamebase moved to AWS SDK v3: a gone WebSocket client now surfaces as GoneException instead of a statusCode === 410 error, but broadcast/dropConnection still report such connections as undelivered/dropped, so this package's enter/leave and end-of-game processing behaves as before.