UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

139 lines 5.88 kB
/** * Reliable, at-least-once delivery layer over a {@link Channel}. * * Used for messages that must arrive: chat, lobby/room state changes, * level transitions, kick / disconnect notifications. Distinct from the * action-stream traffic the rest of the netcode uses, which is UDP-style * best-effort with per-frame back-fill compensating for losses. * * Wire layout (after the {@link Channel}'s 8-byte header and the * `packet_type` byte that {@link NetworkPeer}'s dispatcher consumes): * * uintVar logical_seq sender-assigned, monotonically increasing * bytes command_payload * * Reliability mechanism: each outgoing command goes out as one * Channel.send call. The returned channel-level seq is tracked in * `__unacked`. When the Channel later fires: * - `onPacketAcked(seq)`: drop the entry — the command was delivered * (the receiver de-duplicated by logical_seq, so a "duplicate" from * a previous retransmit is harmless). * - `onPacketLost(seq)`: the Channel's 33-packet ack window aged out * this seq without seeing it acked. Re-send the same logical * command on a fresh channel seq. * * Receiver side: a sliding-window de-dupe of recent `logical_seq` values. * Anything seen within the last `max_received_history` is silently * dropped; first-time arrivals fire `onCommand` with the payload bytes. * * Ordering: NOT guaranteed. Commands may arrive in any order. If you * need in-order delivery (level transitions before subsequent chat), * key payloads with a counter and reorder in the application handler. * * Lifecycle constraints: * - `max_unacked` caps outstanding commands. Send throws when full * (signals real backpressure — receiver gone or path is broken). * - `max_received_history` caps the de-dupe set. A retransmit older * than this window will be re-delivered as a duplicate to the * application; not a problem in practice because the sender's * `max_unacked` keeps the inflight set bounded too. * * Caveats: * - Loss detection relies on the Channel's seq window, which advances * when subsequent packets are sent. Under sustained no-traffic * conditions a single lost reliable command may stay unacked * indefinitely — by convention the action-stream's per-tick traffic * keeps the window moving even when the application is otherwise * idle. Real production code may want a timer-based retransmit * fallback layered on top of this. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class ReliableCommandPipeline { /** * @param {{ * channel: import("./Channel.js").Channel, * packet_type: number, * max_unacked?: number, * max_received_history?: number, * max_retries?: number, * }} options */ constructor({ channel, packet_type, max_unacked, max_received_history, max_retries, }: { channel: import("./Channel.js").Channel; packet_type: number; max_unacked?: number; max_received_history?: number; max_retries?: number; }); /** @type {import("./Channel.js").Channel} */ channel: import("./Channel.js").Channel; /** @type {number} @readonly */ readonly packet_type: number; /** @type {number} @readonly */ readonly max_unacked: number; /** @type {number} @readonly */ readonly max_received_history: number; /** * Maximum retransmissions per logical command before it is dropped * and {@link onCommandAbandoned} fires. Bounded so a permanently- * black-holed peer can't pin retransmit traffic on the channel * forever. The default (16) tolerates roughly half a second of * sustained loss at typical channel cadences before giving up. * @type {number} @readonly */ readonly max_retries: number; /** * Fired when a brand-new logical command arrives (not a duplicate). * Args: `(buf, payload_offset, payload_length)`. The buffer is * shared scratch; consumers must copy bytes if they need them to * outlive the handler. * @type {Signal} */ onCommand: Signal; /** * Fired when a sent command has been retransmitted `max_retries` * times without an ack and is being given up on. Args: * `(logical_seq)`. Subscribers typically surface this as a "command * failed to deliver" event to the application, or treat it as a * hard signal to tear down the session. * @type {Signal} */ onCommandAbandoned: Signal; /** * Send a logical command. Returns the assigned `logical_seq` (useful * for application-level diagnostics; the receiver dedups internally * regardless). * * @param {Uint8Array} payload * @param {number} length * @returns {number} */ send(payload: Uint8Array, length: number): number; /** * Called by the orchestrator (typically {@link NetworkPeer}'s * dispatcher) when a packet of this pipeline's type arrives. `buf` * is positioned just past the packet-type byte. * * @param {BinaryBuffer} buf * @param {number} total_length full transport-level payload length, * including the consumed packet-type byte */ handle_inbound(buf: BinaryBuffer, total_length: number): void; /** * Number of commands sent that haven't yet been acked. Useful for * backpressure / progress UI. * @returns {number} */ unacked_count(): number; /** * Drop subscriptions and clear state. Required before discarding a * pipeline whose channel will outlive it. */ dispose(): void; #private; } import Signal from "../../../core/events/signal/Signal.js"; import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js"; //# sourceMappingURL=ReliableCommandPipeline.d.ts.map