UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

201 lines (200 loc) • 6.69 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { o as runOutsideAsyncWorkScope, t as AsyncWorkScope } from "./async-work-scope-BYeX3gh6.mjs"; import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/infra/gateway-scheduler.ts const log = createSubsystemLogger("gateway/scheduler"); const hostClock = { now: () => Date.now(), monotonicNow: () => performance.now(), arm: (run, delayMs) => { const timer = setTimeout(() => { run(); }, delayMs); timer.unref(); return () => clearTimeout(timer); } }; /** Owns Gateway wakeups; stores and execution owners retain their deadlines and authority. */ var GatewayScheduler = class { constructor(options = {}) { this.jobs = /* @__PURE__ */ new Map(); this.pending = /* @__PURE__ */ new Set(); this.dispatching = false; this.timerGeneration = 0; this.controller = new AbortController(); this.clock = options.clock ?? hostClock; } get signal() { return this.controller.signal; } now() { return this.clock.now(); } get nextWakeAtMs() { const nowMs = this.now(); const elapsedMs = this.clock.monotonicNow(); let next = null; for (const job of this.jobs.values()) { if (job.running) continue; const atMs = nowMs + this.remaining(job, nowMs, elapsedMs); next = next === null ? atMs : Math.min(next, atMs); } return next; } /** Closes one owner's timers without retiring siblings or losing in-flight joins. */ scope() { const controller = new AbortController(); const owner = { signal: AbortSignal.any([this.signal, controller.signal]), jobs: /* @__PURE__ */ new Set() }; const beginClose = () => { controller.abort(); for (const job of owner.jobs) this.cancel(job); }; const close = () => { beginClose(); const running = [...owner.jobs].flatMap((job) => job.running ?? []); return running.length > 0 ? Promise.all(running).then(() => void 0) : void 0; }; return { signal: owner.signal, now: () => this.now(), schedule: (params) => this.scheduleOwned(params, owner), beginClose, close, stop: async () => { await close(); } }; } schedule(params) { return this.scheduleOwned(params); } scheduleOwned(params, owner) { const relative = "delayMs" in params; const nowMs = this.now(); const atMs = relative ? nowMs + params.delayMs : params.atMs; if (!Number.isFinite(atMs) || params.everyMs !== void 0 && (!Number.isFinite(params.everyMs) || params.everyMs <= 0)) throw new Error(`Invalid Gateway schedule: ${params.id}`); const cancelled = this.signal.aborted || owner?.signal.aborted === true; const previous = cancelled ? void 0 : this.jobs.get(params.id); const job = { id: params.id, run: params.run, everyMs: params.everyMs, atMs, elapsedAtMs: relative || params.everyMs !== void 0 ? this.clock.monotonicNow() + Math.max(0, atMs - nowMs) : void 0, context: runOutsideAsyncWorkScope(() => AsyncLocalStorage.snapshot()), cancelled, owner }; if (params.mode === "earliest" && previous && !previous.running) { job.atMs = Math.min(job.atMs, previous.atMs); if (previous.elapsedAtMs !== void 0) job.elapsedAtMs = job.elapsedAtMs === void 0 ? previous.elapsedAtMs : Math.min(job.elapsedAtMs, previous.elapsedAtMs); } const cancel = () => this.cancel(job); if (!cancelled) { owner?.jobs.add(job); this.jobs.set(job.id, job); if (previous) this.cancel(previous); this.arm(); } return { cancel, stop: async () => { cancel(); await job.running; } }; } cancel(job) { job.cancelled = true; if (!job.running) job.owner?.jobs.delete(job); if (this.jobs.get(job.id) === job) { this.jobs.delete(job.id); this.arm(); } } beginClose() { this.controller.abort(); this.timerGeneration += 1; this.cancelTimer?.(); this.cancelTimer = void 0; for (const job of this.jobs.values()) this.cancel(job); this.jobs.clear(); } async stop() { this.beginClose(); await Promise.all(this.pending); } remaining(job, nowMs, elapsedMs) { const wallDelay = job.atMs - nowMs; return job.elapsedAtMs === void 0 ? wallDelay : Math.min(wallDelay, job.elapsedAtMs - elapsedMs); } arm() { if (this.signal.aborted || this.dispatching) return; this.cancelTimer?.(); this.cancelTimer = void 0; const generation = ++this.timerGeneration; const next = this.nextWakeAtMs; if (next !== null) { const nowMs = this.now(); const delayMs = Math.min(2147483647, Math.max(0, next - nowMs)); this.cancelTimer = this.clock.arm(() => generation === this.timerGeneration ? this.wake(nowMs + delayMs) : void 0, delayMs); } } wake(expectedAtMs) { this.cancelTimer = void 0; if (this.signal.aborted) return; const nowMs = this.now(); const elapsedMs = this.clock.monotonicNow(); const due = [...this.jobs.values()].filter((job) => !job.running && this.remaining(job, nowMs, elapsedMs) <= 0).toSorted((a, b) => this.remaining(a, nowMs, elapsedMs) - this.remaining(b, nowMs, elapsedMs)); const started = []; if (nowMs - expectedAtMs > 6e4) log.debug(`late wake by ${nowMs - expectedAtMs}ms; coalescing ${due.length} due jobs`); this.dispatching = true; try { for (const job of due) { if (this.signal.aborted || job.cancelled || job.owner?.signal.aborted || this.jobs.get(job.id) !== job) continue; if (job.everyMs === void 0) this.jobs.delete(job.id); started.push(this.run(job)); } } finally { this.dispatching = false; this.arm(); } return Promise.all(started).then(() => void 0); } run(job) { if (job.everyMs === void 0) log.debug(`running ${job.id}`); else log.trace(`running ${job.id}`); const done = createDeferredCore(); const work = new AsyncWorkScope(); job.running = done.promise; this.pending.add(done.promise); const finish = () => { job.running = void 0; this.pending.delete(done.promise); if (job.everyMs !== void 0 && !job.cancelled && !this.signal.aborted) { job.atMs = this.now() + job.everyMs; job.elapsedAtMs = this.clock.monotonicNow() + job.everyMs; } else job.owner?.jobs.delete(job); done.resolve(); this.arm(); }; let result = void 0; try { result = job.context(() => work.run(job.run)); } catch (error) { log.error(`${job.id} failed: ${String(error)}`); } if (!result && !work.hasPendingWork) { finish(); return done.promise; } Promise.resolve(result).catch((error) => log.error(`${job.id} failed: ${String(error)}`)).then(() => AsyncWorkScope.runWhenAllIdle(() => [work], () => work.drain())).finally(finish); return done.promise; } }; //#endregion export { GatewayScheduler as t };