93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import {
|
|
CorrectiveCaseRunnerError,
|
|
MainMutationCommand,
|
|
MainMutationResult,
|
|
} from "./corrective-case-runner";
|
|
|
|
export interface MainSnapshot<T> {
|
|
value: T;
|
|
revision: number;
|
|
dirty: boolean;
|
|
}
|
|
|
|
export interface AtomicMutationOutcome {
|
|
delta: Record<string, unknown>;
|
|
persistence?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export type DraftMutation<T> = (draft: T, command: MainMutationCommand) => AtomicMutationOutcome | Promise<AtomicMutationOutcome>;
|
|
|
|
export class InMemoryMainStore<T> {
|
|
private value: T;
|
|
private currentRevision: number;
|
|
private currentDirty: boolean;
|
|
private readonly clone: (value: T) => T;
|
|
|
|
constructor(value: T, revision: number, dirty: boolean, clone: (value: T) => T) {
|
|
if (!Number.isSafeInteger(revision) || revision < 0) throw new Error("revision must be a non-negative integer");
|
|
this.clone = clone;
|
|
this.value = clone(value);
|
|
this.currentRevision = revision;
|
|
this.currentDirty = dirty;
|
|
}
|
|
|
|
snapshot(): MainSnapshot<T> {
|
|
return { value: this.clone(this.value), revision: this.currentRevision, dirty: this.currentDirty };
|
|
}
|
|
|
|
commit(value: T, revision: number, dirty: boolean): void {
|
|
if (!Number.isSafeInteger(revision) || revision < 0) throw new Error("revision must be a non-negative integer");
|
|
this.value = this.clone(value);
|
|
this.currentRevision = revision;
|
|
this.currentDirty = dirty;
|
|
}
|
|
|
|
restore(snapshot: MainSnapshot<T>): void {
|
|
this.commit(snapshot.value, snapshot.revision, snapshot.dirty);
|
|
}
|
|
}
|
|
|
|
function validOutcome(value: unknown): value is AtomicMutationOutcome {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
const candidate = value as Record<string, unknown>;
|
|
if (!candidate.delta || typeof candidate.delta !== "object" || Array.isArray(candidate.delta)) return false;
|
|
return candidate.persistence === undefined || candidate.persistence === null ||
|
|
(typeof candidate.persistence === "object" && !Array.isArray(candidate.persistence));
|
|
}
|
|
|
|
export async function runAtomicMutation<T>(
|
|
store: InMemoryMainStore<T>,
|
|
command: MainMutationCommand,
|
|
mutate: DraftMutation<T>,
|
|
): Promise<MainMutationResult> {
|
|
const before = store.snapshot();
|
|
if (command.baseRevision !== before.revision) {
|
|
throw new CorrectiveCaseRunnerError("REVISION_CONFLICT", "base revision does not match current Main");
|
|
}
|
|
try {
|
|
const draft = store.snapshot().value;
|
|
const outcome = await mutate(draft, command);
|
|
if (!validOutcome(outcome)) {
|
|
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", "atomic mutation outcome is invalid");
|
|
}
|
|
store.commit(draft, before.revision + 1, true);
|
|
return {
|
|
schemaVersion: 1,
|
|
requestId: command.requestId,
|
|
status: "FINISHED",
|
|
code: "OK",
|
|
baseRevision: command.baseRevision,
|
|
mainRevisionBefore: before.revision,
|
|
mainRevisionAfter: before.revision + 1,
|
|
mainMutation: "CHANGED",
|
|
delta: { ...outcome.delta },
|
|
persistence: outcome.persistence === undefined ? { dirty: true } : outcome.persistence,
|
|
error: null,
|
|
};
|
|
} catch (error) {
|
|
store.restore(before);
|
|
throw error;
|
|
}
|
|
}
|
|
|