Skip to content

Commit 8c9e182

Browse files
fix(infinite-get-session): pass session once per tree using session provider + multiple fixes (#1085)
* fix(infinite-get-session): pass session using session provider * prevent auto refetch * fix typing: * fix types * fix * fix oauth token for microsoft file selector * fix start block required error
1 parent 33dd59f commit 8c9e182

File tree

12 files changed

+156
-47
lines changed

12 files changed

+156
-47
lines changed

apps/sim/app/api/auth/socket-token/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { auth } from '@/lib/auth'
44

55
export async function POST() {
66
try {
7+
const hdrs = await headers()
78
const response = await auth.api.generateOneTimeToken({
8-
headers: await headers(),
9+
headers: hdrs,
910
})
1011

1112
if (!response) {
@@ -14,7 +15,6 @@ export async function POST() {
1415

1516
return NextResponse.json({ token: response.token })
1617
} catch (error) {
17-
console.error('Error generating one-time token:', error)
1818
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
1919
}
2020
}

apps/sim/app/layout.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createLogger } from '@/lib/logs/console/logger'
1010
import { getAssetUrl } from '@/lib/utils'
1111
import '@/app/globals.css'
1212

13+
import { SessionProvider } from '@/lib/session-context'
1314
import { ThemeProvider } from '@/app/theme-provider'
1415
import { ZoomPrevention } from '@/app/zoom-prevention'
1516

@@ -111,16 +112,18 @@ export default function RootLayout({ children }: { children: React.ReactNode })
111112
</head>
112113
<body suppressHydrationWarning>
113114
<ThemeProvider>
114-
<BrandedLayout>
115-
<ZoomPrevention />
116-
{children}
117-
{isHosted && (
118-
<>
119-
<SpeedInsights />
120-
<Analytics />
121-
</>
122-
)}
123-
</BrandedLayout>
115+
<SessionProvider>
116+
<BrandedLayout>
117+
<ZoomPrevention />
118+
{children}
119+
{isHosted && (
120+
<>
121+
<SpeedInsights />
122+
<Analytics />
123+
</>
124+
)}
125+
</BrandedLayout>
126+
</SessionProvider>
124127
</ThemeProvider>
125128
</body>
126129
</html>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export function MicrosoftFileSelector({
8888
const [showOAuthModal, setShowOAuthModal] = useState(false)
8989
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
9090
const initialFetchRef = useRef(false)
91+
// Track the last (credentialId, fileId) we attempted to resolve to avoid tight retry loops
92+
const lastMetaAttemptRef = useRef<string>('')
9193

9294
// Handle Microsoft Planner task selection
9395
const [plannerTasks, setPlannerTasks] = useState<PlannerTask[]>([])
@@ -496,11 +498,15 @@ export function MicrosoftFileSelector({
496498
setSelectedFileId('')
497499
onChange('')
498500
}
501+
// Reset memo when credential is cleared
502+
lastMetaAttemptRef.current = ''
499503
} else if (prevCredentialId && prevCredentialId !== selectedCredentialId) {
500504
// Credentials changed (not initial load) - clear file info to force refetch
501505
if (selectedFile) {
502506
setSelectedFile(null)
503507
}
508+
// Reset memo when switching credentials
509+
lastMetaAttemptRef.current = ''
504510
}
505511
}, [selectedCredentialId, selectedFile, onChange])
506512

@@ -514,10 +520,17 @@ export function MicrosoftFileSelector({
514520
(!selectedFile || selectedFile.id !== value) &&
515521
!isLoadingSelectedFile
516522
) {
523+
// Avoid tight retry loops by memoizing the last attempt tuple
524+
const attemptKey = `${selectedCredentialId}::${value}`
525+
if (lastMetaAttemptRef.current === attemptKey) {
526+
return
527+
}
528+
lastMetaAttemptRef.current = attemptKey
529+
517530
if (serviceId === 'microsoft-planner') {
518531
void fetchPlannerTaskById(value)
519532
} else {
520-
fetchFileById(value)
533+
void fetchFileById(value)
521534
}
522535
}
523536
}, [

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export function useSubBlockValue<T = any>(
6565
const storeValue = useSubBlockStore(
6666
useCallback(
6767
(state) => {
68-
if (!activeWorkflowId) return null
68+
// If the active workflow ID isn't available yet, return undefined so we can fall back to initialValue
69+
if (!activeWorkflowId) return undefined
6970
return state.workflowValues[activeWorkflowId]?.[blockId]?.[subBlockId] ?? null
7071
},
7172
[activeWorkflowId, blockId, subBlockId]

apps/sim/app/workspace/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function WorkspaceRootLayout({ children }: WorkspaceRootLayoutPro
1313
const user = session.data?.user
1414
? {
1515
id: session.data.user.id,
16-
name: session.data.user.name,
16+
name: session.data.user.name ?? undefined,
1717
email: session.data.user.email,
1818
}
1919
: undefined

apps/sim/contexts/socket-context.tsx

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
108108
const [isConnecting, setIsConnecting] = useState(false)
109109
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null)
110110
const [presenceUsers, setPresenceUsers] = useState<PresenceUser[]>([])
111+
const initializedRef = useRef(false)
111112

112113
// Get current workflow ID from URL params
113114
const params = useParams()
@@ -131,16 +132,16 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
131132

132133
// Helper function to generate a fresh socket token
133134
const generateSocketToken = async (): Promise<string> => {
134-
const tokenResponse = await fetch('/api/auth/socket-token', {
135+
// Avoid overlapping token requests
136+
const res = await fetch('/api/auth/socket-token', {
135137
method: 'POST',
136138
credentials: 'include',
139+
headers: { 'cache-control': 'no-store' },
137140
})
138-
139-
if (!tokenResponse.ok) {
140-
throw new Error('Failed to generate socket token')
141-
}
142-
143-
const { token } = await tokenResponse.json()
141+
if (!res.ok) throw new Error('Failed to generate socket token')
142+
const body = await res.json().catch(() => ({}))
143+
const token = body?.token
144+
if (!token || typeof token !== 'string') throw new Error('Invalid socket token')
144145
return token
145146
}
146147

@@ -149,12 +150,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
149150
if (!user?.id) return
150151

151152
// Only initialize if we don't have a socket and aren't already connecting
152-
if (socket || isConnecting) {
153+
if (initializedRef.current || socket || isConnecting) {
153154
logger.info('Socket already exists or is connecting, skipping initialization')
154155
return
155156
}
156157

157158
logger.info('Initializing socket connection for user:', user.id)
159+
initializedRef.current = true
158160
setIsConnecting(true)
159161

160162
const initializeSocket = async () => {
@@ -178,17 +180,14 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
178180
reconnectionDelay: 1000, // Start with 1 second delay
179181
reconnectionDelayMax: 30000, // Max 30 second delay
180182
timeout: 10000, // Back to original timeout
181-
auth: (cb) => {
182-
// Generate a fresh token for each connection attempt (including reconnections)
183-
generateSocketToken()
184-
.then((freshToken) => {
185-
logger.info('Generated fresh token for connection attempt')
186-
cb({ token: freshToken })
187-
})
188-
.catch((error) => {
189-
logger.error('Failed to generate fresh token for connection:', error)
190-
cb({ token: null }) // This will cause authentication to fail gracefully
191-
})
183+
auth: async (cb) => {
184+
try {
185+
const freshToken = await generateSocketToken()
186+
cb({ token: freshToken })
187+
} catch (error) {
188+
logger.error('Failed to generate fresh token for connection:', error)
189+
cb({ token: null })
190+
}
192191
},
193192
})
194193

apps/sim/hooks/use-user-permissions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export function useUserPermissions(
3434
const { data: session } = useSession()
3535

3636
const userPermissions = useMemo((): WorkspaceUserPermissions => {
37-
// If still loading or no session, return safe defaults
38-
if (permissionsLoading || !session?.user?.email) {
37+
const sessionEmail = session?.user?.email
38+
if (permissionsLoading || !sessionEmail) {
3939
return {
4040
canRead: false,
4141
canEdit: false,
@@ -48,13 +48,13 @@ export function useUserPermissions(
4848

4949
// Find current user in workspace permissions (case-insensitive)
5050
const currentUser = workspacePermissions?.users?.find(
51-
(user) => user.email.toLowerCase() === session.user.email.toLowerCase()
51+
(user) => user.email.toLowerCase() === sessionEmail.toLowerCase()
5252
)
5353

5454
// If user not found in workspace, they have no permissions
5555
if (!currentUser) {
5656
logger.warn('User not found in workspace permissions', {
57-
userEmail: session.user.email,
57+
userEmail: sessionEmail,
5858
hasPermissions: !!workspacePermissions,
5959
userCount: workspacePermissions?.users?.length || 0,
6060
})

apps/sim/lib/auth-client.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import { useContext } from 'react'
12
import { stripeClient } from '@better-auth/stripe/client'
2-
import { emailOTPClient, genericOAuthClient, organizationClient } from 'better-auth/client/plugins'
3+
import {
4+
customSessionClient,
5+
emailOTPClient,
6+
genericOAuthClient,
7+
organizationClient,
8+
} from 'better-auth/client/plugins'
39
import { createAuthClient } from 'better-auth/react'
10+
import type { auth } from '@/lib/auth'
411
import { env, getEnv } from '@/lib/env'
512
import { isDev, isProd } from '@/lib/environment'
13+
import { SessionContext, type SessionHookResult } from '@/lib/session-context'
614

715
export function getBaseURL() {
816
let baseURL
@@ -25,6 +33,7 @@ export const client = createAuthClient({
2533
plugins: [
2634
emailOTPClient(),
2735
genericOAuthClient(),
36+
customSessionClient<typeof auth>(),
2837
// Only include Stripe client in production
2938
...(isProd
3039
? [
@@ -37,7 +46,17 @@ export const client = createAuthClient({
3746
],
3847
})
3948

40-
export const { useSession, useActiveOrganization } = client
49+
export function useSession(): SessionHookResult {
50+
const ctx = useContext(SessionContext)
51+
if (!ctx) {
52+
throw new Error(
53+
'SessionProvider is not mounted. Wrap your app with <SessionProvider> in app/layout.tsx.'
54+
)
55+
}
56+
return ctx
57+
}
58+
59+
export const { useActiveOrganization } = client
4160

4261
export const useSubscription = () => {
4362
// In development, provide mock implementations

apps/sim/lib/auth.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
44
import { nextCookies } from 'better-auth/next-js'
55
import {
66
createAuthMiddleware,
7+
customSession,
78
emailOTP,
89
genericOAuth,
910
oneTimeToken,
@@ -208,6 +209,10 @@ export const auth = betterAuth({
208209
oneTimeToken({
209210
expiresIn: 24 * 60 * 60, // 24 hours - Socket.IO handles connection persistence with heartbeats
210211
}),
212+
customSession(async ({ user, session }) => ({
213+
user,
214+
session,
215+
})),
211216
emailOTP({
212217
sendVerificationOTP: async (data: {
213218
email: string
@@ -1480,8 +1485,9 @@ export const auth = betterAuth({
14801485

14811486
// Server-side auth helpers
14821487
export async function getSession() {
1488+
const hdrs = await headers()
14831489
return await auth.api.getSession({
1484-
headers: await headers(),
1490+
headers: hdrs,
14851491
})
14861492
}
14871493

apps/sim/lib/session-context.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client'
2+
3+
import type React from 'react'
4+
import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
5+
import { client } from '@/lib/auth-client'
6+
7+
export type AppSession = {
8+
user: {
9+
id: string
10+
email: string
11+
emailVerified?: boolean
12+
name?: string | null
13+
image?: string | null
14+
createdAt?: Date
15+
updatedAt?: Date
16+
} | null
17+
session?: {
18+
id?: string
19+
userId?: string
20+
activeOrganizationId?: string
21+
}
22+
} | null
23+
24+
export type SessionHookResult = {
25+
data: AppSession
26+
isPending: boolean
27+
error: Error | null
28+
refetch: () => Promise<void>
29+
}
30+
31+
export const SessionContext = createContext<SessionHookResult | null>(null)
32+
33+
export function SessionProvider({ children }: { children: React.ReactNode }) {
34+
const [data, setData] = useState<AppSession>(null)
35+
const [isPending, setIsPending] = useState(true)
36+
const [error, setError] = useState<Error | null>(null)
37+
38+
const loadSession = useCallback(async () => {
39+
try {
40+
setIsPending(true)
41+
setError(null)
42+
const res = await client.getSession()
43+
setData(res?.data ?? null)
44+
} catch (e) {
45+
setError(e instanceof Error ? e : new Error('Failed to fetch session'))
46+
} finally {
47+
setIsPending(false)
48+
}
49+
}, [])
50+
51+
useEffect(() => {
52+
loadSession()
53+
}, [loadSession])
54+
55+
const value = useMemo<SessionHookResult>(
56+
() => ({ data, isPending, error, refetch: loadSession }),
57+
[data, isPending, error, loadSession]
58+
)
59+
60+
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
61+
}

0 commit comments

Comments
 (0)