Files
2026-05-27 23:47:53 +01:00

4.2 KiB

SessionPool

(no submodules)

Purpose

SessionPool is a session-specific wrapper around ObjectPool<T> that recycles PooledSession records — plain-data session containers (id, type, metadata, children[]) — between game-loop frames. The wrapper presents a "fresh session per call" API (acquireSession(data) → PooledSession) while internally amortising the underlying allocation across borrows, which keeps GC pressure low in workloads that churn through thousands of short-lived game sessions per minute (matchmaking server, tournament orchestrator, tutorial sim).

The pool is universal. It owns no logic beyond what ObjectPool already provides; the wrapper exists so the factory + reset semantics stay close to the session shape and so callers can inject a Clock for deterministic placeholder timestamps without poking at the underlying pool's factory.

Features

  • Typed wrapper over ObjectPool<PooledSession>.
  • Configurable initialSize (default 20), maxSize (default 100), and preAllocate toggle.
  • Injectable Clock for deterministic placeholder timestamps.
  • acquireSession(data), releaseSession(session), and auto-release withSession(data, callback).
  • getStats() forwards ObjectPool's PoolStats for dashboard reuse.
  • Forwarded lifecycle: clear(), dispose(), disposed getter.
  • Default session type configurable via defaultSessionType.

Usage: Basic

import { SessionPool } from "@suaveplan/game-server/performance/session-pool";

const pool = new SessionPool();
const session = pool.acquireSession({
    id: "match-42",
    type: "game",
    metadata: { createdAt: Date.now(), updatedAt: Date.now() },
});
session.children.push("player-alice", "player-bob");

pool.releaseSession(session);

Usage: Advanced

import { SessionPool } from "@suaveplan/game-server/performance/session-pool";
import { metrics } from "@opentelemetry/api";

const pool = new SessionPool(
    {
        initialSize: 100,
        maxSize: 2_000,
        defaultSessionType: "queue",
    },
    {
        clock: () => Date.now(),
        meter: metrics.getMeter("game-server.sessions"),
    },
);

// Hot loop: every match-found event borrows a session, populates it,
// hands it to a downstream coordinator, and releases it on cleanup.
await pool.withSession(
    {
        id: `match-${crypto.randomUUID()}`,
        type: "game",
        metadata: {
            createdAt: Date.now(),
            updatedAt: Date.now(),
            queueKey: "ranked",
        },
    },
    async (session) => {
        for (const playerId of matchedPlayers) {
            session.children.push(playerId);
        }
        await coordinator.start(session);
    },
);

API Reference

  • new SessionPool(config?: SessionPoolConfig, deps?: SessionPoolDeps).
  • acquireSession(data: PooledSessionData): PooledSession — stamps the borrowed record with caller fields, clears children, returns.
  • releaseSession(session: PooledSession): void — forwards to ObjectPool.release; throws RELEASE_FOREIGN_OBJECT on a foreign object, POOL_DISPOSED after dispose.
  • withSession(data, callback): Promise<R> — borrows + finally-releases.
  • getStats(): PoolStats.
  • clear() / dispose() / disposed: boolean.

Implementation Notes

acquireSession always overwrites the record's identity and metadata fields before returning it, so callers never observe stale placeholder data left over from a previous borrow. The children array is emptied via length = 0 rather than reassigned to a new array, which keeps the underlying buffer (and its hidden-class shape) alive — a small but measurable win in V8 hot loops. The factory's placeholder id counter increments monotonically across both factory + reset paths so production logs that capture a borrowed session before the caller overwrote its id can be debugged unambiguously. The wrapper does not re-export ObjectPool's underlying counters — instead the OTel meter is forwarded into the underlying pool so the same pool.acquire_total / pool.release_total instruments carry the session pool's contribution. Dashboards can disambiguate by attaching pool-name attributes on a thin wrapper if needed.