refactor: nodes as builder pattern
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
*
|
||||
* - **AbstractNodeProps<TData>** — Typed props (id, data, width?, height?, selected?) for your node.
|
||||
* - **useAbstractNode(id, data)** — Flow context plus helpers: nodes, edges, setNodes, setEdges,
|
||||
* updateData(partial), incomingEdges, outgoingEdges, sourceIds, targetIds.
|
||||
* updateData(partial), incomingEdges, outgoingEdges, sourceIds, targetIds. Calling updateData()
|
||||
* also reports this node as a trigger for connection path (lifecycle "trigger").
|
||||
* - **createAbstractNodeComponent(displayName, Component)** — Wraps with memo + nodePropsAreEqual.
|
||||
*
|
||||
* **Node lifecycle / connection status:** Nodes that can be updating, paused, or in error should
|
||||
* call useSyncConnectionStatus(id, { updating, error, paused }) from @/lib/nodeLifecycle so edge
|
||||
* colors and path animation stay correct. See nodeLifecycle.ts for the full contract.
|
||||
*
|
||||
* Example: define NodeData type, Props = AbstractNodeProps<NodeData>, use useAbstractNode in the
|
||||
* component, then export const MyNode = createAbstractNodeComponent('MyNode', MyNodeComponent).
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* Connection status: visual state of an edge (color/class).
|
||||
* Priority when multiple apply: error > paused > updating > default.
|
||||
* Nodes report state via FlowContext (e.g. addConnectionPathError); edges derive status here.
|
||||
*
|
||||
* Status is derived from node lifecycle state: nodes report updating / paused / error
|
||||
* via useSyncConnectionStatus() in nodeLifecycle.ts, which updates FlowContext sets.
|
||||
* Edges read those sets here to pick the single status per edge.
|
||||
*/
|
||||
|
||||
export type ConnectionStatus = 'default' | 'updating' | 'paused' | 'error'
|
||||
|
||||
98
frontend/src/lib/nodeLifecycle.ts
Normal file
98
frontend/src/lib/nodeLifecycle.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Node lifecycle: contract that nodes implement so the graph can show the right
|
||||
* connection status (edge colors) and path animation.
|
||||
*
|
||||
* ## Lifecycle phases (conceptual)
|
||||
*
|
||||
* - **Idle** – Node is not on an active path; edges to/from it use default style.
|
||||
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
||||
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
||||
* path is computed from triggers + updating nodes.
|
||||
* - **Updating** – Node is doing async work (e.g. agent running, renderer loading).
|
||||
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
||||
* edges on the path show "updating" (blue).
|
||||
* - **Paused** – Node is on hold (e.g. agent waiting for Run after inputs changed).
|
||||
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
||||
* segment show "paused" (yellow).
|
||||
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
||||
* `error: false` when cleared. Incoming edges to this node show "error" (red).
|
||||
*
|
||||
* ## Connection status integration
|
||||
*
|
||||
* Edge status is derived in getConnectionStatus() from the sets that this lifecycle
|
||||
* feeds: connectionPathUpdatingNodeIds, connectionPathPausedNodeIds,
|
||||
* connectionPathErrorNodeIds (plus path/paused segment from graphPath). Priority:
|
||||
* error > paused > updating > default.
|
||||
*
|
||||
* Nodes that can be updating, paused, or in error should call useSyncConnectionStatus()
|
||||
* with their current state so edges update correctly.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useRef } from 'react'
|
||||
import FlowContext from './flowContext'
|
||||
|
||||
export type NodeLifecyclePhase = 'idle' | 'trigger' | 'updating' | 'paused' | 'error'
|
||||
|
||||
/**
|
||||
* State that drives connection status for this node.
|
||||
* Pass the current values from your node; the hook syncs them to FlowContext.
|
||||
*/
|
||||
export type NodeConnectionStatusState = {
|
||||
/** Node is doing async work (e.g. loading, running). Incoming/outgoing path edges show blue. */
|
||||
updating?: boolean
|
||||
/** Node has an error. Incoming edges to this node show red. */
|
||||
error?: boolean
|
||||
/** Node is on hold (e.g. agent waiting for Run). Path edges in paused segment show yellow. */
|
||||
paused?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs this node's lifecycle state to FlowContext so connection status (edge colors)
|
||||
* and path animation are correct. Call once per node with the current updating/error/paused
|
||||
* state; the hook will add/remove this node from the appropriate sets.
|
||||
*
|
||||
* Use in any node that can be updating, in error, or paused:
|
||||
*
|
||||
* const [loading, setLoading] = useState(false)
|
||||
* const [error, setError] = useState(null)
|
||||
* const hasPendingInputs = ...
|
||||
* useSyncConnectionStatus(id, { updating: loading, error: !!error, paused: hasPendingInputs })
|
||||
*/
|
||||
export function useSyncConnectionStatus(
|
||||
nodeId: string,
|
||||
state: NodeConnectionStatusState
|
||||
): void {
|
||||
const ctx = useContext(FlowContext)
|
||||
const { updating, error, paused } = state
|
||||
const prevRef = useRef({ updating: false, error: false, paused: false })
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevRef.current
|
||||
const nowUpdating = Boolean(updating)
|
||||
const nowError = Boolean(error)
|
||||
const nowPaused = Boolean(paused)
|
||||
|
||||
if (prev.updating !== nowUpdating) {
|
||||
if (nowUpdating) ctx?.startConnectionPathUpdate?.(nodeId)
|
||||
else ctx?.endConnectionPathUpdate?.(nodeId)
|
||||
prev.updating = nowUpdating
|
||||
}
|
||||
if (prev.error !== nowError) {
|
||||
if (nowError) ctx?.addConnectionPathError?.(nodeId)
|
||||
else ctx?.removeConnectionPathError?.(nodeId)
|
||||
prev.error = nowError
|
||||
}
|
||||
if (prev.paused !== nowPaused) {
|
||||
if (nowPaused) ctx?.addConnectionPathPausedNode?.(nodeId)
|
||||
else ctx?.removeConnectionPathPausedNode?.(nodeId)
|
||||
prev.paused = nowPaused
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (prevRef.current.updating) ctx?.endConnectionPathUpdate?.(nodeId)
|
||||
if (prevRef.current.error) ctx?.removeConnectionPathError?.(nodeId)
|
||||
if (prevRef.current.paused) ctx?.removeConnectionPathPausedNode?.(nodeId)
|
||||
prevRef.current = { updating: false, error: false, paused: false }
|
||||
}
|
||||
}, [nodeId, updating, error, paused, ctx])
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Extensible node type registry. Register node types with registerNodeType();
|
||||
* built-in types are registered in registerBuiltinNodes.ts.
|
||||
* Extensible node type registry. Register node types with registerNodeType() or use NodeTypeBuilder.
|
||||
* Built-in types are registered in registerBuiltinNodes.ts.
|
||||
* Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI.
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import type { Node } from '@xyflow/react'
|
||||
import { registerSourceRenderingLogic } from './sourceRenderingLogic'
|
||||
import type { SourceRenderingLogic } from './sourceRenderingLogic'
|
||||
|
||||
/** Node classification for UI: Psyche, Pneuma, Physis, Archon (AI Agent). */
|
||||
export type NodeClassification = 'psyche' | 'pneuma' | 'physis' | 'archon'
|
||||
@@ -47,6 +48,8 @@ export type NodeTypeDescriptor = {
|
||||
connectionLabel?: string
|
||||
/** When true, double-clicking the node header opens a fullscreen dialog for this node. */
|
||||
supportsFullscreen?: boolean
|
||||
/** When set, this type can feed the Renderer; registration will also register source rendering logic. */
|
||||
sourceRenderingLogic?: SourceRenderingLogic
|
||||
}
|
||||
|
||||
const registry = new Map<string, NodeTypeDescriptor>()
|
||||
@@ -55,6 +58,9 @@ export function registerNodeType(descriptor: NodeTypeDescriptor): void {
|
||||
if (registry.has(descriptor.id)) {
|
||||
console.warn(`[nodeRegistry] Overwriting existing node type: ${descriptor.id}`)
|
||||
}
|
||||
if (descriptor.sourceRenderingLogic) {
|
||||
registerSourceRenderingLogic(descriptor.id, descriptor.sourceRenderingLogic)
|
||||
}
|
||||
registry.set(descriptor.id, descriptor)
|
||||
}
|
||||
|
||||
|
||||
165
frontend/src/lib/nodeTypeBuilder.ts
Normal file
165
frontend/src/lib/nodeTypeBuilder.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Fluent builder for NodeTypeDescriptor. Use to define node types with common behavior
|
||||
* and optional source rendering logic (for types that feed the Renderer).
|
||||
*
|
||||
* Example:
|
||||
* createNodeTypeBuilder('config', ConfigNode, { width: 320, height: 320 }, { configType: 'plantuml', content: '', title: '' })
|
||||
* .idPrefix('cfg_')
|
||||
* .withInputOutput(true, true)
|
||||
* .classification('psyche')
|
||||
* .allowedSourceTypes(['config', 'variable', 'function', 'data'])
|
||||
* .allowedTargetTypes(['config', 'render', 'agent'])
|
||||
* .help(NODE_HELP.config)
|
||||
* .menu('Config', <ScrollText />)
|
||||
* .connectionLabel('adding input')
|
||||
* .withFullscreen()
|
||||
* .sourceRenderingLogic({ defaultUpdateMode: 'auto', getResolvedContent: ... })
|
||||
* .build()
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import type { NodeTypeDescriptor, NodeClassification, NodeHelpEntry } from './nodeRegistry'
|
||||
import type { SourceRenderingLogic } from './sourceRenderingLogic'
|
||||
|
||||
type OptionalDescriptor = Partial<
|
||||
Omit<
|
||||
NodeTypeDescriptor,
|
||||
'id' | 'component' | 'defaultStyle' | 'defaultData' | 'idPrefix' | 'hasInput' | 'hasOutput' | 'help' | 'menuLabel' | 'menuIcon'
|
||||
>
|
||||
>
|
||||
|
||||
export class NodeTypeBuilder {
|
||||
private readonly id: string
|
||||
private readonly component: React.ComponentType<any>
|
||||
private readonly defaultStyle: { width: number; height: number }
|
||||
private readonly defaultData: Record<string, unknown>
|
||||
private partial: OptionalDescriptor & {
|
||||
idPrefix?: string
|
||||
hasInput?: boolean
|
||||
hasOutput?: boolean
|
||||
help?: NodeHelpEntry
|
||||
menuLabel?: string
|
||||
menuIcon?: React.ReactNode
|
||||
} = {}
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
component: React.ComponentType<any>,
|
||||
defaultStyle: { width: number; height: number },
|
||||
defaultData: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.component = component
|
||||
this.defaultStyle = defaultStyle
|
||||
this.defaultData = defaultData
|
||||
}
|
||||
|
||||
idPrefix(prefix: string): this {
|
||||
this.partial.idPrefix = prefix
|
||||
return this
|
||||
}
|
||||
|
||||
withInputOutput(hasInput: boolean, hasOutput: boolean): this {
|
||||
this.partial.hasInput = hasInput
|
||||
this.partial.hasOutput = hasOutput
|
||||
return this
|
||||
}
|
||||
|
||||
classification(c: NodeClassification): this {
|
||||
this.partial.classification = c
|
||||
return this
|
||||
}
|
||||
|
||||
allowedSourceTypes(types: string[]): this {
|
||||
this.partial.allowedSourceTypes = types
|
||||
return this
|
||||
}
|
||||
|
||||
allowedTargetTypes(types: string[]): this {
|
||||
this.partial.allowedTargetTypes = types
|
||||
return this
|
||||
}
|
||||
|
||||
help(help: NodeHelpEntry): this {
|
||||
this.partial.help = help
|
||||
return this
|
||||
}
|
||||
|
||||
menu(label: string, icon: React.ReactNode): this {
|
||||
this.partial.menuLabel = label
|
||||
this.partial.menuIcon = icon
|
||||
return this
|
||||
}
|
||||
|
||||
getDefaultData(fn: (newId?: string) => Record<string, unknown>): this {
|
||||
this.partial.getDefaultData = fn
|
||||
return this
|
||||
}
|
||||
|
||||
getResetData(fn: (nodeId?: string) => Record<string, unknown>): this {
|
||||
this.partial.getResetData = fn
|
||||
return this
|
||||
}
|
||||
|
||||
connectionLabel(label: string): this {
|
||||
this.partial.connectionLabel = label
|
||||
return this
|
||||
}
|
||||
|
||||
withFullscreen(): this {
|
||||
this.partial.supportsFullscreen = true
|
||||
return this
|
||||
}
|
||||
|
||||
sourceRenderingLogic(logic: SourceRenderingLogic): this {
|
||||
this.partial.sourceRenderingLogic = logic
|
||||
return this
|
||||
}
|
||||
|
||||
build(): NodeTypeDescriptor {
|
||||
const {
|
||||
idPrefix,
|
||||
hasInput,
|
||||
hasOutput,
|
||||
help,
|
||||
menuLabel,
|
||||
menuIcon,
|
||||
sourceRenderingLogic,
|
||||
...rest
|
||||
} = this.partial
|
||||
if (idPrefix == null || hasInput == null || hasOutput == null || !help || !menuLabel || menuIcon == null) {
|
||||
throw new Error(
|
||||
`NodeTypeBuilder.build(): missing required fields for "${this.id}". Set idPrefix, withInputOutput, help, and menu.`
|
||||
)
|
||||
}
|
||||
const descriptor: NodeTypeDescriptor = {
|
||||
id: this.id,
|
||||
component: this.component,
|
||||
defaultStyle: this.defaultStyle,
|
||||
defaultData: this.defaultData,
|
||||
idPrefix,
|
||||
hasInput,
|
||||
hasOutput,
|
||||
help,
|
||||
menuLabel,
|
||||
menuIcon,
|
||||
...rest,
|
||||
}
|
||||
if (sourceRenderingLogic != null) {
|
||||
descriptor.sourceRenderingLogic = sourceRenderingLogic
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building a node type descriptor. Required chain: idPrefix, withInputOutput, help, menu, then build().
|
||||
*/
|
||||
export function createNodeTypeBuilder(
|
||||
id: string,
|
||||
component: React.ComponentType<any>,
|
||||
defaultStyle: { width: number; height: number },
|
||||
defaultData: Record<string, unknown>
|
||||
): NodeTypeBuilder {
|
||||
return new NodeTypeBuilder(id, component, defaultStyle, defaultData)
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
import type { ConfigNodeData } from '@/components/nodes/ConfigNode'
|
||||
import type { RenderingNodeData } from '@/components/nodes/RenderingNode'
|
||||
import type { VariableNodeData } from '@/components/nodes/VariableNode'
|
||||
import type { FunctionNodeData } from '@/components/nodes/FunctionNode'
|
||||
import type { DataNodeData } from '@/components/nodes/DataNode'
|
||||
import type { AgentNodeData } from '@/components/nodes/AgentNode'
|
||||
import type { ConfigNodeData } from '@/components/nodes/config'
|
||||
import type { RenderingNodeData } from '@/components/nodes/render'
|
||||
import type { VariableNodeData } from '@/components/nodes/variable'
|
||||
import type { FunctionNodeData } from '@/components/nodes/function'
|
||||
import type { DataNodeData } from '@/components/nodes/data'
|
||||
import type { AgentNodeData } from '@/components/nodes/agent'
|
||||
|
||||
export type AppNodeData = ConfigNodeData | RenderingNodeData | VariableNodeData | FunctionNodeData | DataNodeData | AgentNodeData
|
||||
export type AppNode = Node<AppNodeData>
|
||||
|
||||
@@ -1,137 +1,22 @@
|
||||
/**
|
||||
* Registers built-in node types (config, render, variable, function).
|
||||
* Import this once at app startup (e.g. in main.tsx) so the registry is populated.
|
||||
* Registers built-in node types using the node type builder.
|
||||
* Each node type is defined in its own folder under components/nodes/<type>/ and
|
||||
* provides a getXxxNodeDescriptor() that uses the builder (including optional source rendering logic).
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { ScrollText, Sparkles, Variable, Code2, Database, Bot } from 'lucide-react'
|
||||
import { registerNodeType } from './nodeRegistry'
|
||||
import { NODE_HELP } from './nodeHelp'
|
||||
import ConfigNode from '../components/nodes/ConfigNode'
|
||||
import RenderingNode from '../components/nodes/RenderingNode'
|
||||
import VariableNode from '../components/nodes/VariableNode'
|
||||
import FunctionNode from '../components/nodes/FunctionNode'
|
||||
import DataNode from '../components/nodes/DataNode'
|
||||
import AgentNode from '../components/nodes/AgentNode'
|
||||
|
||||
const ICON_CLASS = 'mr-2 h-4 w-4'
|
||||
import { getConfigNodeDescriptor } from '@/components/nodes/config'
|
||||
import { getAgentNodeDescriptor } from '@/components/nodes/agent'
|
||||
import { getRenderNodeDescriptor } from '@/components/nodes/render'
|
||||
import { getVariableNodeDescriptor } from '@/components/nodes/variable'
|
||||
import { getFunctionNodeDescriptor } from '@/components/nodes/function'
|
||||
import { getDataNodeDescriptor } from '@/components/nodes/data'
|
||||
|
||||
export function registerBuiltinNodes(): void {
|
||||
registerNodeType({
|
||||
id: 'config',
|
||||
component: ConfigNode,
|
||||
defaultStyle: { width: 320, height: 320 },
|
||||
defaultData: { configType: 'plantuml', content: '@startuml\n\n@enduml\n', title: '' },
|
||||
idPrefix: 'cfg_',
|
||||
hasInput: true,
|
||||
hasOutput: true,
|
||||
classification: 'psyche',
|
||||
allowedSourceTypes: ['config', 'variable', 'function', 'data'],
|
||||
allowedTargetTypes: ['config', 'render', 'agent'],
|
||||
help: NODE_HELP.config,
|
||||
menuLabel: 'Config',
|
||||
menuIcon: <ScrollText className={ICON_CLASS} />,
|
||||
getDefaultData: (newId) => ({
|
||||
configType: 'plantuml',
|
||||
content: '@startuml\n\n@enduml\n',
|
||||
title: newId ?? '',
|
||||
}),
|
||||
getResetData: (nodeId) => ({
|
||||
configType: 'plantuml',
|
||||
content: '@startuml\n\n@enduml\n',
|
||||
title: nodeId ?? '',
|
||||
}),
|
||||
connectionLabel: 'adding input',
|
||||
supportsFullscreen: true,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'render',
|
||||
component: RenderingNode,
|
||||
defaultStyle: { width: 384, height: 320 },
|
||||
defaultData: { viewportWidth: 1200, viewportHeight: 800 },
|
||||
idPrefix: 'rnd_',
|
||||
hasInput: true,
|
||||
hasOutput: false,
|
||||
classification: 'pneuma',
|
||||
allowedSourceTypes: ['config', 'agent'],
|
||||
help: NODE_HELP.render,
|
||||
menuLabel: 'Renderer',
|
||||
menuIcon: <Sparkles className={ICON_CLASS} />,
|
||||
connectionLabel: 'rendering',
|
||||
supportsFullscreen: true,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'agent',
|
||||
component: AgentNode,
|
||||
defaultStyle: { width: 360, height: 320 },
|
||||
defaultData: { context: '' },
|
||||
idPrefix: 'agt_',
|
||||
hasInput: true,
|
||||
hasOutput: true,
|
||||
classification: 'archon',
|
||||
allowedSourceTypes: ['config', 'variable', 'data'],
|
||||
allowedTargetTypes: ['render'],
|
||||
help: NODE_HELP.agent,
|
||||
menuLabel: 'Agent',
|
||||
menuIcon: <Bot className={ICON_CLASS} />,
|
||||
connectionLabel: 'prompt/context',
|
||||
supportsFullscreen: true,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'variable',
|
||||
component: VariableNode,
|
||||
defaultStyle: { width: 224, height: 240 },
|
||||
defaultData: { value: '', valueType: 'string' },
|
||||
idPrefix: 'var_',
|
||||
hasInput: false,
|
||||
hasOutput: true,
|
||||
classification: 'psyche',
|
||||
allowedTargetTypes: ['config', 'function', 'agent'],
|
||||
help: NODE_HELP.variable,
|
||||
menuLabel: 'Variable',
|
||||
menuIcon: <Variable className={ICON_CLASS} />,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'data',
|
||||
component: DataNode,
|
||||
defaultStyle: { width: 360, height: 280 },
|
||||
defaultData: { rows: [], columns: [], fileName: '' },
|
||||
idPrefix: 'data_',
|
||||
hasInput: false,
|
||||
hasOutput: true,
|
||||
classification: 'physis',
|
||||
allowedTargetTypes: ['config', 'agent'],
|
||||
help: NODE_HELP.data,
|
||||
menuLabel: 'Data',
|
||||
menuIcon: <Database className={ICON_CLASS} />,
|
||||
connectionLabel: 'data source',
|
||||
supportsFullscreen: true,
|
||||
})
|
||||
|
||||
registerNodeType({
|
||||
id: 'function',
|
||||
component: FunctionNode,
|
||||
defaultStyle: { width: 288, height: 260 },
|
||||
defaultData: {
|
||||
body: `function(num, kwargs) {
|
||||
return num + (kwargs.bar || 0);
|
||||
}`,
|
||||
},
|
||||
idPrefix: 'fn_',
|
||||
hasInput: true,
|
||||
hasOutput: true,
|
||||
classification: 'psyche',
|
||||
allowedSourceTypes: ['config', 'variable', 'function'],
|
||||
allowedTargetTypes: ['config', 'function'],
|
||||
help: NODE_HELP.function,
|
||||
menuLabel: 'Function',
|
||||
menuIcon: <Code2 className={ICON_CLASS} />,
|
||||
getResetData: () => ({ body: '' }),
|
||||
connectionLabel: 'adding input',
|
||||
supportsFullscreen: true,
|
||||
})
|
||||
registerNodeType(getConfigNodeDescriptor())
|
||||
registerNodeType(getAgentNodeDescriptor())
|
||||
registerNodeType(getRenderNodeDescriptor())
|
||||
registerNodeType(getVariableNodeDescriptor())
|
||||
registerNodeType(getFunctionNodeDescriptor())
|
||||
registerNodeType(getDataNodeDescriptor())
|
||||
}
|
||||
|
||||
47
frontend/src/lib/sourceRenderingLogic.ts
Normal file
47
frontend/src/lib/sourceRenderingLogic.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Source rendering logic: each node type that can feed the Rendering node
|
||||
* registers how to get "resolved" content and default update behavior.
|
||||
* The Rendering node uses this to run the right logic and respect auto vs manual updates.
|
||||
*
|
||||
* Register in registerBuiltinNodes (or at app init) via registerSourceRenderingLogic(nodeType, logic).
|
||||
* Node types that are allowed sources for the Renderer should register here (see nodeRegistry NodeTypeDescriptor).
|
||||
*/
|
||||
|
||||
import type { ConfigTypeId } from './configTypes'
|
||||
|
||||
export type SourceRenderingLogicContext = {
|
||||
nodes: { id: string; type?: string; data?: unknown }[]
|
||||
edges: { id: string; source: string; target: string }[]
|
||||
sourceNodeId: string
|
||||
renderNodeId: string
|
||||
viewportWidth?: number
|
||||
viewportHeight?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of getResolvedContent: resolved string plus the config type to use for final render.
|
||||
*/
|
||||
export type ResolvedContentResult = {
|
||||
resolved: string
|
||||
outputTypeId: ConfigTypeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering logic provided by a source node type (e.g. config, agent).
|
||||
* - defaultUpdateMode: 'auto' = re-render on upstream changes; 'manual' = only on Run
|
||||
* - getResolvedContent: async resolve step; returns resolved string and which renderer (ConfigTypeId) to use
|
||||
*/
|
||||
export type SourceRenderingLogic = {
|
||||
defaultUpdateMode: 'auto' | 'manual'
|
||||
getResolvedContent: (context: SourceRenderingLogicContext) => Promise<ResolvedContentResult>
|
||||
}
|
||||
|
||||
const registry = new Map<string, SourceRenderingLogic>()
|
||||
|
||||
export function registerSourceRenderingLogic(nodeType: string, logic: SourceRenderingLogic): void {
|
||||
registry.set(nodeType, logic)
|
||||
}
|
||||
|
||||
export function getSourceRenderingLogic(nodeType: string): SourceRenderingLogic | null {
|
||||
return registry.get(nodeType) ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user