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(["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 { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new SequencerMediaRevisionValidationError("SEQUENCER_SCHEMA_INVALID", `${label} must be an object`); } return value as Record; } function exactKeys(value: Record, keys: ReadonlySet, 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, }; }