openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
175 lines (174 loc) • 7.72 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
//#region src/infra/node-duplex-framing.ts
const NODE_DUPLEX_FRAGMENT_BYTES = 8192;
const NODE_DUPLEX_MAX_MESSAGE_BYTES = 104857600;
const MAX_PENDING_MESSAGES = 8;
const MAX_PENDING_BYTES = 1048576;
/** Owns ordered, bounded binary messages carried by existing node-invoke string frames. */
function createNodeDuplexEndpoint(options) {
const maxMessageBytes = options.maxMessageBytes ?? NODE_DUPLEX_MAX_MESSAGE_BYTES;
if (!Number.isSafeInteger(maxMessageBytes) || maxMessageBytes < 1 || maxMessageBytes > NODE_DUPLEX_MAX_MESSAGE_BYTES) throw new Error("node duplex maximum message bytes must be between 1 and 100 MiB");
const maxOutstandingDeliveryBytes = options.maxOutstandingDeliveryBytes ?? maxMessageBytes;
if (!Number.isSafeInteger(maxOutstandingDeliveryBytes) || maxOutstandingDeliveryBytes < maxMessageBytes || maxOutstandingDeliveryBytes > NODE_DUPLEX_MAX_MESSAGE_BYTES) throw new Error("node duplex maximum outstanding delivery bytes must be between the message limit and 100 MiB");
let closed = false;
const ready = {
sent: false,
received: false
};
let nextOutgoingMessage = 0;
const incoming = {
message: 0,
fragment: 0
};
let activeDeliveryBytes = 0;
let listener;
let sendQueue = Promise.resolve();
const drainClosed = createDeferredCore();
drainClosed.promise.catch(() => {});
const incomingFragments = [];
const pendingMessages = [];
const activeDeliveries = /* @__PURE__ */ new Set();
const assertOpen = () => {
if (closed) throw new Error("node duplex channel is closed");
};
const close = (reason = /* @__PURE__ */ new Error("node duplex channel is closed")) => {
closed = true;
drainClosed.reject(reason);
listener = void 0;
incomingFragments.length = 0;
pendingMessages.length = 0;
activeDeliveries.clear();
};
const fail = (cause) => {
const error = cause instanceof Error ? cause : new Error(String(cause));
if (!closed) {
close(error);
options.onError?.(error);
}
return error;
};
const enqueue = (task) => {
const operation = sendQueue.then(task);
sendQueue = operation.catch(() => {});
return operation.catch((error) => {
throw fail(error);
});
};
const observeListener = (callback, message) => {
const bytesExceeded = activeDeliveryBytes + message.byteLength > maxOutstandingDeliveryBytes;
if (activeDeliveries.size >= MAX_PENDING_MESSAGES || bytesExceeded) throw new Error("node duplex pending listener delivery exceeded its bounded capacity");
const result = callback(message);
if (!result) return;
activeDeliveryBytes += message.byteLength;
const delivery = Promise.resolve(result).finally(() => {
activeDeliveries.delete(delivery);
activeDeliveryBytes -= message.byteLength;
});
activeDeliveries.add(delivery);
delivery.catch(fail);
};
const acceptData = (frame) => {
if (Object.keys(frame).length !== 6 || !Number.isSafeInteger(frame.message) || !Number.isSafeInteger(frame.index) || typeof frame.last !== "boolean" || typeof frame.data !== "string") throw new Error("node duplex data frame has an invalid closed shape");
if (frame.message !== incoming.message || frame.index !== incoming.fragment) throw new Error("node duplex message or fragment arrived out of order");
const fragment = Buffer.from(frame.data, "base64");
if (fragment.toString("base64") !== frame.data || fragment.byteLength > NODE_DUPLEX_FRAGMENT_BYTES || !frame.last && fragment.byteLength !== NODE_DUPLEX_FRAGMENT_BYTES || frame.last && fragment.byteLength === 0 && incoming.fragment > 0) throw new Error("node duplex fragment has invalid canonical base64 or bounded size");
const incomingBytes = incoming.fragment * NODE_DUPLEX_FRAGMENT_BYTES;
if (incomingBytes + fragment.byteLength > maxMessageBytes) throw new Error("node duplex logical message exceeds its maximum size");
const pendingBytes = pendingMessages.reduce((total, message) => total + message.byteLength, 0);
if (!listener && pendingBytes + incomingBytes + fragment.byteLength > MAX_PENDING_BYTES) throw new Error("node duplex pending message buffer exceeded its bounded capacity");
incomingFragments.push(fragment);
incoming.fragment += 1;
if (!frame.last) return;
const assembled = Buffer.concat(incomingFragments, incomingBytes + fragment.byteLength);
const message = new Uint8Array(assembled.buffer, assembled.byteOffset, assembled.byteLength);
incomingFragments.length = 0;
incoming.fragment = 0;
incoming.message += 1;
if (listener) {
observeListener(listener, message);
return;
}
if (pendingMessages.length >= MAX_PENDING_MESSAGES) throw new Error("node duplex pending message buffer exceeded its bounded capacity");
pendingMessages.push(message);
};
return {
send(message) {
return enqueue(async () => {
if (!(message instanceof Uint8Array) || message.byteLength > maxMessageBytes) throw new Error("node duplex logical message exceeds its maximum size");
if (!Number.isSafeInteger(nextOutgoingMessage)) throw new Error("node duplex message sequence exceeded its maximum");
const messageId = nextOutgoingMessage++;
const fragments = Math.max(1, Math.ceil(message.byteLength / NODE_DUPLEX_FRAGMENT_BYTES));
for (let index = 0; index < fragments; index += 1) {
assertOpen();
const start = index * NODE_DUPLEX_FRAGMENT_BYTES;
const fragment = message.subarray(start, start + NODE_DUPLEX_FRAGMENT_BYTES);
await options.sendFrame(JSON.stringify({
v: 1,
kind: "data",
message: messageId,
index,
last: index === fragments - 1,
data: Buffer.from(fragment.buffer, fragment.byteOffset, fragment.byteLength).toString("base64")
}));
assertOpen();
}
});
},
sendReady() {
return enqueue(async () => {
assertOpen();
if (ready.sent || nextOutgoingMessage > 0) throw new Error("node duplex framed readiness is duplicate or out of order");
ready.sent = true;
await options.sendFrame(JSON.stringify({
v: 1,
kind: "ready"
}));
assertOpen();
});
},
receive(frame) {
if (!frame) return;
assertOpen();
try {
if (Buffer.byteLength(frame, "utf8") > 16384) throw new Error("node duplex wire frame exceeds the 16 KiB transport limit");
const parsed = JSON.parse(frame);
if (!isRecord(parsed) || parsed.v !== 1) throw new Error("node duplex frame has an unsupported version or shape");
if (parsed.kind === "ready") {
const receivedData = incoming.message > 0 || incoming.fragment > 0;
if (Object.keys(parsed).length !== 2 || ready.received || receivedData) throw new Error("node duplex framed readiness is malformed, duplicate, or late");
ready.received = true;
options.onReady?.();
return;
}
if (parsed.kind !== "data" || options.requireReady && !ready.received) throw new Error("node duplex frame has unsupported kind or arrived before framed readiness");
acceptData(parsed);
} catch (error) {
throw fail(error);
}
},
onMessage(callback) {
assertOpen();
if (listener) throw new Error("node duplex channel already has an active message listener");
listener = callback;
try {
while (pendingMessages.length > 0) {
const message = pendingMessages.shift();
observeListener(callback, message);
}
} catch (error) {
throw fail(error);
}
return () => {
if (listener === callback) listener = void 0;
};
},
close,
drain: async () => {
assertOpen();
while (activeDeliveries.size > 0) await Promise.race([drainClosed.promise, Promise.all(activeDeliveries)]);
}
};
}
//#endregion
export { createNodeDuplexEndpoint as t };