UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

235 lines (234 loc) • 8.1 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs"; import { c as readDatabasePathIdentitySync } from "./sqlite-worker-identity-B_aK_kcL.mjs"; import { c as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-DiWMV5LG.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { performance } from "node:perf_hooks"; import { setImmediate } from "node:timers/promises"; //#region src/shared/store-writer-queue.ts const MAX_WRITERS_PER_TURN = 4; const WRITER_TURN_BUDGET_MS = 4; const activeStoreWriters = resolveGlobalSingleton(Symbol.for("openclaw.activeStoreWriters"), () => new AsyncLocalStorage()); const writerTurn = resolveGlobalSingleton(Symbol.for("openclaw.storeWriterTurn"), () => ({ started: 0, startedAt: 0, reset: void 0, wait: void 0 })); function claimStoreWriterTurn(immediate) { const now = performance.now(); if (!writerTurn.reset) { writerTurn.started = 0; writerTurn.startedAt = now; writerTurn.reset = setImmediate().then(() => { writerTurn.reset = void 0; }); } if (!immediate && (writerTurn.wait || writerTurn.started >= MAX_WRITERS_PER_TURN || now - writerTurn.startedAt >= WRITER_TURN_BUDGET_MS)) return writerTurn.wait ??= setImmediate().then(() => { writerTurn.wait = void 0; }); writerTurn.started++; } function isActiveStoreWriter(queues, storePath, keys) { if (!queues.has(storePath)) return false; let active = activeStoreWriters.getStore(); while (active) { if (active.active && active.queues === queues && active.storePath === storePath) { const heldKeys = active.keys; if (heldKeys && (!keys || [...keys].some((key) => !heldKeys.has(key)))) throw new Error("Cannot expand an active store writer's keys"); return true; } active = active.parent; } return false; } async function runActiveStoreWriter(queues, storePath, fn, timing, keys) { const writer = { active: true, parent: activeStoreWriters.getStore(), queues, storePath, keys }; if (timing) { timing.reentrant = false; timing.startedAt = performance.now(); } try { return await activeStoreWriters.run(writer, fn); } finally { if (timing) timing.finishedAt = performance.now(); writer.active = false; } } function getOrCreateStoreWriterQueue(queues, storePath) { const existing = queues.get(storePath); if (existing) return existing; const created = { pending: [], drainPromise: null }; queues.set(storePath, created); return created; } async function drainStoreWriterQueue(queues, storePath) { const queue = queues.get(storePath); if (!queue || queue.drainPromise) return; const drain = createDeferredCore(); queue.drainPromise = drain.promise; const heldKeys = /* @__PURE__ */ new Set(); let active = 0; let changed = createDeferredCore(); queue.wake = () => changed.resolve(); let first = true; try { while (queue.pending.length > 0 || active > 0) { const blocked = /* @__PURE__ */ new Set(); let task; for (const pending of queue.pending) { const keys = pending.keys; if (!keys) { if (active === 0 && blocked.size === 0) task = pending; break; } if (![...keys].some((key) => heldKeys.has(key) || blocked.has(key))) { task = pending; break; } for (const key of keys) blocked.add(key); } if (!task) { await changed.promise; changed = createDeferredCore(); continue; } let wait; while (wait = claimStoreWriterTurn(first)) await wait; first = false; const index = queue.pending.indexOf(task); if (index < 0) continue; queue.pending.splice(index, 1); if (!task.keys) { await task.fn().then(task.resolve, task.reject); continue; } const keys = task.keys; for (const key of keys) heldKeys.add(key); active++; const finish = () => { for (const key of keys) heldKeys.delete(key); active--; queue.wake?.(); }; task.fn().then((value) => { finish(); task.resolve(value); }, (error) => { finish(); task.reject(error); }); } } finally { queue.drainPromise = null; queue.wake = void 0; queues.delete(storePath); drain.resolve(); } } /** Runs one store write after prior writes for the same store path have finished. */ async function runQueuedStoreWrite(params) { if (!params.storePath || typeof params.storePath !== "string") throw new Error(`${params.label}: storePath must be a non-empty string, got ${JSON.stringify(params.storePath)}`); params.signal?.throwIfAborted(); const keys = params.keys?.length ? new Set(params.keys) : void 0; if (params.reentrant === true && isActiveStoreWriter(params.queues, params.storePath, keys)) { if (params.timing) { params.timing.reentrant = true; params.timing.startedAt = performance.now(); } try { return await params.fn(); } finally { if (params.timing) params.timing.finishedAt = performance.now(); } } const runInAsyncContext = AsyncLocalStorage.snapshot(); const queue = getOrCreateStoreWriterQueue(params.queues, params.storePath); let detach = () => {}; const completion = new Promise((resolve, reject) => { detach = () => params.signal?.removeEventListener("abort", abort); const abort = () => { const index = queue.pending.indexOf(task); if (index !== -1) { queue.pending.splice(index, 1); task.reject(params.signal?.reason); queue.wake?.(); } }; const task = { keys, fn: async () => { detach(); return await runInAsyncContext(runActiveStoreWriter, params.queues, params.storePath, params.fn, params.timing, keys); }, resolve: (value) => resolve(value), reject }; queue.pending.push(task); queue.wake?.(); params.signal?.addEventListener("abort", abort, { once: true }); drainStoreWriterQueue(params.queues, params.storePath); }); if (params.signal) completion.then(detach, detach); return await completion; } /** Rejects pending queued writes and clears idle queue state for test cleanup. */ function clearStoreWriterQueuesForTest(queues, message) { for (const [storePath, queue] of queues) { for (const task of queue.pending) task.reject(new Error(message)); queue.pending.length = 0; queue.wake?.(); if (!queue.drainPromise) queues.delete(storePath); } } //#endregion //#region src/state/openclaw-agent-write-admission.ts const admission = resolveGlobalSingleton(Symbol.for("openclaw.agentDatabaseWriteAdmission"), () => ({ queues: /* @__PURE__ */ new Map(), workers: /* @__PURE__ */ new Map() })); const SQLITE_SESSION_WRITER_QUEUES = admission.queues; function runOpenClawAgentWriteAdmission(options, run, reentrant = false, timing, signal) { const pathname = resolveOpenClawAgentSqlitePath(options); const identity = readDatabasePathIdentitySync(pathname); const storePath = identity.canonicalPath; const assertCurrent = () => { const current = readDatabasePathIdentitySync(pathname); if (current.canonicalPath !== storePath || identity.key.startsWith("file:") && (current.key !== identity.key || current.birthtime !== identity.birthtime)) throw new Error("Agent database target changed before write admission"); }; return runQueuedStoreWrite({ queues: admission.queues, storePath, label: "agent database write admission", reentrant: reentrant && !admission.workers.has(storePath), fn: async () => { assertCurrent(); return await run(identity, assertCurrent); }, timing, signal }); } /** Reserve a native write permit without admitting inherited foreground callbacks. */ function runOpenClawAgentWorkerWrite(options, run, timing, signal) { return runOpenClawAgentWriteAdmission(options, async ({ canonicalPath: storePath }) => { const owner = {}; admission.workers.set(storePath, owner); try { return await run(); } finally { if (admission.workers.get(storePath) === owner) admission.workers.delete(storePath); } }, true, timing, signal); } //#endregion export { runQueuedStoreWrite as a, clearStoreWriterQueuesForTest as i, runOpenClawAgentWorkerWrite as n, runOpenClawAgentWriteAdmission as r, SQLITE_SESSION_WRITER_QUEUES as t };