marmar-context / web / context / server / durable-sessions.ts
durable-sessions.ts
Raw
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;
  }
}