Files
workinf_Blender_Wasm/web/protocol/sequencer-media-revision.ts
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

163 lines
6.7 KiB
TypeScript

import type { ErrorCode } from "./error";
export const SEQUENCER_MEDIA_REVISION_SCHEMA = 1 as const;
export type SequencerMediaOperation = "SEEK" | "SCRUB" | "DECODE";
export interface SequencerMediaRevisionStateIR {
schemaVersion: typeof SEQUENCER_MEDIA_REVISION_SCHEMA;
timelineId: string;
timelineRevision: number;
latestRequestRevision: number;
}
export interface SequencerMediaRevisionRequestIR {
schemaVersion: typeof SEQUENCER_MEDIA_REVISION_SCHEMA;
requestId: string;
timelineId: string;
timelineRevision: number;
requestRevision: number;
operation: SequencerMediaOperation;
frame: number;
}
export interface SequencerMediaRevisionResultIR extends SequencerMediaRevisionRequestIR {
status: "COMPLETED";
sourceFrame: number;
payloadSha256: string;
}
export interface SequencerMediaRevisionDecisionIR {
status: "PUBLISH" | "STALE";
code: ErrorCode | null;
operation: SequencerMediaOperation;
requestRevision: number;
}
export class SequencerMediaRevisionValidationError extends Error {
readonly code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "SequencerMediaRevisionValidationError";
this.code = code;
}
}
const SHA256 = /^[a-f0-9]{64}$/;
const ID = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,127}$/;
const OPERATIONS = new Set<SequencerMediaOperation>(["SEEK", "SCRUB", "DECODE"]);
const STATE_KEYS = new Set(["schemaVersion", "timelineId", "timelineRevision", "latestRequestRevision"]);
const REQUEST_KEYS = new Set([
"schemaVersion", "requestId", "timelineId", "timelineRevision", "requestRevision", "operation", "frame",
]);
const RESULT_KEYS = new Set([...REQUEST_KEYS, "status", "sourceFrame", "payloadSha256"]);
function record(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", `${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(value: Record<string, unknown>, keys: ReadonlySet<string>, label: string): void {
const actual = Object.keys(value);
if (actual.length !== keys.size || actual.some((key) => !keys.has(key))) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", `${label} fields are invalid`);
}
}
function identity(value: unknown, label: string): string {
if (typeof value !== "string" || !ID.test(value)) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", `${label} is invalid`);
}
return value;
}
function integer(value: unknown, label: string, maximum = Number.MAX_SAFE_INTEGER): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", `${label} is outside the bounded range`);
}
return value;
}
export function parseSequencerMediaRevisionState(value: unknown): SequencerMediaRevisionStateIR {
const state = record(value, "Sequencer media revision state");
exactKeys(state, STATE_KEYS, "Sequencer media revision state");
if (state.schemaVersion !== SEQUENCER_MEDIA_REVISION_SCHEMA) {
throw new SequencerMediaRevisionValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer media revision schema");
}
return {
schemaVersion: SEQUENCER_MEDIA_REVISION_SCHEMA,
timelineId: identity(state.timelineId, "timelineId"),
timelineRevision: integer(state.timelineRevision, "timelineRevision"),
latestRequestRevision: integer(state.latestRequestRevision, "latestRequestRevision"),
};
}
export function parseSequencerMediaRevisionRequest(value: unknown): SequencerMediaRevisionRequestIR {
const request = record(value, "Sequencer media revision request");
exactKeys(request, REQUEST_KEYS, "Sequencer media revision request");
if (request.schemaVersion !== SEQUENCER_MEDIA_REVISION_SCHEMA) {
throw new SequencerMediaRevisionValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer media revision schema");
}
if (!OPERATIONS.has(request.operation as SequencerMediaOperation)) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", "Sequencer media operation is invalid");
}
return {
schemaVersion: SEQUENCER_MEDIA_REVISION_SCHEMA,
requestId: identity(request.requestId, "requestId"),
timelineId: identity(request.timelineId, "timelineId"),
timelineRevision: integer(request.timelineRevision, "timelineRevision"),
requestRevision: integer(request.requestRevision, "requestRevision"),
operation: request.operation as SequencerMediaOperation,
frame: integer(request.frame, "frame", 1_000_000),
};
}
export function parseSequencerMediaRevisionResult(value: unknown): SequencerMediaRevisionResultIR {
const result = record(value, "Sequencer media revision result");
exactKeys(result, RESULT_KEYS, "Sequencer media revision result");
const request = parseSequencerMediaRevisionRequest({
schemaVersion: result.schemaVersion,
requestId: result.requestId,
timelineId: result.timelineId,
timelineRevision: result.timelineRevision,
requestRevision: result.requestRevision,
operation: result.operation,
frame: result.frame,
});
if (result.status !== "COMPLETED" || typeof result.payloadSha256 !== "string" || !SHA256.test(result.payloadSha256)) {
throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", "Sequencer media completed result is invalid");
}
return {
...request,
status: "COMPLETED",
sourceFrame: integer(result.sourceFrame, "sourceFrame", 1_000_000),
payloadSha256: result.payloadSha256,
};
}
function sameRequest(request: SequencerMediaRevisionRequestIR, result: SequencerMediaRevisionResultIR): boolean {
return request.requestId === result.requestId && request.timelineId === result.timelineId &&
request.timelineRevision === result.timelineRevision && request.requestRevision === result.requestRevision &&
request.operation === result.operation && request.frame === result.frame;
}
export function gateSequencerMediaRevision(
requestValue: unknown,
stateValue: unknown,
resultValue: unknown,
): SequencerMediaRevisionDecisionIR {
const request = parseSequencerMediaRevisionRequest(requestValue);
const state = parseSequencerMediaRevisionState(stateValue);
const result = parseSequencerMediaRevisionResult(resultValue);
const stale = !sameRequest(request, result) || request.timelineId !== state.timelineId ||
request.timelineRevision !== state.timelineRevision || request.requestRevision !== state.latestRequestRevision;
return {
status: stale ? "STALE" : "PUBLISH",
code: stale ? "REVISION_CONFLICT" : null,
operation: request.operation,
requestRevision: request.requestRevision,
};
}