src/features/reviews/command-draft.ts
Linked repository file from this build.
import { sameActor } from "@/lib/auth/actor";
import { execute, type ReviewCommand } from "./commands";
import type { PendingCommand } from "./persistence";
import type { SnapReview } from "@/programs/snap-negative/types";
import { RuleError } from "@/lib/errors";
function draftPreviewBase(review: SnapReview, command: ReviewCommand): SnapReview {
// execute replaces answers and activity; only in-place scenario/note writes need copies.
const next = { ...review, facts: { ...review.facts } };
if (command.type === "scenario") {
next.facts.scenarios = { ...review.facts.scenarios };
} else if (command.type === "update_note") {
next.notes = [...review.notes];
}
return next;
}
/** One editable draft previews a single command against a fixed base revision.
* Only take() commits its final value to the persistence queue.
*/
export class CommandDraft {
private draft?: { base: SnapReview; entry: PendingCommand };
static key(command: ReviewCommand): string | undefined {
if (command.type === "update_note") return `note:${command.noteId}`;
if (command.type === "conditional_notes") return "conditional_notes";
if (command.type === "scenario") return `scenario:${command.scenario}`;
if (["identity", "household", "negative_action", "answers"].includes(command.type))
return command.type;
return undefined;
}
accepts(entry: PendingCommand): boolean {
return (
!this.draft ||
(CommandDraft.key(this.draft.entry.command) === CommandDraft.key(entry.command) &&
sameActor(this.draft.entry.actor, entry.actor))
);
}
stage(base: SnapReview, entry: PendingCommand): SnapReview {
if (!CommandDraft.key(entry.command) || !this.accepts(entry))
throw new RuleError("Commit the current draft before another command.");
const previous = this.draft;
let command = entry.command;
if (previous?.entry.command.type === "identity" && command.type === "identity")
command = { ...command, value: { ...previous.entry.command.value, ...command.value } };
if (previous?.entry.command.type === "negative_action" && command.type === "negative_action")
command = { ...command, value: { ...previous.entry.command.value, ...command.value } };
const stableBase = previous?.base ?? base;
const latest = { ...entry, id: previous?.entry.id ?? entry.id, command };
const preview = execute(
draftPreviewBase(stableBase, command),
command,
latest.actor,
{ id: latest.id, at: latest.at },
{ clone: false },
);
this.draft = { base: stableBase, entry: latest };
return preview;
}
get dirty() {
return this.draft !== undefined;
}
take(): PendingCommand | undefined {
const entry = this.draft?.entry;
this.draft = undefined;
return entry;
}
clear() {
this.draft = undefined;
}
}