| 24 | import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' |
| 25 | |
| 26 | export class BlockResolver implements Resolver { |
| 27 | private nameToBlockId: Map<string, string> |
| 28 | private blockById: Map<string, SerializedBlock> |
| 29 | private blockIdsInSubflows: Set<string> |
| 30 | private subflowContainerIds: Set<string> |
| 31 | |
| 32 | constructor( |
| 33 | private workflow: SerializedWorkflow, |
| 34 | private navigatePathAsync?: AsyncPathNavigator |
| 35 | ) { |
| 36 | this.nameToBlockId = new Map() |
| 37 | this.blockById = new Map() |
| 38 | this.blockIdsInSubflows = new Set() |
| 39 | this.subflowContainerIds = new Set([ |
| 40 | ...Object.keys(workflow.loops ?? {}), |
| 41 | ...Object.keys(workflow.parallels ?? {}), |
| 42 | ]) |
| 43 | for (const block of workflow.blocks) { |
| 44 | this.blockById.set(block.id, block) |
| 45 | if (block.metadata?.name) { |
| 46 | // Name uniqueness is enforced at the normalized level on create/rename, |
| 47 | // but legacy workflows may contain names that collide only now that |
| 48 | // normalizeName strips dots. Dot-free names keep ownership of the key so |
| 49 | // previously working references never change targets. |
| 50 | const normalizedName = normalizeName(block.metadata.name) |
| 51 | const incumbentId = this.nameToBlockId.get(normalizedName) |
| 52 | const incumbentName = incumbentId |
| 53 | ? this.blockById.get(incumbentId)?.metadata?.name |
| 54 | : undefined |
| 55 | if (!incumbentId || incumbentName?.includes('.')) { |
| 56 | this.nameToBlockId.set(normalizedName, block.id) |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | for (const loop of Object.values(workflow.loops ?? {})) { |
| 61 | for (const blockId of loop.nodes ?? []) { |
| 62 | this.blockIdsInSubflows.add(blockId) |
| 63 | } |
| 64 | } |
| 65 | for (const parallel of Object.values(workflow.parallels ?? {})) { |
| 66 | for (const blockId of parallel.nodes ?? []) { |
| 67 | this.blockIdsInSubflows.add(blockId) |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | canResolve(reference: string): boolean { |
| 73 | if (!isReference(reference)) { |
| 74 | return false |
| 75 | } |
| 76 | const parts = parseReferencePath(reference) |
| 77 | if (parts.length === 0) { |
| 78 | return false |
| 79 | } |
| 80 | const [type] = parts |
| 81 | return !(SPECIAL_REFERENCE_PREFIXES as readonly string[]).includes(type) |
| 82 | } |
| 83 |
nothing calls this directly
no outgoing calls
no test coverage detected