Skip to content

Commit a13a7fd

Browse files
committed
feat(react): allow passing a signal to refreshToken
Allow passing a signal to the `refreshToken` call to for example set timeout using an AbortController
1 parent fdc9ea8 commit a13a7fd

File tree

3 files changed

+122
-32
lines changed

3 files changed

+122
-32
lines changed

.changeset/proud-olives-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@labdigital/federated-token-react": minor
3+
---
4+
5+
Allow passing a signal to the `refreshToken` call to for example set timeout using an AbortController

packages/react/README.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Use the token data
3232
```tsx
3333
import { useAuth } from '@labdigital/federated-token-react';
3434

35-
const { loading, isAuthenticated, values } = useAuth();
35+
const { loading, isAuthenticated, values, refreshToken } = useAuth();
3636

3737
if (loading) {
3838
return <div>Loading...</div>;
@@ -42,3 +42,73 @@ return (
4242
<div>Values: {values}</div>
4343
)
4444
```
45+
46+
## Manual Token Refresh with AbortSignal
47+
48+
You can manually refresh the token and optionally provide an AbortSignal for cancellation:
49+
50+
```tsx
51+
import { useAuth } from '@labdigital/federated-token-react';
52+
53+
function MyComponent() {
54+
const { refreshToken } = useAuth();
55+
56+
const handleRefresh = async () => {
57+
try {
58+
// Simple refresh (uses default 10s timeout)
59+
await refreshToken();
60+
console.log('Token refreshed successfully');
61+
} catch (error) {
62+
console.error('Token refresh failed:', error);
63+
}
64+
};
65+
66+
const handleRefreshWithTimeout = async () => {
67+
try {
68+
// Use AbortSignal.timeout for clean timeout handling
69+
await refreshToken(AbortSignal.timeout(5000)); // 5 second timeout
70+
console.log('Token refreshed successfully');
71+
} catch (error) {
72+
if (error.name === 'AbortError') {
73+
console.error('Token refresh timed out');
74+
} else {
75+
console.error('Token refresh failed:', error);
76+
}
77+
}
78+
};
79+
80+
const handleRefreshWithCancel = async () => {
81+
const controller = new AbortController();
82+
83+
// Start the refresh
84+
const refreshPromise = refreshToken(controller.signal);
85+
86+
// Cancel after 3 seconds if still pending
87+
setTimeout(() => {
88+
console.log('Cancelling refresh request...');
89+
controller.abort();
90+
}, 3000);
91+
92+
try {
93+
await refreshPromise;
94+
} catch (error) {
95+
console.error('Refresh was cancelled or failed:', error);
96+
}
97+
};
98+
99+
return (
100+
<div>
101+
<button onClick={handleRefresh}>Refresh Token</button>
102+
<button onClick={handleRefreshWithTimeout}>Refresh with 5s Timeout</button>
103+
<button onClick={handleRefreshWithCancel}>Refresh with Cancel</button>
104+
</div>
105+
);
106+
}
107+
```
108+
109+
## Configuration Options
110+
111+
The `AuthProvider` accepts the following options:
112+
113+
- `refreshTimeoutMs`: Default timeout in milliseconds for token refresh requests (default: 10000ms)
114+
- `refreshHandler`: Custom function to handle token refresh with optional AbortSignal support

packages/react/src/provider.tsx

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ type AuthContextType = {
5151
logout: () => Promise<void>;
5252
validateLocalToken: () => void;
5353
checkToken: () => Promise<void>;
54-
refreshToken: () => Promise<boolean>;
54+
refreshToken: (signal?: AbortSignal) => Promise<boolean>;
5555
};
5656

5757
const AuthContext = createContext<AuthContextType | undefined>(undefined);
5858

5959
export type AuthProviderProps = {
6060
cookieNames?: CookieNames;
6161
logoutHandler?: () => void;
62-
refreshHandler?: () => Promise<boolean>;
62+
refreshHandler?: (signal?: AbortSignal) => Promise<boolean>;
63+
refreshTimeoutMs?: number;
6364

6465
// Deprecated
6566
refreshTokenEndpoint?: string;
@@ -300,46 +301,60 @@ export function AuthProvider({
300301
return undefined;
301302
};
302303

303-
const refreshAccessToken = async (): Promise<boolean> => {
304-
if (options.refreshHandler) {
305-
return await options.refreshHandler();
304+
const refreshAccessToken = async (signal?: AbortSignal): Promise<boolean> => {
305+
// If no signal is provided, create one with default timeout using AbortSignal.timeout
306+
if (!signal) {
307+
const timeout = options.refreshTimeoutMs ?? 10000; // Default 10 seconds
308+
signal = AbortSignal.timeout(timeout);
306309
}
307310

308-
if (!options.refreshTokenEndpoint || !options.refreshTokenMutation) {
309-
throw new Error("No refresh token endpoint or mutation provided");
310-
}
311+
try {
312+
if (options.refreshHandler) {
313+
return await options.refreshHandler(signal);
314+
}
311315

312-
// Since we are storing the refresh token in a cookie this will be sent
313-
// automatically by the browser.
314-
const response = await fetch(options.refreshTokenEndpoint, {
315-
method: "POST",
316-
body: options.refreshTokenMutation,
317-
headers: {
318-
"Content-Type": "application/json",
319-
},
320-
credentials: "include",
321-
});
316+
if (!options.refreshTokenEndpoint || !options.refreshTokenMutation) {
317+
throw new Error("No refresh token endpoint or mutation provided");
318+
}
322319

323-
if (!response.ok) {
324-
throw new Error("Failed to refresh token");
325-
}
320+
// Since we are storing the refresh token in a cookie this will be sent
321+
// automatically by the browser.
322+
const response = await fetch(options.refreshTokenEndpoint, {
323+
method: "POST",
324+
body: options.refreshTokenMutation,
325+
headers: {
326+
"Content-Type": "application/json",
327+
},
328+
credentials: "include",
329+
signal: signal,
330+
});
326331

327-
const data = await response.json();
328-
if (!data) {
329-
throw new Error("Failed to refresh token");
330-
}
332+
if (!response.ok) {
333+
throw new Error("Failed to refresh token");
334+
}
335+
336+
const data = await response.json();
337+
if (!data) {
338+
throw new Error("Failed to refresh token");
339+
}
331340

332-
// Check if there is a GraphQL error
333-
if (data.errors && data.errors.length > 0) {
334-
throw new Error("Failed to refresh token");
341+
// Check if there is a GraphQL error
342+
if (data.errors && data.errors.length > 0) {
343+
throw new Error("Failed to refresh token");
344+
}
345+
return data;
346+
} catch (error) {
347+
if (error instanceof Error && error.name === "AbortError") {
348+
const timeout = options.refreshTimeoutMs ?? 10000;
349+
throw new Error(`Token refresh timed out after ${timeout}ms`);
350+
}
351+
throw error;
335352
}
336-
return data;
337353
};
338354

339355
const clearTokens = async () => {
340356
if (options.logoutHandler) {
341-
await options.logoutHandler();
342-
return;
357+
return options.logoutHandler();
343358
}
344359

345360
if (!options.logoutEndpoint || !options.logoutMutation) {

0 commit comments

Comments
 (0)