(waitId)
| 512 | }, |
| 513 | |
| 514 | async continueRun(waitId) { |
| 515 | const state = get() |
| 516 | if (state.runningBranchId !== null) return |
| 517 | // Only runnable Waits: blocked (parent not done) and running are not. |
| 518 | const ws = state.waitStates[waitId] |
| 519 | if (ws !== 'pending' && ws !== 'done' && ws !== 'error') return |
| 520 | // A pending Wait only runs after a clean handoff. If the run errored in the |
| 521 | // pre-phase, it never handed off — don't start a branch with missing inputs. |
| 522 | if (ws === 'pending' && state.runState.status === 'error') return |
| 523 | const ctx = _ctx.current |
| 524 | if (!ctx) { |
| 525 | console.warn('continueRun: no active run context — was the module hot-reloaded mid-run?') |
| 526 | return |
| 527 | } |
| 528 | |
| 529 | const branch = ctx.branches.get(waitId) ?? [] |
| 530 | // Re-running a Wait invalidates everything downstream: descendant branches |
| 531 | // were computed against the old output, so drop their outputs and reset |
| 532 | // them to blocked until this branch produces a fresh result. |
| 533 | const descendants = descendantWaits(waitId, ctx) |
| 534 | |
| 535 | _cancel.current = false |
| 536 | |
| 537 | // Reset outputs for this branch's nodes so Retry re-executes cleanly. |
| 538 | for (const node of branch) ctx.nodeOutputs.delete(node.id) |
| 539 | for (const d of descendants) for (const node of ctx.branches.get(d) ?? []) ctx.nodeOutputs.delete(node.id) |
| 540 | |
| 541 | set((s) => { |
| 542 | const waitStates = { ...s.waitStates, [waitId]: 'running' as WaitState } |
| 543 | for (const d of descendants) waitStates[d] = 'blocked' |
| 544 | return { |
| 545 | runningBranchId: waitId, |
| 546 | waitStates, |
| 547 | runState: { ...s.runState, status: 'running', blockIndex: 0, blockTotal: branch.length, blockProgress: 0, blockStep: branch.length === 0 ? 'Done' : 'Starting…' }, |
| 548 | } |
| 549 | }) |
| 550 | |
| 551 | const finishBranch = (next: WaitState, err?: string): void => { |
| 552 | if (_cancel.current) return |
| 553 | const newWaitStates = { ...get().waitStates, [waitId]: next } |
| 554 | // Unblock nested Waits whose parent branch just finished, and push this |
| 555 | // branch's scene output to the viewer. |
| 556 | if (next === 'done') { |
| 557 | for (const w of ctx.waitIds) { |
| 558 | if (ctx.parentWait.get(w) === waitId && newWaitStates[w] === 'blocked') newWaitStates[w] = 'pending' |
| 559 | } |
| 560 | pushBranchSceneMesh(ctx, waitId) |
| 561 | } |
| 562 | // A failed branch can never feed its descendants — surface them as error |
| 563 | // too, otherwise they stay 'blocked' and the run hangs on 'paused' forever. |
| 564 | if (next === 'error') { |
| 565 | for (const d of descendantWaits(waitId, ctx)) { |
| 566 | if (newWaitStates[d] === 'blocked') newWaitStates[d] = 'error' |
| 567 | } |
| 568 | } |
| 569 | const allFinished = ctx.waitIds.every((id) => newWaitStates[id] === 'done' || newWaitStates[id] === 'error') |
| 570 | const anyError = ctx.waitIds.some((id) => newWaitStates[id] === 'error') |
| 571 |
no test coverage detected