UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

209 lines (208 loc) 7.68 kB
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 };