Compare commits

...

1 Commits

Author SHA1 Message Date
Vikhyath Mondreti
b3960ad77a v0.2.2: fix (#588)
* fix(loop-foreach): forEach loop iterations (#581)

* fix: working on fixing foreach loop max

* fix: removed default of 5 iterations

* fix: bun run lint

* fix: greptile comments (#581)

* fix: removed safety max (#581)

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>

* improvement(sockets conn): sockets connection refresh warning (#580)

* working conn status

* improvement(sockets conn): sockets connection refresh warning

* fix styling

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>

* fix(variable resolution): use variable references to not have escaping issues (#587)

* fix(variable-resolution): don't inject stringified json, use var refs

* fix lint

* remove unused var

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>

---------

Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>
2025-06-30 11:50:59 -07:00
6 changed files with 105 additions and 61 deletions

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export const maxDuration = 60
const logger = createLogger('FunctionExecuteAPI')
@@ -14,45 +15,14 @@ const logger = createLogger('FunctionExecuteAPI')
* @param envVars - Environment variables from the workflow
* @returns Resolved code
*/
/**
* Safely serialize a value to JSON string with proper escaping
* This prevents JavaScript syntax errors when the serialized data is injected into code
*/
function safeJSONStringify(value: any): string {
try {
// Use JSON.stringify with proper escaping
// The key is to let JSON.stringify handle the escaping properly
return JSON.stringify(value)
} catch (error) {
// If JSON.stringify fails (e.g., circular references), return a safe fallback
try {
// Try to create a safe representation by removing circular references
const seen = new WeakSet()
const cleanValue = JSON.parse(
JSON.stringify(value, (key, val) => {
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) {
return '[Circular Reference]'
}
seen.add(val)
}
return val
})
)
return JSON.stringify(cleanValue)
} catch {
// If that also fails, return a safe string representation
return JSON.stringify(String(value))
}
}
}
function resolveCodeVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string> = {}
): string {
): { resolvedCode: string; contextVariables: Record<string, any> } {
let resolvedCode = code
const contextVariables: Record<string, any> = {}
// Resolve environment variables with {{var_name}} syntax
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
@@ -60,11 +30,13 @@ function resolveCodeVariables(
const varName = match.slice(2, -2).trim()
// Priority: 1. Environment variables from workflow, 2. Params
const varValue = envVars[varName] || params[varName] || ''
// Use safe JSON stringify to prevent syntax errors
resolvedCode = resolvedCode.replace(
new RegExp(escapeRegExp(match), 'g'),
safeJSONStringify(varValue)
)
// Instead of injecting large JSON directly, create a variable reference
const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = varValue
// Replace the template with a variable reference
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
// Resolve tags with <tag_name> syntax
@@ -72,13 +44,16 @@ function resolveCodeVariables(
for (const match of tagMatches) {
const tagName = match.slice(1, -1).trim()
const tagValue = params[tagName] || ''
resolvedCode = resolvedCode.replace(
new RegExp(escapeRegExp(match), 'g'),
safeJSONStringify(tagValue)
)
// Instead of injecting large JSON directly, create a variable reference
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = tagValue
// Replace the template with a variable reference
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
return resolvedCode
return { resolvedCode, contextVariables }
}
/**
@@ -118,7 +93,7 @@ export async function POST(req: NextRequest) {
})
// Resolve variables in the code with workflow environment variables
const resolvedCode = resolveCodeVariables(code, executionParams, envVars)
const { resolvedCode, contextVariables } = resolveCodeVariables(code, executionParams, envVars)
const executionMethod = 'vm' // Default execution method
@@ -280,6 +255,7 @@ export async function POST(req: NextRequest) {
const context = createContext({
params: executionParams,
environmentVariables: envVars,
...contextVariables, // Add resolved variables directly to context
fetch: globalThis.fetch || require('node-fetch').default,
console: {
log: (...args: any[]) => {

View File

@@ -0,0 +1,53 @@
'use client'
import { useEffect, useState } from 'react'
interface ConnectionStatusProps {
isConnected: boolean
}
export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
const [showOfflineNotice, setShowOfflineNotice] = useState(false)
useEffect(() => {
let timeoutId: NodeJS.Timeout
if (!isConnected) {
// Show offline notice after 6 seconds of being disconnected
timeoutId = setTimeout(() => {
setShowOfflineNotice(true)
}, 6000) // 6 seconds
} else {
// Hide notice immediately when reconnected
setShowOfflineNotice(false)
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [isConnected])
// Don't render anything if connected or if we haven't been disconnected long enough
if (!showOfflineNotice) {
return null
}
return (
<div className='flex items-center gap-1.5'>
<div className='flex items-center gap-1.5 text-red-600'>
<div className='relative flex items-center justify-center'>
<div className='absolute h-3 w-3 animate-ping rounded-full bg-red-500/20' />
<div className='relative h-2 w-2 rounded-full bg-red-500' />
</div>
<div className='flex flex-col'>
<span className='font-medium text-xs leading-tight'>Connection lost</span>
<span className='text-xs leading-tight opacity-90'>
Changes not saved - please refresh
</span>
</div>
</div>
</div>
)
}

View File

@@ -2,6 +2,7 @@
import { useMemo } from 'react'
import { usePresence } from '../../../../hooks/use-presence'
import { ConnectionStatus } from './components/connection-status/connection-status'
import { UserAvatar } from './components/user-avatar/user-avatar'
interface User {
@@ -25,7 +26,7 @@ export function UserAvatarStack({
className = '',
}: UserAvatarStackProps) {
// Use presence data if no users are provided via props
const { users: presenceUsers } = usePresence()
const { users: presenceUsers, isConnected } = usePresence()
const users = propUsers || presenceUsers
// Memoize the processed users to avoid unnecessary re-renders
@@ -43,10 +44,14 @@ export function UserAvatarStack({
}
}, [users, maxVisible])
// Show connection status component regardless of user count
// This will handle the offline notice when disconnected for 15 seconds
const connectionStatusElement = <ConnectionStatus isConnected={isConnected} />
// Only show presence when there are multiple users (>1)
// Don't render anything if there are no users or only 1 user
// But always show connection status
if (users.length <= 1) {
return null
return connectionStatusElement
}
// Determine spacing based on size
@@ -58,6 +63,9 @@ export function UserAvatarStack({
return (
<div className={`flex items-center ${spacingClass} ${className}`}>
{/* Connection status - always present */}
{connectionStatusElement}
{/* Render visible user avatars */}
{visibleUsers.map((user, index) => (
<UserAvatar

View File

@@ -683,7 +683,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
</p>
)}
</div>
<UserAvatarStack className='ml-3' />
</div>
)
}
@@ -1275,8 +1274,10 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
{/* Left Section - Workflow Info */}
<div className='pl-4'>{renderWorkflowName()}</div>
{/* Middle Section - Reserved for future use */}
<div className='flex-1' />
{/* Middle Section - Connection Status */}
<div className='flex flex-1 justify-center'>
<UserAvatarStack />
</div>
{/* Right Section - Actions */}
<div className='flex items-center gap-1 pr-4'>

View File

@@ -44,9 +44,7 @@ export class LoopBlockHandler implements BlockHandler {
}
const currentIteration = context.loopIterations.get(block.id) || 0
let maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
// For forEach loops, we need to check the actual items length
let maxIterations: number
let forEachItems: any[] | Record<string, any> | null = null
if (loop.loopType === 'forEach') {
if (
@@ -71,14 +69,19 @@ export class LoopBlockHandler implements BlockHandler {
)
}
// Adjust max iterations based on actual items
// For forEach, max iterations = items length
const itemsLength = Array.isArray(forEachItems)
? forEachItems.length
: Object.keys(forEachItems).length
maxIterations = Math.min(maxIterations, itemsLength)
maxIterations = itemsLength
logger.info(
`Loop ${block.id} max iterations set to ${maxIterations} based on ${itemsLength} items`
`forEach loop ${block.id} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
)
} else {
maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
logger.info(`For loop ${block.id} - Max iterations: ${maxIterations}`)
}
logger.info(

View File

@@ -81,7 +81,7 @@ export class LoopManager {
// Determine the maximum iterations
let maxIterations = loop.iterations || this.defaultIterations
// For forEach loops, check the actual items length
// For forEach loops, use the actual items length
if (loop.loopType === 'forEach' && loop.forEachItems) {
// First check if the items have already been evaluated and stored by the loop handler
const storedItems = context.loopItems.get(`${loopId}_items`)
@@ -89,15 +89,18 @@ export class LoopManager {
const itemsLength = Array.isArray(storedItems)
? storedItems.length
: Object.keys(storedItems).length
maxIterations = Math.min(maxIterations, itemsLength)
maxIterations = itemsLength
logger.info(
`Loop ${loopId} using stored items length: ${itemsLength} (max iterations: ${maxIterations})`
`forEach loop ${loopId} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
)
} else {
// Fallback to parsing the forEachItems string if it's not a reference
const itemsLength = this.getItemsLength(loop.forEachItems)
if (itemsLength > 0) {
maxIterations = Math.min(maxIterations, itemsLength)
maxIterations = itemsLength
logger.info(
`forEach loop ${loopId} - Parsed items: ${itemsLength}, Max iterations: ${maxIterations}`
)
}
}
}