Reorganize Blender Web execution around capabilities
This commit is contained in:
92
web/protocol/corrective-main-transaction.ts
Normal file
92
web/protocol/corrective-main-transaction.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user