Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

264 lines
8.0 KiB
TypeScript

import { LOOP, PARALLEL } from '@/executor/constants'
/**
* Single source of truth for parsing and building subflow node IDs.
*
* Runtime node IDs layer several encodings onto a workflow-level block ID:
* - Branch subscripts `₍N₎` mark a parallel branch instance.
* - Loop/parallel sentinels wrap a container ID (`loop-{id}-sentinel-start`).
* - Outer-branch clone suffixes `__obranch-N` and clone digests `__clone{hex}`
* scope a cloned subflow to a global outer parallel branch.
* - Loop digests `_loopN` scope a block output to a loop iteration.
*
* All regexes and string templates for these encodings live here so callers
* never reconstruct them inline.
*/
const SENTINEL = {
LOOP_START: new RegExp(`${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.START_SUFFIX}`),
LOOP_END: new RegExp(`${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.END_SUFFIX}`),
PARALLEL_START: new RegExp(`${PARALLEL.SENTINEL.PREFIX}(.+)${PARALLEL.SENTINEL.START_SUFFIX}`),
PARALLEL_END: new RegExp(`${PARALLEL.SENTINEL.PREFIX}(.+)${PARALLEL.SENTINEL.END_SUFFIX}`),
} as const
const BRANCH = {
MATCH: new RegExp(`${PARALLEL.BRANCH.PREFIX}\\d+${PARALLEL.BRANCH.SUFFIX}$`),
INDEX: new RegExp(`${PARALLEL.BRANCH.PREFIX}(\\d+)${PARALLEL.BRANCH.SUFFIX}$`),
SUFFIX: /₍\d+₎/u,
SUFFIX_GLOBAL: /₍\d+₎/gu,
} as const
const OUTER_BRANCH = {
MATCH: /__obranch-(\d+)/,
MATCH_GLOBAL: /__obranch-(\d+)/g,
STRIP: /__obranch-\d+/g,
} as const
const CLONE = {
DIGEST_STRIP: /__clone[0-9a-f]+/gi,
MARKER: '__clone',
} as const
const LOOP_DIGEST = {
MATCH: /_loop\d+/,
STRIP: /_loop\d+/g,
} as const
/**
* Builds the loop sentinel-start node ID for a container.
*/
function buildLoopSentinelStartId(loopId: string): string {
return `${LOOP.SENTINEL.PREFIX}${loopId}${LOOP.SENTINEL.START_SUFFIX}`
}
/**
* Builds the loop sentinel-end node ID for a container.
*/
function buildLoopSentinelEndId(loopId: string): string {
return `${LOOP.SENTINEL.PREFIX}${loopId}${LOOP.SENTINEL.END_SUFFIX}`
}
/**
* Builds the parallel sentinel-start node ID for a container.
*/
function buildParallelSentinelStartId(parallelId: string): string {
return `${PARALLEL.SENTINEL.PREFIX}${parallelId}${PARALLEL.SENTINEL.START_SUFFIX}`
}
/**
* Builds the parallel sentinel-end node ID for a container.
*/
function buildParallelSentinelEndId(parallelId: string): string {
return `${PARALLEL.SENTINEL.PREFIX}${parallelId}${PARALLEL.SENTINEL.END_SUFFIX}`
}
function isLoopSentinelNodeId(nodeId: string): boolean {
return (
nodeId.startsWith(LOOP.SENTINEL.PREFIX) &&
(nodeId.endsWith(LOOP.SENTINEL.START_SUFFIX) || nodeId.endsWith(LOOP.SENTINEL.END_SUFFIX))
)
}
function isParallelSentinelNodeId(nodeId: string): boolean {
return (
nodeId.startsWith(PARALLEL.SENTINEL.PREFIX) &&
(nodeId.endsWith(PARALLEL.SENTINEL.START_SUFFIX) ||
nodeId.endsWith(PARALLEL.SENTINEL.END_SUFFIX))
)
}
function extractLoopIdFromSentinel(sentinelId: string): string | null {
const startMatch = sentinelId.match(SENTINEL.LOOP_START)
if (startMatch) return startMatch[1]
const endMatch = sentinelId.match(SENTINEL.LOOP_END)
if (endMatch) return endMatch[1]
return null
}
function extractParallelIdFromSentinel(sentinelId: string): string | null {
const startMatch = sentinelId.match(SENTINEL.PARALLEL_START)
if (startMatch) return startMatch[1]
const endMatch = sentinelId.match(SENTINEL.PARALLEL_END)
if (endMatch) return endMatch[1]
return null
}
function buildBranchNodeId(baseId: string, branchIndex: number): string {
return `${baseId}${PARALLEL.BRANCH.PREFIX}${branchIndex}${PARALLEL.BRANCH.SUFFIX}`
}
function extractBaseBlockId(branchNodeId: string): string {
return branchNodeId.replace(BRANCH.MATCH, '')
}
function extractBranchIndex(branchNodeId: string): number | null {
const match = branchNodeId.match(BRANCH.INDEX)
return match ? Number.parseInt(match[1], 10) : null
}
function isBranchNodeId(nodeId: string): boolean {
return BRANCH.MATCH.test(nodeId)
}
function extractOuterBranchIndex(clonedId: string): number | undefined {
const match = clonedId.match(OUTER_BRANCH.MATCH)
return match ? Number.parseInt(match[1], 10) : undefined
}
function extractInnermostOuterBranchIndex(clonedId: string): number | undefined {
const matches = Array.from(clonedId.matchAll(OUTER_BRANCH.MATCH_GLOBAL))
const lastMatch = matches.at(-1)
return lastMatch ? Number.parseInt(lastMatch[1], 10) : undefined
}
function stripCloneSuffixes(nodeId: string): string {
return extractBaseBlockId(nodeId.replace(OUTER_BRANCH.STRIP, '').replace(CLONE.DIGEST_STRIP, ''))
}
function buildOuterBranchScopedId(originalId: string, branchIndex: number): string {
return `${originalId}__obranch-${branchIndex}`
}
function stripOuterBranchSuffix(id: string): string {
return id.replace(OUTER_BRANCH.STRIP, '').replace(CLONE.DIGEST_STRIP, '')
}
function hasCloneMarker(id: string): boolean {
return id.includes(CLONE.MARKER)
}
function normalizeNodeId(nodeId: string): string {
if (isBranchNodeId(nodeId)) {
return extractBaseBlockId(nodeId)
}
if (isLoopSentinelNodeId(nodeId)) {
return extractLoopIdFromSentinel(nodeId) || nodeId
}
if (isParallelSentinelNodeId(nodeId)) {
return extractParallelIdFromSentinel(nodeId) || nodeId
}
return nodeId
}
function findEffectiveContainerId(
originalId: string,
currentNodeId: string,
executionMap: Map<string, unknown>,
mappedBranchIndex?: number
): string {
if (mappedBranchIndex !== undefined && mappedBranchIndex > 0) {
const cloneSuffix = `__obranch-${mappedBranchIndex}`
const candidateId = buildOuterBranchScopedId(originalId, mappedBranchIndex)
if (executionMap.has(candidateId)) {
return candidateId
}
for (const scopeId of executionMap.keys()) {
if (scopeId.endsWith(cloneSuffix) && stripOuterBranchSuffix(scopeId) === originalId) {
return scopeId
}
}
}
const match = currentNodeId.match(OUTER_BRANCH.MATCH)
if (match) {
const branchIndex = Number.parseInt(match[1], 10)
const cloneSuffix = `__obranch-${branchIndex}`
if (hasCloneMarker(currentNodeId)) {
for (const scopeId of executionMap.keys()) {
if (
hasCloneMarker(scopeId) &&
scopeId.endsWith(cloneSuffix) &&
stripOuterBranchSuffix(scopeId) === originalId
) {
return scopeId
}
}
}
const candidateId = buildOuterBranchScopedId(originalId, branchIndex)
if (executionMap.has(candidateId)) {
return candidateId
}
for (const scopeId of executionMap.keys()) {
if (scopeId.endsWith(cloneSuffix) && stripOuterBranchSuffix(scopeId) === originalId) {
return scopeId
}
}
}
return originalId
}
/**
* Strips branch subscripts (`₍N₎`) and loop digests (`_loopN`) from a node ID,
* yielding the lookup key used by execution-state block-output resolution.
*/
function normalizeLookupId(id: string): string {
return id.replace(BRANCH.SUFFIX_GLOBAL, '').replace(LOOP_DIGEST.STRIP, '')
}
/**
* Returns the leading branch subscript (`₍N₎`) of a node ID, or '' when absent.
*/
function extractBranchSuffix(id: string): string {
return id.match(BRANCH.SUFFIX)?.[0] ?? ''
}
/**
* Returns the loop digest segment (`_loopN`) of a node ID, or '' when absent.
*/
function extractLoopSuffix(id: string): string {
return id.match(LOOP_DIGEST.MATCH)?.[0] ?? ''
}
/**
* Codec exposing all subflow node-ID parsing/building operations as a single,
* pattern-free interface. Implementation owns every regex and string template.
*/
export const SubflowNodeIdCodec = {
buildLoopSentinelStartId,
buildLoopSentinelEndId,
buildParallelSentinelStartId,
buildParallelSentinelEndId,
isLoopSentinelNodeId,
isParallelSentinelNodeId,
extractLoopIdFromSentinel,
extractParallelIdFromSentinel,
buildBranchNodeId,
extractBaseBlockId,
extractBranchIndex,
isBranchNodeId,
extractOuterBranchIndex,
extractInnermostOuterBranchIndex,
stripCloneSuffixes,
buildOuterBranchScopedId,
stripOuterBranchSuffix,
normalizeNodeId,
findEffectiveContainerId,
normalizeLookupId,
extractBranchSuffix,
extractLoopSuffix,
} as const