Link storage operations to a Storage node on the canvas
A Storage node has a 'Used by' handle and a storage operation a 'Works on' one; the link drawn between them, teal and dotted, is what sets the operation's storageNode. It is neither a connection nor a dependency: it carries no value and orders nothing, and loops leave it out. An operation works on one node, so a new link replaces the old one, and removing the link empties the field. On load the links are drawn back from what each operation says. Fields the schema marks x-ui-hidden are left out of every form and chip - storageNode is set by the link, not typed - and kept in the configuration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a7d139535
commit
ee2696814c
|
|
@ -278,6 +278,15 @@ export type GroupedFlowValidation = {
|
|||
export const FLOW_DEPENDANT_PORT_KEY = '__dependant';
|
||||
export const FLOW_DEPENDENCY_PORT_KEY = '__dependency';
|
||||
export const FLOW_DEPENDENCY_SOCKET_TYPE = '__FLOW_DEPENDENCY__';
|
||||
/** A Storage node's handle, and a StorageOperation's: the link between them is not a connection. */
|
||||
export const FLOW_STORAGE_PORT_KEY = '__storage';
|
||||
export const FLOW_STORAGE_LINK_PORT_KEY = '__storageNode';
|
||||
export const FLOW_STORAGE_SOCKET_TYPE = '__FLOW_STORAGE__';
|
||||
|
||||
/** A block that works on a Storage node, linked to it rather than connected. */
|
||||
export function isStorageOperationNode(node: { typeName?: string; specificConfiguration?: Record<string, unknown> | null }): boolean {
|
||||
return node.typeName === 'StorageOperation' || node.specificConfiguration?.['type'] === 'StorageOperationBlockConfiguration';
|
||||
}
|
||||
|
||||
export function normalizeFlowValidationErrors(raw: unknown): FlowValidationError[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
DEFAULT_LOOP_MAX_ITERATIONS,
|
||||
FLOW_DEPENDANT_PORT_KEY,
|
||||
FLOW_DEPENDENCY_PORT_KEY,
|
||||
FLOW_STORAGE_LINK_PORT_KEY,
|
||||
FLOW_STORAGE_PORT_KEY,
|
||||
MAX_LOOP_MAX_ITERATIONS
|
||||
} from '@models/flow';
|
||||
import type { LoopAwareConnection } from '@utilities/rete-editor';
|
||||
|
|
@ -199,6 +201,11 @@ export class CustomConnectionComponent {
|
|||
@Input() end!: { x: number; y: number };
|
||||
@Input() path = '';
|
||||
|
||||
/** A storage operation's link to the Storage node it works on: not a connection, orders nothing. */
|
||||
get isStorageLink(): boolean {
|
||||
return this.data?.sourceOutput === FLOW_STORAGE_PORT_KEY && this.data?.targetInput === FLOW_STORAGE_LINK_PORT_KEY;
|
||||
}
|
||||
|
||||
get isDependencyConnection(): boolean {
|
||||
return this.data?.sourceOutput === FLOW_DEPENDANT_PORT_KEY && this.data?.targetInput === FLOW_DEPENDENCY_PORT_KEY;
|
||||
}
|
||||
|
|
@ -278,17 +285,19 @@ export class CustomConnectionComponent {
|
|||
if (this.isSelected) return '#f97316';
|
||||
if (this.isBiasRoutingChange) return '#b45309';
|
||||
if (this.isLoopBack) return '#7c3aed';
|
||||
if (this.isStorageLink) return '#0f766e';
|
||||
return this.isDependencyConnection ? '#7c8ba1' : '#4682b4';
|
||||
}
|
||||
|
||||
get strokeWidth(): number {
|
||||
if (this.isSelected) return this.isDependencyConnection ? 3.25 : 6;
|
||||
if (this.isSelected) return this.isDependencyConnection || this.isStorageLink ? 3.25 : 6;
|
||||
if (this.isBiasRoutingChange) return 6;
|
||||
return this.isDependencyConnection ? 2.25 : 5;
|
||||
return this.isDependencyConnection || this.isStorageLink ? 2.25 : 5;
|
||||
}
|
||||
|
||||
get strokeDasharray(): string | null {
|
||||
if (this.isLoopBack) return '12 7';
|
||||
if (this.isStorageLink) return '2 6';
|
||||
return this.isDependencyConnection ? '6 6' : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export class CustomSocket {
|
|||
@Input() emit!: any;
|
||||
@Input() rendered!: any;
|
||||
private side: "input" | "output" = "input";
|
||||
private kind: "data" | "dependency" = "data";
|
||||
private kind: "data" | "dependency" | "storage" = "data";
|
||||
|
||||
@HostBinding("title") get title() {
|
||||
return this.data.name;
|
||||
|
|
@ -34,6 +34,9 @@ export class CustomSocket {
|
|||
|
||||
@HostBinding("style.background")
|
||||
get bg() {
|
||||
if (this.kind === "storage") {
|
||||
return "linear-gradient(145deg, #2dd4bf 0%, #0f766e 100%)";
|
||||
}
|
||||
if (this.kind === "dependency") {
|
||||
return this.side === "input"
|
||||
? "linear-gradient(145deg, #fbbf24 0%, #d97706 100%)"
|
||||
|
|
@ -46,6 +49,9 @@ export class CustomSocket {
|
|||
|
||||
@HostBinding("style.boxShadow")
|
||||
get sh() {
|
||||
if (this.kind === "storage") {
|
||||
return "0 2px 6px rgba(15,118,110,0.45), 0 0 0 1px rgba(15,118,110,0.45)";
|
||||
}
|
||||
if (this.kind === "dependency") {
|
||||
const c = this.side === "input" ? "rgba(217,119,6,0.45)" : "rgba(37,99,235,0.45)";
|
||||
return `0 2px 6px ${c}, 0 0 0 1px ${c}`;
|
||||
|
|
@ -59,8 +65,9 @@ export class CustomSocket {
|
|||
return side === "output" ? "output" : "input";
|
||||
}
|
||||
|
||||
private resolveSocketKind(): "data" | "dependency" {
|
||||
return this.data?.__hfKind === "dependency" ? "dependency" : "data";
|
||||
private resolveSocketKind(): "data" | "dependency" | "storage" {
|
||||
const kind = this.data?.__hfKind;
|
||||
return kind === "dependency" || kind === "storage" ? kind : "data";
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
|
|
|
|||
|
|
@ -999,3 +999,9 @@
|
|||
background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);
|
||||
border-bottom-color: #99f6e4;
|
||||
}
|
||||
|
||||
.llm-pill-storage {
|
||||
border-color: #5eead4;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,6 +217,54 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if (storageLinkOutput || storageLinkInput) {
|
||||
<div class="llm-body llm-body-dependency llm-body-storage">
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title">Storage</div>
|
||||
@if (storageLinkInput; as storageLinkInput) {
|
||||
<div class="llm-row llm-row-input">
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: storageLinkInput.key,
|
||||
nodeId: data.id,
|
||||
__readonly: isReadonly,
|
||||
payload: storageLinkInput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input llm-pill-storage">Works on</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title llm-column-title-right"> </div>
|
||||
@if (storageLinkOutput; as storageLinkOutput) {
|
||||
<div class="llm-row llm-row-output">
|
||||
<span class="llm-pill llm-pill-output llm-pill-storage">Used by</span>
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket llm-socket-right"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: storageLinkOutput.key,
|
||||
nodeId: data.id,
|
||||
__readonly: isReadonly,
|
||||
payload: storageLinkOutput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasExecutionDependencyPorts()) {
|
||||
<div class="llm-body llm-body-dependency">
|
||||
<div class="llm-column">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, effect, ElementR
|
|||
import { FormsModule } from '@angular/forms';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { environment } from '@environment';
|
||||
import { BiasAnnotation, BiasAnnotationsDescriptor, BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowData, FlowGlobalInput, FlowPort, FlowValueKind, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, isProbeExecutable, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
import { BiasAnnotation, BiasAnnotationsDescriptor, BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowData, FlowGlobalInput, FlowPort, FlowValueKind, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY, isProbeExecutable, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
import { BiasAnnotationsComponent } from '../../bias-annotations/bias-annotations';
|
||||
import { NodeFocusModalController } from '../node-focus-modal-controller';
|
||||
import { ClassicPreset } from 'rete';
|
||||
|
|
@ -245,7 +245,7 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
|
||||
get outputs(): RenderedSocketPort[] {
|
||||
return Object.entries(this.data?.outputs ?? {})
|
||||
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY)
|
||||
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY && key !== FLOW_STORAGE_PORT_KEY)
|
||||
.map(([key, output]) => ({
|
||||
key,
|
||||
socket: (output as any).socket as ClassicPreset.Socket
|
||||
|
|
@ -254,13 +254,25 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
|
||||
get inputs(): RenderedSocketPort[] {
|
||||
return Object.entries(this.data?.inputs ?? {})
|
||||
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY)
|
||||
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY && key !== FLOW_STORAGE_LINK_PORT_KEY)
|
||||
.map(([key, input]) => ({
|
||||
key,
|
||||
socket: (input as any).socket as ClassicPreset.Socket
|
||||
}));
|
||||
}
|
||||
|
||||
/** A Storage node's handle, which storage operations are linked to. */
|
||||
get storageLinkOutput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const output = this.data?.outputs?.[FLOW_STORAGE_PORT_KEY];
|
||||
return output ? { key: FLOW_STORAGE_PORT_KEY, socket: (output as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
/** A storage operation's handle, linked to the Storage node it works on. */
|
||||
get storageLinkInput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const input = this.data?.inputs?.[FLOW_STORAGE_LINK_PORT_KEY];
|
||||
return input ? { key: FLOW_STORAGE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
get dependantOutput(): RenderedSocketPort | null {
|
||||
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -177,6 +177,9 @@ export function orderedSchemaPropertyEntries(
|
|||
isTechnical
|
||||
};
|
||||
})
|
||||
// Said by something else on the canvas - a link drawn between two nodes - not a field. Left
|
||||
// out of every form and chip, and kept in the configuration, which this does not touch.
|
||||
.filter((entry) => entry.schema?.['x-ui-hidden'] !== true)
|
||||
.sort((left, right) => {
|
||||
const leftBucket = left.priorityIndex != null ? 0 : left.isTechnical ? 2 : 1;
|
||||
const rightBucket = right.priorityIndex != null ? 0 : right.isTechnical ? 2 : 1;
|
||||
|
|
|
|||
|
|
@ -841,3 +841,9 @@ button.llm-node-bias-summary:focus-visible {
|
|||
background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);
|
||||
border-bottom-color: #99f6e4;
|
||||
}
|
||||
|
||||
.llm-pill-storage {
|
||||
border-color: #5eead4;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,54 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if (storageLinkOutput || storageLinkInput) {
|
||||
<div class="llm-body llm-body-dependency llm-body-storage">
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title">Storage</div>
|
||||
@if (storageLinkInput; as storageLinkInput) {
|
||||
<div class="llm-row llm-row-input">
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: storageLinkInput.key,
|
||||
nodeId: data.id,
|
||||
__readonly: true,
|
||||
payload: storageLinkInput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input llm-pill-storage">Works on</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title llm-column-title-right"> </div>
|
||||
@if (storageLinkOutput; as storageLinkOutput) {
|
||||
<div class="llm-row llm-row-output">
|
||||
<span class="llm-pill llm-pill-output llm-pill-storage">Used by</span>
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket llm-socket-right"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: storageLinkOutput.key,
|
||||
nodeId: data.id,
|
||||
__readonly: true,
|
||||
payload: storageLinkOutput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasExecutionDependencyPorts()) {
|
||||
<div class="llm-body llm-body-dependency">
|
||||
<div class="llm-column">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { CommonModule } from '@angular/common';
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, inject, viewChild } from '@angular/core';
|
||||
import { ClassicPreset } from 'rete';
|
||||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import { BiasAnnotation, BlockInteractionContract, BlockType, DEFAULT_NODE_CAPABILITIES, FlowData, FlowNode, FlowPort, isProbeExecutable, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, NodeTypeCapabilities } from '@models/flow';
|
||||
import { BiasAnnotation, BlockInteractionContract, BlockType, DEFAULT_NODE_CAPABILITIES, FlowData, FlowNode, FlowPort, isProbeExecutable, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY, NodeTypeCapabilities } from '@models/flow';
|
||||
import { BiasCapabilities } from '@models/bias-impact';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
|
|
@ -152,16 +152,28 @@ export class TaskStepNodeComponent {
|
|||
|
||||
get outputs(): { key: string; socket: ClassicPreset.Socket }[] {
|
||||
return Object.entries(this.data?.outputs ?? {})
|
||||
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY)
|
||||
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY && key !== FLOW_STORAGE_PORT_KEY)
|
||||
.map(([key, output]) => ({ key, socket: (output as any).socket as ClassicPreset.Socket }));
|
||||
}
|
||||
|
||||
get inputs(): { key: string; socket: ClassicPreset.Socket }[] {
|
||||
return Object.entries(this.data?.inputs ?? {})
|
||||
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY)
|
||||
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY && key !== FLOW_STORAGE_LINK_PORT_KEY)
|
||||
.map(([key, input]) => ({ key, socket: (input as any).socket as ClassicPreset.Socket }));
|
||||
}
|
||||
|
||||
/** A Storage node's handle, which storage operations are linked to. */
|
||||
get storageLinkOutput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const output = this.data?.outputs?.[FLOW_STORAGE_PORT_KEY];
|
||||
return output ? { key: FLOW_STORAGE_PORT_KEY, socket: (output as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
/** A storage operation's handle, linked to the Storage node it works on. */
|
||||
get storageLinkInput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const input = this.data?.inputs?.[FLOW_STORAGE_LINK_PORT_KEY];
|
||||
return input ? { key: FLOW_STORAGE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
get dependantOutput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -83,3 +83,34 @@ describe('isResourceNode', () => {
|
|||
expect(isResourceNode(undefined, makeNode('Storage', 'block'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the link between a Storage node and a storage operation', () => {
|
||||
it('is what says which node the operation works on, and nothing when it is gone', async () => {
|
||||
const { NodeEditor, ClassicPreset } = await import('rete');
|
||||
const { exportGraph } = await import('./rete-editor');
|
||||
const { FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY } = await import('@models/flow');
|
||||
const editor = new NodeEditor<any>();
|
||||
const socket = new ClassicPreset.Socket('__FLOW_STORAGE__');
|
||||
|
||||
const storage = new ClassicPreset.Node('Storage') as any;
|
||||
storage.data = { id: 'store-1', typeName: 'Storage', nodeFamily: 'block', specificConfiguration: { type: 'StorageNodeConfiguration' } };
|
||||
storage.addOutput(FLOW_STORAGE_PORT_KEY, new ClassicPreset.Output(socket));
|
||||
const operation = new ClassicPreset.Node('StorageOperation') as any;
|
||||
operation.data = { id: 'op-1', typeName: 'StorageOperation', nodeFamily: 'block',
|
||||
specificConfiguration: { type: 'StorageOperationBlockConfiguration', storageNode: 'stale' } };
|
||||
operation.addInput(FLOW_STORAGE_LINK_PORT_KEY, new ClassicPreset.Input(socket, undefined, true));
|
||||
await editor.addNode(storage);
|
||||
await editor.addNode(operation);
|
||||
|
||||
const link = new ClassicPreset.Connection(storage, FLOW_STORAGE_PORT_KEY, operation, FLOW_STORAGE_LINK_PORT_KEY);
|
||||
await editor.addConnection(link);
|
||||
let graph = exportGraph(editor);
|
||||
expect((graph.blocks.find((block) => block.id === 'op-1')?.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode']).toBe('store-1');
|
||||
expect(graph.connections).toEqual([]);
|
||||
expect(graph.dependencies).toEqual([]);
|
||||
|
||||
await editor.removeConnection(link.id);
|
||||
graph = exportGraph(editor);
|
||||
expect((graph.blocks.find((block) => block.id === 'op-1')?.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode']).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ import {
|
|||
FLOW_DEPENDANT_PORT_KEY,
|
||||
FLOW_DEPENDENCY_PORT_KEY,
|
||||
FLOW_DEPENDENCY_SOCKET_TYPE,
|
||||
FLOW_STORAGE_LINK_PORT_KEY,
|
||||
FLOW_STORAGE_PORT_KEY,
|
||||
FLOW_STORAGE_SOCKET_TYPE,
|
||||
isStorageOperationNode,
|
||||
FlowNode,
|
||||
NodeTypeCapabilities,
|
||||
normalizeFlowPortValueKinds
|
||||
|
|
@ -53,7 +57,7 @@ export type ReteEditorInstance = {
|
|||
area: AreaPlugin<HFSchemes, AreaExtra>;
|
||||
};
|
||||
|
||||
type GraphConnectionKind = "data" | "dependency";
|
||||
type GraphConnectionKind = "data" | "dependency" | "storage";
|
||||
|
||||
export type ReteRuntimeContext = {
|
||||
blocksService: BlocksService;
|
||||
|
|
@ -145,6 +149,13 @@ export async function createEditor(
|
|||
const targetCapabilities = resolveNodeCapabilities(runtime, targetNode);
|
||||
|
||||
const connectionKind = getGraphConnectionKind(context.data.sourceOutput, context.data.targetInput);
|
||||
if (connectionKind === "storage") {
|
||||
// From a Storage node to a storage operation, and one node per operation: a new link
|
||||
// replaces the operation's old one, the way a data input keeps to one connection.
|
||||
if (!isResourceNode(runtime, sourceNode)) return;
|
||||
if (!runtime.restoringConnections) await replaceStorageLink(editor, context.data.target);
|
||||
return context;
|
||||
}
|
||||
if (connectionKind === "dependency") {
|
||||
if (context.data.source === context.data.target) return;
|
||||
if (!sourceCapabilities.canHaveDependentNodes || !targetCapabilities.canDependOnOtherNodes) return;
|
||||
|
|
@ -281,6 +292,19 @@ export function exportGraph(editor: NodeEditor<HFSchemes>) {
|
|||
};
|
||||
});
|
||||
|
||||
// A storage operation says which Storage node it works on in its configuration; the link on the
|
||||
// canvas is how that is set, so the configuration follows the links as they now stand.
|
||||
const storageLinks = new Map(allConnections
|
||||
.filter((connection) => connection.kind === 'storage')
|
||||
.map((connection) => [connection.targetId, connection.sourceId] as const));
|
||||
for (const node of nodes) {
|
||||
if (!isStorageOperationNode(node as any)) continue;
|
||||
node.specificConfiguration = {
|
||||
...(node.specificConfiguration ?? {}),
|
||||
storageNode: storageLinks.get(node.id) ?? null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
blocks: nodes.filter((node): node is FlowBlock => node.nodeFamily === 'block'),
|
||||
containers: nodes.filter((node) => node.nodeFamily === 'container'),
|
||||
|
|
@ -638,6 +662,12 @@ export async function addBlockToEditor(
|
|||
};
|
||||
|
||||
const capabilities = resolveNodeCapabilities(resolvedRuntime, node);
|
||||
if (capabilities.visualRole === 'RESOURCE') {
|
||||
node.addOutput(FLOW_STORAGE_PORT_KEY, new ClassicPreset.Output(getSocket(editor, FLOW_STORAGE_SOCKET_TYPE)));
|
||||
}
|
||||
if (isStorageOperationNode(block as any)) {
|
||||
node.addInput(FLOW_STORAGE_LINK_PORT_KEY, new ClassicPreset.Input(getSocket(editor, FLOW_STORAGE_SOCKET_TYPE), undefined, true));
|
||||
}
|
||||
if (capabilities.canHaveDependentNodes) {
|
||||
node.addOutput(FLOW_DEPENDANT_PORT_KEY, new ClassicPreset.Output(getSocket(editor, FLOW_DEPENDENCY_SOCKET_TYPE)));
|
||||
}
|
||||
|
|
@ -718,9 +748,31 @@ async function loadFlowData(
|
|||
new ClassicPreset.Connection(sourceNode, FLOW_DEPENDANT_PORT_KEY, targetNode, FLOW_DEPENDENCY_PORT_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
// Drawn back from what each storage operation says it works on.
|
||||
for (const block of flowData.blocks ?? []) {
|
||||
if (isStale()) return;
|
||||
const storageNodeId = (block.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode'];
|
||||
if (!isStorageOperationNode(block as any) || typeof storageNodeId !== 'string') continue;
|
||||
if (!nodeMapping.has(storageNodeId) || !nodeMapping.has(block.id)) continue;
|
||||
const sourceNode = editor.getNode(nodeMapping.get(storageNodeId)) as any;
|
||||
const targetNode = editor.getNode(nodeMapping.get(block.id)) as any;
|
||||
await editor.addConnection(
|
||||
new ClassicPreset.Connection(sourceNode, FLOW_STORAGE_PORT_KEY, targetNode, FLOW_STORAGE_LINK_PORT_KEY)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** An operation works on one Storage node: linking it to another drops the link it had. */
|
||||
async function replaceStorageLink(editor: NodeEditor<HFSchemes>, operationNodeId: string) {
|
||||
const existing = editor.getConnections().filter((connection) =>
|
||||
connection.target === operationNodeId && connection.targetInput === FLOW_STORAGE_LINK_PORT_KEY);
|
||||
for (const connection of existing) {
|
||||
await editor.removeConnection(connection.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function withRestoredConnections<T>(runtime: ReteRuntimeContext | undefined, restore: () => Promise<T>): Promise<T> {
|
||||
if (!runtime) return restore();
|
||||
runtime.restoringConnections = (runtime.restoringConnections ?? 0) + 1;
|
||||
|
|
@ -743,11 +795,6 @@ type NewConnection = { source: string; sourceOutput: string; target: string; tar
|
|||
* new one replaces it, as dropping onto a taken input always did.
|
||||
*/
|
||||
async function makeRoomOnInput(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, created: NewConnection) {
|
||||
// A storage node's target takes every step that writes there. Its view parameters, named
|
||||
// <view>.<parameter>, take one value like any input.
|
||||
if (isResourceNode(runtime, editor.getNode(created.target) as HFNode | undefined) && !created.targetInput.includes('.')) {
|
||||
return false;
|
||||
}
|
||||
const newLeadsBack = leadsBack(editor, runtime, created, null);
|
||||
const existing = editor.getConnections().filter((connection) =>
|
||||
connection.target === created.target && connection.targetInput === created.targetInput);
|
||||
|
|
@ -766,8 +813,8 @@ function leadsBack(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, c
|
|||
const outgoing = new Map<string, string[]>();
|
||||
for (const candidate of editor.getConnections()) {
|
||||
if (candidate.id === ignoredConnectionId) continue;
|
||||
// A storage node is not a step: writing to it and reading from it is not a path round.
|
||||
if (touchesResource(editor, runtime, candidate)) continue;
|
||||
// A link to a Storage node orders nothing: it is not a path round.
|
||||
if (getGraphConnectionKind(candidate.sourceOutput, candidate.targetInput) === 'storage') continue;
|
||||
outgoing.set(candidate.source, [...(outgoing.get(candidate.source) ?? []), candidate.target]);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
|
|
@ -862,7 +909,7 @@ function getSocket(editor: NodeEditor<HFSchemes>, type: string) {
|
|||
const map = editorSockets.get(editor)!;
|
||||
if (!map.has(type)) {
|
||||
const socket = new ClassicPreset.Socket(type) as ClassicPreset.Socket & { __hfKind?: GraphConnectionKind };
|
||||
socket.__hfKind = type === FLOW_DEPENDENCY_SOCKET_TYPE ? 'dependency' : 'data';
|
||||
socket.__hfKind = type === FLOW_DEPENDENCY_SOCKET_TYPE ? 'dependency' : type === FLOW_STORAGE_SOCKET_TYPE ? 'storage' : 'data';
|
||||
map.set(type, socket);
|
||||
}
|
||||
return map.get(type)!;
|
||||
|
|
@ -872,11 +919,6 @@ export function isResourceNode(runtime: ReteRuntimeContext | undefined, node: HF
|
|||
return resolveNodeCapabilities(runtime, node).visualRole === 'RESOURCE';
|
||||
}
|
||||
|
||||
function touchesResource(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, connection: { source: string; target: string }) {
|
||||
return isResourceNode(runtime, editor.getNode(connection.source) as HFNode | undefined)
|
||||
|| isResourceNode(runtime, editor.getNode(connection.target) as HFNode | undefined);
|
||||
}
|
||||
|
||||
export function resolveNodeCapabilities(runtime: ReteRuntimeContext | undefined, node: HFNode | undefined): NodeTypeCapabilities {
|
||||
if (node?.data?.capabilities) return node.data.capabilities;
|
||||
|
||||
|
|
@ -922,7 +964,7 @@ function cloneValue<T>(value: T): T {
|
|||
}
|
||||
|
||||
function getGraphConnectionKind(sourceOutput: string, targetInput: string): GraphConnectionKind {
|
||||
return sourceOutput === FLOW_DEPENDANT_PORT_KEY && targetInput === FLOW_DEPENDENCY_PORT_KEY
|
||||
? 'dependency'
|
||||
: 'data';
|
||||
if (sourceOutput === FLOW_DEPENDANT_PORT_KEY && targetInput === FLOW_DEPENDENCY_PORT_KEY) return 'dependency';
|
||||
if (sourceOutput === FLOW_STORAGE_PORT_KEY && targetInput === FLOW_STORAGE_LINK_PORT_KEY) return 'storage';
|
||||
return 'data';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue