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), andpreAllocatetoggle. - Injectable
Clockfor deterministic placeholder timestamps. acquireSession(data),releaseSession(session), and auto-releasewithSession(data, callback).getStats()forwardsObjectPool'sPoolStatsfor dashboard reuse.- Forwarded lifecycle:
clear(),dispose(),disposedgetter. - 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, clearschildren, returns.releaseSession(session: PooledSession): void— forwards toObjectPool.release; throwsRELEASE_FOREIGN_OBJECTon a foreign object,POOL_DISPOSEDafter 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.