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<any>;
getWithMetadata(
key: string,
options: { type: "json" }
): Promise<{ data: any; etag: string } | null>;
setJSON(
key: string,
value: unknown,
options?: {
onlyIfMatch?: string;
onlyIfNew?: boolean;
metadata?: Record<string, unknown>;
}
): Promise<{ modified: boolean }>;
delete(key: string): Promise<void>;
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<Draft> {
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;
}
}