/** * Actor stores — the SDK-managed bookkeeping every multiplayer game * otherwise hand-writes: your own actor's identity, typed state, and send * loop ({@link LocalActorStore}), created via {@link attachLocalActor} on a * world session. */ import type { ChunkCoordinatesInput } from '../generated/graphql.js'; import type { UdpNotificationHandlers } from '../realtime.js'; import type { StateCodec } from './codec.js'; import type { WorldSessionContext } from './session.js'; /** The notification type delivered for actor updates (incl. your own echo). */ export type ActorUpdateEcho = Parameters>[0]; /** The error notification type. */ export type GenericErrorEcho = Parameters>[0]; /** Where the local actor's 32-char uuid lives across page loads. */ export interface UuidStore { load(): string | null; save(uuid: string): void; } /** Keep the uuid for this tab session only (a fresh actor per reload). */ export declare function memoryUuidStore(): UuidStore; /** * Persist the uuid in `localStorage` so the player keeps a stable actor * identity across reloads (the convention games converge on). Falls back to * {@link memoryUuidStore} behavior outside the browser. */ export declare function localStorageUuidStore(key?: string): UuidStore; /** Why a send happened — recorded on {@link SentActorUpdate}. */ export type SendReason = 'join' | 'move' | 'interval' | 'keyframe' | 'manual' | 'visibility' | 'refresh'; /** The record of the most recent outbound actor update. */ export interface SentActorUpdate { /** The typed state that was sent. */ state: T; /** The encoded (base64) wire form. */ encoded: string; chunk: ChunkCoordinatesInput; sequenceNumber: number; sentAt: number; reason: SendReason; } /** The record of the most recent server-applied echo of our own update. */ export interface AckedActorUpdate { /** The typed state decoded from the echo. */ state: T; /** The raw self-echo notification. */ notification: ActorUpdateEcho; receivedAt: number; } /** The record of the most recent send error attributed to this actor. */ export interface ActorSendError { errorCode: string; sequenceNumber: number; receivedAt: number; } /** Lifecycle of the local actor's replication. */ export type LocalActorStatus = 'idle' | 'pending' | 'acked' | 'error'; /** Options for {@link attachLocalActor}. */ export interface LocalActorConfig { /** Codec between your typed replication state and the base64 wire form. */ codec: StateCodec; /** The state sent until {@link LocalActorStore.setState} changes it. */ initialState: T; /** Explicit 32-char uuid (wins over `uuidStore`). */ uuid?: string; /** * Where the minted uuid persists. Defaults to {@link memoryUuidStore}; * pass {@link localStorageUuidStore} for a stable identity across reloads. */ uuidStore?: UuidStore; /** * Send-loop cadence in ms. Defaults to **200 (5 Hz)** — the proven * cadence for player presence. Set `0` or `false` to disable the loop and * drive {@link LocalActorStore.sendNow} yourself. Runs on the session * {@link Ticker}: pass `workerTicker()` to `createWorldSession` to hold * the rate in backgrounded tabs. */ sendIntervalMs?: number | false; /** * Skip loop sends whose encoded state is byte-identical to the last send. * Defaults to true. Explicit sends (`sendNow`, `join`, `moveTo`) always go * out. */ sendOnChange?: boolean; /** * With `sendOnChange`, still force a keyframe send after this many ms of * dedup silence so presence never starves. Defaults to 3000. */ keyframeEveryMs?: number; /** Default replication radius in chunk units (0-8). */ distance?: number; /** Default replication decay algorithm (0-5). */ decayRate?: number; /** * Re-send presence when the browser tab becomes visible again (timers may * have been throttled while hidden). Defaults to true in browsers. */ refreshOnVisibility?: boolean; /** Clock override for tests. Defaults to `Date.now`. */ now?: () => number; } /** * The SDK-managed **local actor**: identity (minted + persisted uuid), typed * replication state, current chunk, an automatic 5 Hz send loop with * send-on-change dedup, and queryable send bookkeeping — {@link lastSent}, * {@link lastAck} (the server-applied self-echo), {@link lastError}, and * {@link status}. Replaces the hand-written codec + sender + uuid plumbing * every game rebuilds. * * Reads are synchronous; all record updates happen on WebSocket events, so * the render loop can query freely regardless of tab visibility. */ export declare class LocalActorStore { private readonly ctx; private readonly config; /** This actor's 32-char wire id. */ readonly uuid: string; private currentState; private currentChunk; private lastSentRecord; private lastAckRecord; private lastErrorRecord; private readonly inFlight; private readonly sequences; private readonly now; constructor(ctx: WorldSessionContext, config: LocalActorConfig); /** The current typed replication state (what the loop sends). */ get state(): T; /** The actor's current chunk (null before {@link join}). */ get chunk(): ChunkCoordinatesInput | null; /** The most recent outbound update (typed + encoded + seq + timestamp). */ get lastSent(): SentActorUpdate | null; /** The most recent server-applied self-echo. */ get lastAck(): AckedActorUpdate | null; /** The most recent send error attributed to this actor. */ get lastError(): ActorSendError | null; /** * Replication lifecycle: `idle` (nothing sent), `pending` (sent, no echo * yet), `acked` (echo at or after the last send), `error` (an error * arrived after the last send). */ get status(): LocalActorStatus; /** Update the typed state; the next loop tick (or `sendNow`) sends it. */ setState(state: T): void; /** Merge a partial update into the typed state (object states only). */ patchState(patch: Partial): void; /** * Enter a chunk: records it as the actor's current chunk and immediately * sends presence there. (The first message to a brand-new chunk may be * dropped server-side while grid permissions load — if {@link status} * stays `pending`, call {@link refresh}.) */ join(chunk: ChunkCoordinatesInput, state?: T): Promise; /** Move to another chunk and immediately send presence there. */ moveTo(chunk: ChunkCoordinatesInput): Promise; /** Send the current state now, bypassing dedup. */ sendNow(): Promise; /** Re-register presence (reconnects, tab return, dropped first join). */ refresh(reason?: SendReason): Promise; /** One send-loop tick: dedup unchanged state, keyframe when quiet. */ private tick; private send; } /** * Attach a {@link LocalActorStore} to a world session context. Prefer the * `self` key of `createWorldSession`'s config; use this directly for custom * compositions. */ export declare function attachLocalActor(ctx: WorldSessionContext, config: LocalActorConfig): LocalActorStore; /** One timestamped state sample of a remote actor (newest first in history). */ export interface RemoteActorSample { state: T; chunk: ChunkCoordinatesInput; /** Server-stamped epoch ms of the update. */ epochMillis: number; /** Local receive time (ms). */ receivedAt: number; } /** * A tracked remote actor. The object identity is **stable** across updates * (fields mutate in place), so render code can hold references; check the * lane's `revision` for cheap change detection. */ export interface RemoteActor { readonly uuid: string; /** The latest decoded state. */ state: T; /** The chunk of the latest update. */ chunk: ChunkCoordinatesInput; /** Replication radius of the latest update. */ distance: number; /** Server-stamped epoch ms of the latest update. */ epochMillis: number; /** Local receive time of the latest update (ms). */ receivedAt: number; /** * Recent samples, newest first (length ≤ `historySize`) — the data an * interpolating renderer needs without keeping its own buffers. */ samples: Array>; } /** Options for {@link attachRemoteActors}. */ export interface RemoteActorsConfig { /** Codec between the base64 wire state and the typed actor state. */ codec: StateCodec; /** * The local actor's uuid (or a getter), filtered out as the self-echo. * `createWorldSession` wires this automatically from `config.self`. */ selfUuid?: string | (() => string | null); /** * Actors quieter than this are considered gone: excluded from reads and * physically reaped (with `onLeave`) by the reap timer. Defaults to * 12 000 ms. `false` disables staleness entirely. */ staleAfterMs?: number | false; /** * Reap-timer cadence (physically deletes stale records and fires * `onLeave`). Defaults to 1000 ms; `false` relies on read-time filtering + * manual {@link RemoteActorStore.reap} only. Reads are always correct * regardless — staleness is ALSO computed at read time, so a throttled * timer can never serve stale actors. */ reapIntervalMs?: number | false; /** Samples kept per actor for interpolation. Defaults to 2. */ historySize?: number; /** * Named lanes routing one decoded notification to the first matching * sub-registry — e.g. `{ players: (s) => !(s.flags & 2), mobs: (s) => !!(s.flags & 2) }` * lets a player renderer and a mob system share the stream without * double-decoding. Omit for a single implicit lane. */ lanes?: Record boolean>; /** Clock override for tests. Defaults to `Date.now`. */ now?: () => number; } /** One lane's registry of remote actors. */ export declare class RemoteActorLane { private readonly historySize; private readonly staleAfterMs; private readonly now; private readonly actors; private readonly joinListeners; private readonly updateListeners; private readonly leaveListeners; private revisionValue; constructor(historySize: number, staleAfterMs: number | false, now: () => number); /** Bumped on every change — poll it cheaply from a render loop. */ get revision(): number; /** Live actors (stale ones filtered at read time), unordered. */ list(): Array>; /** One live actor, or undefined when unknown/stale. */ get(uuid: string): RemoteActor | undefined; /** Live actor count. */ get count(): number; /** A new actor appeared. @returns off. */ onJoin(listener: (actor: RemoteActor) => void): () => void; /** An actor's state updated (fires after `onJoin` for the first update). @returns off. */ onUpdate(listener: (actor: RemoteActor) => void): () => void; /** An actor went stale and was reaped (or the store was cleared). @returns off. */ onLeave(listener: (actor: RemoteActor) => void): () => void; /** Apply one decoded update (internal). */ apply(uuid: string, state: T, chunk: ChunkCoordinatesInput, distance: number, epochMillis: number): void; /** Physically delete stale records, firing `onLeave` for each. */ reap(): void; /** Drop every record (fires `onLeave` for each live one). */ clear(): void; private isStale; } /** * The SDK-managed **remote actor registry**: subscribes to `actorUpdate`, * decodes each notification ONCE, filters the local self-echo, and maintains * per-actor records with timestamped sample history, staleness, and * join/update/leave events. With `lanes`, one decoded stream feeds several * consumers (players vs mobs) without double-decoding. * * Reads are synchronous and always live-filtered (staleness is computed at * read time), so render loops can query at any cadence — including after a * backgrounded tab resumes. */ export declare class RemoteActorStore { private readonly config; private readonly lanes; private readonly laneFilters; private readonly defaultLane; private decodeFailureCount; constructor(ctx: WorldSessionContext, config: RemoteActorsConfig); /** A named lane's registry (throws for unknown names). */ lane(name: string): RemoteActorLane; /** Live actors across every lane (the default lane when none configured). */ list(): Array>; /** One live actor, searched across lanes. */ get(uuid: string): RemoteActor | undefined; /** Live actor count across lanes. */ get count(): number; /** Sum of lane revisions — poll it cheaply from a render loop. */ get revision(): number; /** Notifications whose state failed to decode (foreign layouts). */ get decodeFailures(): number; /** A new actor appeared (default/single-lane sugar; use `lane()` with lanes). */ onJoin(listener: (actor: RemoteActor) => void): () => void; /** An actor updated. */ onUpdate(listener: (actor: RemoteActor) => void): () => void; /** An actor was reaped. */ onLeave(listener: (actor: RemoteActor) => void): () => void; /** Physically delete stale records in every lane. */ reap(): void; /** Drop every record in every lane. */ clear(): void; private everyLane; } /** * Attach a {@link RemoteActorStore} to a world session context. Prefer the * `actors` key of `createWorldSession`'s config. */ export declare function attachRemoteActors(ctx: WorldSessionContext, config: RemoteActorsConfig): RemoteActorStore; //# sourceMappingURL=actors.d.ts.map