Skip to content

Commit b488fff

Browse files
committed
chore(utils): replace deepClone wrapper with structuredClone built-in
deepClone() was a one-line wrapper around structuredClone(), which is universally available in Node 17+ and all modern browsers. Removing the abstraction reduces indirection and means contributors don't need to learn a project-specific name for a well-known built-in. - Remove deepClone from packages/utils/src/object.ts and index.ts - Replace all 17 call sites with structuredClone() directly - Update check:utils script suggestion text - Update CLAUDE.md and global.md docs
1 parent bac2224 commit b488fff

22 files changed

Lines changed: 39 additions & 61 deletions

File tree

.claude/rules/global.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Use shared helpers from `@sim/utils` instead of writing inline implementations:
3939
- `sleep(ms)` from `@sim/utils/helpers` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
4040
- `toError(value)` from `@sim/utils/errors` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
4141
- `getErrorMessage(value, fallback?)` from `@sim/utils/errors` — extract error message string. Never write `e instanceof Error ? e.message : 'fallback'`
42-
- `deepClone(value)` from `@sim/utils/object` — structural clone. Never write `JSON.parse(JSON.stringify(obj))`
42+
- `structuredClone(value)` — built-in deep clone, no import needed. Never write `JSON.parse(JSON.stringify(obj))`
4343
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
4444
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
4545
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
@@ -56,10 +56,10 @@ const filtered = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !==
5656
// ✓ Good
5757
import { sleep } from '@sim/utils/helpers'
5858
import { getErrorMessage, toError } from '@sim/utils/errors'
59-
import { deepClone, filterUndefined } from '@sim/utils/object'
59+
import { filterUndefined } from '@sim/utils/object'
6060
await sleep(1000)
6161
const msg = getErrorMessage(error, 'Unknown error')
62-
const clone = deepClone(obj)
62+
const clone = structuredClone(obj)
6363
const filtered = filterUndefined(obj)
6464
```
6565

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ You are a professional software engineer. All code must follow best practices: a
1414
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
1515
- `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`
1616
- `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`
17-
- `deepClone(value)` from `@sim/utils/object` — structural clone; never `JSON.parse(JSON.stringify(...))`
17+
- `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`
1818
- `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`
1919
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis
2020
- `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline

apps/sim/app/resume/[workflowId]/[executionId]/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { deepClone } from '@sim/utils/object'
21
import type { Metadata } from 'next'
32
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
43
import ResumeExecutionPage from '@/app/resume/[workflowId]/[executionId]/resume-page-client'
@@ -40,7 +39,7 @@ export default async function ResumeExecutionPageWrapper({
4039
return (
4140
<ResumeExecutionPage
4241
params={resolvedParams}
43-
initialExecutionDetail={detail ? deepClone(detail) : null}
42+
initialExecutionDetail={detail ? structuredClone(detail) : null}
4443
initialContextId={initialContextId}
4544
/>
4645
)

apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
6-
import { deepClone } from '@sim/utils/object'
76
import {
87
Button,
98
Input as EmcnInput,
@@ -329,7 +328,7 @@ export function McpServerFormModal({
329328
if (open && !prevOpen) {
330329
const data = initialData ?? DEFAULT_FORM_DATA
331330
setFormData(data)
332-
setOriginalData(deepClone(data))
331+
setOriginalData(structuredClone(data))
333332
setFormMode('form')
334333
setJsonInput('')
335334
setJsonError(null)

apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-manager.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { generateShortId } from '@sim/utils/id'
7-
import { deepClone } from '@sim/utils/object'
87
import { useQueryClient } from '@tanstack/react-query'
98
import { Check, Clipboard, Key, Search } from 'lucide-react'
109
import { useParams, useRouter } from 'next/navigation'
@@ -599,8 +598,8 @@ export function SecretsManager() {
599598
})),
600599
createEmptyEnvVar(),
601600
]
602-
initialVarsRef.current = deepClone(initialVars)
603-
setEnvVars(deepClone(initialVars))
601+
initialVarsRef.current = structuredClone(initialVars)
602+
setEnvVars(structuredClone(initialVars))
604603
}, [personalEnvData])
605604

606605
useEffect(() => {
@@ -957,7 +956,7 @@ export function SecretsManager() {
957956
}
958957

959958
const resetToSaved = () => {
960-
setEnvVars(deepClone(initialVarsRef.current))
959+
setEnvVars(structuredClone(initialVarsRef.current))
961960
setWorkspaceVars({ ...initialWorkspaceVarsRef.current })
962961
setNewWorkspaceRows([createEmptyEnvVar()])
963962
setShowUnsavedChanges(false)
@@ -1038,7 +1037,7 @@ export function SecretsManager() {
10381037
if (firstFailure) throw firstFailure.reason
10391038

10401039
initialWorkspaceVarsRef.current = { ...mergedWorkspaceVars }
1041-
initialVarsRef.current = deepClone(envVars.filter((v) => v.key && v.value))
1040+
initialVarsRef.current = structuredClone(envVars.filter((v) => v.key && v.value))
10421041

10431042
setWorkspaceVars(mergedWorkspaceVars)
10441043
setNewWorkspaceRows([createEmptyEnvVar()])

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useCallback, useEffect, useRef } from 'react'
22
import { createLogger } from '@sim/logger'
3-
import { deepClone } from '@sim/utils/object'
43
import { isEqual } from 'es-toolkit'
54
import { useShallow } from 'zustand/react/shallow'
65
import { useStoreWithEqualityFn } from 'zustand/traditional'
@@ -150,7 +149,11 @@ export function useSubBlockValue<T = any>(
150149

151150
// Ensure we're passing the actual value, not a reference that might change
152151
const valueCopy =
153-
newValue === null ? null : typeof newValue === 'object' ? deepClone(newValue) : newValue
152+
newValue === null
153+
? null
154+
: typeof newValue === 'object'
155+
? structuredClone(newValue)
156+
: newValue
154157

155158
// If streaming, hold value locally and do not update global store to avoid render-phase updates
156159
if (isStreaming) {

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/engine.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createLogger } from '@sim/logger'
2-
import { deepClone } from '@sim/utils/object'
32
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
43
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
54
import { validateEdges } from '@/stores/workflows/workflow/edge-validation'
@@ -153,7 +152,7 @@ export function applyOperationsToWorkflowState(
153152
permissionConfig: PermissionGroupConfig | null = null
154153
): ApplyOperationsResult {
155154
// Deep clone the workflow state to avoid mutations
156-
const modifiedState = deepClone(workflowState)
155+
const modifiedState = structuredClone(workflowState)
157156

158157
// Collect validation errors across all operations
159158
const validationErrors: ValidationError[] = []

apps/sim/lib/core/security/csp.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createEnvMock, featureFlagsMock } from '@sim/testing'
2-
import { deepClone } from '@sim/utils/object'
32
import { afterEach, describe, expect, it, vi } from 'vitest'
43

54
vi.mock('@/lib/core/config/env', () =>
@@ -186,7 +185,7 @@ describe('generateRuntimeCSP', () => {
186185
})
187186

188187
describe('addCSPSource', () => {
189-
const originalDirectives = deepClone(buildTimeCSPDirectives)
188+
const originalDirectives = structuredClone(buildTimeCSPDirectives)
190189

191190
afterEach(() => {
192191
Object.keys(buildTimeCSPDirectives).forEach((key) => {
@@ -223,7 +222,7 @@ describe('addCSPSource', () => {
223222
})
224223

225224
describe('removeCSPSource', () => {
226-
const originalDirectives = deepClone(buildTimeCSPDirectives)
225+
const originalDirectives = structuredClone(buildTimeCSPDirectives)
227226

228227
afterEach(() => {
229228
Object.keys(buildTimeCSPDirectives).forEach((key) => {

apps/sim/lib/core/security/redaction.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { deepClone } from '@sim/utils/object'
21
import { describe, expect, it } from 'vitest'
32
import {
43
isLargeDataKey,
@@ -538,7 +537,7 @@ describe('Security edge cases', () => {
538537
},
539538
}
540539

541-
const originalCopy = deepClone(original)
540+
const originalCopy = structuredClone(original)
542541
redactApiKeys(original)
543542

544543
expect(original).toEqual(originalCopy)

apps/sim/lib/workflows/autolayout/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
3-
import { deepClone } from '@sim/utils/object'
43
import {
54
DEFAULT_HORIZONTAL_SPACING,
65
DEFAULT_VERTICAL_SPACING,
@@ -33,7 +32,7 @@ export function applyAutoLayout(
3332
edgeCount: edges.length,
3433
})
3534

36-
const blocksCopy: Record<string, BlockState> = deepClone(blocks)
35+
const blocksCopy: Record<string, BlockState> = structuredClone(blocks)
3736

3837
const horizontalSpacing = options.horizontalSpacing ?? DEFAULT_HORIZONTAL_SPACING
3938
const verticalSpacing = options.verticalSpacing ?? DEFAULT_VERTICAL_SPACING

0 commit comments

Comments
 (0)