(workflowId: string, reason: string)
| 607 | } |
| 608 | |
| 609 | const reloadWorkflowFromApi = async (workflowId: string, reason: string): Promise<boolean> => { |
| 610 | const reloadSequence = (reloadSequencesRef.current[workflowId] ?? 0) + 1 |
| 611 | reloadSequencesRef.current[workflowId] = reloadSequence |
| 612 | const isLatestReload = () => reloadSequencesRef.current[workflowId] === reloadSequence |
| 613 | const pendingExternalUpdateAtStart = |
| 614 | useWorkflowDiffStore.getState().pendingExternalUpdates[workflowId] ?? 0 |
| 615 | useWorkflowDiffStore.getState().setWorkflowReconciliationInProgress(workflowId, true) |
| 616 | const failLatestReconciliation = (message: string) => { |
| 617 | if (!isLatestReload()) return |
| 618 | const diffStore = useWorkflowDiffStore.getState() |
| 619 | if ((diffStore.pendingExternalUpdates[workflowId] ?? 0) <= pendingExternalUpdateAtStart) { |
| 620 | diffStore.clearExternalUpdatePending(workflowId) |
| 621 | } |
| 622 | diffStore.setWorkflowReconciliationInProgress(workflowId, false) |
| 623 | diffStore.setWorkflowReconciliationError(workflowId, message) |
| 624 | if ((useWorkflowDiffStore.getState().pendingExternalUpdates[workflowId] ?? 0) > 0) { |
| 625 | window.dispatchEvent( |
| 626 | new CustomEvent(WORKFLOW_DIFF_SETTLED_EVENT, { detail: { workflowId } }) |
| 627 | ) |
| 628 | } |
| 629 | } |
| 630 | // The contract's `state` is `workflowStateSchema` (loose at the wire |
| 631 | // level — `subBlocks.value` is `unknown`, optional flags omitted), |
| 632 | // but downstream consumers (replaceWorkflowState, the undo/redo |
| 633 | // graph) operate on the store's narrower `WorkflowState`. The |
| 634 | // server-of-record persists store-shaped values, so the runtime |
| 635 | // shape is the store type; we narrow once here at the trust |
| 636 | // boundary instead of sprinkling per-field casts. |
| 637 | let workflowState: WorkflowState | null = null |
| 638 | try { |
| 639 | const responseData = await requestJson(getWorkflowStateContract, { |
| 640 | params: { id: workflowId }, |
| 641 | }) |
| 642 | const wireState = responseData.data?.state |
| 643 | if (wireState) { |
| 644 | // double-cast-allowed: workflowStateSchema is structurally a supertype of the store's WorkflowState (subBlocks.value is `unknown`, optional booleans, etc.); the server persists store-shaped values so the runtime shape matches |
| 645 | workflowState = wireState as unknown as WorkflowState |
| 646 | if (Object.hasOwn(responseData.data, 'variables')) { |
| 647 | workflowState.variables = responseData.data.variables || {} |
| 648 | } |
| 649 | } |
| 650 | } catch (error) { |
| 651 | logger.error(`Failed to fetch workflow data after ${reason}`, { error }) |
| 652 | failLatestReconciliation( |
| 653 | 'Failed to sync the latest workflow changes. Refresh and try again.' |
| 654 | ) |
| 655 | return false |
| 656 | } |
| 657 | |
| 658 | if (!isLatestReload()) { |
| 659 | logger.debug(`Ignoring stale workflow reload after ${reason}`, { workflowId }) |
| 660 | return false |
| 661 | } |
| 662 | |
| 663 | if (!workflowState) { |
| 664 | logger.error(`No state found in workflow data after ${reason}`, { workflowId }) |
| 665 | failLatestReconciliation('No workflow state was returned while syncing latest changes.') |
| 666 | return false |
no test coverage detected