improvement(perf): apply react and js performance optimizations across codebase (#3459)

* improvement(perf): apply react and js performance optimizations across codebase

- Parallelize independent DB queries with Promise.all in API routes
- Defer PostHog and OneDollarStats via dynamic import() to reduce bundle size
- Use functional setState in countdown timers to prevent stale closures
- Replace O(n*m) .filter().find() with Set-based O(n) lookups in undo-redo
- Use .toSorted() instead of .sort() for immutable state operations
- Use lazy initializers for useState(new Set()) across 20 components
- Remove useMemo wrapping trivially cheap expressions (typeof, ternary, template strings)
- Add passive: true to scroll event listener

* fix(perf): address PR review feedback

- Extract IIFE Set patterns to named consts for readability in use-undo-redo
- Hoist Set construction above loops in BATCH_UPDATE_PARENT cases
- Add .catch() error handler to PostHog dynamic import
- Convert session-provider posthog import to dynamic import() to complete bundle split

* fix(analytics): add .catch() to onedollarstats dynamic import
This commit is contained in:
Waleed
2026-03-07 13:08:26 -08:00
committed by GitHub
parent 1324987def
commit 0d9e04181f
39 changed files with 208 additions and 173 deletions

View File

@@ -43,7 +43,7 @@ function VerificationForm({
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000)
return () => clearTimeout(timer)
}
if (countdown === 0 && isResendDisabled) {

View File

@@ -1,41 +1,64 @@
'use client'
import { useEffect } from 'react'
import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { useEffect, useRef, useState } from 'react'
import type { PostHog } from 'posthog-js'
import { createLogger } from '@sim/logger'
import { getEnv, isTruthy } from '@/lib/core/config/env'
const logger = createLogger('PostHogProvider')
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const [Provider, setProvider] = useState<React.ComponentType<{
client: PostHog
children: React.ReactNode
}> | null>(null)
const clientRef = useRef<PostHog | null>(null)
useEffect(() => {
const posthogEnabled = getEnv('NEXT_PUBLIC_POSTHOG_ENABLED')
const posthogKey = getEnv('NEXT_PUBLIC_POSTHOG_KEY')
if (isTruthy(posthogEnabled) && posthogKey && !posthog.__loaded) {
posthog.init(posthogKey, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2025-05-24',
person_profiles: 'identified_only',
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
capture_performance: false,
capture_dead_clicks: false,
enable_heatmaps: false,
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true,
email: false,
},
recordCrossOriginIframes: false,
recordHeaders: false,
recordBody: false,
},
persistence: 'localStorage+cookie',
if (!isTruthy(posthogEnabled) || !posthogKey) return
Promise.all([import('posthog-js'), import('posthog-js/react')])
.then(([posthogModule, { PostHogProvider: PHProvider }]) => {
const posthog = posthogModule.default
if (!posthog.__loaded) {
posthog.init(posthogKey, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2025-05-24',
person_profiles: 'identified_only',
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
capture_performance: false,
capture_dead_clicks: false,
enable_heatmaps: false,
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true,
email: false,
},
recordCrossOriginIframes: false,
recordHeaders: false,
recordBody: false,
},
persistence: 'localStorage+cookie',
})
}
clientRef.current = posthog
setProvider(() => PHProvider)
})
.catch((err) => {
logger.error('Failed to load PostHog', { error: err })
})
}
}, [])
return <PHProvider client={posthog}>{children}</PHProvider>
if (Provider && clientRef.current) {
return <Provider client={clientRef.current}>{children}</Provider>
}
return <>{children}</>
}

View File

@@ -3,7 +3,6 @@
import type React from 'react'
import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import posthog from 'posthog-js'
import { client } from '@/lib/auth/auth-client'
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
@@ -77,22 +76,24 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
}, [loadSession, queryClient])
useEffect(() => {
if (isPending || typeof posthog.identify !== 'function') {
return
}
if (isPending) return
try {
if (data?.user) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
} else {
posthog.reset()
}
} catch {}
import('posthog-js').then(({ default: posthog }) => {
try {
if (typeof posthog.identify !== 'function') return
if (data?.user) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
} else {
posthog.reset()
}
} catch {}
}).catch(() => {})
}, [data, isPending])
const value = useMemo<SessionHookResult>(

View File

@@ -43,27 +43,42 @@ async function getEffectiveBillingStatus(userId: string): Promise<{
.from(member)
.where(eq(member.userId, userId))
for (const m of memberships) {
const owners = await db
.select({ userId: member.userId })
.from(member)
.where(and(eq(member.organizationId, m.organizationId), eq(member.role, 'owner')))
.limit(1)
if (owners.length > 0 && owners[0].userId !== userId) {
const ownerStats = await db
.select({
blocked: userStats.billingBlocked,
blockedReason: userStats.billingBlockedReason,
})
.from(userStats)
.where(eq(userStats.userId, owners[0].userId))
// Fetch all org owners in parallel
const ownerResults = await Promise.all(
memberships.map((m) =>
db
.select({ userId: member.userId })
.from(member)
.where(and(eq(member.organizationId, m.organizationId), eq(member.role, 'owner')))
.limit(1)
)
)
if (ownerStats.length > 0 && ownerStats[0].blocked) {
// Collect owner IDs that are not the current user
const otherOwnerIds = ownerResults
.filter((owners) => owners.length > 0 && owners[0].userId !== userId)
.map((owners) => owners[0].userId)
if (otherOwnerIds.length > 0) {
// Fetch all owner stats in parallel
const ownerStatsResults = await Promise.all(
otherOwnerIds.map((ownerId) =>
db
.select({
blocked: userStats.billingBlocked,
blockedReason: userStats.billingBlockedReason,
})
.from(userStats)
.where(eq(userStats.userId, ownerId))
.limit(1)
)
)
for (const stats of ownerStatsResults) {
if (stats.length > 0 && stats[0].blocked) {
return {
billingBlocked: true,
billingBlockedReason: ownerStats[0].blockedReason,
billingBlockedReason: stats[0].blockedReason,
blockedByOrgOwner: true,
}
}
@@ -114,11 +129,12 @@ export async function GET(request: NextRequest) {
let billingData
if (context === 'user') {
// Get user billing (may include organization if they're part of one)
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
// Attach effective billing blocked status (includes org owner check)
const billingStatus = await getEffectiveBillingStatus(session.user.id)
// Get user billing and billing blocked status in parallel
const [billingResult, billingStatus] = await Promise.all([
getSimplifiedBillingSummary(session.user.id, contextId || undefined),
getEffectiveBillingStatus(session.user.id),
])
billingData = billingResult
billingData = {
...billingData,

View File

@@ -106,23 +106,16 @@ export async function POST(request: NextRequest) {
)
}
// Check if identifier is available
const existingIdentifier = await db
.select()
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
// Check identifier availability and workflow access in parallel
const [existingIdentifier, { hasAccess, workflow: workflowRecord }] = await Promise.all([
db.select().from(chat).where(eq(chat.identifier, identifier)).limit(1),
checkWorkflowAccessForChatCreation(workflowId, session.user.id),
])
if (existingIdentifier.length > 0) {
return createErrorResponse('Identifier already in use', 400)
}
// Check if user has permission to create chat for this workflow
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForChatCreation(
workflowId,
session.user.id
)
if (!hasAccess || !workflowRecord) {
return createErrorResponse('Workflow not found or access denied', 404)
}

View File

@@ -123,22 +123,24 @@ export async function POST(req: Request) {
)
}
const orgExists = await db
.select({ id: organization.id })
.from(organization)
.where(eq(organization.id, organizationId))
.limit(1)
// Check org existence and name uniqueness in parallel
const [orgExists, existingSet] = await Promise.all([
db
.select({ id: organization.id })
.from(organization)
.where(eq(organization.id, organizationId))
.limit(1),
db
.select({ id: credentialSet.id })
.from(credentialSet)
.where(and(eq(credentialSet.organizationId, organizationId), eq(credentialSet.name, name)))
.limit(1),
])
if (orgExists.length === 0) {
return NextResponse.json({ error: 'Organization not found' }, { status: 404 })
}
const existingSet = await db
.select({ id: credentialSet.id })
.from(credentialSet)
.where(and(eq(credentialSet.organizationId, organizationId), eq(credentialSet.name, name)))
.limit(1)
if (existingSet.length > 0) {
return NextResponse.json(
{ error: 'A credential set with this name already exists' },

View File

@@ -118,21 +118,16 @@ export async function POST(request: NextRequest) {
)
}
const existingIdentifier = await db
.select()
.from(form)
.where(eq(form.identifier, identifier))
.limit(1)
// Check identifier availability and workflow access in parallel
const [existingIdentifier, { hasAccess, workflow: workflowRecord }] = await Promise.all([
db.select().from(form).where(eq(form.identifier, identifier)).limit(1),
checkWorkflowAccessForFormCreation(workflowId, session.user.id),
])
if (existingIdentifier.length > 0) {
return createErrorResponse('Identifier already in use', 400)
}
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForFormCreation(
workflowId,
session.user.id
)
if (!hasAccess || !workflowRecord) {
return createErrorResponse('Workflow not found or access denied', 404)
}

View File

@@ -52,7 +52,7 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000)
return () => clearTimeout(timer)
}
if (countdown === 0 && isResendDisabled) {

View File

@@ -1,6 +1,6 @@
'use client'
import { memo, useMemo, useState } from 'react'
import { memo, useState } from 'react'
import { Check, Copy, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import {
@@ -46,9 +46,7 @@ export const ClientChatMessage = memo(
function ClientChatMessage({ message }: { message: ChatMessage }) {
const [isCopied, setIsCopied] = useState(false)
const isJsonObject = useMemo(() => {
return typeof message.content === 'object' && message.content !== null
}, [message.content])
const isJsonObject = typeof message.content === 'object' && message.content !== null
// Since tool calls are now handled via SSE events and stored in message.toolCalls,
// we can use the content directly without parsing

View File

@@ -294,7 +294,7 @@ export function Document({
const searchError = searchQueryError instanceof Error ? searchQueryError.message : null
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(new Set())
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(() => new Set())
const [selectedChunk, setSelectedChunk] = useState<ChunkData | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)

View File

@@ -392,7 +392,7 @@ export function KnowledgeBase({
setCurrentPage(1)
}, [])
const [selectedDocuments, setSelectedDocuments] = useState<Set<string>>(new Set())
const [selectedDocuments, setSelectedDocuments] = useState<Set<string>>(() => new Set())
const [isSelectAllMode, setIsSelectAllMode] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showAddDocumentsModal, setShowAddDocumentsModal] = useState(false)

View File

@@ -51,7 +51,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
const [syncInterval, setSyncInterval] = useState(1440)
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(new Set())
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(() => new Set())
const [error, setError] = useState<string | null>(null)
const [showOAuthModal, setShowOAuthModal] = useState(false)

View File

@@ -48,7 +48,7 @@ export function AddDocumentsModal({
const [fileError, setFileError] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const [dragCounter, setDragCounter] = useState(0)
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(() => new Set())
const { isUploading, uploadProgress, uploadFiles, uploadError, clearError } = useKnowledgeUpload({
workspaceId,

View File

@@ -91,7 +91,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
const [fileError, setFileError] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const [dragCounter, setDragCounter] = useState(0)
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(() => new Set())
const scrollContainerRef = useRef<HTMLDivElement>(null)

View File

@@ -693,7 +693,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
*/
export const TraceSpans = memo(function TraceSpans({ traceSpans }: TraceSpansProps) {
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set())
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
const [expandedSections, setExpandedSections] = useState<Set<string>>(() => new Set())
const toggleSet = useSetToggle()
const { workflowStartTime, actualTotalDuration, normalizedSpans } = useMemo(() => {

View File

@@ -289,9 +289,7 @@ export const LogDetails = memo(function LogDetails({
)
}, [log])
const hasCostInfo = useMemo(() => {
return isWorkflowExecutionLog && log?.cost
}, [log, isWorkflowExecutionLog])
const hasCostInfo = isWorkflowExecutionLog && log?.cost
const workflowOutput = useMemo(() => {
const executionData = log?.executionData as
@@ -329,7 +327,7 @@ export const LogDetails = memo(function LogDetails({
[log?.createdAt]
)
const logStatus = useMemo(() => getDisplayStatus(log?.status), [log?.status])
const logStatus = getDisplayStatus(log?.status)
return (
<>

View File

@@ -130,9 +130,9 @@ export function CredentialSets() {
const resendInvitation = useResendCredentialSetInvitation()
const [deletingSet, setDeletingSet] = useState<{ id: string; name: string } | null>(null)
const [deletingSetIds, setDeletingSetIds] = useState<Set<string>>(new Set())
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(new Set())
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(new Set())
const [deletingSetIds, setDeletingSetIds] = useState<Set<string>>(() => new Set())
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(() => new Set())
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(() => new Set())
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
const addEmail = useCallback(

View File

@@ -34,7 +34,7 @@ export function CustomTools() {
const deleteToolMutation = useDeleteCustomTool()
const [searchTerm, setSearchTerm] = useState('')
const [deletingTools, setDeletingTools] = useState<Set<string>>(new Set())
const [deletingTools, setDeletingTools] = useState<Set<string>>(() => new Set())
const [editingTool, setEditingTool] = useState<string | null>(null)
const [showAddForm, setShowAddForm] = useState(false)
const [toolToDelete, setToolToDelete] = useState<{ id: string; name: string } | null>(null)

View File

@@ -440,7 +440,7 @@ export function MCP({ initialServerId }: MCPProps) {
const [isAddingServer, setIsAddingServer] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
const [deletingServers, setDeletingServers] = useState<Set<string>>(() => new Set())
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null)
@@ -449,7 +449,7 @@ export function MCP({ initialServerId }: MCPProps) {
const [refreshingServers, setRefreshingServers] = useState<
Record<string, { status: 'refreshing' | 'refreshed'; workflowsUpdated?: number }>
>({})
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set())
const [expandedTools, setExpandedTools] = useState<Set<string>>(() => new Set())
const [showEnvVars, setShowEnvVars] = useState(false)
const [envSearchTerm, setEnvSearchTerm] = useState('')

View File

@@ -35,7 +35,7 @@ export function Skills() {
const deleteSkillMutation = useDeleteSkill()
const [searchTerm, setSearchTerm] = useState('')
const [deletingSkills, setDeletingSkills] = useState<Set<string>>(new Set())
const [deletingSkills, setDeletingSkills] = useState<Set<string>>(() => new Set())
const [editingSkill, setEditingSkill] = useState<SkillDefinition | null>(null)
const [showAddForm, setShowAddForm] = useState(false)
const [skillToDelete, setSkillToDelete] = useState<{ id: string; name: string } | null>(null)

View File

@@ -49,9 +49,9 @@ export function TeamMembers({
isAdminOrOwner,
onRemoveMember,
}: TeamMembersProps) {
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(new Set())
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(new Set())
const [resentInvitations, setResentInvitations] = useState<Set<string>>(new Set())
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(() => new Set())
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(() => new Set())
const [resentInvitations, setResentInvitations] = useState<Set<string>>(() => new Set())
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
const { data: memberUsageResponse, isLoading: isLoadingUsage } = useOrganizationMembers(

View File

@@ -117,9 +117,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
}, [toolToView])
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null)
const mcpServerUrl = useMemo(() => {
return `${getBaseUrl()}/api/mcp/serve/${serverId}`
}, [serverId])
const mcpServerUrl = `${getBaseUrl()}/api/mcp/serve/${serverId}`
const handleDeleteTool = async () => {
if (!toolToDelete) return
@@ -879,7 +877,7 @@ export function WorkflowMcpServers() {
const [selectedWorkflowIds, setSelectedWorkflowIds] = useState<string[]>([])
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
const [serverToDelete, setServerToDelete] = useState<WorkflowMcpServer | null>(null)
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
const [deletingServers, setDeletingServers] = useState<Set<string>>(() => new Set())
const filteredServers = useMemo(() => {
if (!searchTerm.trim()) return servers

View File

@@ -29,7 +29,7 @@ interface FileAttachmentDisplayProps {
*/
export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDisplayProps) => {
const [fileUrls, setFileUrls] = useState<Record<string, string>>({})
const [failedImages, setFailedImages] = useState<Set<string>>(new Set())
const [failedImages, setFailedImages] = useState<Set<string>>(() => new Set())
/**
* Formats file size in bytes to human-readable format

View File

@@ -236,7 +236,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
scrollContainer.addEventListener('scroll', checkPosition, { passive: true })
}
window.addEventListener('scroll', checkPosition, true)
window.addEventListener('scroll', checkPosition, { capture: true, passive: true })
window.addEventListener('resize', checkPosition)
return () => {

View File

@@ -148,7 +148,7 @@ export function McpDeploy({
return isDefaultDescription ? '' : workflowDescription
})
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(new Set())
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(() => new Set())
const [saveErrors, setSaveErrors] = useState<string[]>([])
const parameterSchema = useMemo(

View File

@@ -135,8 +135,8 @@ function ConnectionItem({
* Connection blocks component that displays incoming connections with their schemas
*/
export function ConnectionBlocks({ connections, currentBlockId }: ConnectionBlocksProps) {
const [expandedConnections, setExpandedConnections] = useState<Set<string>>(new Set())
const [expandedFieldPaths, setExpandedFieldPaths] = useState<Set<string>>(new Set())
const [expandedConnections, setExpandedConnections] = useState<Set<string>>(() => new Set())
const [expandedFieldPaths, setExpandedFieldPaths] = useState<Set<string>>(() => new Set())
const scrollContainerRef = useRef<HTMLDivElement>(null)
const connectionRefs = useRef<Map<string, HTMLDivElement>>(new Map())

View File

@@ -4,7 +4,6 @@ import {
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
@@ -182,10 +181,7 @@ export function LongInput({
)
// During streaming, use local content; otherwise use the controller value
const value = useMemo(() => {
if (wandHook.isStreaming) return localContent
return ctrl.valueString
}, [wandHook.isStreaming, localContent, ctrl.valueString])
const value = wandHook.isStreaming ? localContent : ctrl.valueString
// Base value for syncing (not including streaming)
const baseValue = isPreview

View File

@@ -620,7 +620,7 @@ export const Terminal = memo(function Terminal() {
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set())
const [isToggling, setIsToggling] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [showInput, setShowInput] = useState(false)

View File

@@ -65,12 +65,12 @@ export function TrainingModal() {
const [copiedId, setCopiedId] = useState<string | null>(null)
const [viewingDataset, setViewingDataset] = useState<string | null>(null)
const [expandedDataset, setExpandedDataset] = useState<string | null>(null)
const [sendingDatasets, setSendingDatasets] = useState<Set<string>>(new Set())
const [sendingDatasets, setSendingDatasets] = useState<Set<string>>(() => new Set())
const [sendingAll, setSendingAll] = useState(false)
const [selectedDatasets, setSelectedDatasets] = useState<Set<string>>(new Set())
const [selectedDatasets, setSelectedDatasets] = useState<Set<string>>(() => new Set())
const [sendingSelected, setSendingSelected] = useState(false)
const [sentDatasets, setSentDatasets] = useState<Set<string>>(new Set())
const [failedDatasets, setFailedDatasets] = useState<Set<string>>(new Set())
const [sentDatasets, setSentDatasets] = useState<Set<string>>(() => new Set())
const [failedDatasets, setFailedDatasets] = useState<Set<string>>(() => new Set())
const [sendingLiveWorkflow, setSendingLiveWorkflow] = useState(false)
const [liveWorkflowSent, setLiveWorkflowSent] = useState(false)
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)

View File

@@ -277,7 +277,7 @@ function ConnectionsSection({
onResizeMouseDown,
onToggleCollapsed,
}: ConnectionsSectionProps) {
const [expandedBlocks, setExpandedBlocks] = useState<Set<string>>(new Set())
const [expandedBlocks, setExpandedBlocks] = useState<Set<string>>(() => new Set())
const [expandedVariables, setExpandedVariables] = useState(true)
const [expandedEnvVars, setExpandedEnvVars] = useState(true)

View File

@@ -1,7 +1,6 @@
'use client'
import { useEffect } from 'react'
import { configure } from 'onedollarstats'
import { env } from '@/lib/core/config/env'
export function OneDollarStats() {
@@ -12,11 +11,15 @@ export function OneDollarStats() {
return
}
configure({
collectorUrl: 'https://collector.onedollarstats.com/events',
autocollect: true,
hashRouting: true,
})
import('onedollarstats')
.then(({ configure }) => {
configure({
collectorUrl: 'https://collector.onedollarstats.com/events',
autocollect: true,
hashRouting: true,
})
})
.catch(() => {})
}, [])
return null

View File

@@ -260,7 +260,7 @@ function useJsonCollapse(
visibleLineIndices: number[]
toggleCollapse: (lineIndex: number) => void
} {
const [collapsedLines, setCollapsedLines] = useState<Set<number>>(new Set())
const [collapsedLines, setCollapsedLines] = useState<Set<number>>(() => new Set())
const collapsibleRegions = useMemo(() => {
if (!showCollapseColumn || language !== 'json') return new Map<number, CollapsibleRegion>()

View File

@@ -271,7 +271,7 @@ export function AccessControl() {
const [newGroupAutoAdd, setNewGroupAutoAdd] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const [deletingGroup, setDeletingGroup] = useState<{ id: string; name: string } | null>(null)
const [deletingGroupIds, setDeletingGroupIds] = useState<Set<string>>(new Set())
const [deletingGroupIds, setDeletingGroupIds] = useState<Set<string>>(() => new Set())
const { data: members = [], isPending: membersLoading } = usePermissionGroupMembers(
viewingGroup?.id
@@ -281,7 +281,7 @@ export function AccessControl() {
const [showConfigModal, setShowConfigModal] = useState(false)
const [editingConfig, setEditingConfig] = useState<PermissionGroupConfig | null>(null)
const [showAddMembersModal, setShowAddMembersModal] = useState(false)
const [selectedMemberIds, setSelectedMemberIds] = useState<Set<string>>(new Set())
const [selectedMemberIds, setSelectedMemberIds] = useState<Set<string>>(() => new Set())
const [providerSearchTerm, setProviderSearchTerm] = useState('')
const [integrationSearchTerm, setIntegrationSearchTerm] = useState('')
const [platformSearchTerm, setPlatformSearchTerm] = useState('')

View File

@@ -551,8 +551,9 @@ export function useUndoRedo() {
const batchRemoveInverse = entry.inverse as BatchRemoveEdgesOperation
const { edgeSnapshots } = batchRemoveInverse.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToRemove = edgeSnapshots
.filter((e) => useWorkflowStore.getState().edges.find((edge) => edge.id === e.id))
.filter((e) => existingEdgeIds.has(e.id))
.map((e) => e.id)
if (edgesToRemove.length > 0) {
@@ -576,8 +577,9 @@ export function useUndoRedo() {
const batchAddInverse = entry.inverse as BatchAddEdgesOperation
const { edgeSnapshots } = batchAddInverse.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = edgeSnapshots.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
if (edgesToAdd.length > 0) {
@@ -631,8 +633,9 @@ export function useUndoRedo() {
if (useWorkflowStore.getState().blocks[blockId]) {
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = affectedEdges.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
if (edgesToAdd.length > 0) {
addToQueue({
@@ -695,8 +698,9 @@ export function useUndoRedo() {
// If we're removing FROM a subflow (undo of add to subflow), remove edges after
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgeIdsToRemove = affectedEdges
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
if (edgeIdsToRemove.length > 0) {
useWorkflowStore.getState().batchRemoveEdges(edgeIdsToRemove)
@@ -732,6 +736,7 @@ export function useUndoRedo() {
// Collect all edge operations first
const allEdgesToAdd: Edge[] = []
const allEdgeIdsToRemove: string[] = []
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
for (const update of validUpdates) {
const { newParentId, affectedEdges } = update
@@ -739,7 +744,7 @@ export function useUndoRedo() {
// Moving OUT of subflow (undoing insert) → restore edges first
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const edgesToAdd = affectedEdges.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
allEdgesToAdd.push(...edgesToAdd)
}
@@ -747,7 +752,7 @@ export function useUndoRedo() {
// Moving INTO subflow (undoing removal) → remove edges first
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const edgeIds = affectedEdges
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
allEdgeIdsToRemove.push(...edgeIds)
}
@@ -1165,8 +1170,9 @@ export function useUndoRedo() {
const batchRemoveOp = entry.operation as BatchRemoveEdgesOperation
const { edgeSnapshots } = batchRemoveOp.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToRemove = edgeSnapshots
.filter((e) => useWorkflowStore.getState().edges.find((edge) => edge.id === e.id))
.filter((e) => existingEdgeIds.has(e.id))
.map((e) => e.id)
if (edgesToRemove.length > 0) {
@@ -1191,8 +1197,9 @@ export function useUndoRedo() {
const batchAddOp = entry.operation as BatchAddEdgesOperation
const { edgeSnapshots } = batchAddOp.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = edgeSnapshots.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
if (edgesToAdd.length > 0) {
@@ -1249,8 +1256,9 @@ export function useUndoRedo() {
if (useWorkflowStore.getState().blocks[blockId]) {
// If we're removing FROM a subflow, remove edges first
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgeIdsToRemove = affectedEdges
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
if (edgeIdsToRemove.length > 0) {
useWorkflowStore.getState().batchRemoveEdges(edgeIdsToRemove)
@@ -1316,8 +1324,9 @@ export function useUndoRedo() {
// If we're adding TO a subflow, restore edges after
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = affectedEdges.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
if (edgesToAdd.length > 0) {
addToQueue({
@@ -1351,6 +1360,7 @@ export function useUndoRedo() {
// Collect all edge operations first
const allEdgesToAdd: Edge[] = []
const allEdgeIdsToRemove: string[] = []
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
for (const update of validUpdates) {
const { newParentId, affectedEdges } = update
@@ -1358,7 +1368,7 @@ export function useUndoRedo() {
// Moving INTO subflow (redoing insert) → remove edges first
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const edgeIds = affectedEdges
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
allEdgeIdsToRemove.push(...edgeIds)
}
@@ -1366,7 +1376,7 @@ export function useUndoRedo() {
// Moving OUT of subflow (redoing removal) → restore edges after
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const edgesToAdd = affectedEdges.filter(
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
(e) => !existingEdgeIds.has(e.id)
)
allEdgesToAdd.push(...edgesToAdd)
}

View File

@@ -956,13 +956,15 @@ export function trackForcedToolUsage(
if (forcedToolNames.length > 0 && toolCallsResponse && toolCallsResponse.length > 0) {
const toolNames = toolCallsResponse.map((tc) => tc.function?.name || tc.name || tc.id)
const usedTools = forcedToolNames.filter((toolName) => toolNames.includes(toolName))
const toolNameSet = new Set(toolNames)
const usedTools = forcedToolNames.filter((toolName) => toolNameSet.has(toolName))
if (usedTools.length > 0) {
hasUsedForcedTool = true
updatedUsedForcedTools.push(...usedTools)
const remainingTools = forcedTools.filter((tool) => !updatedUsedForcedTools.includes(tool))
const usedSet = new Set(updatedUsedForcedTools)
const remainingTools = forcedTools.filter((tool) => !usedSet.has(tool))
if (remainingTools.length > 0) {
const nextToolToForce = remainingTools[0]

View File

@@ -127,7 +127,7 @@ export const useChatStore = create<ChatState>()(
const headers = ['timestamp', 'type', 'content']
const sortedMessages = messages.sort(
const sortedMessages = messages.toSorted(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)

View File

@@ -215,7 +215,7 @@ export const useFolderStore = create<FolderState>()(
const buildTree = (parentId: string | null, level = 0): FolderTreeNode[] => {
return folders
.filter((folder) => folder.parentId === parentId)
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
.toSorted((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
.map((folder) => ({
...folder,
children: buildTree(folder.id, level + 1),
@@ -231,7 +231,7 @@ export const useFolderStore = create<FolderState>()(
getChildFolders: (parentId) =>
Object.values(get().folders)
.filter((folder) => folder.parentId === parentId)
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
.toSorted((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
getFolderPath: (folderId) => {
const folders = get().folders

View File

@@ -97,7 +97,7 @@ export const useSearchModalStore = create<SearchModalState>()(
const filteredTriggers = filterBlocks(allTriggers) as typeof allTriggers
const priorityOrder = ['Start', 'Schedule', 'Webhook']
const sortedTriggers = [...filteredTriggers].sort((a, b) => {
const sortedTriggers = filteredTriggers.toSorted((a, b) => {
const aIndex = priorityOrder.indexOf(a.name)
const bIndex = priorityOrder.indexOf(b.name)
const aHasPriority = aIndex !== -1

View File

@@ -2494,7 +2494,7 @@ export const useCopilotStore = create<CopilotStore>()(
let masked = value
// Sort by length descending to mask longer IDs first
const sortedIds = Array.from(sensitiveCredentialIds).sort((a, b) => b.length - a.length)
const sortedIds = Array.from(sensitiveCredentialIds).toSorted((a, b) => b.length - a.length)
for (const id of sortedIds) {
if (id && masked.includes(id)) {
masked = masked.split(id).join('••••••••')