(params: PromoteForkParams)
| 332 | * without mutating only when required references are unmapped. |
| 333 | */ |
| 334 | export async function promoteFork(params: PromoteForkParams): Promise<PromoteForkResult> { |
| 335 | const { edge, sourceWorkspaceId, targetWorkspaceId, direction, userId } = params |
| 336 | const requestId = params.requestId ?? 'unknown' |
| 337 | |
| 338 | // Distinguish an OMITTED dependent mapping (leave the store as-is) from an explicit empty |
| 339 | // array (clear it). When values are PROVIDED the apply map is plan-independent, so build it |
| 340 | // here - BEFORE the transaction - to keep the advisory lock tight (pure in-memory, no DB), |
| 341 | // mirroring how the source states are pre-loaded above. The OMITTED path needs the plan's |
| 342 | // replace targets, so it loads + builds inside the tx below. |
| 343 | const dependentValuesProvided = params.dependentValues !== undefined |
| 344 | const providedOverridesByWorkflow = dependentValuesProvided |
| 345 | ? groupDependentOverrides( |
| 346 | (params.dependentValues ?? []).map((entry) => ({ |
| 347 | targetWorkflowId: entry.workflowId, |
| 348 | targetBlockId: entry.blockId, |
| 349 | subBlockKey: entry.subBlockKey, |
| 350 | value: entry.value, |
| 351 | })) |
| 352 | ) |
| 353 | : null |
| 354 | |
| 355 | const targetMembers = (await getUsersWithPermissions(targetWorkspaceId)).map((m) => m.userId) |
| 356 | |
| 357 | // Read the source's deployed workflows + states BEFORE the transaction so these |
| 358 | // heavy per-workflow reads never check out a second pooled connection from inside |
| 359 | // the promote tx (which can deadlock the pool at saturation). The source is |
| 360 | // read-only here, so this pre-tx snapshot is exactly what gets force-pushed. |
| 361 | const { deployedWorkflows, sourceStates } = await loadSourceDeployedStates(sourceWorkspaceId) |
| 362 | |
| 363 | const txResult: PromoteTxBlocked | PromoteTxApplied = await db.transaction(async (tx) => { |
| 364 | // Bound lock waits so a contended sync into this target fails fast instead of |
| 365 | // stagnating the pool. Must run before acquiring the advisory locks below. |
| 366 | await setForkLockTimeout(tx) |
| 367 | // Target lock before edge lock (consistent ordering): the target lock serializes |
| 368 | // every sync into this target so sibling forks can't interleave writes, and so |
| 369 | // rollback's "newest sync" check stays race-free against a concurrent promote. |
| 370 | await acquireForkTargetLock(tx, targetWorkspaceId) |
| 371 | await acquireForkEdgeLock(tx, edge.childWorkspaceId) |
| 372 | |
| 373 | const plan = await computeForkPromotePlan({ |
| 374 | executor: tx, |
| 375 | edge, |
| 376 | sourceWorkspaceId, |
| 377 | targetWorkspaceId, |
| 378 | direction, |
| 379 | deployedSourceWorkflows: deployedWorkflows, |
| 380 | sourceStates, |
| 381 | }) |
| 382 | |
| 383 | const now = new Date() |
| 384 | |
| 385 | // Copy the selected referenced-but-unmapped resources into the target BEFORE the gate, so a |
| 386 | // user can copy rather than map each one. The gate is evaluated against the post-copy state |
| 387 | // (the copy resolves the selected refs), so the copy only runs when the sync will actually |
| 388 | // proceed - if required refs remain unmapped, we block without copying anything. |
| 389 | const { selection: copySelection, willResolve } = buildPromoteCopySelection( |
| 390 | params.copyResources, |
| 391 | plan.copyableUnmapped |
no test coverage detected