@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
148 lines (136 loc) • 5.72 kB
JavaScript
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);
}