Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 20 additions & 44 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -14,71 +15,45 @@ 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) || []
for (const match of envVarMatches) {
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
Comment on lines +35 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Using a '_var' prefix for internal variables may conflict with user code. Consider using a more unique prefix like '_sim_var' to prevent collisions.


// Replace the template with a variable reference
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}

// Resolve tags with <tag_name> syntax
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_]*)>/g) || []
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 }
}

/**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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[]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: NodeJS.Timeout type not needed here - setTimeout returns number in browser context

Suggested change
let timeoutId: NodeJS.Timeout
let timeoutId: number


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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
</p>
)}
</div>
<UserAvatarStack className='ml-3' />
</div>
)
}
Expand Down Expand Up @@ -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'>
Expand Down
15 changes: 9 additions & 6 deletions apps/sim/executor/handlers/loop/loop-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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(
Expand Down
13 changes: 8 additions & 5 deletions apps/sim/executor/loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,26 @@ 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`)
if (storedItems) {
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}`
)
}
}
}
Expand Down