import { randomUUID } from "node:crypto"; import type { Assessment, Draft, Review, Source } from "../src/types"; import { fingerprint } from "./context"; import { ModelError } from "./model-runtime"; export interface BlobStore { get(key: string, options: { type: "json" }): Promise; getWithMetadata( key: string, options: { type: "json" } ): Promise<{ data: any; etag: string } | null>; setJSON( key: string, value: unknown, options?: { onlyIfMatch?: string; onlyIfNew?: boolean; metadata?: Record; } ): Promise<{ modified: boolean }>; delete(key: string): Promise; list(options?: { prefix?: string }): Promise<{ blobs: { key: string }[] }>; } const TTL = 2 * 60 * 60 * 1000; export class DurableSessions { constructor(private store: BlobStore, private now = () => Date.now()) {} async keepDraft(draft: Draft, sources: Source[], owner: string) { const id = randomUUID(), saved = { ...draft, id }; const result = await this.store.setJSON( "draft/" + id, { owner, expires: this.now() + TTL, draft: saved, sources }, { onlyIfNew: true } ); if (!result.modified) throw new ModelError( "Could not save the extracted context. Your source notes are unchanged.", 503 ); return saved; } private async entry(id: string, owner: string) { if (!/^[a-f0-9-]{36}$/.test(id)) throw new ModelError("Invalid review session.", 400); const result = await this.store.getWithMetadata("draft/" + id, { type: "json", }); if ( !result || result.data.owner !== owner || result.data.expires <= this.now() ) throw new ModelError( "The review session expired. Your browser-saved work is intact. Restore the review session to continue.", 400 ); if (!result.etag) throw new ModelError("Storage could not verify the saved session version. Your browser-saved work is unchanged. Retry later.", 503); return result; } async getDraft(id: string, sources: Source[], owner: string): Promise { const { data } = await this.entry(id, owner); if (data.draft.fingerprint !== fingerprint(sources)) throw new ModelError( "Sources changed. Extract and review this version again.", 400 ); return structuredClone(data.draft); } async keepReport( id: string, assessment: Assessment, review: Review, owner: string ) { const { data, etag } = await this.entry(id, owner); const reportId = randomUUID(); const saved = { ...assessment, reportId, extraction: { mode: data.draft.mode, model: data.draft.model, durationMs: data.draft.durationMs, attempts: data.draft.attempts, strategy: data.draft.strategy, metrics: data.draft.metrics, }, }; const report = { id: reportId, assessment: saved, review, createdAt: new Date(this.now()).toISOString(), }; await this.store.setJSON( "report/" + reportId, { owner, expires: data.expires, draftId: id, report }, { onlyIfNew: true } ); const committed = await this.store.setJSON( "draft/" + id, { ...data, reportId }, { onlyIfMatch: etag } ); if (!committed.modified) throw new ModelError( "Another assessment updated this session. Review your current decisions and run the check again.", 409 ); return saved; } async getReport(id: string, owner: string) { if (!/^[a-f0-9-]{36}$/.test(id)) throw new ModelError("Invalid report.", 400); const report = await this.store.get("report/" + id, { type: "json" }); if (!report || report.owner !== owner || report.expires <= this.now()) throw new ModelError( "This report expired. Restore the review session and run the check again.", 400 ); const { data } = await this.entry(report.draftId, owner); if (data.reportId !== id) throw new ModelError( "This report is no longer current. Run the assessment again.", 400 ); return { product: "MARMAR Context", schemaVersion: 1, synthetic: true, sources: data.sources, extraction: data.draft, ...report.report, }; } async limit(owner: string) { const now = this.now(); for (const [key, maximum, expires] of [ [`quota/minute/${Math.floor(now / 60000)}/global`, 12, now + 120000], [`quota/day/${Math.floor(now / 86400000)}/global`, 120, now + 86400000], [`quota/minute/${Math.floor(now / 60000)}/${owner}`, 6, now + 120000], ] as [string, number, number][]) { let committed = false; for (let attempt = 0; attempt < 4; attempt++) { const current = await this.store.getWithMetadata(key, { type: "json" }); if (current && !current.etag) throw new ModelError("Storage could not verify the usage allowance. Retry later.", 503); const count = current?.data.count || 0; if (count >= maximum) throw new ModelError( "Preview extraction allowance reached. Your saved work is unchanged. Retry later.", 429, 60 ); const result = await this.store.setJSON( key, { count: count + 1, expires }, current ? { onlyIfMatch: current.etag } : { onlyIfNew: true } ); if (result.modified) { committed = true; break; } } if (!committed) throw new ModelError( "The preview is busy. Retry shortly; your sources are unchanged.", 503, 3 ); } } async purge() { let deleted = 0; for (const prefix of ["draft/", "report/", "quota/"]) { const { blobs } = await this.store.list({ prefix }); for (const blob of blobs) { const value = await this.store.get(blob.key, { type: "json" }); if (value && value.expires <= this.now()) { await this.store.delete(blob.key); deleted++; } } } return deleted; } }