//#region src/server.ts
/** Hoisted per R3 — TextEncoder is expensive native, never reconstruct per call.
*  verifyInternalSecret runs on the internal-broadcast trust path (per fan-out). */
const utf8 = new TextEncoder();
/** Stamp id + timestamp, fire-and-forget to a CF Queue. Never mutates the caller's object.
*  Call via ctx.waitUntil(...) in a route handler. */
async function publishEvent(queue, event) {
	const stamped = {
		...event,
		id: crypto.randomUUID(),
		timestamp: (/* @__PURE__ */ new Date()).toISOString()
	};
	await queue.send(stamped);
}
/** Hibernatable accept. Supports BOTH accept mechanisms — host picks, module forces neither:
*  - tags       → ctx.acceptWebSocket(ws, tags)
*  - attachment → ws.serializeAttachment(attachment) after accept */
function acceptHibernatable(ctx, ws, opts) {
	if (opts?.tags) ctx.acceptWebSocket(ws, opts.tags);
	else ctx.acceptWebSocket(ws);
	if (opts?.attachment !== void 0) ws.serializeAttachment(opts.attachment);
}
/** getWebSockets → safe send (ignore closed). targetTag scopes via getWebSockets(targetTag) only. */
function broadcastFrame(ctx, frame, opts) {
	const sockets = opts?.targetTag ? ctx.getWebSockets(opts.targetTag) : ctx.getWebSockets();
	for (const ws of sockets) {
		if (opts?.exclude && ws === opts.exclude) continue;
		try {
			ws.send(frame);
		} catch {}
	}
}
/** Drain a queue batch → dispatch each event → ack on success, retry on throw. */
function makeQueueConsumer(dispatch) {
	return async (batch, env) => {
		for (const msg of batch.messages) try {
			await dispatch(msg.body, env);
			msg.ack();
		} catch {
			msg.retry();
		}
	};
}
/** Constant-time internal-secret check. Length-guard before timingSafeEqual. */
function verifyInternalSecret(provided, expected) {
	if (provided == null || provided.length !== expected.length) return false;
	return crypto.subtle.timingSafeEqual(utf8.encode(provided), utf8.encode(expected));
}
/** Participants minus those currently connected (mechanism-agnostic). */
function computeOffline(participantIds, online) {
	return participantIds.filter((id) => !online.has(id));
}
//#endregion
export { acceptHibernatable, broadcastFrame, computeOffline, makeQueueConsumer, publishEvent, verifyInternalSecret };
