import type { SerializedProjectCommand } from "./project-command.model.js"; import { assertCircuitInsertProjectCommand, circuitInsertCommandType, circuitStructureCommandSchemaVersion, type CircuitSnapshot, } from "./circuit-structure-project-command.model.js"; export const circuitDeviceRowMoveCommandType = "circuit-device-row.move" as const; export const circuitDeviceRowMoveWithNewCircuitCommandType = "circuit-device-row.move-with-new-circuit" as const; export const circuitDeviceRowMoveCommandSchemaVersion = 1 as const; export interface CircuitDeviceRowMoveAssignment { rowId: string; expectedCircuitId: string; expectedSortOrder: number; targetCircuitId: string; targetSortOrder: number; } export interface CircuitDeviceRowMoveCommandPayload { moves: CircuitDeviceRowMoveAssignment[]; } export interface CircuitDeviceRowMoveProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitDeviceRowMoveCommandSchemaVersion; type: typeof circuitDeviceRowMoveCommandType; } export type CircuitDeviceRowMoveTargetCircuitAction = | "create" | "delete"; export interface CircuitDeviceRowMoveWithNewCircuitCommandPayload { targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction; targetCircuit: CircuitSnapshot; moves: CircuitDeviceRowMoveAssignment[]; } export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitDeviceRowMoveCommandSchemaVersion; type: typeof circuitDeviceRowMoveWithNewCircuitCommandType; } export type CircuitDeviceRowMoveHistoryProjectCommand = | CircuitDeviceRowMoveProjectCommand | CircuitDeviceRowMoveWithNewCircuitProjectCommand; export function createCircuitDeviceRowMoveProjectCommand( moves: CircuitDeviceRowMoveAssignment[] ): CircuitDeviceRowMoveProjectCommand { const command: CircuitDeviceRowMoveProjectCommand = { schemaVersion: circuitDeviceRowMoveCommandSchemaVersion, type: circuitDeviceRowMoveCommandType, payload: { moves }, }; assertCircuitDeviceRowMoveProjectCommand(command); return command; } export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand( targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction, targetCircuit: CircuitSnapshot, moves: CircuitDeviceRowMoveAssignment[] ): CircuitDeviceRowMoveWithNewCircuitProjectCommand { const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = { schemaVersion: circuitDeviceRowMoveCommandSchemaVersion, type: circuitDeviceRowMoveWithNewCircuitCommandType, payload: { targetCircuitAction, targetCircuit, moves, }, }; assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command); return command; } export function assertCircuitDeviceRowMoveProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitDeviceRowMoveProjectCommand { if ( command.schemaVersion !== circuitDeviceRowMoveCommandSchemaVersion || command.type !== circuitDeviceRowMoveCommandType ) { throw new Error("Unsupported circuit device-row move command."); } if (!isPlainObject(command.payload)) { throw new Error( "Circuit device-row move payload must be an object." ); } const moves = command.payload.moves; if (!Array.isArray(moves) || moves.length === 0) { throw new Error( "Circuit device-row move command requires at least one move." ); } const rowIds = new Set(); for (const move of moves) { if (!isPlainObject(move)) { throw new Error( "Circuit device-row move command contains an invalid move." ); } assertNonEmptyString(move.rowId, "rowId"); assertNonEmptyString( move.expectedCircuitId, "expectedCircuitId" ); assertFiniteNumber( move.expectedSortOrder, "expectedSortOrder" ); assertNonEmptyString(move.targetCircuitId, "targetCircuitId"); assertFiniteNumber(move.targetSortOrder, "targetSortOrder"); if (rowIds.has(move.rowId)) { throw new Error( "Circuit device-row move command contains duplicate row ids." ); } if ( move.expectedCircuitId === move.targetCircuitId && move.expectedSortOrder === move.targetSortOrder ) { throw new Error( "Circuit device-row move command contains a no-op move." ); } rowIds.add(move.rowId); } } export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitDeviceRowMoveWithNewCircuitProjectCommand { if ( command.schemaVersion !== circuitDeviceRowMoveCommandSchemaVersion || command.type !== circuitDeviceRowMoveWithNewCircuitCommandType ) { throw new Error( "Unsupported circuit device-row move-with-new-circuit command." ); } if (!isPlainObject(command.payload)) { throw new Error( "Circuit device-row move-with-new-circuit payload must be an object." ); } const { targetCircuitAction, targetCircuit, moves } = command.payload; if ( targetCircuitAction !== "create" && targetCircuitAction !== "delete" ) { throw new Error( "Target circuit action must be create or delete." ); } assertCircuitInsertProjectCommand({ schemaVersion: circuitStructureCommandSchemaVersion, type: circuitInsertCommandType, payload: { circuit: targetCircuit }, }); const validatedTargetCircuit = targetCircuit as CircuitSnapshot; if ( validatedTargetCircuit.isReserve !== true || validatedTargetCircuit.deviceRows.length !== 0 ) { throw new Error( "Move target circuit snapshot must describe an empty reserve circuit." ); } assertCircuitDeviceRowMoveProjectCommand({ schemaVersion: circuitDeviceRowMoveCommandSchemaVersion, type: circuitDeviceRowMoveCommandType, payload: { moves }, }); const validatedMoves = moves as CircuitDeviceRowMoveAssignment[]; for (const move of validatedMoves) { if ( targetCircuitAction === "create" && (move.expectedCircuitId === validatedTargetCircuit.id || move.targetCircuitId !== validatedTargetCircuit.id) ) { throw new Error( "Create-target moves must move rows into the new circuit." ); } if ( targetCircuitAction === "delete" && (move.expectedCircuitId !== validatedTargetCircuit.id || move.targetCircuitId === validatedTargetCircuit.id) ) { throw new Error( "Delete-target moves must move rows out of the created circuit." ); } } } function assertNonEmptyString( value: unknown, field: string ): asserts value is string { if (typeof value !== "string" || !value.trim()) { throw new Error(`${field} must be a non-empty string.`); } } function assertFiniteNumber( value: unknown, field: string ): asserts value is number { if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(`${field} must be a finite number.`); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }