openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
172 lines (171 loc) • 6.58 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { t as AsyncWorkScope } from "./async-work-scope-DDiH0P7W.mjs";
import { v as registerOpenClawStateDatabaseAsyncResource } from "./openclaw-state-db-cache-CmYLFRSt.mjs";
import { o as runWithSqliteWorkerStateContext, t as captureSqliteWorkerStateContext } from "./sqlite-worker-state-context-G3Xbcj1W.mjs";
import { r as captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context-BvvgY6-t.mjs";
import { t as runBestEffortCleanup } from "./non-fatal-cleanup-BoCq8AND.mjs";
import { l as releaseSessionSourceAuthorities } from "./session-source-authority-h1UO3u0I.mjs";
import { AsyncLocalStorage } from "node:async_hooks";
//#region src/talk/client-voice-session-lifecycle.ts
const current = new AsyncLocalStorage();
const lifetimes = /* @__PURE__ */ new Map();
function createLifetime({ admission }) {
let work = new AsyncWorkScope();
const scopes = /* @__PURE__ */ new Set([work]);
let gateways = 0;
let closing = false;
const drain = (scope) => AsyncWorkScope.runWhenAllIdle(() => [scope], async () => {
await scope.drain();
scopes.delete(scope);
});
const owner = {
matchesDatabase(context) {
const identity = context.admission.identity;
return admission.identity.key === identity.key && admission.identity.birthtime === identity.birthtime;
},
retainGateway() {
if (closing) {
work = new AsyncWorkScope();
scopes.add(work);
closing = false;
}
gateways += 1;
let released = false;
let pending;
const beginClose = () => {
if (!released) {
released = true;
if (--gateways === 0) {
closing = true;
pending = drain(work);
}
}
};
return {
beginClose,
drain: () => {
beginClose();
return pending ?? Promise.resolve();
}
};
},
assertOpen(inherited) {
if (closing && !inherited) throw new Error("Voice session persistence admission is closed");
},
scope: () => work
};
const unregister = registerOpenClawStateDatabaseAsyncResource({ async close(identity) {
if (!identity || identity.key === admission.identity.key || identity.canonicalPath === admission.identity.canonicalPath) {
closing = true;
await Promise.all([...scopes].map(drain));
if (gateways > 0) {
work = new AsyncWorkScope();
scopes.add(work);
closing = false;
} else {
for (const [key, candidate] of lifetimes) if (candidate === owner) lifetimes.delete(key);
unregister();
}
}
} });
return owner;
}
function lifetime(context) {
let owner = lifetimes.get(context.admission.coordinationKey);
if (!owner) {
owner = [...lifetimes.values()].find((candidate) => candidate.matchesDatabase(context));
owner ??= createLifetime(context);
lifetimes.set(context.admission.coordinationKey, owner);
}
return owner;
}
/** Retain the original owner for later admissions without retaining an accepted permit. */
function captureClientVoiceSessionSettlementContext(env) {
const inherited = current.getStore()?.context;
const selected = inherited && inherited.environment.OPENCLAW_STATE_DIR === env.OPENCLAW_STATE_DIR ? inherited : captureOpenClawStateWorkerContext({ env });
return {
...captureSqliteWorkerStateContext(selected),
admission: selected.admission
};
}
/** Retain accepted work before a bounded queue or provider can yield. */
function captureClientVoiceSessionSettlement(source) {
const inherited = current.getStore();
const selected = source ?? inherited?.context ?? captureOpenClawStateWorkerContext();
const inheritedSource = inherited && matchesSettlementContext(inherited.context, selected) ? inherited : void 0;
if (inheritedSource && !inheritedSource.active) throw new Error("Voice session persistence lost its accepted owner");
selected.admission.assertCurrent();
const context = inheritedSource?.context ?? {
...captureSqliteWorkerStateContext(selected),
admission: selected.admission
};
const owner = lifetime(context);
owner.assertOpen(Boolean(inheritedSource));
const scope = inheritedSource?.scope ?? owner.scope();
const settled = createDeferredCore();
scope.track(() => settled.promise);
const operation = {
context,
scope,
active: true
};
return {
run(run) {
if (!operation.active) throw new Error("Voice session persistence lost its accepted owner");
context.admission.assertCurrent();
return scope.run(() => current.run(operation, () => runWithSqliteWorkerStateContext(context, run)));
},
release() {
operation.active = false;
settled.resolve();
}
};
}
async function withClientVoiceSessionSettlement(run, onAdmissionFailure, source) {
let accepted;
let entered = false;
try {
accepted = captureClientVoiceSessionSettlement(source);
return await accepted.run(() => {
entered = true;
return run();
});
} catch (error) {
if (!entered && onAdmissionFailure) return await onAdmissionFailure(error);
throw error;
} finally {
accepted?.release();
}
}
function matchesSettlementContext(left, right) {
return left.admission.coordinationKey === right.admission.coordinationKey && left.admission.identity.key === right.admission.identity.key && left.admission.identity.birthtime === right.admission.identity.birthtime;
}
function assertClientVoiceSessionSettlementCurrent(source) {
const accepted = current.getStore();
if (accepted && (!source || matchesSettlementContext(accepted.context, source))) {
if (!accepted.active) throw new Error("Voice session persistence lost its accepted owner");
accepted.context.admission.assertCurrent();
}
}
function prepareClientVoiceSessionClose() {
return lifetime(captureOpenClawStateWorkerContext()).retainGateway();
}
/** Await all cleanup without making an acknowledged operation appear replayable. */
async function withClientVoiceSessionResources(resources, operation) {
const errors = [];
const release = () => releaseSessionSourceAuthorities(typeof resources === "function" ? resources() : resources, errors);
try {
return await operation();
} catch (error) {
errors.push(error);
throw error;
} finally {
if (errors.length > 0) await release();
else await runBestEffortCleanup({
cleanup: release,
onError: (error) => console.warn("[talk] voice session operation completed before cleanup failed", error)
});
}
}
//#endregion
export { withClientVoiceSessionResources as a, prepareClientVoiceSessionClose as i, captureClientVoiceSessionSettlement as n, withClientVoiceSessionSettlement as o, captureClientVoiceSessionSettlementContext as r, assertClientVoiceSessionSettlementCurrent as t };