openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
485 lines (484 loc) • 18.1 kB
JavaScript
import { i as extractErrorCode } from "./error-coercion-1P2One0X.mjs";
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { c as readDatabasePathIdentitySync, i as inspectDatabasePathIdentitySync, r as findChangedDatabasePaths } from "./sqlite-worker-identity-B_aK_kcL.mjs";
import { s as throwSqliteLifecycleErrors } from "./sqlite-lifecycle-errors-DuOygjcn.mjs";
import path from "node:path";
import { AsyncLocalStorage } from "node:async_hooks";
//#region src/state/openclaw-state-db-async-lifecycle.ts
const STATE_DATABASE_READ_ADMISSION_INVALIDATED = "STATE_DATABASE_READ_ADMISSION_INVALIDATED";
var StateDatabaseReadAdmissionInvalidatedError = class extends Error {
constructor(..._args) {
super(..._args);
this.code = STATE_DATABASE_READ_ADMISSION_INVALIDATED;
}
};
function isStateDatabaseReadAdmissionInvalidatedError(error) {
return extractErrorCode(error) === STATE_DATABASE_READ_ADMISSION_INVALIDATED;
}
const maintenanceResources = resolveGlobalSingleton(Symbol.for("openclaw.databaseMaintenanceResources"), () => ({
current: new AsyncLocalStorage(),
claims: /* @__PURE__ */ new WeakMap(),
parents: /* @__PURE__ */ new WeakMap()
}));
function getOpenClawDatabaseMaintenanceScope() {
return maintenanceResources.current.getStore()?.scope;
}
/** Delayed work acquires its own resources instead of inheriting the completed scope. */
function runOutsideOpenClawDatabaseMaintenanceScope(operation) {
return maintenanceResources.current.exit(operation);
}
function isOpenClawDatabaseMaintenanceResourceOwned(resource, scope) {
return maintenanceResources.claims.get(resource)?.scope === scope;
}
function getOpenClawDatabaseMaintenanceResourceScope(resource) {
return maintenanceResources.claims.get(resource)?.scope;
}
function runMaintenance(scope, operation) {
const accepted = {
scope,
active: true
};
try {
const result = maintenanceResources.current.run(accepted, operation);
if (result instanceof Promise) {
const settled = () => {
accepted.active = false;
};
result.then(settled, settled);
scope.track(result);
} else accepted.active = false;
return result;
} catch (error) {
accepted.active = false;
throw error;
}
}
/** Retain one exact resource claim for finite commands while its maintenance scope drains. */
function captureOpenClawDatabaseMaintenanceResource(resource, expectedScope) {
const claim = maintenanceResources.claims.get(resource);
const assertCurrent = () => {
expectedScope.assertOwnerCurrent();
if (claim?.scope !== expectedScope || maintenanceResources.claims.get(resource) !== claim) throw new Error("Database maintenance resource owner changed");
};
assertCurrent();
return {
assertCurrent,
async run(operation) {
assertCurrent();
return runMaintenance(expectedScope, operation);
}
};
}
/** A cached handle used by an independent caller remains with the ordinary cache owner. */
function observeOpenClawDatabaseMaintenanceResource(resource) {
if (!resource) return;
const claim = maintenanceResources.claims.get(resource);
const current = getOpenClawDatabaseMaintenanceScope();
if (!claim) return;
const owner = commonMaintenanceAncestor(claim.scope, current);
if (owner === claim.scope) return;
claim.scope.assertAdmission();
claim.release();
maintenanceResources.claims.delete(resource);
if (owner) owner.own(resource, claim.phase, claim.close);
}
function commonMaintenanceAncestor(owner, scope) {
const ancestors = /* @__PURE__ */ new Set();
for (let current = owner; current; current = maintenanceResources.parents.get(current)) ancestors.add(current);
for (let current = scope; current; current = maintenanceResources.parents.get(current)) if (ancestors.has(current)) return current;
}
/** Associate lexical database work with exact resources, never all files beneath a root. */
function createOpenClawDatabaseMaintenanceScope(options) {
const parent = getOpenClawDatabaseMaintenanceScope();
const assertOwnerCurrent = options?.assertOwnerCurrent;
const assertDatabaseAccess = options?.assertDatabaseAccess;
const pending = /* @__PURE__ */ new Set();
const schemaMigrationChecks = /* @__PURE__ */ new Set();
const resources = /* @__PURE__ */ new Map();
let closed = false;
let checkingOwner = false;
let closing;
const assertOpen = () => {
if (closed) throw new Error("Database maintenance resource scope is closed");
};
const assertAdmissionLifecycle = () => {
assertOpen();
const inherited = maintenanceResources.current.getStore();
if (closing && !(inherited?.scope === scope && inherited.active)) throw new Error("Database maintenance resource admission is closed");
};
const scope = {
ownsSchemaMaintenance: options?.schemaMaintenance === true || parent?.ownsSchemaMaintenance === true,
assertOwnerCurrent(access) {
assertOpen();
if (checkingOwner) {
if (access === "read") return;
throw new Error("Database maintenance authority check cannot admit a nested effect");
}
checkingOwner = true;
try {
parent?.assertOwnerCurrent(access);
assertOwnerCurrent?.(access);
} finally {
checkingOwner = false;
}
},
assertAdmission() {
assertOpen();
scope.assertOwnerCurrent();
assertAdmissionLifecycle();
},
assertReadAdmission() {
assertAdmissionLifecycle();
scope.assertOwnerCurrent("read");
assertAdmissionLifecycle();
},
assertDatabaseAccess(databasePath) {
scope.assertAdmission();
if (assertDatabaseAccess) assertDatabaseAccess(databasePath);
else if (parent) parent.assertDatabaseAccess(databasePath);
else throw new Error("Database maintenance scope does not own this database");
},
addAgentSchemaMigrationCheck(check) {
scope.assertAdmission();
schemaMigrationChecks.add(check);
},
assertAgentSchemaMigration(migration) {
assertOpen();
scope.assertOwnerCurrent();
parent?.assertAgentSchemaMigration(migration);
for (const check of schemaMigrationChecks) check(migration);
},
run(operation) {
assertAdmissionLifecycle();
return runMaintenance(scope, () => {
scope.assertAdmission();
return operation();
});
},
track(operation) {
assertOpen();
pending.add(operation);
const settled = () => pending.delete(operation);
operation.then(settled, settled);
return operation;
},
own(resource, phase, close) {
assertOpen();
resources.set(resource, {
phase,
close
});
maintenanceResources.claims.set(resource, {
scope,
phase,
close,
release: () => resources.delete(resource)
});
},
close() {
if (closing) return closing;
const completion = createDeferredCore();
closing = completion.promise;
maintenanceResources.current.run({
scope,
active: true
}, async () => {
while (pending.size || resources.size) {
while (pending.size) await Promise.allSettled(pending);
for (const phase of [
"agent-resources",
"agent-handles",
"shared-leases",
"shared-resources",
"shared-references",
"shared-handles"
]) while ([...resources.values()].some((resource) => resource.phase === phase)) {
while (pending.size) await Promise.allSettled(pending);
const batch = [...resources].filter(([, resource]) => resource.phase === phase);
const results = await Promise.allSettled(batch.map(async ([key, resource]) => {
const claim = maintenanceResources.claims.get(key);
await resource.close();
if (resources.get(key) === resource) resources.delete(key);
if (maintenanceResources.claims.get(key) === claim) maintenanceResources.claims.delete(key);
}));
while (pending.size) await Promise.allSettled(pending);
const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
throwSqliteLifecycleErrors(errors, "Maintenance resource cleanup failed");
}
}
schemaMigrationChecks.clear();
closed = true;
}).then(completion.resolve, (error) => {
closing = void 0;
completion.reject(error);
});
return completion.promise;
}
};
if (parent) maintenanceResources.parents.set(scope, parent);
return scope;
}
/** The cache owns physical identity and admission across drainage and file exclusion. */
function createOpenClawStateDatabaseAsyncLifecycle() {
const resources = /* @__PURE__ */ new Set();
const records = /* @__PURE__ */ new Map();
const recordsByPath = /* @__PURE__ */ new Map();
const seals = /* @__PURE__ */ new Set();
const attempts = /* @__PURE__ */ new Map();
let tail = Promise.resolve();
const known = (pathname) => recordsByPath.get(pathname) ?? recordsByPath.get(path.resolve(pathname));
const bindPath = (record, pathname) => {
record.paths.add(pathname);
if (!recordsByPath.has(pathname)) recordsByPath.set(pathname, record);
};
const overlaps = (left, right) => {
if (left.identity.key === right.identity.key) return true;
for (const pathname of left.paths) if (right.paths.has(pathname)) return true;
return false;
};
const isSealed = (record) => {
if (seals.size === 0) return false;
for (const held of seals) if (held.record === void 0 || overlaps(held.record, record)) return true;
return false;
};
const hasCloseCustody = (record) => isSealed(record) || attempts.has(record);
const assertOpen = (record) => {
if (isSealed(record)) throw new StateDatabaseReadAdmissionInvalidatedError("OpenClaw state database read admission is closed");
};
const findPhysicalRecord = (identity) => {
const record = records.get(identity.key);
if (!record || !identity.key.startsWith("file:") || hasCloseCustody(record)) return record;
for (const pathname of findChangedDatabasePaths(record.paths, identity)) {
record.admissions.delete(pathname);
record.paths.delete(pathname);
if (recordsByPath.get(pathname) === record) recordsByPath.delete(pathname);
}
if (record.paths.size > 0) {
if (!record.paths.has(record.identity.canonicalPath)) record.identity = identity;
return record;
}
invalidate(record);
forget(record);
};
const resolve = (resolvedPath, preparedIdentity) => {
const cached = recordsByPath.get(resolvedPath);
if (cached && (!preparedIdentity || cached.identity.key === preparedIdentity.key)) return !preparedIdentity && cached.identity.key.startsWith("path:") ? resolve(resolvedPath, readDatabasePathIdentitySync(resolvedPath)) : cached;
const identity = preparedIdentity ?? readDatabasePathIdentitySync(resolvedPath);
let record = findPhysicalRecord(identity);
if (!record && identity.key.startsWith("file:")) {
record = [...records.values()].find((candidate) => {
if (!candidate.identity.key.startsWith("path:")) return false;
try {
return readDatabasePathIdentitySync(candidate.identity.canonicalPath).key === identity.key;
} catch {
return false;
}
});
if (record) {
records.delete(record.identity.key);
record.identity = identity;
records.set(identity.key, record);
}
}
if (!record) {
record = {
coordinationKey: identity.key,
identity,
paths: /* @__PURE__ */ new Set(),
generation: {},
admissions: /* @__PURE__ */ new Map()
};
records.set(identity.key, record);
}
bindPath(record, resolvedPath);
bindPath(record, identity.canonicalPath);
return record;
};
const resolveForNative = (pathname) => {
const resolvedPath = path.resolve(pathname);
const cached = recordsByPath.get(resolvedPath);
if (cached) return cached;
const identity = inspectDatabasePathIdentitySync(resolvedPath);
return identity ? resolve(resolvedPath, identity) : void 0;
};
const invalidate = (record) => {
for (const current of record ? [record] : records.values()) {
current.generation = {};
current.admissions.clear();
}
};
const seal = (record) => {
invalidate(record);
const held = { record };
seals.add(held);
return held;
};
const release = (attempt) => {
if (attempt.seal) {
seals.delete(attempt.seal);
attempt.seal = void 0;
}
};
const forget = (record) => {
if (!hasCloseCustody(record) && records.get(record.identity.key) === record) {
records.delete(record.identity.key);
for (const pathname of record.paths) if (recordsByPath.get(pathname) === record) {
recordsByPath.delete(pathname);
for (const replacement of records.values()) if (replacement.paths.has(pathname)) {
recordsByPath.set(pathname, replacement);
break;
}
}
}
};
const captureRecord = (record, databasePath) => {
const previous = record.admissions.get(databasePath);
if (previous) return previous;
const generation = record.generation;
const admission = Object.freeze({
databasePath,
coordinationKey: record.coordinationKey,
get identity() {
return record.identity;
},
assertCurrent() {
assertOpen(record);
if (records.get(record.identity.key) !== record || record.generation !== generation || record.admissions.get(databasePath) !== admission) throw new StateDatabaseReadAdmissionInvalidatedError("OpenClaw state database read admission changed");
}
});
record.admissions.set(databasePath, admission);
return admission;
};
const captureResolved = (databasePath) => {
const record = resolve(databasePath);
assertOpen(record);
return captureRecord(record, databasePath);
};
return {
identity(pathname) {
return known(pathname)?.identity ?? inspectDatabasePathIdentitySync(pathname);
},
knownIdentity(pathname) {
return known(pathname)?.identity;
},
publish(pathname) {
const resolvedPath = path.resolve(pathname);
const identity = readDatabasePathIdentitySync(resolvedPath);
const previous = recordsByPath.get(resolvedPath);
let record = findPhysicalRecord(identity);
if (previous && previous.identity.key !== identity.key) {
if (previous.identity.key.startsWith("path:") && !record) {
records.delete(previous.identity.key);
previous.identity = identity;
records.set(identity.key, previous);
record = previous;
} else {
const failed = attempts.get(previous);
if (failed) {
failed.superseded = true;
failed.seal ??= seal(previous);
} else invalidate(previous);
forget(previous);
}
}
if (!record) record = resolve(resolvedPath, identity);
bindPath(record, resolvedPath);
bindPath(record, identity.canonicalPath);
return {
identity,
admission: captureRecord(record, resolvedPath)
};
},
invalidate(pathname) {
if (pathname === void 0) invalidate();
else {
const record = known(pathname);
if (record) invalidate(record);
}
},
register(resource) {
resources.add(resource);
for (const attempt of attempts.values()) attempt.queue?.add(resource);
return () => {
resources.delete(resource);
};
},
capture(pathname) {
const cached = recordsByPath.get(pathname);
const retained = cached?.admissions.get(pathname);
if (cached && retained && cached.identity.key.startsWith("file:")) {
retained.assertCurrent();
return retained;
}
return captureResolved(path.resolve(pathname));
},
holdExclusion(pathname) {
const record = resolve(path.resolve(pathname));
const held = seal(record);
return () => {
seals.delete(held);
for (const current of records.values()) if (overlaps(record, current)) forget(current);
};
},
close(pathname, retireNative) {
const record = pathname === void 0 ? void 0 : resolveForNative(pathname);
if (pathname !== void 0 && !record) return Promise.resolve(retireNative());
let attempt = attempts.get(record);
if (attempt?.pending) return attempt.pending;
if (!attempt) {
attempt = { retained: /* @__PURE__ */ new Set() };
attempts.set(record, attempt);
}
attempt.seal ??= seal(record);
const current = attempt;
const pending = tail.then(async () => {
const closing = /* @__PURE__ */ new Set([...resources, ...current.retained]);
for (const entry of attempts.values()) for (const resource of entry.retained) closing.add(resource);
current.queue = closing;
const errors = [];
while (current.queue.size) {
const ordinary = [...current.queue].filter((resource) => resource.phase !== "after-resources");
if (!ordinary.length && errors.length) {
for (const resource of current.queue) current.retained.add(resource);
break;
}
const batch = ordinary.length ? ordinary : [...current.queue];
for (const resource of batch) current.queue.delete(resource);
await Promise.all(batch.map(async (resource) => {
try {
await resource.close(record?.identity);
current.retained.delete(resource);
} catch (error) {
current.retained.add(resource);
errors.push(error);
}
}));
}
if (errors.length && !current.superseded) release(current);
throwSqliteLifecycleErrors(errors, "OpenClaw state resource drainage failed");
const retired = retireNative(record?.identity);
attempts.delete(record);
release(current);
if (record === void 0) {
for (const [key, entry] of attempts) if (!entry.pending) {
attempts.delete(key);
release(entry);
}
for (const entry of records.values()) forget(entry);
} else forget(record);
return retired;
}).finally(() => {
current.queue = void 0;
});
current.pending = pending;
tail = pending.then(() => void 0, () => void 0);
pending.catch(() => {
current.pending = void 0;
});
return pending;
}
};
}
//#endregion
export { getOpenClawDatabaseMaintenanceResourceScope as a, isStateDatabaseReadAdmissionInvalidatedError as c, createOpenClawStateDatabaseAsyncLifecycle as i, observeOpenClawDatabaseMaintenanceResource as l, captureOpenClawDatabaseMaintenanceResource as n, getOpenClawDatabaseMaintenanceScope as o, createOpenClawDatabaseMaintenanceScope as r, isOpenClawDatabaseMaintenanceResourceOwned as s, StateDatabaseReadAdmissionInvalidatedError as t, runOutsideOpenClawDatabaseMaintenanceScope as u };