openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
249 lines (248 loc) • 8.67 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import { randomUUID } from "node:crypto";
//#region src/gateway/desktop/session-registry.ts
const DEFAULT_LINGER_MS = 6e4;
const MAX_OBSERVERS = 8;
var DesktopSessionStaleOwnerError = class extends Error {
constructor() {
super("Desktop session owner epoch is stale");
this.name = "DesktopSessionStaleOwnerError";
}
};
var DesktopSessionStoppedError = class extends Error {
constructor() {
super("Desktop session stopped before connecting");
this.name = "DesktopSessionStoppedError";
}
};
/** Owns per-source desktop sessions and their connected observer lifetimes. */
function createDesktopSessionRegistry(deps = {}) {
const lingerMs = deps.lingerMs ?? DEFAULT_LINGER_MS;
const entries = /* @__PURE__ */ new Map();
const claimedOwnerEpochs = /* @__PURE__ */ new Map();
const claimOwnerEpoch = (sourceKey, ownerEpoch) => {
const claimedEpoch = claimedOwnerEpochs.get(sourceKey);
if (claimedEpoch !== void 0 && ownerEpoch < claimedEpoch) throw new DesktopSessionStaleOwnerError();
if (claimedEpoch === void 0 || ownerEpoch > claimedEpoch) {
claimedOwnerEpochs.set(sourceKey, ownerEpoch);
return true;
}
return false;
};
const isCurrent = (entry) => entries.get(entry.sourceKey) === entry && !entry.stopped;
const closeObserver = (observer, code, reason) => {
try {
observer.close(code, reason);
} catch {}
};
const stopEntry = (entry) => {
if (entry.stopPromise) return entry.stopPromise;
const stopped = createDeferredCore();
entry.stopPromise = stopped.promise;
(async () => {
entry.stopped = true;
clearTimeout(entry.lingerTimer);
entry.lingerTimer = void 0;
for (const observer of entry.observers) {
observer.released = true;
closeObserver(observer, 1012, "desktop tunnel closed");
}
entry.observers.clear();
entry.controller = void 0;
for (const pending of entry.pendingStreams.values()) {
pending.reservation.release();
pending.stream.destroy();
}
entry.pendingStreams.clear();
entry.observerReservations.clear();
if (!entry.readySettled) {
entry.readySettled = true;
entry.ready.reject(new DesktopSessionStoppedError());
}
await entry.teardown?.().catch(() => void 0);
await entry.initialization?.catch(() => void 0);
await entry.teardown?.().catch(() => void 0);
})().finally(() => {
if (entries.get(entry.sourceKey) === entry) entries.delete(entry.sourceKey);
}).then(stopped.resolve, stopped.reject);
return entry.stopPromise;
};
const scheduleLinger = (entry) => {
if (!isCurrent(entry) || entry.observers.size > 0 || entry.observerReservations.size > 0) return;
clearTimeout(entry.lingerTimer);
entry.lingerTimer = setTimeout(() => void stopEntry(entry), lingerMs);
entry.lingerTimer.unref?.();
};
const waitForReady = async (entry) => {
const result = await entry.ready.promise;
scheduleLinger(entry);
return result;
};
async function startSession(request) {
claimOwnerEpoch(request.sourceKey, request.ownerEpoch);
const current = entries.get(request.sourceKey);
if (current) {
if (request.ownerEpoch < current.ownerEpoch) throw new DesktopSessionStaleOwnerError();
if (request.ownerEpoch === current.ownerEpoch && !current.stopped) return await waitForReady(current);
}
const ready = createDeferredCore();
ready.promise.catch(() => void 0);
const entry = {
sourceKey: request.sourceKey,
ownerEpoch: request.ownerEpoch,
ready,
readySettled: false,
observers: /* @__PURE__ */ new Set(),
observerReservations: /* @__PURE__ */ new Set(),
pendingStreams: /* @__PURE__ */ new Map(),
stopped: false,
start: request.start,
...request.teardown ? { teardown: request.teardown } : {}
};
entries.set(request.sourceKey, entry);
entry.initialization = (async () => {
if (current) await stopEntry(current);
if (!isCurrent(entry)) return;
const result = await entry.start(() => isCurrent(entry));
if (!isCurrent(entry)) return;
entry.readySettled = true;
entry.ready.resolve(result);
})();
entry.initialization.catch((error) => {
if (!entry.readySettled) {
entry.readySettled = true;
entry.ready.reject(error instanceof Error ? error : /* @__PURE__ */ new Error("Desktop session failed"));
}
stopEntry(entry);
});
return await waitForReady(entry);
}
async function acquire(request) {
const result = await startSession(request);
if (!result) throw new Error("Desktop session attachment is unavailable");
return result;
}
async function activate(request) {
await startSession({
...request,
start: async () => void 0
});
}
function attachObserver(sourceKey, observer) {
const entry = entries.get(sourceKey);
if (!entry || !entry.readySettled || entry.stopped || entry.observers.size + entry.observerReservations.size >= MAX_OBSERVERS) return;
if (observer.ownerEpoch !== entry.ownerEpoch) return;
clearTimeout(entry.lingerTimer);
entry.lingerTimer = void 0;
if (observer.control && entry.controller) {
const previous = entry.controller;
previous.released = true;
entry.observers.delete(previous);
entry.controller = void 0;
closeObserver(previous, 4e3, "control-taken");
}
const attached = {
...observer,
released: false
};
entry.observers.add(attached);
if (attached.control) entry.controller = attached;
return { release() {
if (attached.released) return;
attached.released = true;
entry.observers.delete(attached);
if (entry.controller === attached) entry.controller = void 0;
scheduleLinger(entry);
} };
}
function reserveObserver(sourceKey, ownerEpoch) {
const entry = entries.get(sourceKey);
if (!entry || entry.stopped || entry.ownerEpoch !== ownerEpoch || entry.observers.size + entry.observerReservations.size >= MAX_OBSERVERS) return;
const reservationId = Symbol("desktop-observer");
entry.observerReservations.add(reservationId);
clearTimeout(entry.lingerTimer);
entry.lingerTimer = void 0;
let released = false;
return {
sourceKey,
ownerEpoch,
release() {
if (released) return;
released = true;
entry.observerReservations.delete(reservationId);
scheduleLinger(entry);
}
};
}
function publishStream(params) {
const entry = entries.get(params.sourceKey);
if (!entry || entry.stopped || entry.ownerEpoch !== params.ownerEpoch || params.reservation.sourceKey !== params.sourceKey || params.reservation.ownerEpoch !== params.ownerEpoch || params.stream.destroyed || params.stream.readableEnded || params.stream.writableEnded) {
params.reservation.release();
params.stream.destroy();
return;
}
const streamId = randomUUID();
const pending = {
stream: params.stream,
reservation: params.reservation
};
entry.pendingStreams.set(streamId, pending);
params.stream.once("close", () => {
if (entry.pendingStreams.get(streamId) === pending) {
entry.pendingStreams.delete(streamId);
params.reservation.release();
}
});
return {
kind: "stream",
streamId
};
}
function claimStream(sourceKey, attachment) {
const entry = entries.get(sourceKey);
const pending = entry?.pendingStreams.get(attachment.streamId);
if (!entry || !pending) return;
entry.pendingStreams.delete(attachment.streamId);
pending.reservation.release();
const stream = pending.stream;
if (stream.destroyed || stream.readableEnded || stream.writableEnded) {
stream.destroy();
return;
}
return stream;
}
function hasPendingStream(sourceKey, attachment) {
return entries.get(sourceKey)?.pendingStreams.has(attachment.streamId) ?? false;
}
async function stop(sourceKey, ownerEpoch) {
const entry = entries.get(sourceKey);
if (entry && (ownerEpoch === void 0 || ownerEpoch === entry.ownerEpoch)) await stopEntry(entry);
}
/**
* Retires only owners strictly older than the claimant. An equal epoch shares the
* session, so fencing must not tear down a peer that claimed the same generation.
*/
async function stopSuperseded(sourceKey, ownerEpoch) {
const entry = entries.get(sourceKey);
if (entry && entry.ownerEpoch < ownerEpoch) await stopEntry(entry);
}
async function stopAll() {
await Promise.all([...entries.values()].map(stopEntry));
}
return {
acquire,
activate,
attachObserver,
publishStream,
claimStream,
hasPendingStream,
reserveObserver,
claimOwnerEpoch,
isOwnerEpochCurrent: (sourceKey, ownerEpoch) => claimedOwnerEpochs.get(sourceKey) === ownerEpoch,
stop,
stopSuperseded,
stopAll
};
}
//#endregion
export { DesktopSessionStoppedError as n, createDesktopSessionRegistry as r, DesktopSessionStaleOwnerError as t };