openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
415 lines (414 loc) • 18.9 kB
JavaScript
import { D as resolveExpiresAtMsFromDurationMs } from "./number-coercion-CkhMiRw8.mjs";
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { a as executeSqliteQueryTakeFirstSync, i as executeSqliteQuerySync, l as prepareSqliteQuerySync, n as createSqliteQueryCache, o as getNodeSqliteKysely, s as iterateSqliteQuerySync } from "./kysely-sync-DUWzQvCU.mjs";
import { n as registerListener, t as notifyListeners } from "./listeners-BogSNJ-R.mjs";
import { a as stageSqliteCommittedPublication, i as publishSqliteCommittedState, o as stageSqliteTransactionState } from "./sqlite-post-commit-DENvzR6w.mjs";
import { n as normalizeSqliteNumber, t as coerceRequiredSqliteNumber } from "./sqlite-number-DZt1m75H.mjs";
import { u as PluginStateStoreError } from "./openclaw-state-worker-error-Bi-BnNrV.mjs";
import "./sqlite-worker-contract-C51cJHzg.mjs";
import { a as observeSqliteWorkerCommittedFacts, i as deferSqliteWorkerCommitReceipt } from "./sqlite-worker-operation-admission-C5lBzzTk.mjs";
import { o as readTrackedStateDatabaseIdentity } from "./openclaw-state-db-integrity-admission-D2ZPZec-.mjs";
import { n as hasSqliteCommitReceiptCoverage, t as createSqliteCommitReceipt } from "./sqlite-commit-receipt-DafgyYOc.mjs";
import { serialize } from "node:v8";
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
//#region src/plugin-state/plugin-state-publication.ts
const state = resolveGlobalSingleton(Symbol.for("openclaw.pluginStatePublication"), () => ({
sources: /* @__PURE__ */ new WeakMap(),
capture: new AsyncLocalStorage(),
installingFacts: 0,
facts: /* @__PURE__ */ new Set(),
observers: /* @__PURE__ */ new Set()
}));
function sourceFor(db) {
let source = state.sources.get(db);
if (!source) {
source = {
identity: readTrackedStateDatabaseIdentity(db)?.key ?? Symbol("untracked-plugin-state"),
incarnation: randomUUID()
};
state.sources.set(db, source);
}
return source;
}
function keyFor(row) {
return JSON.stringify([
row.plugin_id,
row.namespace,
row.entry_key
]);
}
function captureReceipt(db, facts) {
return createSqliteCommitReceipt({
source: sourceFor(db),
domain: "plugin-state",
keys: [...facts.keys()],
readFact: (key) => facts.get(key)
});
}
function install(change) {
const errors = [];
state.installingFacts++;
try {
notifyListeners(state.facts, change, (error) => errors.push(error));
} finally {
state.installingFacts--;
}
if (errors.length) throw new AggregateError(errors, "Plugin state fact installation failed");
}
function publication(receipt) {
let change = {
kind: "committed",
receipt
};
return {
installFacts: () => install(change),
invalidate() {
change = {
kind: "unknown",
identity: receipt.source.identity
};
install(change);
},
notify: () => notifyListeners(state.observers, change)
};
}
/** Owns committed-fact staging and observation; raw/foreign authority remains separate. */
const pluginStatePublication = {
stagePostimage: stagePluginStatePostimage,
stageDeletions: stagePluginStateDeletions,
/** Install/invalidate prepared facts only; storage mutations belong in postcommit observers. */
subscribeFacts: (listener) => registerListener(state.facts, listener),
subscribe: (listener) => registerListener(state.observers, listener)
};
function stage(db, facts) {
if (!facts.size) return;
if (state.installingFacts) throw new Error("Plugin state cannot mutate during fact installation");
const capture = state.capture.getStore();
if (capture) {
const previous = new Map([...facts.keys()].map((key) => [key, capture.get(key)]));
stageSqliteTransactionState(db, {
stage: () => {
for (const [key, fact] of facts) capture.set(key, fact);
},
commit() {},
rollback: () => {
for (const [key, fact] of previous) if (fact) capture.set(key, fact);
else capture.delete(key);
}
});
}
const next = publication(captureReceipt(db, facts));
if (!stageSqliteCommittedPublication(db, next) && !db.isTransaction) publishSqliteCommittedState(next);
}
function stagePluginStatePostimage(db, row) {
stage(db, /* @__PURE__ */ new Map([[keyFor(row), {
kind: "postimage",
value: { ...row }
}]]));
}
/** DELETE RETURNING captures exact tombstones, including eviction and whole-namespace clear. */
function stagePluginStateDeletions(db, rows) {
stage(db, new Map(rows.map((row) => [keyFor(row), { kind: "absent" }])));
}
/** Capture the whole native transaction before COMMIT, including keys changed by retention. */
function withPluginStateWorkerReceipt(db, write) {
return state.capture.run(/* @__PURE__ */ new Map(), () => {
const result = write();
const facts = state.capture.getStore();
const receipt = captureReceipt(db, facts);
deferSqliteWorkerCommitReceipt(db, serialize(receipt).byteLength <= 33554432 ? receipt : {
kind: "unknown",
identity: receipt.source.identity
}, facts.size === 0 ? "settlement" : "commit");
return result;
});
}
function isRow(value) {
return isRecord(value) && typeof value.plugin_id === "string" && typeof value.namespace === "string" && typeof value.entry_key === "string" && typeof value.value_json === "string" && (typeof value.created_at === "number" || typeof value.created_at === "bigint") && (value.expires_at === null || typeof value.expires_at === "number" || typeof value.expires_at === "bigint");
}
function readReceipt(value) {
if (!isRecord(value) || !isRecord(value.source) || typeof value.source.identity !== "string" || typeof value.source.incarnation !== "string" || !(value.facts instanceof Map) || !hasSqliteCommitReceiptCoverage(value, {
source: {
identity: value.source.identity,
incarnation: value.source.incarnation
},
domain: "plugin-state",
keys: [...value.facts.keys()]
}) || ![...value.facts].every(([key, fact]) => typeof key === "string" && (fact.kind === "absent" || fact.kind === "postimage" && isRow(fact.value) && keyFor(fact.value) === key))) throw new Error("Plugin state commit receipt is invalid");
return value;
}
/** A late worker reply cannot overwrite a newer native publication or reopen a closed owner. */
function withPluginStatePublication(createAdmission, context) {
return (operation) => {
const owner = createAdmission(operation);
const identity = () => context.admission.identity.key;
const superseded = /* @__PURE__ */ new Set();
const operationId = randomUUID();
let installing = false;
let unknown = false;
let received = false;
let finished = false;
const unsubscribe = pluginStatePublication.subscribeFacts((change) => {
if (installing) return;
if (change.kind === "committed" && change.receipt.source.identity === identity()) for (const key of change.receipt.facts.keys()) superseded.add(key);
else if (change.kind === "unknown" && change.identity === identity()) unknown = true;
});
const finish = owner.admission.finish.bind(owner.admission);
owner.admission.finish = () => {
try {
finish();
} finally {
if (!finished) {
finished = true;
unsubscribe();
if (!received || owner.admission.settlement?.kind !== "completed") install({
kind: "unknown",
identity: identity()
});
install({
kind: "settled",
identity: identity(),
operationId
});
}
}
};
try {
install({
kind: "pending",
identity: identity(),
operationId
});
} catch (error) {
unsubscribe();
owner.admission.finish();
throw error;
}
observeSqliteWorkerCommittedFacts(owner.admission, ({ facts }) => {
try {
if (isRecord(facts) && facts.kind === "unknown" && facts.identity === identity()) {
received = true;
install({
kind: "unknown",
identity: identity()
});
return;
}
const receipt = readReceipt(facts);
(context.assertPublicationCurrent ?? context.admission.assertCurrent)();
if (receipt.source.identity !== identity()) throw new Error("Plugin state receipt changed owner");
const current = {
...receipt,
facts: new Map([...receipt.facts].map(([key, fact]) => [key, unknown || superseded.has(key) ? { kind: "unknown" } : fact]))
};
installing = true;
publishSqliteCommittedState(publication(current));
received = true;
} catch (error) {
install({
kind: "unknown",
identity: identity()
});
throw error;
} finally {
installing = false;
}
});
return owner;
};
}
//#endregion
//#region src/plugin-state/plugin-state-store.kernel.ts
const MAX_PLUGIN_STATE_VALUE_BYTES = 1048576;
const RETAINED_PLUGIN_STATE_NAMESPACE_PREFIX = "@retained.";
function isRetainedPluginStateNamespace(namespace) {
return namespace.startsWith(RETAINED_PLUGIN_STATE_NAMESPACE_PREFIX);
}
const PLUGIN_STATE_EXPIRY_BATCH_ROWS = 1024;
function createPluginStateError(params) {
return new PluginStateStoreError(params.message, {
code: params.code,
operation: params.operation,
...params.path ? { path: params.path } : {},
cause: params.cause
});
}
function resolvePluginStateExpiresAtMs(params) {
if (params.ttlMs == null) return null;
if (params.namespace && isRetainedPluginStateNamespace(params.namespace)) throw createPluginStateError({
code: "PLUGIN_STATE_INVALID_INPUT",
operation: params.operation,
message: "Retained plugin state does not accept a TTL.",
path: params.path
});
const expiresAt = resolveExpiresAtMsFromDurationMs(params.ttlMs, { nowMs: params.now });
if (expiresAt === void 0) throw createPluginStateError({
code: "PLUGIN_STATE_INVALID_INPUT",
operation: params.operation,
message: "Plugin state ttlMs cannot produce a valid expiry timestamp.",
...params.path ? { path: params.path } : {}
});
return expiresAt;
}
function parseStoredJson(raw, operation, databasePath) {
try {
return JSON.parse(raw);
} catch (error) {
throw createPluginStateError({
code: "PLUGIN_STATE_CORRUPT",
operation,
message: "Plugin state entry contains corrupt JSON.",
path: databasePath,
cause: error
});
}
}
function rowToEntry(row, operation, databasePath) {
const expiresAt = normalizeSqliteNumber(row.expires_at);
return {
key: row.entry_key,
value: parseStoredJson(row.value_json, operation, databasePath),
createdAt: normalizeSqliteNumber(row.created_at) ?? 0,
...expiresAt != null ? { expiresAt } : {}
};
}
function getPluginStateKysely(db) {
return getNodeSqliteKysely(db);
}
function bindPluginStateEntry(params) {
return {
plugin_id: params.pluginId,
namespace: params.namespace,
entry_key: params.key,
value_json: params.valueJson,
created_at: params.createdAt,
expires_at: params.expiresAt
};
}
const pluginStateUpsertQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => getPluginStateKysely(db).insertInto("plugin_state_entries").values({
plugin_id: parameter((value) => value.plugin_id),
namespace: parameter((value) => value.namespace),
entry_key: parameter((value) => value.entry_key),
value_json: parameter((value) => value.value_json),
created_at: parameter((value) => value.created_at),
expires_at: parameter((value) => value.expires_at)
}).onConflict((conflict) => conflict.columns([
"plugin_id",
"namespace",
"entry_key"
]).doUpdateSet({
value_json: (eb) => eb.ref("excluded.value_json"),
created_at: (eb) => eb.ref("excluded.created_at"),
expires_at: (eb) => eb.ref("excluded.expires_at")
})).returningAll()));
function upsertPluginStateEntry(db, row) {
const result = pluginStateUpsertQuery(db)(row);
for (const current of result.rows) pluginStatePublication.stagePostimage(db, current);
}
const pluginStateInsertIfAbsentQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => getPluginStateKysely(db).insertInto("plugin_state_entries").orIgnore().values({
plugin_id: parameter((value) => value.plugin_id),
namespace: parameter((value) => value.namespace),
entry_key: parameter((value) => value.entry_key),
value_json: parameter((value) => value.value_json),
created_at: parameter((value) => value.created_at),
expires_at: parameter((value) => value.expires_at)
}).returningAll()));
function insertPluginStateEntryIfAbsent(db, row) {
const result = pluginStateInsertIfAbsentQuery(db)(row);
for (const current of result.rows) pluginStatePublication.stagePostimage(db, current);
return result.rows.length > 0;
}
const pluginStateEntryExistsQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => {
const pluginId = parameter((value) => value.pluginId);
const namespace = parameter((value) => value.namespace);
const key = parameter((value) => value.key);
const now = parameter((value) => value.now);
return getPluginStateKysely(db).selectFrom("plugin_state_entries").select("entry_key").where("plugin_id", "=", pluginId).where("namespace", "=", namespace).where("entry_key", "=", key).where((eb) => eb.or([eb("expires_at", "is", null), eb("expires_at", ">", now)]));
}));
function hasPluginStateEntry(db, params) {
return pluginStateEntryExistsQuery(db)(params).rows.length !== 0;
}
const pluginStateEntryQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => {
const pluginId = parameter((value) => value.pluginId);
const namespace = parameter((value) => value.namespace);
const key = parameter((value) => value.key);
const now = parameter((value) => value.now);
return getPluginStateKysely(db).selectFrom("plugin_state_entries").select([
"entry_key",
"value_json",
"created_at",
"expires_at"
]).where("plugin_id", "=", pluginId).where("namespace", "=", namespace).where("entry_key", "=", key).where((eb) => eb.or([eb("expires_at", "is", null), eb("expires_at", ">", now)]));
}));
function selectPluginStateEntry(db, params) {
return pluginStateEntryQuery(db)(params).rows[0];
}
function iteratePluginStateEntries(db, params) {
return iterateSqliteQuerySync(db, getPluginStateKysely(db).selectFrom("plugin_state_entries").select([
"entry_key",
"value_json",
"created_at",
"expires_at"
]).where("plugin_id", "=", params.pluginId).where("namespace", "=", params.namespace).where((eb) => eb.or([eb("expires_at", "is", null), eb("expires_at", ">", params.now)])).orderBy("created_at", "asc").orderBy("entry_key", "asc"));
}
function selectPluginStateEntriesInKeyRange(db, params) {
return executeSqliteQuerySync(db, getPluginStateKysely(db).selectFrom("plugin_state_entries").select([
"entry_key",
"value_json",
"created_at",
"expires_at"
]).where("plugin_id", "=", params.pluginId).where("namespace", "=", params.namespace).where("entry_key", ">=", params.keyStartInclusive).where("entry_key", "<", params.keyEndExclusive).where((eb) => eb.or([eb("expires_at", "is", null), eb("expires_at", ">", params.now)])).orderBy("entry_key", params.order).limit(params.limit)).rows;
}
function deletePluginStateEntry(db, params) {
const result = executeSqliteQuerySync(db, getPluginStateKysely(db).deleteFrom("plugin_state_entries").where("plugin_id", "=", params.pluginId).where("namespace", "=", params.namespace).where("entry_key", "=", params.key).returning([
"plugin_id",
"namespace",
"entry_key"
]));
pluginStatePublication.stageDeletions(db, result.rows);
return result.rows.length;
}
const pluginStateExpiryQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => getPluginStateKysely(db).selectFrom("plugin_state_entries").select("expires_at").where("expires_at", "is not", null).where("expires_at", "<=", parameter((value) => value)).limit(1)));
function deleteExpiredPluginStateEntries(db, now, scope) {
if (scope && isRetainedPluginStateNamespace(scope.namespace)) return 0;
const kysely = getPluginStateKysely(db);
if (scope) {
if (pluginStateExpiryQuery(db)(now).rows.length === 0) return 0;
}
let expiredEntries = kysely.selectFrom("plugin_state_entries").select([
"plugin_id",
"namespace",
"entry_key"
]).where("expires_at", "is not", null).where("expires_at", "<=", now);
expiredEntries = scope ? expiredEntries.where("plugin_id", "=", scope.pluginId).where("namespace", "=", scope.namespace) : expiredEntries.orderBy("expires_at", "asc");
const result = executeSqliteQuerySync(db, kysely.deleteFrom("plugin_state_entries").where((expression) => expression(expression.refTuple("plugin_id", "namespace", "entry_key"), "in", expiredEntries.limit(PLUGIN_STATE_EXPIRY_BATCH_ROWS).$asTuple("plugin_id", "namespace", "entry_key"))).returning([
"plugin_id",
"namespace",
"entry_key"
]));
pluginStatePublication.stageDeletions(db, result.rows);
return result.rows.length;
}
const pluginStateNamespaceCountQuery = createSqliteQueryCache((db) => prepareSqliteQuerySync(db, (parameter) => getPluginStateKysely(db).selectFrom("plugin_state_entries").select((eb) => eb.fn.countAll().as("count")).where("plugin_id", "=", parameter((value) => value.pluginId)).where("namespace", "=", parameter((value) => value.namespace)).where((eb) => eb.or([eb("expires_at", "is", null), eb("expires_at", ">", parameter((value) => value.now))]))));
function countLivePluginStateNamespaceEntries(db, params) {
const row = pluginStateNamespaceCountQuery(db)(params).rows[0];
return coerceRequiredSqliteNumber(row?.count ?? 0);
}
function allocatePluginStateNamespaceCreatedAt(db, params) {
const row = executeSqliteQueryTakeFirstSync(db, getPluginStateKysely(db).selectFrom("plugin_state_entries").select((eb) => eb.fn.max("created_at").as("max_created_at")).where("plugin_id", "=", params.pluginId).where("namespace", "=", params.namespace));
const previous = normalizeSqliteNumber(row?.max_created_at ?? null);
const next = previous === void 0 ? params.now : Math.max(params.now, previous + 1);
if (!Number.isSafeInteger(next)) throw new RangeError("Plugin state namespace append order exhausted safe integer range");
return next;
}
function lookupPluginStateEntry(store, params) {
const row = selectPluginStateEntry(store.db, {
...params,
now: Date.now()
});
return row ? parseStoredJson(row.value_json, "lookup", store.path) : void 0;
}
//#endregion
export { withPluginStatePublication as C, pluginStatePublication as S, resolvePluginStateExpiresAtMs as _, bindPluginStateEntry as a, selectPluginStateEntry as b, deleteExpiredPluginStateEntries as c, hasPluginStateEntry as d, insertPluginStateEntryIfAbsent as f, parseStoredJson as g, lookupPluginStateEntry as h, allocatePluginStateNamespaceCreatedAt as i, deletePluginStateEntry as l, iteratePluginStateEntries as m, PLUGIN_STATE_EXPIRY_BATCH_ROWS as n, countLivePluginStateNamespaceEntries as o, isRetainedPluginStateNamespace as p, RETAINED_PLUGIN_STATE_NAMESPACE_PREFIX as r, createPluginStateError as s, MAX_PLUGIN_STATE_VALUE_BYTES as t, getPluginStateKysely as u, rowToEntry as v, withPluginStateWorkerReceipt as w, upsertPluginStateEntry as x, selectPluginStateEntriesInKeyRange as y };