DOM ENGINEV0.10.0FIZLET

PRESENCE V0.10.0

fizlet/presence

Lockstep input exchange for multiplayer, behind a tiny transport adapter. Every peer runs the same fixed-step simulation from the same seed. Each sim frame needs every player's input for that frame; your own input is sent delay frames ahead, so on a good link the game never waits. A missing input stalls the sim (never guesses), so peers can't drift apart. Inputs live in fixed rings of typed arrays: a confirmed per-frame input log a rollback layer can replay. State hashes are exchanged every hashEvery frames to catch a desync early.

fizlet/presence · GZIP1,792 B1.8 KB of a 2.0 KB budget
fizlet/presence/check · GZIP4,788 B4.7 KB of a 5.0 KB budget

Measured on this build: minified and gzipped with everything it imports, the way npm run size checks the budget in CI.

MODULES · hosted on this site
import { INPUT, HASH, WELCOME, LEAVE, … } from "https://coded-dot-design.github.io/fizlet/dist/presence.js";
import { syncCheck } from "https://coded-dot-design.github.io/fizlet/dist/presence-check.js";
NPM · COMING SOON
npm install fizlet   # not published yet
import { INPUT, HASH, WELCOME, LEAVE, … } from "fizlet/presence";
import { syncCheck } from "fizlet/presence/check";

API

Every export, with its doc comment, read from the published type declarations of v0.10.0.

fizlet/presence

Lockstep input exchange for multiplayer, behind a tiny transport adapter. Every peer runs the same fixed-step simulation from the same seed. Each sim frame needs every player's input for that frame; your own input is sent delay frames ahead, so on a good link the game never waits. A missing input stalls the sim (never guesses), so peers can't drift apart. Inputs live in fixed rings of typed arrays: a confirmed per-frame input log a rollback layer can replay. State hashes are exchanged every hashEvery frames to catch a desync early.

INPUT, HASH, WELCOME, LEAVE const

Wire kinds. Every message is 12 bytes: kind, slot, players, 0, u32 frame (or seed), u32 value.

export declare const INPUT = 1, HASH = 2, WELCOME = 3, LEAVE = 4;

Transport interface

Moves 12-byte messages to the room (or the other peers) and back.

export interface Transport {
    /** Send one message. The buffer is reused: copy it before returning. */
    send(data: Uint8Array): void;
    /** Deliver every incoming message to `fn` (messages that arrived earlier are flushed to it). */
    listen(fn: ((data: Uint8Array) => void) | null): void;
    close(): void;
}

PresenceOptions interface

export interface PresenceOptions {
    transport: Transport;
    /** This peer's seat, 0..players-1 (the room says which). */
    slot: number;
    players: number;
    /** Frames of input delay (default 2: 33 ms at 60 Hz). */
    delay?: number;
    /** Input frames kept per player (default 64; at least 2 × delay + 2). */
    cap?: number;
    /** This player's input for the frame being scheduled, as bits (read latched intents here). */
    input(): number;
    /** The simulation for one frame: same inputs + same seed → same state on every peer. */
    step(e: Engine, inputs: Uint32Array, frame: number): void;
    /** A hash of the sim state (see `hash32`); compared across peers to catch a desync. */
    hash?(): number;
    /** Frames between hash checks (default 30). */
    hashEvery?: number;
}

presence function

export declare function presence(e: Engine, o: PresenceOptions): {
    system: System;
    /** Sim frames run so far (every peer passes the same frames, maybe at different times). */
    readonly frame: number;
    /** Engine steps spent waiting for someone's input. */
    readonly stalls: number;
    /** Messages refused (out of the window, bad seat). */
    readonly dropped: number;
    /** First frame whose state hash differed from a peer's, or -1. */
    readonly desync: number;
    /** Bitmask of seats the room said left (their frames never arrive: the sim waits). */
    readonly lost: number;
    /** The confirmed input of `slot` for `frame`, or -1 if it isn't known (yet, or any more). */
    confirmed(slot: number, frame: number): number;
    destroy: () => void;
};

Presence type

export type Presence = ReturnType<typeof presence>;

hash32 function

FNV-1a over bytes: hash a byte view of your state's typed arrays (make the view once).

export declare function hash32(bytes: Uint8Array, h?: number): number;

memory function

In-memory hub: peers transports that broadcast to each other (like the room relay) after latency ± jitter ticks. Call tick() once per frame. Bounded: cap messages in flight.

export declare function memory(o: {
    peers: number;
    latency?: number;
    jitter?: number;
    seed?: number;
    cap?: number;
}): {
    ends: Transport[];
    /** Advance the link clock one tick and deliver what's due (later messages may overtake). */
    tick(): void;
    readonly queued: number;
    readonly dropped: number;
};

websocket function

Wrap an open-or-opening WebSocket as a transport. Messages before listen are kept (≤ 256).

export declare function websocket(ws: WebSocket): Transport;

join function

Join a room (fizlet-rooms): resolves when the room is full with your seat and the shared seed.

export declare function join(url: string, signal?: AbortSignal): Promise<{
    transport: Transport;
    slot: number;
    players: number;
    seed: number;
}>;

fizlet/presence/check

The determinism check as one function any game can call from a test. It runs players copies of your sim as lockstep clients over the in-memory link (each with only its own input), hashes the state after every frame, and reports the first frame that differs.

SyncGame interface

export interface SyncGame {
    /** This player's input bits for the next scheduled frame (only this client knows them). */
    input(): number;
    /** One sim frame from every player's inputs. Use `e.random`, never Math.random or the clock. */
    step(e: Engine, inputs: Uint32Array, frame: number): void;
    /** A byte view of the whole sim state (typed arrays; make the view once). */
    state: Uint8Array;
}

SyncOptions interface

export interface SyncOptions {
    players?: number;
    frames?: number;
    seed?: number;
    delay?: number;
    latency?: number;
    jitter?: number;
}

SyncResult interface

export interface SyncResult {
    /** Every client reached `frames` with the same state hash on every frame. */
    ok: boolean;
    /** Frames every client completed (≤ frames). */
    frames: number;
    /** First frame whose state differed between clients, or -1. */
    diverged: number;
    /** Engine steps spent waiting for inputs, summed over clients. */
    stalls: number;
}

syncCheck function

export declare function syncCheck(setup: (e: Engine, slot: number, players: number) => SyncGame, o?: SyncOptions): SyncResult;