/** * @pwngh/economy-lab * * Copyright (c) Preston Neal * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import type { InstanceSession, Reservations, SessionOptions } from './netting.js'; import type { OrphanSweepSummary } from './worker/orphans.js'; import type { Clock, Digest, Ids, Logger, Meter, Store } from './ports.js'; /** The structural subset of Ports a cluster node needs; an openPorts host passes through. */ export interface ClusterNodeDeps { store: Store; digest: Digest; clock: Clock; ids: Ids; logger?: Logger; meter?: Meter; } /** * The node's identity and peers, plus the two bounds the epoch-age law binds together: every * epoch on this node rotates within `epochMaxAgeMs`, and a due rotation is only observed a * sweep cadence later, so the orphan sweep settles nothing younger than twice that bound. * Construction throws CONFIG_INVALID on a `settleOlderThanMs` under `2 * epochMaxAgeMs` — a * lower bound would let the sweep settle an epoch a live node could still be filling. */ export interface ClusterNodeOptions { /** This node's name; must appear in `nodes`. */ nodeId: string; /** Every node in the deployment. All nodes must construct from the same list. */ nodes: ReadonlyArray; /** * The rotation bound every epoch on this node lives under — the lane manager's * `epochMaxAgeMs` when lanes run here (see `laneOptions`), the host's own rotation cadence * for raw sessions. Default 60_000 ms. */ epochMaxAgeMs?: number; /** * Orphan settling opt-in (settling moves money). Absent, `sweepOrphans` still reports * crashed epochs; it settles nothing. */ sweep?: { /** * The sweep settles sessions at least this old (ms). Held to the epoch-age law: at least * `2 * epochMaxAgeMs`, enforced at construction. */ settleOlderThanMs: number; /** Max sessions inspected per sweep run. Default 100. */ limit?: number; }; } /** * Opens this process's node of a multi-node deployment — one construction for what every * multi-node host was hand-wiring: the scope router bound to this node's identity, the * store-backed shared reservation registry, ownership-gated session opening, crash recovery, * and the orphan sweep. Every node constructs from the same `nodes` list over the same shared * database, and the store must offer the reservation counter (the SQL engines and the memory * adapter all do). * * @example * const node = openClusterNode(ports, { * nodeId: 'economy-a', * nodes: ['economy-a', 'economy-b', 'economy-c'], * sweep: { settleOlderThanMs: 120_000 }, * }); * const lanes = openInstanceEconomies(ports, { ...node.laneOptions() }); * * // Per request from a game server: * node.assertOwns(worldInstanceId); * await lanes.laneFor(worldInstanceId).purchase(order); * * // On the worker's schedule: * await node.sweepOrphans(); */ export declare function openClusterNode(deps: ClusterNodeDeps, options: ClusterNodeOptions): ClusterNode; /** * One node's handle. `ownerOf`/`owns`/`assertOwns` answer the shared rendezvous assignment; * `openSession` opens ownership-gated epochs; `recover` finishes a crashed node's session; * `laneOptions` spreads into the lane manager; `sweepOrphans` runs the shared-journal sweep. * Construct through {@link openClusterNode}. */ export declare class ClusterNode { readonly nodeId: string; /** * The store-backed registry every session on this node screens against. Pass it wherever a * registry is asked for (the worker's orphans job, a lane manager not built from * `laneOptions`); never build a second one per node. */ readonly reservations: Reservations; private readonly deps; private readonly route; private readonly epochMaxAgeMs; private readonly sweep; private readonly mint; constructor(deps: ClusterNodeDeps, options: ClusterNodeOptions); /** The scope's owning node per the rendezvous assignment — the same answer on every node. */ ownerOf(scope: string): string; /** Whether the assignment routes the scope here; the boolean form of {@link ClusterNode.ownerOf}. */ owns(scope: string): boolean; /** Throws SESSION_MISROUTED unless this node owns the scope — the gate for a request edge. */ assertOwns(scope: string): void; /** * Opens the scope's next raw netting epoch: ownership-gated, screened by the shared * registry, session id minted as `sess::-`. The caller owes the epoch * discipline — settle within `epochMaxAgeMs`, then open the next epoch (the lane manager * does both on cadence; see `laneOptions`). */ openSession(scope: string, options?: Omit): InstanceSession; /** * Rebuilds a journaled session with the shared registry wired in — the failover path for a * session id the sweep reported. The shared counter already holds the crashed node's * reservations, and wiring the registry here is what tells recovery not to re-apply them. */ recover(sessionId: string): Promise; /** * Spread into `openInstanceEconomies` so the manager's lanes share this node's registry and * rotate under the same bound the sweep law was validated against. */ laneOptions(): { reservations: Reservations; epochMaxAgeMs: number; }; /** * One orphan-sweep pass over the shared journal: reports crashed epochs, settles the ones * older than the validated bound when the `sweep` opt-in is set, and releases their * reservations. Schedule it like any worker sweep; more than one node running it is safe * (settling is idempotent against stored evidence). */ sweepOrphans(input?: { now?: number; limit?: number; }): Promise; }