Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions packages/core/realtime-js/src/RealtimeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,14 +482,18 @@ export default class RealtimeClient {
*
* On callback used, it will set the value of the token internal to the client.
*
* When a token is explicitly provided, it will be preserved across channel operations
* (including removeChannel and resubscribe). The `accessToken` callback will not be
* invoked until `setAuth()` is called without arguments.
* When a token is explicitly provided AND no `accessToken` callback is configured,
* it will be preserved across channel operations (including removeChannel and
* resubscribe) and the client stays in manual-token mode.
*
* When an `accessToken` callback IS configured, the callback is the source of truth:
* the client remains in callback mode and continues to refresh from it on heartbeat,
* even after a bootstrap/override `setAuth(token)` call.
*
* @param token A JWT string to override the token set on the client.
*
* @example Setting the authorization header
* // Use a manual token (preserved across resubscribes, ignores accessToken callback)
* // Use a manual token (preserved across resubscribes when no accessToken callback is set)
* client.realtime.setAuth('my-custom-jwt')
*
* // Switch back to using the accessToken callback
Expand Down Expand Up @@ -625,12 +629,12 @@ export default class RealtimeClient {
tokenToSend = this.accessTokenValue
}

// Track whether this token was manually set or fetched via callback
if (isManualToken) {
this._manuallySetToken = true
} else if (this.accessToken) {
// If we used the callback, clear the manual flag
// Track whether this token was manually set or fetched via callback.
// The callback is the source of truth for token refresh
if (this.accessToken) {
this._manuallySetToken = false
} else if (isManualToken) {
this._manuallySetToken = true
}

if (this.accessTokenValue != tokenToSend) {
Expand Down
16 changes: 16 additions & 0 deletions packages/core/supabase-js/test/unit/SupabaseClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,22 @@ describe('SupabaseClient', () => {
client.realtime.disconnect()
})

test('keeps Realtime in callback mode (auto-refresh enabled) after the accessToken bootstrap', async () => {
const customToken = 'custom-jwt-token'
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)

const client = createClient(URL, KEY, { accessToken: customAccessTokenFn })

// Wait for the constructor's async setAuth bootstrap to complete.
await new Promise((resolve) => setTimeout(resolve, 0))

// Still in callback mode -> the accessToken callback will keep being
// invoked on heartbeat to refresh the token.
expect((client.realtime as any)._isManualToken()).toBe(false)

client.realtime.disconnect()
})

test('should automatically populate token in channels when using custom JWT', async () => {
const customToken = 'custom-channel-token'
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)
Expand Down
Loading