blob: c0a614b5bcc4467b7f99aaa8a1f541258aa00ce4 [file]
import type { TaskLedgerStore } from '@maka/core';
import {
assertStorageRootLease,
runWithStorageRootLease,
StorageRootAuthorityError,
type StorageRootLease,
} from './root-authority.js';
import {
createSqliteTaskLedgerStore,
type ConversationTaskLedgerCopyInput,
type SqliteTaskLedgerStore,
} from './task-ledger-store.js';
import {
getTaskLedgerCanonicalReader,
type TaskLedgerCanonicalReader,
} from './task-ledger-store-internal.js';
export type { TaskLedgerCanonicalReader } from './task-ledger-store-internal.js';
export type { ConversationTaskLedgerCopyInput } from './task-ledger-store.js';
const writerBrand: unique symbol = Symbol('InteractiveTaskLedgerWriter');
const writers = new WeakSet<object>();
const writerByLease = new WeakMap<object, InteractiveTaskLedgerWriter>();
const writerOpeningByLease = new WeakMap<object, Promise<InteractiveTaskLedgerWriter>>();
export interface InteractiveTaskLedgerWriter extends TaskLedgerStore, TaskLedgerCanonicalReader {
readonly kind: 'interactive';
readonly access: 'write';
readonly [writerBrand]: true;
close(): void;
copyConversationTaskLedger(input: ConversationTaskLedgerCopyInput): Promise<void>;
purgeConversationTaskLedger(sessionId: string): Promise<void>;
}
export function authenticateInteractiveTaskLedgerWriter(
writer: InteractiveTaskLedgerWriter,
): InteractiveTaskLedgerWriter {
if (!writers.has(writer)) {
throw new StorageRootAuthorityError(
'invalid_lease',
'Expected an authentic interactive task ledger writer',
);
}
return writer;
}
export async function openInteractiveTaskLedgerStoreForWrite(
lease: StorageRootLease<'interactive', 'write'>,
): Promise<InteractiveTaskLedgerWriter> {
await assertStorageRootLease(lease, 'interactive', 'write');
const existing = writerByLease.get(lease);
if (existing) return existing;
const opening = writerOpeningByLease.get(lease);
if (opening) return opening;
const pending = Promise.resolve().then(async () => {
let store: SqliteTaskLedgerStore | undefined;
try {
store = await runWithStorageRootLease(lease, 'interactive', 'write', async (root) => {
const opened = createSqliteTaskLedgerStore(root);
try {
await opened.ready();
return opened;
} catch (error) {
opened.close();
throw error;
}
});
await assertStorageRootLease(lease, 'interactive', 'write');
const recoveredExisting = writerByLease.get(lease);
if (recoveredExisting) {
store.close();
return recoveredExisting;
}
const writer = createInteractiveWriterFacade(
lease,
store,
getTaskLedgerCanonicalReader(store),
);
writers.add(writer);
writerByLease.set(lease, writer);
return writer;
} catch (error) {
store?.close();
throw error;
}
});
writerOpeningByLease.set(lease, pending);
try {
return await pending;
} finally {
if (writerOpeningByLease.get(lease) === pending) writerOpeningByLease.delete(lease);
}
}
function createInteractiveWriterFacade(
lease: StorageRootLease<'interactive', 'write'>,
store: SqliteTaskLedgerStore,
canonicalReader: TaskLedgerCanonicalReader,
): InteractiveTaskLedgerWriter {
let closed = false;
const run = <T>(operation: () => Promise<T>) => {
if (closed) {
return Promise.reject(
new StorageRootAuthorityError('invalid_lease', 'Task ledger writer is closed'),
);
}
return runWithStorageRootLease(lease, 'interactive', 'write', async () => operation());
};
const writer: InteractiveTaskLedgerWriter = {
kind: 'interactive',
access: 'write',
[writerBrand]: true,
list: (sessionId, options) => run(() => canonicalReader.list(sessionId, options)),
get: (sessionId, id, options) => run(() => canonicalReader.get(sessionId, id, options)),
create: (sessionId, drafts, context) => run(() => store.create(sessionId, drafts, context)),
update: (sessionId, id, patch, context) =>
run(() => store.update(sessionId, id, patch, context)),
claim: (sessionId, id, owner, context) => run(() => store.claim(sessionId, id, owner, context)),
claimAvailable: (sessionId, id, owner, scope, context) =>
run(() => store.claimAvailable(sessionId, id, owner, scope, context)),
settleAgentOutcome: (sessionId, id, outcome, context) =>
run(() => store.settleAgentOutcome(sessionId, id, outcome, context)),
copyConversationTaskLedger: (input) => {
const acceptedInput: ConversationTaskLedgerCopyInput = Object.freeze({
...input,
turnIds: Object.freeze([...input.turnIds]),
runIdMap: Object.freeze(input.runIdMap.map((entry) => Object.freeze({ ...entry }))),
});
return run(() => store.copyConversationTaskLedger(acceptedInput));
},
purgeConversationTaskLedger: (sessionId) =>
run(() => store.purgeConversationTaskLedger(sessionId)),
subscribe: (listener) => store.subscribe(listener),
close: () => {
if (closed) return;
closed = true;
if (writerByLease.get(lease) === writer) writerByLease.delete(lease);
writers.delete(writer);
store.close();
},
};
Object.freeze(writer);
return writer;
}