Skip to content

Commit bac2224

Browse files
committed
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
1 parent 0dc1611 commit bac2224

533 files changed

Lines changed: 1658 additions & 1041 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+
- `deepClone(value)` from `@sim/utils/object` — structural clone. 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 { deepClone, 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 = deepClone(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+
- `deepClone(value)` from `@sim/utils/object` — structural 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)