63 lines
2.9 KiB
TypeScript
63 lines
2.9 KiB
TypeScript
import { setImmediate } from 'node:timers/promises';
|
|
import { getHostOperationPayload, type OperationDeliveryRow } from './index.js';
|
|
|
|
// Even non-ASCII content stays below 64 KiB per encoded chunk.
|
|
const CHUNK_CHARACTERS = 16 * 1024;
|
|
const CHUNK_BYTES = 64 * 1024;
|
|
const encoder = new TextEncoder();
|
|
|
|
function* jsonParts(operations: OperationDeliveryRow[], serverTime: string): Generator<string | Buffer> {
|
|
yield '{"operations":[';
|
|
for (const [index, operation] of operations.entries()) {
|
|
if (index) yield ',';
|
|
const metadata = JSON.stringify({
|
|
id: operation.id, hostId: operation.host_id, kind: operation.kind,
|
|
instanceId: operation.instance_id, generation: operation.generation, state: operation.state,
|
|
error: operation.error, createdAt: operation.created_at, updatedAt: operation.updated_at
|
|
});
|
|
yield metadata.slice(0, -1) + ',"payload":';
|
|
// Load only the immutable payload currently being sent, after the
|
|
// stream has yielded its metadata and had a chance to serve health work.
|
|
yield getHostOperationPayload(operation.host_id, operation.id);
|
|
yield ',"result":';
|
|
yield operation.result ?? 'null';
|
|
yield '}';
|
|
}
|
|
yield '],"serverTime":' + JSON.stringify(serverTime) + '}';
|
|
}
|
|
|
|
function* encodedChunks(operations: OperationDeliveryRow[], serverTime: string): Generator<Uint8Array> {
|
|
for (const part of jsonParts(operations, serverTime)) {
|
|
if (Buffer.isBuffer(part)) {
|
|
for (let start = 0; start < part.length; start += CHUNK_BYTES) yield part.subarray(start, start + CHUNK_BYTES);
|
|
continue;
|
|
}
|
|
for (let start = 0; start < part.length;) {
|
|
let end = Math.min(start + CHUNK_CHARACTERS, part.length);
|
|
// A UTF-16 surrogate pair must be encoded together, including when
|
|
// user-authored configuration text falls on a chunk boundary.
|
|
const last = part.charCodeAt(end - 1);
|
|
if (end < part.length && last >= 0xd800 && last <= 0xdbff) end--;
|
|
yield encoder.encode(part.slice(start, end));
|
|
start = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Bound encoding work and yield between chunks so health requests keep flowing. */
|
|
export function hostOperationsResponse(operations: OperationDeliveryRow[], serverTime = new Date().toISOString()): Response {
|
|
const chunks = encodedChunks(operations, serverTime);
|
|
let cancelled = false;
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
await setImmediate();
|
|
if (cancelled) return;
|
|
const next = chunks.next();
|
|
if (next.done) controller.close();
|
|
else controller.enqueue(next.value);
|
|
},
|
|
cancel() { cancelled = true; chunks.return(undefined); }
|
|
});
|
|
return new Response(stream, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
}
|