82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
export const SAVE_ATTEMPT_STAGES = ["SERIALIZE", "OPFS_STAGE", "SCENE_COMMIT", "METADATA_COMMIT"] as const;
|
|
export type SaveAttemptStage = typeof SAVE_ATTEMPT_STAGES[number];
|
|
|
|
export interface SaveCommitIdentity {
|
|
revision: number;
|
|
sha256: string | null;
|
|
}
|
|
|
|
export interface SaveTransactionState {
|
|
status: "IDLE" | "RUNNING" | "SUCCEEDED" | "FAILED";
|
|
stage: SaveAttemptStage | null;
|
|
committed: SaveCommitIdentity;
|
|
candidate: SaveCommitIdentity | null;
|
|
errorCode: string | null;
|
|
}
|
|
|
|
export type SaveTransactionResult =
|
|
| { ok: true; state: SaveTransactionState }
|
|
| { ok: false; state: SaveTransactionState; errorCode: "SAVE_TRANSACTION_INVALID_TRANSITION" | "SAVE_TRANSACTION_COMMIT_MISMATCH" };
|
|
|
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
|
|
function validIdentity(identity: SaveCommitIdentity, allowNullHash: boolean): boolean {
|
|
return Number.isSafeInteger(identity.revision) && identity.revision >= 0 &&
|
|
(allowNullHash ? identity.sha256 === null || SHA256.test(identity.sha256) : typeof identity.sha256 === "string" && SHA256.test(identity.sha256));
|
|
}
|
|
|
|
export function createSaveTransactionState(committed: SaveCommitIdentity): SaveTransactionState {
|
|
if (!validIdentity(committed, true)) throw new Error("SAVE_TRANSACTION_COMMIT_INVALID");
|
|
return { status: "IDLE", stage: null, committed, candidate: null, errorCode: null };
|
|
}
|
|
|
|
export function beginSaveTransaction(state: SaveTransactionState, targetRevision: number): SaveTransactionResult {
|
|
if (state.status === "RUNNING" || !Number.isSafeInteger(targetRevision) || targetRevision < state.committed.revision) {
|
|
return { ok: false, state, errorCode: "SAVE_TRANSACTION_INVALID_TRANSITION" };
|
|
}
|
|
return {
|
|
ok: true,
|
|
state: {
|
|
status: "RUNNING",
|
|
stage: "SERIALIZE",
|
|
committed: state.committed,
|
|
candidate: { revision: targetRevision, sha256: null },
|
|
errorCode: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function advanceSaveTransaction(state: SaveTransactionState, stage: SaveAttemptStage, candidateSha256?: string): SaveTransactionResult {
|
|
if (state.status !== "RUNNING" || !state.stage || !state.candidate) {
|
|
return { ok: false, state, errorCode: "SAVE_TRANSACTION_INVALID_TRANSITION" };
|
|
}
|
|
const currentIndex = SAVE_ATTEMPT_STAGES.indexOf(state.stage);
|
|
const nextIndex = SAVE_ATTEMPT_STAGES.indexOf(stage);
|
|
if (nextIndex !== currentIndex + 1 || (stage === "OPFS_STAGE" && (!candidateSha256 || !SHA256.test(candidateSha256)))) {
|
|
return { ok: false, state, errorCode: "SAVE_TRANSACTION_INVALID_TRANSITION" };
|
|
}
|
|
return {
|
|
ok: true,
|
|
state: {
|
|
...state,
|
|
stage,
|
|
candidate: stage === "OPFS_STAGE" ? { ...state.candidate, sha256: candidateSha256! } : state.candidate,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function failSaveTransaction(state: SaveTransactionState, errorCode: string): SaveTransactionState {
|
|
if (state.status !== "RUNNING" || !/^[A-Z][A-Z0-9_]*$/.test(errorCode)) return state;
|
|
return { ...state, status: "FAILED", errorCode };
|
|
}
|
|
|
|
export function commitSaveTransaction(state: SaveTransactionState, persisted: SaveCommitIdentity): SaveTransactionResult {
|
|
if (state.status !== "RUNNING" || state.stage !== "METADATA_COMMIT" || !state.candidate || !validIdentity(persisted, false)) {
|
|
return { ok: false, state, errorCode: "SAVE_TRANSACTION_INVALID_TRANSITION" };
|
|
}
|
|
if (persisted.revision !== state.candidate.revision || persisted.sha256 !== state.candidate.sha256) {
|
|
return { ok: false, state, errorCode: "SAVE_TRANSACTION_COMMIT_MISMATCH" };
|
|
}
|
|
return { ok: true, state: { status: "SUCCEEDED", stage: "METADATA_COMMIT", committed: persisted, candidate: persisted, errorCode: null } };
|
|
}
|