Skip to content

Commit 8d7bbbc

Browse files
authored
chore(utils): migrate to shared random/ID utilities and add enforcement linting (#4623)
* chore(utils): migrate to shared random/ID utilities and add enforcement linting - Replace all Math.random(), crypto.randomUUID(), crypto.randomBytes(), nanoid, and uuid usages with shared @sim/utils/random and @sim/utils/id helpers across 72 files - Add new @sim/utils exports: deepClone, omit, filterUndefined (object), truncate (string), backoffWithJitter, parseRetryAfter (retry), getErrorMessage (errors) - Sweep all getErrorMessage, sleep, deepClone callsites across 500+ files to use shared utilities - Add Biome noRestrictedImports rule to catch nanoid, uuid, and crypto named imports at lint time - Add scripts/check-utils-enforcement.ts to catch Math.random and crypto.* global property access - Add check:utils script to package.json * 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 * fix(utils): add missing biome noRestrictedImports rule and correct truncate docs - Add noRestrictedImports to biome.json under style — bans nanoid and uuid package imports at lint time (crypto.randomUUID/randomBytes are caught by the check:utils grep script which handles global property access) - Correct truncate() TSDoc and parameter name: sliceLength makes it clear that total output length is sliceLength + suffix.length, matching the behavior all callers were already written to expect * fix(utils): add missing getErrorMessage imports at 4 call sites The sweep agents added getErrorMessage calls without the corresponding import in 4 files, causing test failures. Added the missing imports. * fix(utils): fix build errors from getErrorMessage sweep and retry.ts Turbopack issue - Fix retry.ts cross-file import: Turbopack cannot resolve './random.js' for internal package imports; inline the jitter crypto call directly - Add missing getErrorMessage imports to 32 files where the sweep added calls without the corresponding import (caught by type-check and test runs) - Remove accidental getErrorMessage import from crowdstrike/query/route.ts which has its own domain-specific getErrorMessage for parsing CrowdStrike's JSON error format - Fix use-sub-block-value.ts type error from structuredClone narrowing: add 'as T' cast at emitValue callsite (safe — valueCopy is always a structural copy of newValue) * fix(tools): use toError in crowdstrike catch block instead of local getErrorMessage The catch block was calling the local getErrorMessage function which parses CrowdStrike API JSON responses, not JavaScript Error objects. Use toError(error).message to correctly extract the message from a caught value in this context.
1 parent c403faf commit 8d7bbbc

534 files changed

Lines changed: 1679 additions & 1050 deletions

File tree

Some content is hidden

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

.claude/rules/global.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,31 @@ const tiny = generateShortId(8)
3636
## Common Utilities
3737
Use shared helpers from `@sim/utils` instead of writing inline implementations:
3838

39-
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
40-
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
41-
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
39+
- `sleep(ms)` from `@sim/utils/helpers` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
40+
- `toError(value)` from `@sim/utils/errors` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
41+
- `getErrorMessage(value, fallback?)` from `@sim/utils/errors` — extract error message string. Never write `e instanceof Error ? e.message : 'fallback'`
42+
- `structuredClone(value)` — built-in deep clone, no import needed. Never write `JSON.parse(JSON.stringify(obj))`
43+
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
44+
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
45+
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
46+
- `backoffWithJitter(attempt, retryAfterMs, options?)` from `@sim/utils/retry` — exponential backoff with jitter
47+
- `parseRetryAfter(header)` from `@sim/utils/retry` — parse HTTP `Retry-After` header to milliseconds
4248

4349
```typescript
4450
// ✗ Bad
4551
await new Promise(resolve => setTimeout(resolve, 1000))
46-
const msg = error instanceof Error ? error.message : String(error)
47-
const err = error instanceof Error ? error : new Error(String(error))
52+
const msg = error instanceof Error ? error.message : 'Unknown error'
53+
const clone = JSON.parse(JSON.stringify(obj))
54+
const filtered = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))
4855

4956
// ✓ Good
5057
import { sleep } from '@sim/utils/helpers'
51-
import { toError } from '@sim/utils/errors'
58+
import { getErrorMessage, toError } from '@sim/utils/errors'
59+
import { filterUndefined } from '@sim/utils/object'
5260
await sleep(1000)
53-
const msg = toError(error).message
54-
const err = toError(error)
61+
const msg = getErrorMessage(error, 'Unknown error')
62+
const clone = structuredClone(obj)
63+
const filtered = filterUndefined(obj)
5564
```
5665

5766
## Package Manager

CLAUDE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ You are a professional software engineer. All code must follow best practices: a
1010
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
1111
- **Styling**: Never update global styles. Keep all styling local to components
1212
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`
13-
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations. `sleep(ms)` from `@sim/utils/helpers` for delays, `toError(e)` from `@sim/utils/errors` to normalize caught values.
13+
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations:
14+
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
15+
- `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`
16+
- `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`
17+
- `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`
18+
- `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`
19+
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis
20+
- `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline
1421
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
1522

1623
## Architecture

apps/realtime/src/database/operations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
VARIABLE_OPERATIONS,
1414
WORKFLOW_OPERATIONS,
1515
} from '@sim/realtime-protocol/constants'
16+
import { randomFloat } from '@sim/utils/random'
1617
import { getActiveWorkflowContext } from '@sim/workflow-authz'
1718
import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load'
1819
import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks'
@@ -204,7 +205,7 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
204205
throw new Error(`Workflow ${workflowId} is archived or unavailable`)
205206
}
206207

207-
if (op === BLOCK_OPERATIONS.UPDATE_POSITION && Math.random() < 0.01) {
208+
if (op === BLOCK_OPERATIONS.UPDATE_POSITION && randomFloat() < 0.01) {
208209
logger.debug('Socket DB operation sample:', {
209210
operation: op,
210211
target,

apps/realtime/src/handlers/operations.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
WORKFLOW_OPERATIONS,
1010
} from '@sim/realtime-protocol/constants'
1111
import { WorkflowOperationSchema } from '@sim/realtime-protocol/schemas'
12+
import { getErrorMessage } from '@sim/utils/errors'
1213
import { generateId } from '@sim/utils/id'
1314
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
1415
import { ZodError } from 'zod'
@@ -205,7 +206,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
205206
if (operationId) {
206207
socket.emit('operation-failed', {
207208
operationId,
208-
error: error instanceof Error ? error.message : 'Database persistence failed',
209+
error: getErrorMessage(error, 'Database persistence failed'),
209210
retryable: true,
210211
})
211212
}
@@ -247,7 +248,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
247248
if (operationId) {
248249
socket.emit('operation-failed', {
249250
operationId,
250-
error: error instanceof Error ? error.message : 'Database persistence failed',
251+
error: getErrorMessage(error, 'Database persistence failed'),
251252
retryable: true,
252253
})
253254
}
@@ -587,7 +588,7 @@ export function setupOperationsHandlers(socket: AuthenticatedSocket, roomManager
587588
})
588589
}
589590
} catch (error) {
590-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
591+
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
591592

592593
if (operationId) {
593594
socket.emit('operation-failed', {

apps/realtime/src/handlers/subblocks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
22
import { workflow, workflowBlocks } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
67
import { isWorkflowBlockProtected } from '@sim/workflow-types/workflow'
78
import { and, eq } from 'drizzle-orm'
@@ -208,7 +209,7 @@ export function setupSubblocksHandlers(socket: AuthenticatedSocket, roomManager:
208209
} catch (error) {
209210
logger.error('Error handling subblock update:', error)
210211

211-
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
212+
const errorMessage = getErrorMessage(error, 'Unknown error')
212213

213214
if (operationId) {
214215
socket.emit('operation-failed', {
@@ -360,7 +361,7 @@ async function flushSubblockUpdate(
360361
pending.opToSocket.forEach((socketId, opId) => {
361362
io.to(socketId).emit('operation-failed', {
362363
operationId: opId,
363-
error: error instanceof Error ? error.message : 'Unknown error',
364+
error: getErrorMessage(error, 'Unknown error'),
364365
retryable: true,
365366
})
366367
})

apps/realtime/src/handlers/variables.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { db } from '@sim/db'
22
import { workflow } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { VARIABLE_OPERATIONS } from '@sim/realtime-protocol/constants'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import { assertWorkflowMutable, WorkflowLockedError } from '@sim/workflow-authz'
67
import { eq } from 'drizzle-orm'
78
import type { AuthenticatedSocket } from '@/middleware/auth'
@@ -195,7 +196,7 @@ export function setupVariablesHandlers(socket: AuthenticatedSocket, roomManager:
195196
} catch (error) {
196197
logger.error('Error handling variable update:', error)
197198

198-
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
199+
const errorMessage = getErrorMessage(error, 'Unknown error')
199200

200201
if (operationId) {
201202
socket.emit('operation-failed', {
@@ -326,7 +327,7 @@ async function flushVariableUpdate(
326327
pending.opToSocket.forEach((socketId, opId) => {
327328
io.to(socketId).emit('operation-failed', {
328329
operationId: opId,
329-
error: error instanceof Error ? error.message : 'Unknown error',
330+
error: getErrorMessage(error, 'Unknown error'),
330331
retryable: true,
331332
})
332333
})

apps/realtime/src/index.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { createServer, request as httpRequest } from 'http'
77
import { createMockLogger } from '@sim/testing'
8+
import { randomInt } from '@sim/utils/random'
89
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
910
import { createSocketIOServer } from '@/config/socket'
1011
import { MemoryRoomManager } from '@/rooms'
@@ -95,7 +96,7 @@ describe('Socket Server Index Integration', () => {
9596
})
9697

9798
beforeEach(async () => {
98-
PORT = 3333 + Math.floor(Math.random() * 1000)
99+
PORT = 3333 + randomInt(0, 1000)
99100

100101
httpServer = createServer()
101102

@@ -120,7 +121,7 @@ describe('Socket Server Index Integration', () => {
120121
httpServer.on('error', (err: any) => {
121122
clearTimeout(timeout)
122123
if (err.code === 'EADDRINUSE') {
123-
PORT = 3333 + Math.floor(Math.random() * 1000)
124+
PORT = 3333 + randomInt(0, 1000)
124125
httpServer.close(() => {
125126
httpServer.listen(PORT, '0.0.0.0', () => {
126127
resolve()

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import { Eye, EyeOff } from 'lucide-react'
67
import Link from 'next/link'
78
import { useRouter, useSearchParams } from 'next/navigation'
@@ -292,8 +293,7 @@ export default function LoginPage({
292293
},
293294
})
294295
} catch (requestError) {
295-
let errorMessage =
296-
requestError instanceof Error ? requestError.message : 'Failed to request password reset'
296+
let errorMessage = getErrorMessage(requestError, 'Failed to request password reset')
297297

298298
if (
299299
errorMessage.includes('Invalid body parameters') ||
@@ -325,7 +325,7 @@ export default function LoginPage({
325325
logger.error('Error requesting password reset:', { error })
326326
setResetStatus({
327327
type: 'error',
328-
message: error instanceof Error ? error.message : 'Failed to request password reset',
328+
message: getErrorMessage(error, 'Failed to request password reset'),
329329
})
330330
} finally {
331331
setIsSubmittingReset(false)

apps/sim/app/(auth)/reset-password/reset-password-content.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Suspense, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import Link from 'next/link'
67
import { useRouter, useSearchParams } from 'next/navigation'
78
import { requestJson } from '@/lib/api/client/request'
@@ -53,7 +54,7 @@ function ResetPasswordContent() {
5354
logger.error('Error resetting password:', { error })
5455
setStatusMessage({
5556
type: 'error',
56-
text: error instanceof Error ? error.message : 'Failed to reset password',
57+
text: getErrorMessage(error, 'Failed to reset password'),
5758
})
5859
} finally {
5960
setIsSubmitting(false)

apps/sim/app/api/a2a/serve/[agentId]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-
22
import { db } from '@sim/db'
33
import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import { generateId } from '@sim/utils/id'
67
import { and, eq, isNull } from 'drizzle-orm'
78
import { type NextRequest, NextResponse } from 'next/server'
@@ -1394,7 +1395,7 @@ async function handleTaskResubscribe(
13941395
logger.error('Error during SSE poll:', error)
13951396
sendEvent('error', {
13961397
code: A2A_ERROR_CODES.INTERNAL_ERROR,
1397-
message: error instanceof Error ? error.message : 'Polling failed',
1398+
message: getErrorMessage(error, 'Polling failed'),
13981399
})
13991400
cleanup()
14001401
try {

0 commit comments

Comments
 (0)