openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
235 lines (234 loc) • 8.1 kB
JavaScript
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 };