Give the Rete plugins one rete in tests, and stop an abandoned editor load
Five unhandled rejections came with every test run, from the pages that render a real graph. In tests, rete-angular-plugin was loaded by Node and got rete's CommonJS build while everything else got its ES module, so the renderer was not a Scope of the rete it was plugged into. Vitest now processes the rete packages itself, and all of them share one rete. With the graph actually built, a real bug showed: an editor still loading when its component was destroyed went on mounting nodes with the component's destroyed injector. A load now stops once its component is destroyed or a newer load begins - between nodes and connections, and at every render, which is dropped for an abandoned editor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7477027319
commit
7c30ba1a38
|
|
@ -93,7 +93,10 @@
|
|||
"defaultConfiguration": "development"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"runnerConfig": "vitest.config.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<void> | 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();
|
||||
|
|
|
|||
|
|
@ -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<ReteEditorInstance> {
|
||||
|
||||
const editor = new NodeEditor<HFSchemes>();
|
||||
|
|
@ -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<HFSchemes>,
|
||||
area: AreaPlugin<HFSchemes, AreaExtra>,
|
||||
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<string, any>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/node_modules\/rete/]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue