diff --git a/angular.json b/angular.json index 983dea2..e6fd4db 100644 --- a/angular.json +++ b/angular.json @@ -93,7 +93,10 @@ "defaultConfiguration": "development" }, "test": { - "builder": "@angular/build:unit-test" + "builder": "@angular/build:unit-test", + "options": { + "runnerConfig": "vitest.config.ts" + } } } } diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index e276437..fdfe25c 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -64,6 +64,7 @@ export class ReteEditor implements OnChanges, OnDestroy { private rete?: ReteEditorInstance; private viewReady = false; private loadVersion = 0; + private destroyed = false; private suppressDirtyEvents = false; private typesLoadingPromise: Promise | null = null; creatingEmptyBlock = false; @@ -127,6 +128,10 @@ export class ReteEditor implements OnChanges, OnDestroy { } ngOnDestroy(): void { + // A load still under way would go on mounting nodes with this component's injector, which is + // about to be destroyed. Moving the version on makes that load stop at its next node. + this.loadVersion++; + this.destroyed = true; this.rete?.area?.destroy?.(); this.rete = undefined; this.graphSelection.clearConnectionSelection(); @@ -296,6 +301,7 @@ export class ReteEditor implements OnChanges, OnDestroy { if (!host) return; await this.ensureNodeTypesLoaded(); + if (this.destroyed) return; const currentVersion = ++this.loadVersion; this.editorMode.set('standard'); @@ -311,7 +317,8 @@ export class ReteEditor implements OnChanges, OnDestroy { const rete = await createEditor(host, this.injector, this.flowData(), { nodeView: this.nodeView(), - readonly: this.readonly() + readonly: this.readonly(), + isStale: () => currentVersion !== this.loadVersion }); if (currentVersion !== this.loadVersion) { rete.area.destroy(); diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 5fa19bd..b5b255b 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -85,7 +85,12 @@ export async function createEditor( container: HTMLElement, injector: Injector, flowData: FlowData, - options?: { nodeView?: "editor" | "execution"; readonly?: boolean } + options?: { + nodeView?: "editor" | "execution"; + readonly?: boolean; + /** True once the caller no longer wants this editor; loading stops at the next node. */ + isStale?: () => boolean; + } ): Promise { const editor = new NodeEditor(); @@ -193,6 +198,15 @@ export async function createEditor( AreaExtensions.simpleNodesOrder(area); + const isStale = options?.isStale ?? (() => false); + // Registered before the Angular renderer, so it sees each render first. An editor its caller has + // given up on - the component destroyed, or a newer load begun - must not mount anything more: + // the components would be created with an injector that is already gone. + area.addPipe((context: any) => { + if (context?.type === 'render' && isStale()) return; + return context; + }); + area.addPipe((context: any) => { if (readonly && nodeView !== "execution" && context?.type === 'nodetranslate') { const nodeId = String(context?.data?.id ?? ''); @@ -211,7 +225,8 @@ export async function createEditor( }); if (flowData) - await loadFlowData(editor, area, flowData, runtime); + await loadFlowData(editor, area, flowData, runtime, isStale); + if (isStale()) return { editor, area }; await refreshLoopMarkers(editor, area, runtime); AreaExtensions.zoomAt(area, editor.getNodes()); @@ -656,7 +671,8 @@ async function loadFlowData( editor: NodeEditor, area: AreaPlugin, flowData: FlowData, - runtime?: ReteRuntimeContext + runtime?: ReteRuntimeContext, + isStale: () => boolean = () => false ) { const topLevelNodes = [...(flowData.blocks ?? []), ...(flowData.containers ?? [])]; if (!topLevelNodes.length) return; @@ -664,6 +680,7 @@ async function loadFlowData( const nodeMapping = new Map(); for (const [index, block] of topLevelNodes.entries()) { + if (isStale()) return; const fallbackPosition = block.position ?? { x: 120 + (index % 3) * 340, y: 100 + Math.floor(index / 3) * 220 @@ -674,6 +691,7 @@ async function loadFlowData( await withRestoredConnections(runtime, async () => { for (const c of flowData.connections ?? []) { + if (isStale()) return; if (!nodeMapping.has(c.sourceId) || !nodeMapping.has(c.targetId)) continue; const sourceNode = editor.getNode(nodeMapping.get(c.sourceId)) as any; @@ -688,6 +706,7 @@ async function loadFlowData( } for (const dependency of flowData.dependencies ?? []) { + if (isStale()) return; if (!nodeMapping.has(dependency.sourceId) || !nodeMapping.has(dependency.targetId)) continue; const sourceNode = editor.getNode(nodeMapping.get(dependency.sourceId)) as any; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..49a632a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + server: { + deps: { + inline: [/node_modules\/rete/] + } + } + } +});