@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
386 lines • 15.8 kB
TypeScript
/**
* Roles a session can occupy. The orchestrator it wires up underneath is
* uniquely determined by the role.
*
* - `'client'` → {@link ServerAuthoritativeClient }: predicts locally
* when an input sampler is registered; reconciles to
* server-authoritative state arriving via AUTH_STATE.
* With no input sampler, a client is a pure spectator
* — it receives the action stream, smooths via
* interpolation, and never predicts or rewinds. This
* is the default role and covers both "playing" and
* "spectating" use cases.
* - `'host'` → {@link ServerAuthoritativeServer }: simulation
* authority for one or more clients; runs the optional
* server-side input buffer (`simulation_delay_ticks`).
*/
export type NetworkSessionRole = string;
/**
* Roles a session can occupy. The orchestrator it wires up underneath is
* uniquely determined by the role.
*
* - `'client'` → {@link ServerAuthoritativeClient}: predicts locally
* when an input sampler is registered; reconciles to
* server-authoritative state arriving via AUTH_STATE.
* With no input sampler, a client is a pure spectator
* — it receives the action stream, smooths via
* interpolation, and never predicts or rewinds. This
* is the default role and covers both "playing" and
* "spectating" use cases.
* - `'host'` → {@link ServerAuthoritativeServer}: simulation
* authority for one or more clients; runs the optional
* server-side input buffer (`simulation_delay_ticks`).
*
* @readonly @enum {string}
*/
export const NetworkSessionRole: Readonly<{
Client: "client";
Host: "host";
}>;
/**
* High-level networking facade. Wraps the orchestrator + state machinery
* into one config + lifecycle surface. The lower-level orchestrators
* remain reachable through `session.server` / `session.client` /
* `session.peer` for callers that need to override defaults post-hoc.
*
* Replication is component-driven: attaching a {@link NetworkIdentity}
* to an entity registers it with the session via an `EntityObserver`.
* Mutation is event-driven: callers fire
* `dataset.sendEvent(entity, "net_mutate_component", payload)`.
*
* Per-tick driving is a single `session.tick(dt_seconds)`. The session
* runs fixed-timestep stepping, client time dilation, server-side input
* buffer, AUTH_STATE dispatch, time-dilation feedback, and interpolated
* rendering for remote-owned entities.
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class NetworkSession {
/**
* @param {{
* entity_manager: EntityManager,
* transport?: object,
* transport_factory?: (() => object),
* role?: 'client'|'host',
* local_peer_id?: number,
* simulation_delay_ticks?: number,
* tick_rate_hz?: number,
* binary_registry?: BinarySerializationRegistry,
* scope_filter?: object,
* time_dilation?: TimeDilation,
* adaptive_render_delay?: AdaptiveRenderDelay,
* server_resume_grace_ms?: number,
* reconnect?: {
* enabled?: boolean,
* max_attempts?: number,
* base_delay_ms?: number,
* max_delay_ms?: number,
* exponential_factor?: number,
* total_timeout_ms?: number,
* accept_state_resync?: boolean,
* },
* }} options
*
* - `transport` is not connected at construction — call {@link connect}
* with an explicit remote peer id after {@link start}.
* - `transport_factory` (client role): zero-arg function that returns
* a fresh `Transport` on each reconnect attempt. Required for
* automatic reconnect; without it, transport-level disconnects fall
* straight through to `onConnectionPermanentlyLost`.
* - `role` defaults to `'client'`. See {@link NetworkSessionRole}.
* - `local_peer_id` defaults to 0 for host, 1 for client. Must be
* unique across peers.
* - `simulation_delay_ticks` is honored only under `role: 'host'`.
* Default 4. See {@link ServerAuthoritativeServer}.
* - `tick_rate_hz` defaults to 60.
* - `scope_filter` defaults to {@link OwnerAwareScope} on the host.
* - `server_resume_grace_ms` (host role): how long peer state is
* retained after a transport drop before being freed. Default 30s.
* - `reconnect` (client role): policy for the reconnect ladder. See
* {@link DEFAULT_RECONNECT_POLICY}. Set `enabled: false` to opt
* out entirely.
*/
constructor({ entity_manager, transport, transport_factory, role, local_peer_id, simulation_delay_ticks, tick_rate_hz, binary_registry, scope_filter, time_dilation, adaptive_render_delay, server_resume_grace_ms, reconnect, }?: {
entity_manager: EntityManager;
transport?: object;
transport_factory?: (() => object);
role?: 'client' | 'host';
local_peer_id?: number;
simulation_delay_ticks?: number;
tick_rate_hz?: number;
binary_registry?: BinarySerializationRegistry;
scope_filter?: object;
time_dilation?: TimeDilation;
adaptive_render_delay?: AdaptiveRenderDelay;
server_resume_grace_ms?: number;
reconnect?: {
enabled?: boolean;
max_attempts?: number;
base_delay_ms?: number;
max_delay_ms?: number;
exponential_factor?: number;
total_timeout_ms?: number;
accept_state_resync?: boolean;
};
});
/**
* Set after `#render_interpolated_entities` writes interp values into
* live components; cleared by `normalize_if_dirty`. Drives the
* canonical-form invariant: live is canonical when this is false,
* polluted (smooth) when true.
* @private @type {boolean}
*/
private render_dirty;
/**
* Server-issued session token (raw UUID v1 bytes). Set on first
* INITIAL_SYNC arrival; refreshed on every Tier-3 resync. Sent in
* `RESUME_HELLO` to claim continuity with a prior session record.
* Observable to game code for diagnostics; not auth-grade.
* @type {UUID|null}
*/
session_token: UUID | null;
/**
* Host: a connected peer's transport reported a disconnect (the peer
* has entered the grace window). Args: `(peer_id, reason)`.
* @type {Signal}
*/
onPeerLost: Signal;
/**
* Host: a peer has been permanently freed — either via
* `drop_peer()`, a grace-window timeout, or a peer-initiated
* DISCONNECT. Args: `(peer_id, reason)`.
* @type {Signal}
*/
onPeerPermanentlyDropped: Signal;
/**
* Client: the transport has reported a disconnect, and the
* reconnect ladder has started. Args: `(reason)`.
* @type {Signal}
*/
onConnectionLost: Signal;
/**
* Client: about to rebuild the transport and dial the host. Args:
* `(attempt_number)`.
* @type {Signal}
*/
onReconnectAttempt: Signal;
/**
* Client: reconnect succeeded. Args: `({ state_resynced: boolean })` —
* `state_resynced` is `true` if we fell through to Tier-3 (server
* had purged our state and re-sent INITIAL_SYNC), `false` for the
* happy Tier-2 path where the action stream just resumed.
* @type {Signal}
*/
onReconnected: Signal;
/**
* Client: the reconnect ladder is exhausted, or the host explicitly
* kicked us with a DISCONNECT packet, or `reconnect.enabled` is
* false and the link dropped. Args: `(reason)`.
* @type {Signal}
*/
onConnectionPermanentlyLost: Signal;
/**
* @readonly
* @type {EntityManager}
*/
readonly entity_manager: EntityManager;
/**
* @readonly
* @type {EntityComponentDataset}
*/
readonly world: EntityComponentDataset;
/**
* @readonly
* @type {NetworkSessionRole|string}
*/
readonly role: NetworkSessionRole | string;
/**
* @readonly
* @type {number}
*/
readonly tick_period_ms: number;
/**
* @readonly
* @type {number}
*/
readonly simulation_delay_ticks: number;
/**
* @readonly
* @type {number}
*/
readonly local_peer_id: number;
/**
* @readonly
* @type {BinarySerializationRegistry}
*/
readonly binary_registry: BinarySerializationRegistry;
/**
* Register a component class for network replication.
*
* The component must have a {@link BinaryClassSerializationAdapter}
* registered for its `typeName` in the session's `binary_registry`
* before {@link start} is called — that adapter handles the wire
* format and rewind capture.
*
* `interpolator` is optional. When omitted, the component's state
* snaps on each received update (no sub-tick smoothing). When
* provided, it must extend {@link BinaryInterpolationAdapter} with
* `kind = InterpolationKind.Linear` — Discrete and Cubic are
* reserved for future support and currently throw.
*
* The order of `replicate()` calls must match across all peers in
* the session: AUTH_STATE payload layout iterates components in
* insertion order.
*
* @param {Function} ComponentClass
* @param {BinaryInterpolationAdapter} [interpolator]
*/
replicate(ComponentClass: Function, interpolator?: BinaryInterpolationAdapter): void;
/**
* Register a user-defined {@link SimAction} subclass with the
* session. The action's `type_id` is assigned by the underlying
* registry when {@link start} runs. Calling code constructs
* instances directly (`new MyAction(...)`) and dispatches via
* {@link send} or returns them from the input sampler.
*
* @param {Function} SimActionClass class extending SimAction
*/
defineAction(SimActionClass: Function): void;
/**
* Install a per-tick input sampler (client role only). The sampler
* is called once per local sim tick at predict time and must return
* an array of {@link SimAction} instances representing the local
* peer's inputs for that frame. The session executes them locally
* (predict), records their serialized bytes for replay, and forwards
* them over the action stream.
*
* The sampler is NOT called during reconciliation replay — the
* recorded bytes are deserialized and re-executed instead, so the
* action set faithfully reproduces what the user originally
* sampled at that frame.
*
* Return `[]` (or `null`) for ticks with no inputs.
*
* @param {(frame: number) => Array<SimAction>|null} sampler_fn
*/
defineInputSampler(sampler_fn: (frame: number) => Array<SimAction> | null): void;
/**
* Wire the session to the engine. After this, the EntityObserver
* is active, the right orchestrator is constructed, and a
* {@link NetworkSystem} is attached. Subsequent {@link replicate}
* / {@link defineAction} / {@link defineInputSampler} calls throw.
*
* Pre-condition: every component class passed to {@link replicate}
* must already have a `BinaryClassSerializationAdapter` registered
* in the session's binary_registry.
*/
start(): Promise<void>;
/**
* Connect to a remote peer. Host can connect many peers (one per
* client); client typically connects one (the server). Called both
* for first-time connects and reconnect attempts — the session
* detects which based on whether a `session_token` is cached
* (client) or whether the peer is in the grace window (host).
*
* @param {number} remote_peer_id
* @param {object} transport
*/
connect(remote_peer_id: number, transport: object): void;
/**
* Host-only: forcibly free a peer's state, regardless of whether
* they are actively connected, in the grace window, or already
* unknown. Fires `onPeerPermanentlyDropped(peer_id, reason)`.
* Sends a DISCONNECT packet if the peer is still actively
* connected so the remote can short-circuit its reconnect attempts.
*
* Policy for *when* to drop (high server load, anti-cheat, admin
* kick, etc.) is the caller's; the session provides the mechanism.
*
* @param {number} peer_id
* @param {string} [reason]
*/
drop_peer(peer_id: number, reason?: string): void;
/**
* Client-only: voluntarily disconnect from the server. Sends a
* DISCONNECT packet so the host can free state immediately and
* skip the grace window. Disables reconnect for this session.
*
* @param {string} [reason]
*/
disconnect(reason?: string): void;
/**
* Tear down. Idempotent — safe to call from cleanup paths.
*/
stop(): void;
/**
* Fixed-timestep accumulator-driven step. Catch-up bounded by
* `MAX_FIXED_STEPS_PER_TICK`. Under `role: 'client'` the local frame
* advances at `1 / dilation_factor` per fixed step.
*
* @param {number} dt_seconds elapsed wall seconds since the previous call
*/
tick(dt_seconds: number): void;
/**
* Undo render-time interpolation on remote-owned components by
* restoring each from its latest InterpolationLog entry. Idempotent
* via the `render_dirty` flag — cheap when false.
*
* Called from `tick()` (top), `executor.before_execute` (per-action
* applies, including transport-delivered action stream), and
* `client.onBeforeReconcile` (before the rewind walk).
*
* @private
*/
private normalize_if_dirty;
/**
* Dispatch one user-defined action. Equivalent to
* `peer.executor.execute(action, this.local_peer_id)` but exposed
* at the session level so callers don't need a reference to the
* executor or to know the local peer id.
*
* @param {SimAction} action_instance
*/
send(action_instance: SimAction): void;
/**
* @returns {ServerAuthoritativeServer|null}
*/
get server(): ServerAuthoritativeServer;
/**
* @returns {ServerAuthoritativeClient|null}
*/
get client(): ServerAuthoritativeClient;
/**
* @returns {NetworkPeer|null}
*/
get peer(): NetworkPeer;
/**
* @returns {InterpolationLog|null}
*/
get interpolation_log(): InterpolationLog;
/**
* @returns {AdaptiveRenderDelay}
*/
get adaptive_render_delay(): AdaptiveRenderDelay;
/**
* @returns {TimeDilation}
*/
get time_dilation(): TimeDilation;
/**
* Most recent locally-completed sim frame.
*/
get current_frame(): number;
#private;
}
import { UUID } from "../ecs/guid/UUID.js";
import Signal from "../../core/events/signal/Signal.js";
import { BinarySerializationRegistry } from "../ecs/storage/binary/BinarySerializationRegistry.js";
import { BinaryInterpolationAdapter } from "./sim/BinaryInterpolationAdapter.js";
import { SimAction } from "./sim/SimAction.js";
import { ServerAuthoritativeServer } from "./orchestrator/ServerAuthoritativeServer.js";
import { ServerAuthoritativeClient } from "./orchestrator/ServerAuthoritativeClient.js";
import { NetworkPeer } from "./orchestrator/NetworkPeer.js";
import { InterpolationLog } from "./sim/InterpolationLog.js";
import { AdaptiveRenderDelay } from "./time/AdaptiveRenderDelay.js";
import { TimeDilation } from "./time/TimeDilation.js";
//# sourceMappingURL=NetworkSession.d.ts.map