DAG: execute recompute levels concurrently
This commit is contained in:
@@ -133,6 +133,7 @@ const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
|
||||
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
|
||||
const systemCommands = new Set(['new-document', 'save', 'select-object'])
|
||||
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'fillet', 'chamfer', 'solve-sketch'])
|
||||
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'fillet', 'chamfer'])
|
||||
const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
'create-body': { label: 'Body', detail: 'Part Design body' },
|
||||
'create-sketch': { label: 'Sketch', detail: 'Fully constrained' },
|
||||
@@ -146,7 +147,8 @@ const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedO
|
||||
const known = systemCommands.has(commandId) || Object.values(workbenchDefinitions).some((definition) => definition.groups.some((group) => group.commands.some((command) => command.id === commandId)))
|
||||
if (!known) return { id: commandId, status: 'disabled', reason: 'Command is not registered in the active manifest.' }
|
||||
if (!implementedCommandIds.has(commandId)) return { id: commandId, status: 'disabled', reason: 'Command is visible in the FreeCAD-compatible manifest but its BitBybit business executor is not implemented yet.' }
|
||||
if (commandId === 'pad' && activeWorkbench !== 'Part Design') return { id: commandId, status: 'disabled', reason: 'Switch to Part Design to use Pad.' }
|
||||
if (partDesignCommands.has(commandId) && activeWorkbench !== 'Part Design') return { id: commandId, status: 'disabled', reason: `Switch to Part Design to use ${commandId}.` }
|
||||
if ((commandId === 'new-sketch' || commandId === 'solve-sketch') && activeWorkbench !== 'Sketcher') return { id: commandId, status: 'disabled', reason: `Switch to Sketcher to use ${commandId}.` }
|
||||
if (selectionRequired.has(commandId) && !selectedObjectId) return { id: commandId, status: 'disabled', reason: 'Select a compatible object or sub-shape first.' }
|
||||
return { id: commandId, status: 'enabled' }
|
||||
}
|
||||
|
||||
@@ -128,52 +128,61 @@ export class RecomputeCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
for (const objectId of plan.order) {
|
||||
let processed = 0
|
||||
for (const level of plan.levels) {
|
||||
if (controller.signal.aborted || this.active?.generation !== generation) return terminalResult('cancelled')
|
||||
if (this.currentDocumentVersion(document.id) !== document.version) return terminalResult('stale')
|
||||
|
||||
const failedDependency = graph.dependenciesOf(objectId).find((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')
|
||||
if (failedDependency) {
|
||||
objectStates[objectId] = 'upstream-failed'
|
||||
skipped.push(objectId)
|
||||
errors.push({ objectId, code: 'UPSTREAM_FAILED', message: `Dependency ${failedDependency} did not recompute successfully.` })
|
||||
options.onProgress?.({ generation, documentVersion: document.version, objectId, completed: completed.length, total: plan.order.length, state: 'upstream-failed' })
|
||||
continue
|
||||
}
|
||||
// Nodes in a level have no dependencies on each other. Execute them together,
|
||||
// then merge outcomes in plan order so persistence and UI events stay deterministic.
|
||||
const outcomes = await Promise.all(level.map(async (objectId) => {
|
||||
const failedDependency = graph.dependenciesOf(objectId).find((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')
|
||||
if (failedDependency) return { objectId, state: 'upstream-failed' as const, error: { objectId, code: 'UPSTREAM_FAILED', message: `Dependency ${failedDependency} did not recompute successfully.` } }
|
||||
|
||||
const object = objectById.get(objectId)
|
||||
if (!object) {
|
||||
objectStates[objectId] = 'error'
|
||||
failed.push(objectId)
|
||||
errors.push({ objectId, code: 'OBJECT_NOT_FOUND', message: `Document object does not exist: ${objectId}` })
|
||||
continue
|
||||
}
|
||||
const object = objectById.get(objectId)
|
||||
if (!object) return { objectId, state: 'error' as const, error: { objectId, code: 'OBJECT_NOT_FOUND', message: `Document object does not exist: ${objectId}` } }
|
||||
|
||||
try {
|
||||
const result = await this.executeNode(object, document, {
|
||||
documentId: document.id,
|
||||
documentVersion: document.version,
|
||||
generation,
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (controller.signal.aborted || this.active?.generation !== generation) return terminalResult('cancelled')
|
||||
if (this.currentDocumentVersion(document.id) !== document.version) return terminalResult('stale')
|
||||
if (result.status === 'failed') {
|
||||
objectStates[objectId] = 'error'
|
||||
failed.push(objectId)
|
||||
errors.push(...(result.errors?.length ? result.errors : [{ objectId, code: 'RECOMPUTE_FAILED', message: `${objectId} failed to recompute.` }]))
|
||||
} else {
|
||||
objectStates[objectId] = 'up-to-date'
|
||||
completed.push(objectId)
|
||||
if (result.updatedObject) objectUpdates.push(result.updatedObject)
|
||||
try {
|
||||
const result = await this.executeNode(object, document, {
|
||||
documentId: document.id,
|
||||
documentVersion: document.version,
|
||||
generation,
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (result.status === 'failed') {
|
||||
return {
|
||||
objectId,
|
||||
state: 'error' as const,
|
||||
errors: result.errors?.length ? result.errors : [{ objectId, code: 'RECOMPUTE_FAILED', message: `${objectId} failed to recompute.` }],
|
||||
}
|
||||
}
|
||||
return { objectId, state: 'up-to-date' as const, updatedObject: result.updatedObject }
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || isAbortError(error)) return { objectId, state: 'cancelled' as const }
|
||||
return { objectId, state: 'error' as const, error: { objectId, code: 'RECOMPUTE_EXCEPTION', message: error instanceof Error ? error.message : String(error) } }
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || isAbortError(error)) return terminalResult('cancelled')
|
||||
objectStates[objectId] = 'error'
|
||||
failed.push(objectId)
|
||||
errors.push({ objectId, code: 'RECOMPUTE_EXCEPTION', message: error instanceof Error ? error.message : String(error) })
|
||||
}))
|
||||
|
||||
if (controller.signal.aborted || this.active?.generation !== generation) return terminalResult('cancelled')
|
||||
if (this.currentDocumentVersion(document.id) !== document.version) return terminalResult('stale')
|
||||
|
||||
for (const outcome of outcomes) {
|
||||
if (outcome.state === 'cancelled') return terminalResult('cancelled')
|
||||
objectStates[outcome.objectId] = outcome.state
|
||||
processed += 1
|
||||
if (outcome.state === 'up-to-date') {
|
||||
completed.push(outcome.objectId)
|
||||
if (outcome.updatedObject) objectUpdates.push(outcome.updatedObject)
|
||||
} else if (outcome.state === 'upstream-failed') {
|
||||
skipped.push(outcome.objectId)
|
||||
if (outcome.error) errors.push(outcome.error)
|
||||
} else {
|
||||
failed.push(outcome.objectId)
|
||||
if ('errors' in outcome && outcome.errors) errors.push(...outcome.errors)
|
||||
else if (outcome.error) errors.push(outcome.error)
|
||||
}
|
||||
options.onProgress?.({ generation, documentVersion: document.version, objectId: outcome.objectId, completed: processed, total: plan.order.length, state: outcome.state })
|
||||
}
|
||||
options.onProgress?.({ generation, documentVersion: document.version, objectId, completed: completed.length, total: plan.order.length, state: objectStates[objectId] })
|
||||
}
|
||||
|
||||
if (this.active?.generation === generation) this.active = null
|
||||
|
||||
Reference in New Issue
Block a user