@@ -5,18 +5,68 @@ import { Token } from './token';
55 * The configuration for retrying token refreshes.
66 */
77export interface RetryPolicy {
8- // The maximum number of attempts to retry token refreshes.
8+ /**
9+ * The maximum number of attempts to retry token refreshes.
10+ */
911 maxAttempts : number ;
10- // The initial delay in milliseconds before the first retry.
12+
13+ /**
14+ * The initial delay in milliseconds before the first retry.
15+ */
1116 initialDelayMs : number ;
12- // The maximum delay in milliseconds between retries (the calculated delay will be capped at this value).
17+
18+ /**
19+ * The maximum delay in milliseconds between retries.
20+ * The calculated delay will be capped at this value.
21+ */
1322 maxDelayMs : number ;
14- // The multiplier for exponential backoff between retries. e.g. 2 will double the delay each time.
23+
24+ /**
25+ * The multiplier for exponential backoff between retries.
26+ * @example
27+ * A value of 2 will double the delay each time:
28+ * - 1st retry: initialDelayMs
29+ * - 2nd retry: initialDelayMs * 2
30+ * - 3rd retry: initialDelayMs * 4
31+ */
1532 backoffMultiplier : number ;
16- // The percentage of jitter to apply to the delay. e.g. 0.1 will add or subtract up to 10% of the delay.
33+
34+ /**
35+ * The percentage of jitter to apply to the delay.
36+ * @example
37+ * A value of 0.1 will add or subtract up to 10% of the delay.
38+ */
1739 jitterPercentage ?: number ;
18- // A custom function to determine if a retry should be attempted based on the error and attempt number.
19- shouldRetry ?: ( error : unknown , attempt : number ) => boolean ;
40+
41+ /**
42+ * Function to classify errors from the identity provider as retryable or non-retryable.
43+ * Used to determine if a token refresh failure should be retried based on the type of error.
44+ *
45+ * Common use cases:
46+ * - Network errors that may be transient (should retry)
47+ * - Invalid credentials (should not retry)
48+ * - Rate limiting responses (should retry)
49+ *
50+ * @param error - The error from the identity provider
51+ * @param attempt - Current retry attempt (0-based)
52+ * @returns `true` if the error is considered transient and the operation should be retried
53+ *
54+ * @example
55+ * ```typescript
56+ * const retryPolicy: RetryPolicy = {
57+ * maxAttempts: 3,
58+ * initialDelayMs: 1000,
59+ * maxDelayMs: 5000,
60+ * backoffMultiplier: 2,
61+ * isRetryable: (error) => {
62+ * // Retry on network errors or rate limiting
63+ * return error instanceof NetworkError ||
64+ * error instanceof RateLimitError;
65+ * }
66+ * };
67+ * ```
68+ */
69+ isRetryable ?: ( error : unknown , attempt : number ) => boolean ;
2070}
2171
2272/**
@@ -36,14 +86,13 @@ export interface TokenManagerConfig {
3686}
3787
3888/**
39- * IDPError is an error that occurs while calling the underlying IdentityProvider .
89+ * IDPError indicates a failure from the identity provider .
4090 *
41- * It can be transient and if retry policy is configured, the token manager will attempt to obtain a token again.
42- * This means that receiving non-fatal error is not a stream termination event.
43- * The stream will be terminated only if the error is fatal.
91+ * The `isRetryable` flag is determined by the RetryPolicy's error classification function - if an error is
92+ * classified as retryable, it will be marked as transient and the token manager will attempt to recover.
4493 */
4594export class IDPError extends Error {
46- constructor ( public readonly message : string , public readonly isFatal : boolean ) {
95+ constructor ( public readonly message : string , public readonly isRetryable : boolean ) {
4796 super ( message ) ;
4897 this . name = 'IDPError' ;
4998 }
@@ -105,7 +154,6 @@ export class TokenManager<T> {
105154 */
106155 public start ( listener : TokenStreamListener < T > , initialDelayMs : number = 0 ) : Disposable {
107156 if ( this . listener ) {
108- console . log ( 'TokenManager is already running, stopping the previous instance' ) ;
109157 this . stop ( ) ;
110158 }
111159
@@ -142,14 +190,14 @@ export class TokenManager<T> {
142190 private shouldRetry ( error : unknown ) : boolean {
143191 if ( ! this . config . retry ) return false ;
144192
145- const { maxAttempts, shouldRetry } = this . config . retry ;
193+ const { maxAttempts, isRetryable } = this . config . retry ;
146194
147195 if ( this . retryAttempt >= maxAttempts ) {
148196 return false ;
149197 }
150198
151- if ( shouldRetry ) {
152- return shouldRetry ( error , this . retryAttempt ) ;
199+ if ( isRetryable ) {
200+ return isRetryable ( error , this . retryAttempt ) ;
153201 }
154202
155203 return false ;
@@ -172,16 +220,16 @@ export class TokenManager<T> {
172220 if ( this . shouldRetry ( error ) ) {
173221 this . retryAttempt ++ ;
174222 const retryDelay = this . calculateRetryDelay ( ) ;
175- this . notifyError ( `Token refresh failed (attempt ${ this . retryAttempt } ), retrying in ${ retryDelay } ms: ${ error } ` , false )
223+ this . notifyError ( `Token refresh failed (attempt ${ this . retryAttempt } ), retrying in ${ retryDelay } ms: ${ error } ` , true )
176224 this . scheduleNextRefresh ( retryDelay ) ;
177225 } else {
178- this . notifyError ( error , true ) ;
226+ this . notifyError ( error , false ) ;
179227 this . stop ( ) ;
180228 }
181229 }
182230 }
183231
184- private handleNewToken = async ( { token : nativeToken , ttlMs } : TokenResponse < T > ) : Promise < void > = > {
232+ private async handleNewToken ( { token : nativeToken , ttlMs } : TokenResponse < T > ) : Promise < void > {
185233 if ( ! this . listener ) {
186234 throw new Error ( 'TokenManager is not running, but a new token was received' ) ;
187235 }
@@ -255,13 +303,14 @@ export class TokenManager<T> {
255303 return this . currentToken ;
256304 }
257305
258- private notifyError = ( error : unknown , isFatal : boolean ) : void => {
306+
307+ private notifyError ( error : unknown , isRetryable : boolean ) : void {
259308 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
260309
261310 if ( ! this . listener ) {
262311 throw new Error ( `TokenManager is not running but received an error: ${ errorMessage } ` ) ;
263312 }
264313
265- this . listener . onError ( new IDPError ( errorMessage , isFatal ) ) ;
314+ this . listener . onError ( new IDPError ( errorMessage , isRetryable ) ) ;
266315 }
267316}
0 commit comments