import { ProjectHistoryOperationUnavailableError } from "../errors/project-history-operation-unavailable.error.js"; import { assertCircuitDeviceRowUpdateProjectCommand, circuitDeviceRowUpdateCommandType, } from "../models/circuit-device-row-project-command.model.js"; import { assertCircuitDeviceRowDeleteProjectCommand, assertCircuitDeviceRowInsertProjectCommand, circuitDeviceRowDeleteCommandType, circuitDeviceRowInsertCommandType, } from "../models/circuit-device-row-structure-project-command.model.js"; import { assertCircuitUpdateProjectCommand, circuitUpdateCommandType, } from "../models/circuit-project-command.model.js"; import { assertCircuitDeleteProjectCommand, assertCircuitInsertProjectCommand, circuitDeleteCommandType, circuitInsertCommandType, } from "../models/circuit-structure-project-command.model.js"; import { assertSerializedProjectCommand, type SerializedProjectCommand, } from "../models/project-command.model.js"; import type { CircuitDeviceRowProjectCommandStore } from "../ports/circuit-device-row-project-command.store.js"; import type { CircuitDeviceRowStructureProjectCommandStore } from "../ports/circuit-device-row-structure-project-command.store.js"; import type { CircuitProjectCommandStore } from "../ports/circuit-project-command.store.js"; import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js"; import type { ExecuteProjectHistoryCommandInput, ExecutedProjectCommand, ExecuteUserProjectCommandInput, ProjectCommandExecutor, } from "../ports/project-command.executor.js"; import type { ProjectHistoryDirection, ProjectHistoryStore, } from "../ports/project-history.store.js"; import type { ProjectRevisionSource } from "../ports/project-revision.store.js"; interface DispatchProjectCommandInput { projectId: string; expectedRevision: number; source: ProjectRevisionSource; description?: string; actorId?: string; historyTargetChangeSetId?: string; command: SerializedProjectCommand; } export class ProjectCommandService implements ProjectCommandExecutor { constructor( private readonly circuitStore: CircuitProjectCommandStore, private readonly deviceRowStore: CircuitDeviceRowProjectCommandStore, private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore, private readonly circuitStructureStore: CircuitStructureProjectCommandStore, private readonly historyStore: ProjectHistoryStore ) {} executeUser( input: ExecuteUserProjectCommandInput ): ExecutedProjectCommand { assertExpectedRevision(input.expectedRevision); assertSerializedProjectCommand(input.command); return this.dispatch({ ...input, source: "user", command: input.command, }); } undo( input: ExecuteProjectHistoryCommandInput ): ExecutedProjectCommand { return this.executeHistory(input, "undo"); } redo( input: ExecuteProjectHistoryCommandInput ): ExecutedProjectCommand { return this.executeHistory(input, "redo"); } private executeHistory( input: ExecuteProjectHistoryCommandInput, direction: ProjectHistoryDirection ) { assertExpectedRevision(input.expectedRevision); const step = this.historyStore.getNextCommand( input.projectId, direction ); if (!step) { throw new ProjectHistoryOperationUnavailableError( input.projectId, direction ); } return this.dispatch({ ...input, source: direction, description: `${direction === "undo" ? "Undo" : "Redo"} ${step.command.type}`, historyTargetChangeSetId: step.changeSetId, command: step.command, }); } private dispatch( input: DispatchProjectCommandInput ): ExecutedProjectCommand { const revision = this.executeTypedCommand(input); const history = this.historyStore.getState(input.projectId); if (!history) { throw new Error("Project history disappeared after command execution."); } return { revision, history }; } private executeTypedCommand(input: DispatchProjectCommandInput) { switch (input.command.type) { case circuitUpdateCommandType: { assertCircuitUpdateProjectCommand(input.command); return this.circuitStore.executeUpdate({ ...input, command: input.command, }).revision; } case circuitDeviceRowUpdateCommandType: { assertCircuitDeviceRowUpdateProjectCommand(input.command); return this.deviceRowStore.executeUpdate({ ...input, command: input.command, }).revision; } case circuitDeviceRowInsertCommandType: { assertCircuitDeviceRowInsertProjectCommand(input.command); return this.deviceRowStructureStore.execute({ ...input, command: input.command, }).revision; } case circuitDeviceRowDeleteCommandType: { assertCircuitDeviceRowDeleteProjectCommand(input.command); return this.deviceRowStructureStore.execute({ ...input, command: input.command, }).revision; } case circuitInsertCommandType: { assertCircuitInsertProjectCommand(input.command); return this.circuitStructureStore.execute({ ...input, command: input.command, }).revision; } case circuitDeleteCommandType: { assertCircuitDeleteProjectCommand(input.command); return this.circuitStructureStore.execute({ ...input, command: input.command, }).revision; } default: throw new Error( `Unsupported project command type: ${input.command.type}.` ); } } } function assertExpectedRevision(expectedRevision: number) { if ( !Number.isSafeInteger(expectedRevision) || expectedRevision < 0 ) { throw new Error( "Expected project revision must be a non-negative integer." ); } }