@quiltt/core 5.1.1 → 5.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +3 -1
- package/dist/api/browser.d.ts +27 -3
- package/dist/api/graphql/index.cjs +2 -2
- package/dist/api/graphql/index.js +2 -2
- package/dist/api/rest/index.cjs +21 -7
- package/dist/api/rest/index.js +21 -7
- package/dist/config/index.cjs +1 -1
- package/dist/config/index.js +1 -1
- package/package.json +3 -3
- package/src/api/browser.ts +27 -3
- package/src/api/graphql/links/ActionCableLink.ts +1 -3
- package/src/api/graphql/links/AuthLink.ts +0 -2
- package/src/api/graphql/links/BatchHttpLink.ts +0 -2
- package/src/api/graphql/links/ErrorLink.ts +0 -2
- package/src/api/graphql/links/ForwardableLink.ts +0 -2
- package/src/api/graphql/links/HttpLink.ts +0 -2
- package/src/api/graphql/links/RetryLink.ts +0 -2
- package/src/api/graphql/links/SubscriptionLink.ts +1 -3
- package/src/api/graphql/links/TerminatingLink.ts +0 -2
- package/src/api/graphql/links/VersionLink.ts +1 -1
- package/src/api/rest/fetchWithRetry.ts +25 -5
- package/src/observables/observable.ts +0 -2
- package/src/timing/timeoutable.ts +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @quiltt/core
|
|
2
2
|
|
|
3
|
+
## 5.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#425](https://github.com/quiltt/quiltt-js/pull/425) [`c684b3b`](https://github.com/quiltt/quiltt-js/commit/c684b3b5f6ea2829e2abfa2a75c0d430edad66a5) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Add @quiltt/capacitor package for Ionic and Capacitor apps
|
|
8
|
+
|
|
9
|
+
- Framework-agnostic by default — works with Vue, Angular, Svelte, or vanilla JS
|
|
10
|
+
- Vue 3 components via `@quiltt/capacitor/vue` subpath
|
|
11
|
+
- React components via `@quiltt/capacitor/react` subpath
|
|
12
|
+
- Native iOS (Swift) and Android (Kotlin) plugins for OAuth deep linking
|
|
13
|
+
- Supports Capacitor 6, 7, and 8
|
|
14
|
+
|
|
15
|
+
Add @quiltt/vue package for Vue 3 applications
|
|
16
|
+
|
|
17
|
+
- `QuilttPlugin` for session management via Vue's provide/inject
|
|
18
|
+
- `useQuilttSession` composable for authentication
|
|
19
|
+
- `useQuilttConnector` composable for programmatic control
|
|
20
|
+
- `QuilttButton`, `QuilttConnector`, `QuilttContainer` components
|
|
21
|
+
- Add `@quiltt/capacitor/vue` entry point for Capacitor apps
|
|
22
|
+
|
|
23
|
+
Rename `oauthRedirectUrl` to `appLauncherUrl` for mobile OAuth flows
|
|
24
|
+
|
|
25
|
+
This change introduces `appLauncherUrl` as the new preferred property name for specifying the Universal Link (iOS) or App Link (Android) that redirects users back to your app after OAuth authentication.
|
|
26
|
+
|
|
27
|
+
**Deprecation Warning:** The `oauthRedirectUrl` property is now deprecated but remains fully functional for backwards compatibility. Existing code using `oauthRedirectUrl` will continue to work without modifications.
|
|
28
|
+
|
|
29
|
+
**Migration:**
|
|
30
|
+
|
|
31
|
+
- Replace `oauthRedirectUrl` with `appLauncherUrl` in your component props
|
|
32
|
+
- The behavior remains identical; only the property name has changed
|
|
33
|
+
|
|
34
|
+
**Example:**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Before (deprecated, still works)
|
|
38
|
+
<QuilttConnector oauthRedirectUrl="https://myapp.com/callback" />
|
|
39
|
+
|
|
40
|
+
// After (recommended)
|
|
41
|
+
<QuilttConnector appLauncherUrl="https://myapp.com/callback" />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 5.1.2
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- [#423](https://github.com/quiltt/quiltt-js/pull/423) [`aca4d51`](https://github.com/quiltt/quiltt-js/commit/aca4d51bc699f80e50977d28b120280db9a4414d) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Fix VersionLink to allow custom Quiltt-SDK-Agent header override
|
|
49
|
+
|
|
3
50
|
## 5.1.1
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
`@quiltt/core` provides essential primitives for building Javascript-based applications with Quiltt. It provides an Auth API client and modules for handling JSON Web Tokens (JWT), observables, storage management, timeouts, API handling, and Typescript types.
|
|
7
7
|
|
|
8
|
-
This package is used by
|
|
8
|
+
This package is used by [`@quiltt/react`](../react#readme), [`@quiltt/vue`](../vue#readme), and [`@quiltt/react-native`](../react-native#readme). If you bundle it separately, we recommend keeping versions in sync to avoid issues with mismatched dependencies.
|
|
9
9
|
|
|
10
10
|
For general project information and contributing guidelines, see the [main repository README](../../README.md).
|
|
11
11
|
|
|
@@ -112,4 +112,6 @@ For information on how to contribute to this project, please refer to the [repos
|
|
|
112
112
|
## Related Packages
|
|
113
113
|
|
|
114
114
|
- [`@quiltt/react`](../react#readme) - React components and hooks
|
|
115
|
+
- [`@quiltt/vue`](../vue#readme) - Vue 3 components and composables
|
|
115
116
|
- [`@quiltt/react-native`](../react-native#readme) - React Native and Expo components
|
|
117
|
+
- [`@quiltt/capacitor`](../capacitor#readme) - Capacitor plugin and mobile framework adapters
|
package/dist/api/browser.d.ts
CHANGED
|
@@ -101,7 +101,15 @@ type ConnectorSDKCallbackMetadata = {
|
|
|
101
101
|
type ConnectorSDKConnectOptions = ConnectorSDKCallbacks & {
|
|
102
102
|
/** The Institution ID or search term to connect */
|
|
103
103
|
institution?: string;
|
|
104
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The app launcher URL for mobile OAuth flows.
|
|
106
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
107
|
+
*/
|
|
108
|
+
appLauncherUrl?: string;
|
|
109
|
+
/**
|
|
110
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
111
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
112
|
+
*/
|
|
105
113
|
oauthRedirectUrl?: string;
|
|
106
114
|
};
|
|
107
115
|
/**
|
|
@@ -111,7 +119,15 @@ type ConnectorSDKConnectOptions = ConnectorSDKCallbacks & {
|
|
|
111
119
|
type ConnectorSDKReconnectOptions = ConnectorSDKCallbacks & {
|
|
112
120
|
/** The ID of the Connection to reconnect */
|
|
113
121
|
connectionId: string;
|
|
114
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* The app launcher URL for mobile OAuth flows.
|
|
124
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
125
|
+
*/
|
|
126
|
+
appLauncherUrl?: string;
|
|
127
|
+
/**
|
|
128
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
129
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
130
|
+
*/
|
|
115
131
|
oauthRedirectUrl?: string;
|
|
116
132
|
};
|
|
117
133
|
/** Options to initialize Connector
|
|
@@ -126,7 +142,15 @@ type ConnectorSDKConnectorOptions = ConnectorSDKCallbacks & {
|
|
|
126
142
|
connectionId?: string;
|
|
127
143
|
/** The nonce to use for the script tag */
|
|
128
144
|
nonce?: string;
|
|
129
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* The app launcher URL for mobile OAuth flows.
|
|
147
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
148
|
+
*/
|
|
149
|
+
appLauncherUrl?: string;
|
|
150
|
+
/**
|
|
151
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
152
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
153
|
+
*/
|
|
130
154
|
oauthRedirectUrl?: string;
|
|
131
155
|
};
|
|
132
156
|
|
|
@@ -199,9 +199,9 @@ const createVersionLink = (platformInfo)=>{
|
|
|
199
199
|
return new core.ApolloLink((operation, forward)=>{
|
|
200
200
|
operation.setContext(({ headers = {} })=>({
|
|
201
201
|
headers: {
|
|
202
|
-
...headers,
|
|
203
202
|
'Quiltt-Client-Version': index_cjs.version,
|
|
204
|
-
'Quiltt-SDK-Agent': sdkAgent
|
|
203
|
+
'Quiltt-SDK-Agent': sdkAgent,
|
|
204
|
+
...headers
|
|
205
205
|
}
|
|
206
206
|
}));
|
|
207
207
|
return forward(operation);
|
|
@@ -194,9 +194,9 @@ const createVersionLink = (platformInfo)=>{
|
|
|
194
194
|
return new ApolloLink((operation, forward)=>{
|
|
195
195
|
operation.setContext(({ headers = {} })=>({
|
|
196
196
|
headers: {
|
|
197
|
-
...headers,
|
|
198
197
|
'Quiltt-Client-Version': version,
|
|
199
|
-
'Quiltt-SDK-Agent': sdkAgent
|
|
198
|
+
'Quiltt-SDK-Agent': sdkAgent,
|
|
199
|
+
...headers
|
|
200
200
|
}
|
|
201
201
|
}));
|
|
202
202
|
return forward(operation);
|
package/dist/api/rest/index.cjs
CHANGED
|
@@ -37,15 +37,20 @@ var crossfetch__default = /*#__PURE__*/_interopDefault(crossfetch);
|
|
|
37
37
|
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch__default.default : fetch;
|
|
38
38
|
const RETRY_DELAY = 150 // ms
|
|
39
39
|
;
|
|
40
|
-
const
|
|
40
|
+
const MAX_RETRY_DELAY = 1500 // ms
|
|
41
41
|
;
|
|
42
|
+
const RETRIES = 10;
|
|
43
|
+
const getRetryDelay = (attemptNumber)=>{
|
|
44
|
+
const exponentialDelay = RETRY_DELAY * 2 ** (attemptNumber - 1);
|
|
45
|
+
return Math.min(exponentialDelay, MAX_RETRY_DELAY);
|
|
46
|
+
};
|
|
42
47
|
/**
|
|
43
48
|
* A wrapper around the native `fetch` function that adds automatic retries on failure, including network errors and HTTP 429 responses.
|
|
44
49
|
* Now treats any response with status < 500 as valid.
|
|
45
50
|
*/ const fetchWithRetry = async (url, options = {
|
|
46
51
|
retry: false
|
|
47
52
|
})=>{
|
|
48
|
-
const { retry, retriesRemaining, validateStatus = (status)=>status >= 200 && status < 300, ...fetchOptions } = options;
|
|
53
|
+
const { retry, retriesRemaining, initialRetries, validateStatus = (status)=>status >= 200 && status < 300, ...fetchOptions } = options;
|
|
49
54
|
try {
|
|
50
55
|
const response = await effectiveFetch(url, fetchOptions);
|
|
51
56
|
const isResponseOk = validateStatus(response.status);
|
|
@@ -60,18 +65,27 @@ const RETRIES = 10 // 150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500 = 8.2
|
|
|
60
65
|
}
|
|
61
66
|
// If validateStatus fails, and retry is enabled, prepare to retry for eligible status codes
|
|
62
67
|
if (retry && (response.status >= 500 || response.status === 429)) {
|
|
63
|
-
|
|
68
|
+
const error = new Error(`HTTP error with status ${response.status}`);
|
|
69
|
+
error.retryable = true;
|
|
70
|
+
throw error;
|
|
64
71
|
}
|
|
65
|
-
|
|
72
|
+
const error = new Error(`HTTP error with status ${response.status}`);
|
|
73
|
+
error.retryable = false;
|
|
74
|
+
throw error;
|
|
66
75
|
} catch (error) {
|
|
67
|
-
|
|
76
|
+
const retryableError = error;
|
|
77
|
+
const shouldRetry = retry && retryableError.retryable !== false;
|
|
78
|
+
if (shouldRetry) {
|
|
68
79
|
const currentRetriesRemaining = retriesRemaining !== undefined ? retriesRemaining : RETRIES;
|
|
80
|
+
const totalRetries = initialRetries ?? currentRetriesRemaining;
|
|
69
81
|
if (currentRetriesRemaining > 0) {
|
|
70
|
-
const
|
|
82
|
+
const attemptNumber = totalRetries - currentRetriesRemaining + 1;
|
|
83
|
+
const delayTime = getRetryDelay(attemptNumber);
|
|
71
84
|
await new Promise((resolve)=>setTimeout(resolve, delayTime));
|
|
72
85
|
return fetchWithRetry(url, {
|
|
73
86
|
...options,
|
|
74
|
-
retriesRemaining: currentRetriesRemaining - 1
|
|
87
|
+
retriesRemaining: currentRetriesRemaining - 1,
|
|
88
|
+
initialRetries: totalRetries
|
|
75
89
|
});
|
|
76
90
|
}
|
|
77
91
|
}
|
package/dist/api/rest/index.js
CHANGED
|
@@ -31,15 +31,20 @@ import crossfetch from 'cross-fetch';
|
|
|
31
31
|
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch;
|
|
32
32
|
const RETRY_DELAY = 150 // ms
|
|
33
33
|
;
|
|
34
|
-
const
|
|
34
|
+
const MAX_RETRY_DELAY = 1500 // ms
|
|
35
35
|
;
|
|
36
|
+
const RETRIES = 10;
|
|
37
|
+
const getRetryDelay = (attemptNumber)=>{
|
|
38
|
+
const exponentialDelay = RETRY_DELAY * 2 ** (attemptNumber - 1);
|
|
39
|
+
return Math.min(exponentialDelay, MAX_RETRY_DELAY);
|
|
40
|
+
};
|
|
36
41
|
/**
|
|
37
42
|
* A wrapper around the native `fetch` function that adds automatic retries on failure, including network errors and HTTP 429 responses.
|
|
38
43
|
* Now treats any response with status < 500 as valid.
|
|
39
44
|
*/ const fetchWithRetry = async (url, options = {
|
|
40
45
|
retry: false
|
|
41
46
|
})=>{
|
|
42
|
-
const { retry, retriesRemaining, validateStatus = (status)=>status >= 200 && status < 300, ...fetchOptions } = options;
|
|
47
|
+
const { retry, retriesRemaining, initialRetries, validateStatus = (status)=>status >= 200 && status < 300, ...fetchOptions } = options;
|
|
43
48
|
try {
|
|
44
49
|
const response = await effectiveFetch(url, fetchOptions);
|
|
45
50
|
const isResponseOk = validateStatus(response.status);
|
|
@@ -54,18 +59,27 @@ const RETRIES = 10 // 150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500 = 8.2
|
|
|
54
59
|
}
|
|
55
60
|
// If validateStatus fails, and retry is enabled, prepare to retry for eligible status codes
|
|
56
61
|
if (retry && (response.status >= 500 || response.status === 429)) {
|
|
57
|
-
|
|
62
|
+
const error = new Error(`HTTP error with status ${response.status}`);
|
|
63
|
+
error.retryable = true;
|
|
64
|
+
throw error;
|
|
58
65
|
}
|
|
59
|
-
|
|
66
|
+
const error = new Error(`HTTP error with status ${response.status}`);
|
|
67
|
+
error.retryable = false;
|
|
68
|
+
throw error;
|
|
60
69
|
} catch (error) {
|
|
61
|
-
|
|
70
|
+
const retryableError = error;
|
|
71
|
+
const shouldRetry = retry && retryableError.retryable !== false;
|
|
72
|
+
if (shouldRetry) {
|
|
62
73
|
const currentRetriesRemaining = retriesRemaining !== undefined ? retriesRemaining : RETRIES;
|
|
74
|
+
const totalRetries = initialRetries ?? currentRetriesRemaining;
|
|
63
75
|
if (currentRetriesRemaining > 0) {
|
|
64
|
-
const
|
|
76
|
+
const attemptNumber = totalRetries - currentRetriesRemaining + 1;
|
|
77
|
+
const delayTime = getRetryDelay(attemptNumber);
|
|
65
78
|
await new Promise((resolve)=>setTimeout(resolve, delayTime));
|
|
66
79
|
return fetchWithRetry(url, {
|
|
67
80
|
...options,
|
|
68
|
-
retriesRemaining: currentRetriesRemaining - 1
|
|
81
|
+
retriesRemaining: currentRetriesRemaining - 1,
|
|
82
|
+
initialRetries: totalRetries
|
|
69
83
|
});
|
|
70
84
|
}
|
|
71
85
|
}
|
package/dist/config/index.cjs
CHANGED
package/dist/config/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiltt/core",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.3",
|
|
4
4
|
"description": "Javascript API client and utilities for Quiltt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quiltt",
|
|
@@ -96,8 +96,8 @@
|
|
|
96
96
|
"rxjs": "^7.8.2"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
|
-
"@biomejs/biome": "2.4.
|
|
100
|
-
"@types/node": "24.
|
|
99
|
+
"@biomejs/biome": "2.4.3",
|
|
100
|
+
"@types/node": "24.11.0",
|
|
101
101
|
"@types/rails__actioncable": "8.0.3",
|
|
102
102
|
"@types/react": "19.2.14",
|
|
103
103
|
"bunchee": "6.9.4",
|
package/src/api/browser.ts
CHANGED
|
@@ -125,7 +125,15 @@ export type ConnectorSDKCallbackMetadata = {
|
|
|
125
125
|
export type ConnectorSDKConnectOptions = ConnectorSDKCallbacks & {
|
|
126
126
|
/** The Institution ID or search term to connect */
|
|
127
127
|
institution?: string
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The app launcher URL for mobile OAuth flows.
|
|
130
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
131
|
+
*/
|
|
132
|
+
appLauncherUrl?: string
|
|
133
|
+
/**
|
|
134
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
135
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
136
|
+
*/
|
|
129
137
|
oauthRedirectUrl?: string
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -136,7 +144,15 @@ export type ConnectorSDKConnectOptions = ConnectorSDKCallbacks & {
|
|
|
136
144
|
export type ConnectorSDKReconnectOptions = ConnectorSDKCallbacks & {
|
|
137
145
|
/** The ID of the Connection to reconnect */
|
|
138
146
|
connectionId: string
|
|
139
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* The app launcher URL for mobile OAuth flows.
|
|
149
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
150
|
+
*/
|
|
151
|
+
appLauncherUrl?: string
|
|
152
|
+
/**
|
|
153
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
154
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
155
|
+
*/
|
|
140
156
|
oauthRedirectUrl?: string
|
|
141
157
|
}
|
|
142
158
|
|
|
@@ -152,6 +168,14 @@ export type ConnectorSDKConnectorOptions = ConnectorSDKCallbacks & {
|
|
|
152
168
|
connectionId?: string
|
|
153
169
|
/** The nonce to use for the script tag */
|
|
154
170
|
nonce?: string
|
|
155
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* The app launcher URL for mobile OAuth flows.
|
|
173
|
+
* This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
|
|
174
|
+
*/
|
|
175
|
+
appLauncherUrl?: string
|
|
176
|
+
/**
|
|
177
|
+
* @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
|
|
178
|
+
* The OAuth redirect URL for mobile or embedded webview flows.
|
|
179
|
+
*/
|
|
156
180
|
oauthRedirectUrl?: string
|
|
157
181
|
}
|
|
@@ -16,7 +16,7 @@ type SubscriptionCallbacks = {
|
|
|
16
16
|
received?: (payload: unknown) => void
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
class ActionCableLink extends ApolloLink {
|
|
19
|
+
export class ActionCableLink extends ApolloLink {
|
|
20
20
|
cables: { [id: string]: Consumer }
|
|
21
21
|
channelName: string
|
|
22
22
|
actionName: string
|
|
@@ -119,5 +119,3 @@ class ActionCableLink extends ApolloLink {
|
|
|
119
119
|
})
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
export default ActionCableLink
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import ActionCableLink from './ActionCableLink'
|
|
3
|
+
import { ActionCableLink } from './ActionCableLink'
|
|
4
4
|
|
|
5
5
|
export class SubscriptionLink extends ActionCableLink {
|
|
6
6
|
constructor() {
|
|
7
7
|
super({ channelName: 'GraphQLChannel' })
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
export default SubscriptionLink
|
|
@@ -10,9 +10,9 @@ export const createVersionLink = (platformInfo: string) => {
|
|
|
10
10
|
return new ApolloLink((operation, forward) => {
|
|
11
11
|
operation.setContext(({ headers = {} }) => ({
|
|
12
12
|
headers: {
|
|
13
|
-
...headers,
|
|
14
13
|
'Quiltt-Client-Version': version,
|
|
15
14
|
'Quiltt-SDK-Agent': sdkAgent,
|
|
15
|
+
...headers,
|
|
16
16
|
},
|
|
17
17
|
}))
|
|
18
18
|
return forward(operation)
|
|
@@ -6,6 +6,7 @@ const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch
|
|
|
6
6
|
type FetchWithRetryOptions = RequestInit & {
|
|
7
7
|
retry?: boolean
|
|
8
8
|
retriesRemaining?: number
|
|
9
|
+
initialRetries?: number
|
|
9
10
|
validateStatus?: (status: number) => boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -18,7 +19,15 @@ export type FetchResponse<T> = {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const RETRY_DELAY = 150 // ms
|
|
21
|
-
const
|
|
22
|
+
const MAX_RETRY_DELAY = 1500 // ms
|
|
23
|
+
const RETRIES = 10
|
|
24
|
+
|
|
25
|
+
const getRetryDelay = (attemptNumber: number): number => {
|
|
26
|
+
const exponentialDelay = RETRY_DELAY * 2 ** (attemptNumber - 1)
|
|
27
|
+
return Math.min(exponentialDelay, MAX_RETRY_DELAY)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type RetryableError = Error & { retryable?: boolean }
|
|
22
31
|
|
|
23
32
|
/**
|
|
24
33
|
* A wrapper around the native `fetch` function that adds automatic retries on failure, including network errors and HTTP 429 responses.
|
|
@@ -31,6 +40,7 @@ export const fetchWithRetry = async <T>(
|
|
|
31
40
|
const {
|
|
32
41
|
retry,
|
|
33
42
|
retriesRemaining,
|
|
43
|
+
initialRetries,
|
|
34
44
|
validateStatus = (status) => status >= 200 && status < 300, // Default to success for 2xx responses
|
|
35
45
|
...fetchOptions
|
|
36
46
|
} = options
|
|
@@ -52,19 +62,29 @@ export const fetchWithRetry = async <T>(
|
|
|
52
62
|
|
|
53
63
|
// If validateStatus fails, and retry is enabled, prepare to retry for eligible status codes
|
|
54
64
|
if (retry && (response.status >= 500 || response.status === 429)) {
|
|
55
|
-
|
|
65
|
+
const error = new Error(`HTTP error with status ${response.status}`) as RetryableError
|
|
66
|
+
error.retryable = true
|
|
67
|
+
throw error
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
const error = new Error(`HTTP error with status ${response.status}`) as RetryableError
|
|
71
|
+
error.retryable = false
|
|
72
|
+
throw error
|
|
59
73
|
} catch (error) {
|
|
60
|
-
|
|
74
|
+
const retryableError = error as RetryableError
|
|
75
|
+
const shouldRetry = retry && retryableError.retryable !== false
|
|
76
|
+
|
|
77
|
+
if (shouldRetry) {
|
|
61
78
|
const currentRetriesRemaining = retriesRemaining !== undefined ? retriesRemaining : RETRIES
|
|
79
|
+
const totalRetries = initialRetries ?? currentRetriesRemaining
|
|
62
80
|
if (currentRetriesRemaining > 0) {
|
|
63
|
-
const
|
|
81
|
+
const attemptNumber = totalRetries - currentRetriesRemaining + 1
|
|
82
|
+
const delayTime = getRetryDelay(attemptNumber)
|
|
64
83
|
await new Promise((resolve) => setTimeout(resolve, delayTime))
|
|
65
84
|
return fetchWithRetry(url, {
|
|
66
85
|
...options,
|
|
67
86
|
retriesRemaining: currentRetriesRemaining - 1,
|
|
87
|
+
initialRetries: totalRetries,
|
|
68
88
|
})
|
|
69
89
|
}
|
|
70
90
|
}
|