Skip to content

Commit 066d0e8

Browse files
committed
fix(secrets): eliminate slow save by parallelizing DB ops and fixing stuck button
Sequential per-variable, per-workspace DB round-trips in syncPersonalEnvCredentialsForUser caused O(W×K) latency (800–1600ms for 10 workspaces). Replaced with parallel workspace processing and batched upserts. Also parallelized secret decryption in the GET handler. On the client, removed the changeToken bug that left the Save button permanently disabled after a failed save, split the shared hasSavedRef into two independent flags to eliminate ordering races, and moved ref updates to after mutation success so optimistic state can never get stuck.
1 parent a1395f4 commit 066d0e8

3 files changed

Lines changed: 163 additions & 183 deletions

File tree

apps/sim/app/api/environment/route.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,22 @@ export const GET = withRouteHandler(async (request: Request) => {
120120
}
121121

122122
const encryptedVariables = result[0].variables as Record<string, string>
123-
const decryptedVariables: Record<string, EnvironmentVariable> = {}
124-
125-
for (const [key, encryptedValue] of Object.entries(encryptedVariables)) {
126-
try {
127-
const { decrypted } = await decryptSecret(encryptedValue)
128-
decryptedVariables[key] = { key, value: decrypted }
129-
} catch (error) {
130-
logger.error(`[${requestId}] Error decrypting variable ${key}`, error)
131-
decryptedVariables[key] = { key, value: '' }
132-
}
133-
}
123+
124+
const decryptedEntries = await Promise.all(
125+
Object.entries(encryptedVariables).map(async ([key, encryptedValue]) => {
126+
try {
127+
const { decrypted } = await decryptSecret(encryptedValue)
128+
return [key, { key, value: decrypted }] as const
129+
} catch (error) {
130+
logger.error(`[${requestId}] Error decrypting variable ${key}`, error)
131+
return [key, { key, value: '' }] as const
132+
}
133+
})
134+
)
135+
const decryptedVariables = Object.fromEntries(decryptedEntries) as Record<
136+
string,
137+
EnvironmentVariable
138+
>
134139

135140
return NextResponse.json({ data: decryptedVariables }, { status: 200 })
136141
} catch (error: any) {

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

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,6 @@ export function CredentialsManager() {
413413
const [workspaceVars, setWorkspaceVars] = useState<Record<string, string>>({})
414414
const [renamingKey, setRenamingKey] = useState<string | null>(null)
415415
const [pendingKeyValue, setPendingKeyValue] = useState<string>('')
416-
const [changeToken, setChangeToken] = useState(0)
417-
418416
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
419417
const [prevSelectedCredentialId, setPrevSelectedCredentialId] = useState<
420418
string | null | undefined
@@ -431,7 +429,8 @@ export function CredentialsManager() {
431429
const scrollContainerRef = useRef<HTMLDivElement>(null)
432430
const initialVarsRef = useRef<UIEnvironmentVariable[]>([])
433431
const hasChangesRef = useRef(false)
434-
const hasSavedRef = useRef(false)
432+
const hasSavedPersonalRef = useRef(false)
433+
const hasSavedWorkspaceRef = useRef(false)
435434
const shouldBlockNavRef = useRef(false)
436435
const pendingNavigationUrlRef = useRef<string | null>(null)
437436

@@ -558,7 +557,7 @@ export function CredentialsManager() {
558557
if (newWorkspaceRows.some((row) => row.key && row.value)) return true
559558

560559
return false
561-
}, [envVars, workspaceVars, newWorkspaceRows, changeToken])
560+
}, [envVars, workspaceVars, newWorkspaceRows])
562561

563562
const hasConflicts = useMemo(() => {
564563
return envVars.some((envVar) => !!envVar.key && allWorkspaceKeys.has(envVar.key))
@@ -588,7 +587,10 @@ export function CredentialsManager() {
588587
useEffect(() => () => resetNavGuard(), [resetNavGuard])
589588

590589
useEffect(() => {
591-
if (hasSavedRef.current) return
590+
if (hasSavedPersonalRef.current) {
591+
hasSavedPersonalRef.current = false
592+
return
593+
}
592594

593595
const existingVars = Object.values(personalEnvData || {})
594596
const initialVars = [
@@ -604,12 +606,12 @@ export function CredentialsManager() {
604606

605607
useEffect(() => {
606608
if (!workspaceEnvData) return
607-
if (hasSavedRef.current) {
608-
hasSavedRef.current = false
609-
} else {
610-
setWorkspaceVars(workspaceEnvData.workspace || {})
611-
initialWorkspaceVarsRef.current = workspaceEnvData.workspace || {}
609+
if (hasSavedWorkspaceRef.current) {
610+
hasSavedWorkspaceRef.current = false
611+
return
612612
}
613+
setWorkspaceVars(workspaceEnvData.workspace || {})
614+
initialWorkspaceVarsRef.current = workspaceEnvData.workspace || {}
613615
}, [workspaceEnvData])
614616

615617
const scrollToBottom = useCallback(() => {
@@ -967,86 +969,86 @@ export function CredentialsManager() {
967969
const handleSave = async () => {
968970
if (isListSaving) return
969971

970-
const prevInitialVars = [...initialVarsRef.current]
971-
const prevInitialWorkspaceVars = { ...initialWorkspaceVarsRef.current }
972972
const mutations: Promise<unknown>[] = []
973973

974-
try {
975-
setShowUnsavedChanges(false)
976-
hasSavedRef.current = true
977-
978-
const mergedWorkspaceVars = { ...workspaceVars }
979-
for (const row of newWorkspaceRows) {
980-
if (row.key && row.value) {
981-
mergedWorkspaceVars[row.key] = row.value
982-
}
974+
setShowUnsavedChanges(false)
975+
976+
const mergedWorkspaceVars = { ...workspaceVars }
977+
for (const row of newWorkspaceRows) {
978+
if (row.key && row.value) {
979+
mergedWorkspaceVars[row.key] = row.value
983980
}
981+
}
984982

985-
initialWorkspaceVarsRef.current = { ...mergedWorkspaceVars }
986-
initialVarsRef.current = JSON.parse(JSON.stringify(envVars.filter((v) => v.key && v.value)))
983+
const validVariables = envVars
984+
.filter((v) => v.key && v.value)
985+
.reduce<Record<string, string>>((acc, { key, value }) => ({ ...acc, [key]: value }), {})
987986

988-
setChangeToken((prev) => prev + 1)
987+
const before = initialWorkspaceVarsRef.current
988+
const after = mergedWorkspaceVars
989+
const toUpsert: Record<string, string> = {}
990+
const toDelete: string[] = []
989991

990-
const validVariables = envVars
991-
.filter((v) => v.key && v.value)
992-
.reduce<Record<string, string>>((acc, { key, value }) => ({ ...acc, [key]: value }), {})
992+
for (const [k, v] of Object.entries(after)) {
993+
if (!(k in before) || before[k] !== v) {
994+
toUpsert[k] = v
995+
}
996+
}
993997

994-
const before = prevInitialWorkspaceVars
995-
const after = mergedWorkspaceVars
996-
const toUpsert: Record<string, string> = {}
997-
const toDelete: string[] = []
998+
for (const k of Object.keys(before)) {
999+
if (!(k in after)) toDelete.push(k)
1000+
}
9981001

999-
for (const [k, v] of Object.entries(after)) {
1000-
if (!(k in before) || before[k] !== v) {
1001-
toUpsert[k] = v
1002-
}
1002+
const personalChanged = (() => {
1003+
const initialMap = new Map(
1004+
initialVarsRef.current.filter((v) => v.key && v.value).map((v) => [v.key, v.value])
1005+
)
1006+
const currentKeys = Object.keys(validVariables)
1007+
if (initialMap.size !== currentKeys.length) return true
1008+
for (const [key, value] of Object.entries(validVariables)) {
1009+
if (initialMap.get(key) !== value) return true
10031010
}
1011+
return false
1012+
})()
10041013

1005-
for (const k of Object.keys(before)) {
1006-
if (!(k in after)) toDelete.push(k)
1007-
}
1014+
const workspaceChanged =
1015+
workspaceId && (Object.keys(toUpsert).length > 0 || toDelete.length > 0)
10081016

1009-
const personalChanged = (() => {
1010-
const initialMap = new Map(
1011-
prevInitialVars.filter((v) => v.key && v.value).map((v) => [v.key, v.value])
1012-
)
1013-
const currentKeys = Object.keys(validVariables)
1014-
if (initialMap.size !== currentKeys.length) return true
1015-
for (const [key, value] of Object.entries(validVariables)) {
1016-
if (initialMap.get(key) !== value) return true
1017-
}
1018-
return false
1019-
})()
1020-
1021-
if (personalChanged) {
1022-
mutations.push(savePersonalMutation.mutateAsync({ variables: validVariables }))
1023-
}
1024-
if (workspaceId && (Object.keys(toUpsert).length || toDelete.length)) {
1025-
mutations.push(
1026-
(async () => {
1027-
if (Object.keys(toUpsert).length) {
1028-
await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert })
1029-
}
1030-
if (toDelete.length) {
1031-
await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete })
1032-
}
1033-
})()
1034-
)
1035-
}
1017+
if (personalChanged) {
1018+
mutations.push(savePersonalMutation.mutateAsync({ variables: validVariables }))
1019+
}
1020+
if (workspaceChanged) {
1021+
mutations.push(
1022+
(async () => {
1023+
if (Object.keys(toUpsert).length) {
1024+
await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert })
1025+
}
1026+
if (toDelete.length) {
1027+
await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete })
1028+
}
1029+
})()
1030+
)
1031+
}
1032+
1033+
hasSavedPersonalRef.current = personalChanged
1034+
hasSavedWorkspaceRef.current = Boolean(workspaceChanged)
10361035

1036+
try {
10371037
const results = await Promise.allSettled(mutations)
10381038
const firstFailure = results.find((r): r is PromiseRejectedResult => r.status === 'rejected')
10391039
if (firstFailure) throw firstFailure.reason
10401040

1041+
initialWorkspaceVarsRef.current = { ...mergedWorkspaceVars }
1042+
initialVarsRef.current = JSON.parse(JSON.stringify(envVars.filter((v) => v.key && v.value)))
1043+
10411044
setWorkspaceVars(mergedWorkspaceVars)
10421045
setNewWorkspaceRows([createEmptyEnvVar()])
10431046
if (mutations.length > 0) {
10441047
toast.success('Secrets saved')
10451048
}
10461049
} catch (error) {
1047-
hasSavedRef.current = false
1048-
initialVarsRef.current = prevInitialVars
1049-
initialWorkspaceVarsRef.current = prevInitialWorkspaceVars
1050+
hasSavedPersonalRef.current = false
1051+
hasSavedWorkspaceRef.current = false
10501052
logger.error('Failed to save environment variables:', error)
10511053
toast.error('Failed to save secrets')
10521054
} finally {

0 commit comments

Comments
 (0)