src/features/reviews/persistence.ts
Linked repository file from this build.
import { reviewKey, reviewIdentity, sameReviewIdentity, type ReviewIdentity } from "./review-key";
import { assertReviewAccess } from "@/lib/auth/scope";
import { actorRef } from "@/lib/auth/actor";
import { execute, type ReviewCommand } from "./commands";
import type { Actor } from "./types";
import type { ReviewStore, StoredReview } from "./review-store";
import { NotFoundError, RuleError } from "@/lib/errors";
import type { SnapReview } from "@/programs/snap-negative/types";
export interface PendingCommand {
command: ReviewCommand;
actor: Actor;
id: string;
at: string;
}
export class StaleReviewError extends Error {
constructor(public current: StoredReview) {
super(
"This case changed elsewhere. The saved server copy has been loaded; review it before editing again.",
);
}
}
export async function saveCommands(
store: ReviewStore,
key: string,
version: number,
requestId: string,
commands: PendingCommand[],
): Promise<StoredReview> {
const [current, repeated] = await Promise.all([store.load(key), store.findRequest(requestId)]);
if (repeated) {
if (!current || reviewKey(repeated.review) !== reviewKey(current.review))
throw new RuleError("Request belongs to another review.");
for (const { actor } of commands) assertReviewAccess(actor, repeated.review);
return repeated;
}
if (!current) throw new NotFoundError("Review not found.");
for (const { actor } of commands) assertReviewAccess(actor, current.review);
if (current.version !== version) throw new StaleReviewError(current);
if (!commands.length) throw new RuleError("No commands supplied.");
let review = current.review;
if (commands.length > 1 && commands.some((entry) => entry.command.type === "reset_demo"))
throw new RuleError("Reset must be sent alone.");
for (const entry of commands)
review = execute(review, entry.command, entry.actor, { id: entry.id, at: entry.at });
const result = {
review,
version: version + 1,
requestId,
actor: actorRef(commands.at(-1)!.actor),
savedAt: new Date().toISOString(),
};
return appendReview(store, result, {
expectedVersion: version,
actors: commands.map(({ actor }) => actor),
previous: current.review,
});
}
/**
* Both edits and intake resolve append races without replaying an accepted request.
* `previous` is the version the caller already loaded, so the store need not read it again.
*/
export async function appendReview(
store: ReviewStore,
result: StoredReview,
options: {
expectedVersion: number | null;
actors: Actor[];
newIdentity?: ReviewIdentity;
previous?: SnapReview;
},
): Promise<StoredReview> {
const { expectedVersion, actors, newIdentity, previous } = options;
const { requestId } = result;
const key = reviewKey(result.review);
try {
await store.append(result, previous);
} catch (error) {
const retry = await store.findRequest(requestId);
if (retry) {
const matches = newIdentity
? sameReviewIdentity(reviewIdentity(retry.review), newIdentity)
: reviewKey(retry.review) === key;
if (!matches) throw new RuleError("Request belongs to another review.");
for (const actor of actors) assertReviewAccess(actor, retry.review);
return retry;
}
const latest = newIdentity ? await store.findBusiness(newIdentity) : await store.load(key);
if (latest) for (const actor of actors) assertReviewAccess(actor, latest.review);
if (latest && latest.version !== expectedVersion) throw new StaleReviewError(latest);
throw error;
}
return result;
}