openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
209 lines (208 loc) • 7.68 kB
JavaScript
import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js";
import { t as AsyncWorkScope } from "./async-work-scope-CMQS2uTf.js";
import { t as CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT } from "./control-ui-contract-zYW4RcpK.js";
import pLimit from "p-limit";
//#region src/gateway/control-ui-session-pr-subscriptions.ts
const CONTROL_UI_SESSION_PR_POLL_INTERVAL_MS = 6e4;
const CONTROL_UI_SESSION_PR_LOAD_CONCURRENCY = 4;
async function loadSessionPullRequests(params) {
const { loadControlUiSessionPullRequests } = await import("./control-ui-session-prs-CgS-ejjF.js");
return await loadControlUiSessionPullRequests(params);
}
function pushedSnapshot(result) {
return {
...result,
status: result.rateLimited ? "rate-limited" : "ready"
};
}
const UNAVAILABLE_SNAPSHOT = {
pullRequests: [],
rateLimited: false,
status: "unavailable"
};
function loaderParams(sessionKey, refresh) {
const parsed = parseAgentSessionKey(sessionKey);
const params = parsed?.rest === "global" ? {
sessionKey: "global",
agentId: parsed.agentId
} : { sessionKey };
return refresh ? {
...params,
refresh: true
} : params;
}
function parseSessionKeys(value) {
if (!Array.isArray(value) || value.length > 200) return null;
const keys = /* @__PURE__ */ new Set();
for (const entry of value) {
if (typeof entry !== "string") return null;
const key = entry.trim();
if (!key || key.length > 512) return null;
keys.add(key);
}
return [...keys];
}
function parseControlUiSessionPullRequestsSubscribeParams(value) {
if (!value || typeof value !== "object" || !("sessionKeys" in value)) return null;
const raw = value;
const sessionKeys = parseSessionKeys(raw.sessionKeys);
const refreshSessionKeys = raw.refreshSessionKeys === void 0 ? [] : parseSessionKeys(raw.refreshSessionKeys);
if (!sessionKeys || !refreshSessionKeys) return null;
const watched = new Set(sessionKeys);
for (const key of refreshSessionKeys) if (!watched.has(key)) return null;
return {
sessionKeys,
refreshSessionKeys
};
}
/**
* Owns the union of connection replace-sets. Only this union drives GitHub
* refreshes, so hidden/disconnected clients cannot leave orphan polling work.
*/
function createControlUiSessionPullRequestSubscriptions(deps) {
const subscriptions = /* @__PURE__ */ new Map();
const keyStates = /* @__PURE__ */ new Map();
const inflight = /* @__PURE__ */ new Map();
const setTimer = deps.setTimer ?? globalThis.setTimeout;
const clearTimer = deps.clearTimer ?? globalThis.clearTimeout;
const load = deps.load ?? loadSessionPullRequests;
const limit = pLimit(CONTROL_UI_SESSION_PR_LOAD_CONCURRENCY);
let timer = null;
const scope = new AsyncWorkScope();
let stopPromise;
const subscribersForKey = (sessionKey) => {
const connIds = /* @__PURE__ */ new Set();
for (const [connId, keys] of subscriptions) if (keys.has(sessionKey)) connIds.add(connId);
return connIds;
};
const watchedKeys = () => {
const keys = /* @__PURE__ */ new Set();
for (const watched of subscriptions.values()) for (const key of watched.keys()) keys.add(key);
return keys;
};
const loadSnapshot = (sessionKey, isCurrent, refresh = false) => {
const state = keyStates.get(sessionKey);
if (scope.isClosing || !state || !isCurrent()) return Promise.resolve(UNAVAILABLE_SNAPSHOT);
const pending = inflight.get(sessionKey);
if (pending) {
if (pending.state === state && (!refresh || pending.refresh)) {
pending.demands.add(isCurrent);
return pending.promise;
}
return pending.promise.then(() => loadSnapshot(sessionKey, isCurrent, refresh));
}
const demands = /* @__PURE__ */ new Set([isCurrent]);
const promise = scope.track(() => limit(async () => {
if (!Array.from(demands).some((current) => current())) return UNAVAILABLE_SNAPSHOT;
const snapshot = await load(loaderParams(sessionKey, refresh)).then(pushedSnapshot).catch(() => ({ ...UNAVAILABLE_SNAPSHOT }));
if (keyStates.get(sessionKey) === state) {
const hash = JSON.stringify(snapshot);
const changed = state.hash !== hash;
Object.assign(state, {
hash,
snapshot
});
if (changed) push(subscribersForKey(sessionKey), sessionKey, snapshot);
}
return snapshot;
}).finally(() => {
if (inflight.get(sessionKey)?.promise === promise) inflight.delete(sessionKey);
}));
inflight.set(sessionKey, {
promise,
refresh,
state,
demands
});
return promise;
};
const push = (connIds, sessionKey, snapshot) => {
if (connIds.size === 0) return;
const sessions = Object.create(null);
sessions[sessionKey] = snapshot;
deps.broadcastToConnIds(CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT, { sessions }, connIds);
for (const connId of connIds) {
const watched = subscriptions.get(connId)?.get(sessionKey);
if (watched) watched.delivered = snapshot;
}
};
const pruneOrphans = () => {
const watched = watchedKeys();
for (const key of keyStates.keys()) if (!watched.has(key)) keyStates.delete(key);
};
const schedulePoll = () => {
if (scope.isClosing || timer !== null || subscriptions.size === 0) return;
timer = setTimer(() => {
timer = null;
pollNow().finally(schedulePoll);
}, CONTROL_UI_SESSION_PR_POLL_INTERVAL_MS);
timer.unref?.();
};
const pollNow = () => {
if (scope.isClosing) return Promise.resolve();
return scope.track(async () => {
await Promise.all(Array.from(keyStates, ([sessionKey, state]) => loadSnapshot(sessionKey, () => keyStates.get(sessionKey) === state)));
});
};
const replace = (connId, sessionKeys, refreshSessionKeys = /* @__PURE__ */ new Set()) => {
if (scope.isClosing) return Promise.resolve();
return scope.track(async () => {
const normalizedConnId = connId.trim();
if (!normalizedConnId || deps.isConnectionActive?.(normalizedConnId) === false) return;
const previousSubscription = subscriptions.get(normalizedConnId);
const subscription = new Map(sessionKeys.map((key) => [key, previousSubscription?.get(key) ?? {}]));
if (subscription.size === 0) {
unsubscribe(normalizedConnId);
return;
}
subscriptions.set(normalizedConnId, subscription);
pruneOrphans();
schedulePoll();
await Promise.all(Array.from(subscription, async ([sessionKey, watched]) => {
let state = keyStates.get(sessionKey);
if (!state) keyStates.set(sessionKey, state = {});
const isCurrent = () => subscriptions.get(normalizedConnId)?.get(sessionKey) === watched;
const refresh = refreshSessionKeys.has(sessionKey);
const cached = refresh ? void 0 : state.snapshot;
if (cached) {
if (!watched.delivered) push(/* @__PURE__ */ new Set([normalizedConnId]), sessionKey, cached);
return;
}
const snapshot = await loadSnapshot(sessionKey, isCurrent, refresh);
if (isCurrent() && (refresh ? watched.delivered !== snapshot : !watched.delivered)) push(/* @__PURE__ */ new Set([normalizedConnId]), sessionKey, snapshot);
}));
});
};
const unsubscribe = (connId) => {
const normalizedConnId = connId.trim();
if (!normalizedConnId) return;
subscriptions.delete(normalizedConnId);
pruneOrphans();
if (subscriptions.size === 0 && timer !== null) {
clearTimer(timer);
timer = null;
}
};
const stop = () => {
if (stopPromise) return stopPromise;
scope.beginClose();
if (timer !== null) {
clearTimer(timer);
timer = null;
}
subscriptions.clear();
keyStates.clear();
stopPromise = scope.drain().then(() => {
inflight.clear();
});
return stopPromise;
};
return {
replace,
unsubscribe,
pollNow,
stop
};
}
//#endregion
export { parseControlUiSessionPullRequestsSubscribeParams as n, createControlUiSessionPullRequestSubscriptions as t };