UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

148 lines (136 loc) 5.72 kB
import { assert } from "../../../../core/assert.js"; import { FRAGMENT_HEADER_BYTES, MAX_CHANNEL_PAYLOAD_BYTES, MAX_CHUNKS_PER_MESSAGE, MAX_FRAGMENT_CHUNK_BYTES, } from "./packet_size.js"; /** * Send a logical payload through a callback, splitting into fragment * packets if it exceeds the channel's single-packet capacity. * * Layout of each emitted fragment packet (before the Channel adds its * 8-byte header): * * uint8 FRAGMENT_PACKET_TYPE * uint16 message_id (little-endian) * uint8 chunk_index * uint8 total_chunks * bytes chunk_payload * * The chunk_payload bytes are slices of the original logical payload, * concatenated in chunk_index order they reconstruct the original * payload byte-for-byte. The logical payload's own packet-type byte is * the first byte of chunk 0 and is preserved by the reassembly. * * Single-packet messages (payload <= MAX_CHANNEL_PAYLOAD_BYTES) are sent * as-is with NO fragmentation header — the receiver dispatches them * normally via the FIRST byte. Fragmentation overhead only applies when * the payload genuinely doesn't fit. * * @param {Uint8Array} payload * @param {number} payload_length * @param {number} message_id sender-assigned, unique per logical message * @param {number} fragment_packet_type packet-type byte for fragment packets * @param {Uint8Array} scratch reusable scratch buffer; must be >= MAX_CHANNEL_PAYLOAD_BYTES * @param {function(Uint8Array, number): void} send_fn * invoked once per emitted packet — single send for non-fragmented case, * N sends for fragmented case */ export function send_fragmented( payload, payload_length, message_id, fragment_packet_type, scratch, send_fn, ) { assert.isNonNegativeInteger(payload_length, 'payload_length'); assert.isNonNegativeInteger(message_id, 'message_id'); assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16'); assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES, 'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES'); if (payload_length <= MAX_CHANNEL_PAYLOAD_BYTES) { // Fits in one packet; send as-is. send_fn(payload, payload_length); return; } const total_chunks = Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES); if (total_chunks > MAX_CHUNKS_PER_MESSAGE) { throw new Error( `send_fragmented: payload of ${payload_length} bytes would require ` + `${total_chunks} chunks, exceeding the ${MAX_CHUNKS_PER_MESSAGE}-chunk wire limit` ); } for (let i = 0; i < total_chunks; i++) { const chunk_offset = i * MAX_FRAGMENT_CHUNK_BYTES; const remaining = payload_length - chunk_offset; const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES; // Build the fragment packet inside scratch. scratch[0] = fragment_packet_type; scratch[1] = message_id & 0xFF; scratch[2] = (message_id >> 8) & 0xFF; scratch[3] = i; scratch[4] = total_chunks; scratch.set( payload.subarray(chunk_offset, chunk_offset + chunk_size), FRAGMENT_HEADER_BYTES, ); send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size); } } /** * Re-emit a single fragment chunk for a previously-sent fragmented * message. Used by the NACK retransmit path: the sender's * {@link FragmentRetention} keeps the source bytes so any chunk can be * deterministically rebuilt. * * The wire layout matches what {@link send_fragmented} would have * produced for the same `(message_id, chunk_index, total_chunks)` — * receivers can't tell a retransmit from an original. * * @param {Uint8Array} payload retained source bytes * @param {number} payload_length * @param {number} message_id * @param {number} chunk_index must be < derived total_chunks; silently ignored otherwise * @param {number} fragment_packet_type * @param {Uint8Array} scratch reusable scratch; must be >= MAX_CHANNEL_PAYLOAD_BYTES * @param {function(Uint8Array, number): void} send_fn */ export function resend_fragment_chunk( payload, payload_length, message_id, chunk_index, fragment_packet_type, scratch, send_fn, ) { assert.isNonNegativeInteger(payload_length, 'payload_length'); assert.isNonNegativeInteger(message_id, 'message_id'); assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16'); assert.isNonNegativeInteger(chunk_index, 'chunk_index'); assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES, 'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES'); // Recover total_chunks from the retained payload length. Same formula // as send_fragmented; payloads short enough to not fragment in the // first place would never have been retained, so we still take the // ceil here as defensive consistency. const total_chunks = payload_length <= MAX_FRAGMENT_CHUNK_BYTES ? 1 : Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES); if (chunk_index >= total_chunks) return; const chunk_offset = chunk_index * MAX_FRAGMENT_CHUNK_BYTES; const remaining = payload_length - chunk_offset; const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES; scratch[0] = fragment_packet_type; scratch[1] = message_id & 0xFF; scratch[2] = (message_id >> 8) & 0xFF; scratch[3] = chunk_index; scratch[4] = total_chunks; scratch.set( payload.subarray(chunk_offset, chunk_offset + chunk_size), FRAGMENT_HEADER_BYTES, ); send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size); }