Files
quixos-nix-helpers/portable-service.mjs
T

172 lines
6.9 KiB
JavaScript

/** Generated-service transport. The supervisor owns process isolation, deadlines and termination.
* stdout is reserved for this protocol; application diagnostics belong on stderr. */
export function servePortableRegistry(
registry,
{ maxBytes = 4 * 1024 * 1024, maxInvocations = 64, maxPortCalls = 10000 } = {},
) {
const abi = "quixos-transaction-ports-v1";
const active = new Map();
const pending = new Map();
let buffer = Buffer.alloc(0),
stopped = false;
const send = (message) => {
const encoded = JSON.stringify(message);
if (Buffer.byteLength(encoded) > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
// A slow or absent coordinator must not cause an unbounded stdout queue.
if (process.stdout.writableLength + Buffer.byteLength(encoded) > maxBytes * 2) throw Error("SERVICE_BACKPRESSURE");
process.stdout.write(encoded + "\n");
};
const errorBody = (error) => ({
code: typeof error?.code === "string" ? error.code.slice(0, 1024) : "PACKAGE_ERROR",
message: String(error?.message ?? error).slice(0, 4096),
});
const rejectPorts = (id, reason) => {
for (const [key, call] of pending)
if (call.invocation === id) {
pending.delete(key);
call.reject(reason);
}
};
const cancel = (id, reason) => {
const token = active.get(id);
if (token) {
token.cancelled = true;
token.controller.abort(reason);
}
rejectPorts(id, reason);
};
const fatal = (error) => {
if (stopped) return;
stopped = true;
for (const id of active.keys()) cancel(id, error);
process.stderr.write(String(error?.message ?? error) + "\n");
process.exitCode = 1;
process.stdin.destroy();
};
const receive = (message) => {
if (!message || message.abi !== abi) throw Error("SERVICE_ABI_MISMATCH");
if (message.kind === "shutdown") {
for (const id of active.keys()) cancel(id, Error("SERVICE_SHUTDOWN"));
stopped = true;
process.stdin.destroy();
return;
}
if (message.kind === "cancel") {
const token = active.get(message.invocation);
if (token && token.frame !== message.frame) throw Error("INVALID_CANCELLATION_FRAME");
cancel(message.invocation, Error("INVOCATION_CANCELLED"));
// A completed result may already be in flight. Acknowledge without resurrecting its context.
if (!token) send({ abi, kind: "cancelled", invocation: message.invocation, frame: message.frame });
return;
}
if (message.kind === "port-result") {
const key = JSON.stringify([message.invocation, message.sequence]);
const call = pending.get(key);
if (!call && (!active.has(message.invocation) || active.get(message.invocation).cancelled)) return;
if (!call || message.frame !== call.frame) throw Error("UNEXPECTED_PORT_RESULT");
pending.delete(key);
if (message.status === "returned") call.resolve(message.output);
else if (message.status === "failed")
call.reject(
Object.assign(Error(String(message.error?.message ?? "Port failed")), {
code: String(message.error?.code ?? "PORT_FAILED"),
}),
);
else throw Error("INVALID_PORT_RESULT");
return;
}
if (
message.kind !== "invoke" ||
typeof message.invocation !== "string" ||
typeof message.frame !== "string" ||
!message.invocation ||
!message.frame ||
message.invocation.length > 4096 ||
message.frame.length > 4096
)
throw Error("INVALID_INVOCATION");
if (active.has(message.invocation) || active.size >= maxInvocations) throw Error("INVOCATION_LIMIT_OR_DUPLICATE");
const token = { frame: message.frame, controller: new AbortController(), cancelled: false };
active.set(message.invocation, token);
let sequence = 0;
const channel = (request) => {
if (active.get(message.invocation) !== token || token.cancelled)
return Promise.reject(Error("EXPIRED_INVOCATION"));
if (++sequence > maxPortCalls || pending.size >= maxPortCalls) return Promise.reject(Error("PORT_CALL_LIMIT"));
const key = JSON.stringify([message.invocation, sequence]);
return new Promise((resolve, reject) => {
pending.set(key, { invocation: message.invocation, frame: message.frame, resolve, reject });
try {
send({
abi,
kind: "port",
frame: message.frame,
invocation: message.invocation,
sequence,
portId: request.portId,
action: { ...request.action, ...(request.key === undefined ? {} : { key: request.key }) },
input: request.input,
});
} catch (error) {
pending.delete(key);
reject(error);
}
});
};
void registry
.invoke(
{ exportId: message.exportId, workspace: message.workspace, receiver: message.receiver, input: message.input },
channel,
Object.freeze({ signal: token.controller.signal, effect: message.effect }),
)
.then(
(output) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "returned", output });
},
(error) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "failed", error: errorBody(error) });
},
)
.catch(fatal)
.finally(() => {
if (active.get(message.invocation) === token) {
active.delete(message.invocation);
token.controller.abort(Error("INVOCATION_ENDED"));
rejectPorts(message.invocation, Error("INVOCATION_ENDED"));
if (token.cancelled && !stopped)
send({ abi, kind: "cancelled", invocation: message.invocation, frame: token.frame });
}
})
.catch(fatal);
};
process.stdin.on("data", (bytes) => {
if (stopped) return;
try {
// Consume each line before retaining the remainder; combined transport chunks may contain many frames.
let offset = 0;
while (offset < bytes.length) {
const newline = bytes.indexOf(10, offset);
const end = newline < 0 ? bytes.length : newline;
if (buffer.length + end - offset > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
buffer = Buffer.concat([buffer, bytes.subarray(offset, end)]);
if (newline < 0) break;
receive(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer)));
buffer = Buffer.alloc(0);
offset = newline + 1;
if (stopped) break;
}
} catch (error) {
fatal(error);
}
});
process.stdin.on("end", () => {
if (buffer.length) fatal(Error("TRUNCATED_SERVICE_MESSAGE"));
for (const id of active.keys()) cancel(id, Error("COORDINATOR_DISCONNECTED"));
});
process.stdin.on("error", fatal);
process.stdout.on("error", fatal);
send({ abi, kind: "ready", exports: registry.exports });
}