@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
139 lines • 5.88 kB
TypeScript
/**
* 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