Skip to content

Commit c5189d8

Browse files
authored
Merge pull request #142 from badaitech/feat/execution-store-pg
Add memory locks for atomic updates, fix flow sync issues, add gate node, improve 'any' type port connections
2 parents 9c7e1d6 + de8e6da commit c5189d8

66 files changed

Lines changed: 2054 additions & 616 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/chaingraph-backend/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# @badaitech/chaingraph-backend
22

3+
## 0.1.21
4+
5+
### Patch Changes
6+
7+
- Add memory locks for atomic updates, fix flow sync issues, add gate node, improve 'any' type port connections
8+
- Updated dependencies
9+
- @badaitech/chaingraph-nodes@0.1.21
10+
- @badaitech/chaingraph-types@0.1.21
11+
- @badaitech/chaingraph-trpc@0.1.21
12+
313
## 0.1.20
414

515
### Patch Changes

apps/chaingraph-backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@badaitech/chaingraph-backend",
33
"type": "module",
4-
"version": "0.1.20",
4+
"version": "0.1.21",
55
"private": false,
66
"description": "Backend server for the Chaingraph project",
77
"license": "BUSL-1.1",

apps/chaingraph-frontend/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# @badaitech/chaingraph-frontend
22

3+
## 0.1.21
4+
5+
### Patch Changes
6+
7+
- Add memory locks for atomic updates, fix flow sync issues, add gate node, improve 'any' type port connections
8+
- Updated dependencies
9+
- @badaitech/chaingraph-nodes@0.1.21
10+
- @badaitech/chaingraph-types@0.1.21
11+
- @badaitech/chaingraph-trpc@0.1.21
12+
313
## 0.1.20
414

515
### Patch Changes

apps/chaingraph-frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@badaitech/chaingraph-frontend",
33
"type": "module",
4-
"version": "0.1.20",
4+
"version": "0.1.21",
55
"private": false,
66
"description": "Frontend application for the Chaingraph project",
77
"license": "BUSL-1.1",

apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/ChaingraphNode.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { useBreakpoint } from '@/store/execution/hooks/useBreakpoint'
2222
import { useNodeExecution } from '@/store/execution/hooks/useNodeExecution'
2323
import { $activeFlowMetadata } from '@/store/flow'
24-
import { removeNodeFromFlow } from '@/store/nodes'
24+
import { removeNodeFromFlow, updateNodeUI } from '@/store/nodes'
2525
import { useNode } from '@/store/nodes/hooks/useNode'
2626
import {
2727
addFieldObjectPort,
@@ -35,6 +35,7 @@ import { NodeResizeControl, ResizeControlVariant } from '@xyflow/react'
3535
import { useUnit } from 'effector-react'
3636
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
3737
import { BreakpointButton } from '../debug/BreakpointButton'
38+
import { useElementResize } from './hooks/useElementResize'
3839
import NodeBody from './NodeBody'
3940
import NodeErrorPorts from './NodeErrorPorts'
4041
import { NodeHeader } from './NodeHeader'
@@ -179,11 +180,45 @@ function ChaingraphNodeComponent({
179180
}
180181
}, [dispatch, getEdgesForPortFunction])
181182

183+
// Use a custom hook to handle element resize
184+
const { ref: cardRef } = useElementResize<HTMLDivElement>({
185+
debounceTime: 500,
186+
onResize: (size) => {
187+
if (!activeFlow || !activeFlow.id || !node)
188+
return
189+
190+
const actualDimensions = node.metadata.ui?.dimensions || {
191+
width: 0,
192+
height: 0,
193+
}
194+
195+
const isDimensionsChanged = size.width !== actualDimensions.width
196+
|| size.height !== actualDimensions.height
197+
198+
if (!isDimensionsChanged)
199+
return
200+
201+
updateNodeUI({
202+
flowId: activeFlow.id!,
203+
nodeId: id,
204+
ui: {
205+
// ...node.metadata.ui,
206+
dimensions: {
207+
width: size.width,
208+
height: size.height,
209+
},
210+
},
211+
version: node.getVersion(),
212+
})
213+
},
214+
})
215+
182216
if (!activeFlow || !activeFlow.id || !node)
183217
return null
184218

185219
return (
186220
<Card
221+
ref={cardRef}
187222
className={cn(
188223
'shadow-none transition-all duration-200',
189224
'bg-card opacity-95',
@@ -240,7 +275,7 @@ function ChaingraphNodeComponent({
240275
background: 'transparent',
241276
border: 'none',
242277
height: '100%',
243-
width: 10,
278+
width: 12,
244279
}}
245280
/>
246281

apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/PortComponent.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
PortContextValue,
1111
} from '@/components/flow/nodes/ChaingraphNode/ports/context/PortContext'
1212
import type {
13+
AnyPortConfig,
1314
ArrayPortConfig,
1415
BooleanPortConfig,
1516
EnumPortConfig,
@@ -22,6 +23,7 @@ import type {
2223
import { BooleanPort } from '@/components/flow/nodes/ChaingraphNode/ports/BooleanPort/BooleanPort'
2324
import { NumberPort } from '@/components/flow/nodes/ChaingraphNode/ports/NumberPort/NumberPort'
2425
import { ObjectPort } from '@/components/flow/nodes/ChaingraphNode/ports/ObjectPort/ObjectPort'
26+
import { AnyPort } from 'components/flow/nodes/ChaingraphNode/ports/AnyPort/AnyPort'
2527
import { ArrayPort } from './ports/ArrayPort/ArrayPort'
2628
import { PortContext } from './ports/context/PortContext'
2729
import { EnumPort } from './ports/EnumPort/EnumPort'
@@ -94,8 +96,12 @@ export function PortComponent(props: PortProps) {
9496
)
9597
}
9698
// case 'secret':
97-
case 'stream':
99+
98100
case 'any': {
101+
return <AnyPort node={node} port={port as IPort<AnyPortConfig>} context={context} />
102+
}
103+
104+
case 'stream': {
99105
return <StubPort port={port} />
100106
}
101107

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright (c) 2025 BadLabs
3+
*
4+
* Use of this software is governed by the Business Source License 1.1 included in the file LICENSE.txt.
5+
*
6+
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed by the Apache License, version 2.0.
7+
*/
8+
9+
import { useCallback, useEffect, useRef } from 'react'
10+
11+
interface ElementSize {
12+
width: number
13+
height: number
14+
}
15+
16+
interface UseElementResizeOptions {
17+
debounceTime?: number
18+
onResize?: (size: ElementSize) => void
19+
minChangeThreshold?: number
20+
maxUpdatesPerSecond?: number
21+
}
22+
23+
/**
24+
* Hook to track element size changes with debouncing
25+
* @param options Configuration options
26+
* @returns Object with ref to attach to the element
27+
*/
28+
export function useElementResize<T extends HTMLElement = HTMLDivElement>({
29+
debounceTime = 200,
30+
onResize,
31+
minChangeThreshold = 0.5,
32+
maxUpdatesPerSecond = 2, // Max updates per second (default: 2)
33+
}: UseElementResizeOptions = {}) {
34+
const elementRef = useRef<T>(null)
35+
const lastReportedSizeRef = useRef<ElementSize>({ width: 0, height: 0 })
36+
const lastSizeRef = useRef<ElementSize>({ width: 0, height: 0 })
37+
const lastUpdateTimeRef = useRef<number>(0)
38+
const isInitializedRef = useRef<boolean>(false)
39+
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
40+
const isResizingRef = useRef<boolean>(false)
41+
const minUpdateIntervalMs = maxUpdatesPerSecond ? (1000 / maxUpdatesPerSecond) : 0
42+
43+
// This function checks if we should allow an update based on time constraints
44+
const shouldAllowUpdate = useCallback(() => {
45+
const now = Date.now()
46+
const timeSinceLastUpdate = now - lastUpdateTimeRef.current
47+
return timeSinceLastUpdate >= minUpdateIntervalMs
48+
}, [minUpdateIntervalMs])
49+
50+
const processSizeChange = useCallback(() => {
51+
const { width, height } = lastSizeRef.current
52+
53+
// Only trigger if dimensions have changed significantly from the last reported size
54+
if (
55+
!isInitializedRef.current
56+
|| Math.abs(width - lastReportedSizeRef.current.width) > minChangeThreshold
57+
|| Math.abs(height - lastReportedSizeRef.current.height) > minChangeThreshold
58+
) {
59+
// Update the last reported size
60+
lastReportedSizeRef.current = { width, height }
61+
lastUpdateTimeRef.current = Date.now()
62+
isInitializedRef.current = true
63+
64+
onResize?.({ width, height })
65+
}
66+
67+
isResizingRef.current = false
68+
}, [onResize, minChangeThreshold])
69+
70+
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
71+
if (!entries.length)
72+
return
73+
74+
const entry = entries[entries.length - 1] // Use only the most recent entry
75+
76+
// Extract size from the entry
77+
let width = 0
78+
let height = 0
79+
80+
if (entry.borderBoxSize && entry.borderBoxSize.length > 0) {
81+
width = entry.borderBoxSize[0].inlineSize
82+
height = entry.borderBoxSize[0].blockSize
83+
} else {
84+
const rect = (entry.target as HTMLElement).getBoundingClientRect()
85+
width = rect.width
86+
height = rect.height
87+
}
88+
89+
// Store the current size
90+
lastSizeRef.current = { width, height }
91+
92+
// If this is the first size measurement, report immediately
93+
if (!isInitializedRef.current) {
94+
processSizeChange()
95+
return
96+
}
97+
98+
isResizingRef.current = true
99+
100+
// Clear any existing timeout
101+
if (debounceTimeoutRef.current) {
102+
clearTimeout(debounceTimeoutRef.current)
103+
}
104+
105+
// If we're allowed to update now based on rate limiting, and the change is significant
106+
if (shouldAllowUpdate()
107+
&& (Math.abs(width - lastReportedSizeRef.current.width) > minChangeThreshold
108+
|| Math.abs(height - lastReportedSizeRef.current.height) > minChangeThreshold)) {
109+
processSizeChange()
110+
}
111+
112+
// Always set a final timeout to ensure we capture the last size
113+
debounceTimeoutRef.current = setTimeout(() => {
114+
if (isResizingRef.current) {
115+
processSizeChange()
116+
}
117+
}, debounceTime)
118+
}, [debounceTime, processSizeChange, shouldAllowUpdate, minChangeThreshold])
119+
120+
useEffect(() => {
121+
const element = elementRef.current
122+
if (!element)
123+
return
124+
125+
// Reset the initialization state
126+
isInitializedRef.current = false
127+
128+
const resizeObserver = new ResizeObserver(handleResize)
129+
resizeObserver.observe(element)
130+
131+
return () => {
132+
if (debounceTimeoutRef.current) {
133+
clearTimeout(debounceTimeoutRef.current)
134+
135+
// Ensure the final size gets processed when component unmounts
136+
if (isResizingRef.current) {
137+
processSizeChange()
138+
}
139+
}
140+
resizeObserver.disconnect()
141+
}
142+
}, [handleResize, processSizeChange])
143+
144+
return { ref: elementRef }
145+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2025 BadLabs
3+
*
4+
* Use of this software is governed by the Business Source License 1.1 included in the file LICENSE.txt.
5+
*
6+
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed by the Apache License, version 2.0.
7+
*/
8+
9+
import type {
10+
PortContextValue,
11+
} from '@/components/flow/nodes/ChaingraphNode/ports/context/PortContext'
12+
/*
13+
* Copyright (c) 2025 BadLabs
14+
*
15+
* Use of this software is governed by the Business Source License 1.1 included in the file LICENSE.txt.
16+
*
17+
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed by the Apache License, version 2.0.
18+
*/
19+
import type {
20+
AnyPortConfig,
21+
INode,
22+
IPort,
23+
} from '@badaitech/chaingraph-types'
24+
import { PortHandle } from '@/components/flow/nodes/ChaingraphNode/ports/ui/PortHandle'
25+
import { cn } from '@/lib/utils'
26+
import { memo } from 'react'
27+
import { PortTitle } from '../ui/PortTitle'
28+
29+
export interface AnyPortProps {
30+
node: INode
31+
port: IPort<AnyPortConfig>
32+
context: PortContextValue
33+
}
34+
35+
function AnyPortComponent(props: AnyPortProps) {
36+
const { port } = props
37+
// const { node, port, context } = props
38+
// const { updatePortValue, getEdgesForPort } = context
39+
40+
const config = port.getConfig()
41+
const ui = config.ui
42+
const title = config.title || config.key
43+
44+
// Memoize the edges for this port
45+
// const connectedEdges = useMemo(() => {
46+
// return getEdgesForPort(port.id)
47+
// }, [getEdgesForPort, port.id])
48+
49+
// const needRenderEditor = useMemo(() => {
50+
// return !isHideEditor(config, connectedEdges)
51+
// }, [config, connectedEdges])
52+
//
53+
// const handleChange = useCallback((value: ExtractValue<AnyPortConfig> | undefined) => {
54+
// updatePortValue({
55+
// nodeId: node.id,
56+
// portId: port.id,
57+
// value,
58+
// })
59+
// }, [node.id, port.id, updatePortValue])
60+
61+
if (ui?.hidden)
62+
return null
63+
64+
return (
65+
<div
66+
key={config.id}
67+
className={cn(
68+
'relative flex gap-2 group/port',
69+
config.direction === 'output' ? 'justify-end' : 'justify-start',
70+
)}
71+
>
72+
{config.direction === 'input' && <PortHandle port={port} />}
73+
74+
<div className={cn(
75+
'flex flex-col',
76+
config.direction === 'output' ? 'items-end' : 'items-start',
77+
)}
78+
>
79+
<PortTitle>
80+
{title}
81+
</PortTitle>
82+
83+
{config.underlyingType && (
84+
<div
85+
className={cn(
86+
'text-xs text-gray-500',
87+
config.direction === 'output' ? 'text-right' : 'text-left',
88+
)}
89+
>
90+
{config.underlyingType.type}
91+
</div>
92+
)}
93+
</div>
94+
95+
{config.direction === 'output' && <PortHandle port={port} />}
96+
</div>
97+
)
98+
}
99+
100+
// Export a memoized version of the component to prevent unnecessary re-renders
101+
export const AnyPort = memo(AnyPortComponent)

0 commit comments

Comments
 (0)