@robosystems/client 1.13.0 → 1.13.1
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/artifacts/InvestorClient.d.ts +7 -0
- package/artifacts/InvestorClient.ts +7 -0
- package/artifacts/LedgerClient.d.ts +7 -0
- package/artifacts/LedgerClient.ts +7 -0
- package/artifacts/LibraryClient.d.ts +7 -0
- package/artifacts/LibraryClient.ts +7 -0
- package/artifacts/graphql/client.d.ts +8 -0
- package/artifacts/graphql/client.js +8 -1
- package/artifacts/graphql/client.ts +17 -1
- package/artifacts/index.d.ts +1 -0
- package/artifacts/index.js +29 -1
- package/artifacts/index.ts +32 -0
- package/artifacts/retry.d.ts +59 -0
- package/artifacts/retry.js +121 -0
- package/artifacts/retry.ts +134 -0
- package/package.json +1 -1
|
@@ -30,6 +30,13 @@ interface InvestorClientConfig {
|
|
|
30
30
|
tokenProvider?: TokenProvider;
|
|
31
31
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
32
32
|
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
35
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
36
|
+
*/
|
|
37
|
+
maxRetries?: number;
|
|
38
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
39
|
+
retryDelay?: number;
|
|
33
40
|
}
|
|
34
41
|
export declare class InvestorClient {
|
|
35
42
|
private config;
|
|
@@ -101,6 +101,13 @@ interface InvestorClientConfig {
|
|
|
101
101
|
tokenProvider?: TokenProvider
|
|
102
102
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
103
103
|
timeout?: number
|
|
104
|
+
/**
|
|
105
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
106
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
107
|
+
*/
|
|
108
|
+
maxRetries?: number
|
|
109
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
110
|
+
retryDelay?: number
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export class InvestorClient {
|
|
@@ -227,6 +227,13 @@ interface LedgerClientConfig {
|
|
|
227
227
|
tokenProvider?: TokenProvider;
|
|
228
228
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
229
229
|
timeout?: number;
|
|
230
|
+
/**
|
|
231
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
232
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
233
|
+
*/
|
|
234
|
+
maxRetries?: number;
|
|
235
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
236
|
+
retryDelay?: number;
|
|
230
237
|
}
|
|
231
238
|
export declare class LedgerClient {
|
|
232
239
|
private config;
|
|
@@ -576,6 +576,13 @@ interface LedgerClientConfig {
|
|
|
576
576
|
tokenProvider?: TokenProvider
|
|
577
577
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
578
578
|
timeout?: number
|
|
579
|
+
/**
|
|
580
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
581
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
582
|
+
*/
|
|
583
|
+
maxRetries?: number
|
|
584
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
585
|
+
retryDelay?: number
|
|
579
586
|
}
|
|
580
587
|
|
|
581
588
|
export class LedgerClient {
|
|
@@ -71,6 +71,13 @@ interface LibraryClientConfig {
|
|
|
71
71
|
tokenProvider?: TokenProvider;
|
|
72
72
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
73
73
|
timeout?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
76
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
77
|
+
*/
|
|
78
|
+
maxRetries?: number;
|
|
79
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
80
|
+
retryDelay?: number;
|
|
74
81
|
}
|
|
75
82
|
export declare class LibraryClient {
|
|
76
83
|
private config;
|
|
@@ -150,6 +150,13 @@ interface LibraryClientConfig {
|
|
|
150
150
|
tokenProvider?: TokenProvider
|
|
151
151
|
/** GraphQL request timeout in milliseconds (default 60s). */
|
|
152
152
|
timeout?: number
|
|
153
|
+
/**
|
|
154
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
155
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection immediately.
|
|
156
|
+
*/
|
|
157
|
+
maxRetries?: number
|
|
158
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
159
|
+
retryDelay?: number
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
export class LibraryClient {
|
|
@@ -93,6 +93,14 @@ export interface GraphQLClientConfig {
|
|
|
93
93
|
* client). Applied per request via `AbortSignal.timeout(...)`.
|
|
94
94
|
*/
|
|
95
95
|
timeout?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
98
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection
|
|
99
|
+
* immediately. See `../retry`.
|
|
100
|
+
*/
|
|
101
|
+
maxRetries?: number;
|
|
102
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
103
|
+
retryDelay?: number;
|
|
96
104
|
}
|
|
97
105
|
/**
|
|
98
106
|
* Build a new GraphQL client for the given graph. Prefer
|
|
@@ -25,6 +25,7 @@ exports.createGraphQLClient = createGraphQLClient;
|
|
|
25
25
|
* from the query files in clients/graphql/queries/.
|
|
26
26
|
*/
|
|
27
27
|
const graphql_request_1 = require("graphql-request");
|
|
28
|
+
const retry_1 = require("../retry");
|
|
28
29
|
/**
|
|
29
30
|
* Default request timeout for GraphQL calls, in milliseconds.
|
|
30
31
|
*
|
|
@@ -124,7 +125,13 @@ function createGraphQLClient(config, graphId) {
|
|
|
124
125
|
const staticHeaders = {
|
|
125
126
|
...(config.headers ?? {}),
|
|
126
127
|
};
|
|
127
|
-
|
|
128
|
+
// Retry wraps timeout, not the other way round, so each replay gets
|
|
129
|
+
// its own full timeout rather than sharing one across attempts.
|
|
130
|
+
const timeoutFetch = (0, retry_1.createRetryingFetch)({
|
|
131
|
+
fetch: createTimeoutFetch(config.timeout ?? exports.DEFAULT_GRAPHQL_TIMEOUT_MS),
|
|
132
|
+
maxRetries: config.maxRetries ?? retry_1.DEFAULT_MAX_RETRIES,
|
|
133
|
+
retryDelay: config.retryDelay ?? retry_1.DEFAULT_RETRY_DELAY_MS,
|
|
134
|
+
});
|
|
128
135
|
// Dynamic-token path: defer credential injection to a per-request
|
|
129
136
|
// middleware so JWT refreshes are picked up without rebuilding or
|
|
130
137
|
// clearing the client. This is the recommended path for browser
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
import { ClientError, GraphQLClient } from 'graphql-request'
|
|
25
25
|
|
|
26
|
+
import { createRetryingFetch, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MS } from '../retry'
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* Default request timeout for GraphQL calls, in milliseconds.
|
|
28
30
|
*
|
|
@@ -113,6 +115,14 @@ export interface GraphQLClientConfig {
|
|
|
113
115
|
* client). Applied per request via `AbortSignal.timeout(...)`.
|
|
114
116
|
*/
|
|
115
117
|
timeout?: number
|
|
118
|
+
/**
|
|
119
|
+
* Replays of a rate-limited (429) request. Defaults to
|
|
120
|
+
* `DEFAULT_MAX_RETRIES`; set `0` to surface the rejection
|
|
121
|
+
* immediately. See `../retry`.
|
|
122
|
+
*/
|
|
123
|
+
maxRetries?: number
|
|
124
|
+
/** Base of the retry backoff, in milliseconds. */
|
|
125
|
+
retryDelay?: number
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
/**
|
|
@@ -175,7 +185,13 @@ export function createGraphQLClient(config: GraphQLClientConfig, graphId: string
|
|
|
175
185
|
const staticHeaders: Record<string, string> = {
|
|
176
186
|
...(config.headers ?? {}),
|
|
177
187
|
}
|
|
178
|
-
|
|
188
|
+
// Retry wraps timeout, not the other way round, so each replay gets
|
|
189
|
+
// its own full timeout rather than sharing one across attempts.
|
|
190
|
+
const timeoutFetch = createRetryingFetch({
|
|
191
|
+
fetch: createTimeoutFetch(config.timeout ?? DEFAULT_GRAPHQL_TIMEOUT_MS),
|
|
192
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
193
|
+
retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
|
|
194
|
+
})
|
|
179
195
|
|
|
180
196
|
// Dynamic-token path: defer credential injection to a per-request
|
|
181
197
|
// middleware so JWT refreshes are picked up without rebuilding or
|
package/artifacts/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { QueryClient } from './QueryClient';
|
|
|
12
12
|
import { SSEClient } from './SSEClient';
|
|
13
13
|
export type { TokenProvider } from './graphql/client';
|
|
14
14
|
export { DEFAULT_GRAPHQL_TIMEOUT_MS, GraphQLError } from './graphql/client';
|
|
15
|
+
export { createRetryingFetch, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MS, MAX_BACKOFF_MS, type RetryOptions, } from './retry';
|
|
15
16
|
export interface RoboSystemsClientConfig {
|
|
16
17
|
baseUrl?: string;
|
|
17
18
|
credentials?: 'include' | 'same-origin' | 'omit';
|
package/artifacts/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
18
18
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.operatorQuery = exports.streamQuery = exports.executeQuery = exports.monitorOperation = exports.clients = exports.useStreamingQuery = exports.useSDKClients = exports.useQuery = exports.useOperation = exports.useMultipleOperations = exports.SSEClient = exports.QueryClient = exports.OperatorClient = exports.OperationClient = exports.LibraryClient = exports.LedgerClient = exports.InvestorClient = exports.RoboSystemsClients = exports.GraphQLError = exports.DEFAULT_GRAPHQL_TIMEOUT_MS = void 0;
|
|
21
|
+
exports.operatorQuery = exports.streamQuery = exports.executeQuery = exports.monitorOperation = exports.clients = exports.useStreamingQuery = exports.useSDKClients = exports.useQuery = exports.useOperation = exports.useMultipleOperations = exports.SSEClient = exports.QueryClient = exports.OperatorClient = exports.OperationClient = exports.LibraryClient = exports.LedgerClient = exports.InvestorClient = exports.RoboSystemsClients = exports.MAX_BACKOFF_MS = exports.DEFAULT_RETRY_DELAY_MS = exports.DEFAULT_MAX_RETRIES = exports.createRetryingFetch = exports.GraphQLError = exports.DEFAULT_GRAPHQL_TIMEOUT_MS = void 0;
|
|
22
22
|
const client_gen_1 = require("../client.gen");
|
|
23
23
|
const config_1 = require("./config");
|
|
24
24
|
const InvestorClient_1 = require("./InvestorClient");
|
|
@@ -33,6 +33,7 @@ const OperatorClient_1 = require("./OperatorClient");
|
|
|
33
33
|
Object.defineProperty(exports, "OperatorClient", { enumerable: true, get: function () { return OperatorClient_1.OperatorClient; } });
|
|
34
34
|
const QueryClient_1 = require("./QueryClient");
|
|
35
35
|
Object.defineProperty(exports, "QueryClient", { enumerable: true, get: function () { return QueryClient_1.QueryClient; } });
|
|
36
|
+
const retry_1 = require("./retry");
|
|
36
37
|
const SSEClient_1 = require("./SSEClient");
|
|
37
38
|
Object.defineProperty(exports, "SSEClient", { enumerable: true, get: function () { return SSEClient_1.SSEClient; } });
|
|
38
39
|
// Structured GraphQL error thrown by facade reads (LedgerClient /
|
|
@@ -40,10 +41,31 @@ Object.defineProperty(exports, "SSEClient", { enumerable: true, get: function ()
|
|
|
40
41
|
var client_1 = require("./graphql/client");
|
|
41
42
|
Object.defineProperty(exports, "DEFAULT_GRAPHQL_TIMEOUT_MS", { enumerable: true, get: function () { return client_1.DEFAULT_GRAPHQL_TIMEOUT_MS; } });
|
|
42
43
|
Object.defineProperty(exports, "GraphQLError", { enumerable: true, get: function () { return client_1.GraphQLError; } });
|
|
44
|
+
// A `fetch` that replays rate-limited (429) requests. The facade
|
|
45
|
+
// clients install it themselves; export it so callers reaching for the
|
|
46
|
+
// raw generated SDK can opt in with
|
|
47
|
+
// `client.setConfig({ fetch: createRetryingFetch() })`.
|
|
48
|
+
var retry_2 = require("./retry");
|
|
49
|
+
Object.defineProperty(exports, "createRetryingFetch", { enumerable: true, get: function () { return retry_2.createRetryingFetch; } });
|
|
50
|
+
Object.defineProperty(exports, "DEFAULT_MAX_RETRIES", { enumerable: true, get: function () { return retry_2.DEFAULT_MAX_RETRIES; } });
|
|
51
|
+
Object.defineProperty(exports, "DEFAULT_RETRY_DELAY_MS", { enumerable: true, get: function () { return retry_2.DEFAULT_RETRY_DELAY_MS; } });
|
|
52
|
+
Object.defineProperty(exports, "MAX_BACKOFF_MS", { enumerable: true, get: function () { return retry_2.MAX_BACKOFF_MS; } });
|
|
43
53
|
class RoboSystemsClients {
|
|
44
54
|
constructor(config = {}) {
|
|
45
55
|
// Get base URL from SDK client config or use provided/default
|
|
46
56
|
const sdkConfig = client_gen_1.client.getConfig();
|
|
57
|
+
// Facade REST calls run through the generated ops, which share this
|
|
58
|
+
// module-level client — its `fetch` is the only interception point,
|
|
59
|
+
// since the ops take no per-call transport. A caller that already
|
|
60
|
+
// supplied one keeps it; we never override an explicit choice.
|
|
61
|
+
if (!sdkConfig.fetch) {
|
|
62
|
+
client_gen_1.client.setConfig({
|
|
63
|
+
fetch: (0, retry_1.createRetryingFetch)({
|
|
64
|
+
maxRetries: config.maxRetries ?? retry_1.DEFAULT_MAX_RETRIES,
|
|
65
|
+
retryDelay: config.retryDelay ?? retry_1.DEFAULT_RETRY_DELAY_MS,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
47
69
|
// Extract JWT token using centralized logic
|
|
48
70
|
const token = config.token || (0, config_1.extractTokenFromSDKClient)();
|
|
49
71
|
// `tokenProvider` falls back to the global SDK clients config so
|
|
@@ -94,6 +116,8 @@ class RoboSystemsClients {
|
|
|
94
116
|
tokenProvider: this.config.tokenProvider,
|
|
95
117
|
headers: this.config.headers,
|
|
96
118
|
timeout: this.config.timeout,
|
|
119
|
+
maxRetries: this.config.maxRetries,
|
|
120
|
+
retryDelay: this.config.retryDelay,
|
|
97
121
|
});
|
|
98
122
|
this.investor = new InvestorClient_1.InvestorClient({
|
|
99
123
|
baseUrl: this.config.baseUrl,
|
|
@@ -102,6 +126,8 @@ class RoboSystemsClients {
|
|
|
102
126
|
tokenProvider: this.config.tokenProvider,
|
|
103
127
|
headers: this.config.headers,
|
|
104
128
|
timeout: this.config.timeout,
|
|
129
|
+
maxRetries: this.config.maxRetries,
|
|
130
|
+
retryDelay: this.config.retryDelay,
|
|
105
131
|
});
|
|
106
132
|
// Library uses GraphQL and accepts graphId per-call — pass either
|
|
107
133
|
// the `"library"` sentinel (canonical) or any tenant graph_id
|
|
@@ -113,6 +139,8 @@ class RoboSystemsClients {
|
|
|
113
139
|
tokenProvider: this.config.tokenProvider,
|
|
114
140
|
headers: this.config.headers,
|
|
115
141
|
timeout: this.config.timeout,
|
|
142
|
+
maxRetries: this.config.maxRetries,
|
|
143
|
+
retryDelay: this.config.retryDelay,
|
|
116
144
|
});
|
|
117
145
|
// Reports consolidated into LedgerClient — alias for backward compat
|
|
118
146
|
this.reports = this.ledger;
|
package/artifacts/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { LibraryClient } from './LibraryClient'
|
|
|
12
12
|
import { OperationClient } from './OperationClient'
|
|
13
13
|
import { OperatorClient } from './OperatorClient'
|
|
14
14
|
import { QueryClient } from './QueryClient'
|
|
15
|
+
import { createRetryingFetch, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MS } from './retry'
|
|
15
16
|
import { SSEClient } from './SSEClient'
|
|
16
17
|
|
|
17
18
|
// Re-export the `TokenProvider` type so consumers who want to type
|
|
@@ -24,6 +25,18 @@ export type { TokenProvider } from './graphql/client'
|
|
|
24
25
|
// InvestorClient / LibraryClient), plus the default request timeout.
|
|
25
26
|
export { DEFAULT_GRAPHQL_TIMEOUT_MS, GraphQLError } from './graphql/client'
|
|
26
27
|
|
|
28
|
+
// A `fetch` that replays rate-limited (429) requests. The facade
|
|
29
|
+
// clients install it themselves; export it so callers reaching for the
|
|
30
|
+
// raw generated SDK can opt in with
|
|
31
|
+
// `client.setConfig({ fetch: createRetryingFetch() })`.
|
|
32
|
+
export {
|
|
33
|
+
createRetryingFetch,
|
|
34
|
+
DEFAULT_MAX_RETRIES,
|
|
35
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
36
|
+
MAX_BACKOFF_MS,
|
|
37
|
+
type RetryOptions,
|
|
38
|
+
} from './retry'
|
|
39
|
+
|
|
27
40
|
export interface RoboSystemsClientConfig {
|
|
28
41
|
baseUrl?: string
|
|
29
42
|
credentials?: 'include' | 'same-origin' | 'omit'
|
|
@@ -82,6 +95,19 @@ export class RoboSystemsClients {
|
|
|
82
95
|
// Get base URL from SDK client config or use provided/default
|
|
83
96
|
const sdkConfig = client.getConfig()
|
|
84
97
|
|
|
98
|
+
// Facade REST calls run through the generated ops, which share this
|
|
99
|
+
// module-level client — its `fetch` is the only interception point,
|
|
100
|
+
// since the ops take no per-call transport. A caller that already
|
|
101
|
+
// supplied one keeps it; we never override an explicit choice.
|
|
102
|
+
if (!sdkConfig.fetch) {
|
|
103
|
+
client.setConfig({
|
|
104
|
+
fetch: createRetryingFetch({
|
|
105
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
106
|
+
retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
// Extract JWT token using centralized logic
|
|
86
112
|
const token = config.token || extractTokenFromSDKClient()
|
|
87
113
|
|
|
@@ -138,6 +164,8 @@ export class RoboSystemsClients {
|
|
|
138
164
|
tokenProvider: this.config.tokenProvider,
|
|
139
165
|
headers: this.config.headers,
|
|
140
166
|
timeout: this.config.timeout,
|
|
167
|
+
maxRetries: this.config.maxRetries,
|
|
168
|
+
retryDelay: this.config.retryDelay,
|
|
141
169
|
})
|
|
142
170
|
|
|
143
171
|
this.investor = new InvestorClient({
|
|
@@ -147,6 +175,8 @@ export class RoboSystemsClients {
|
|
|
147
175
|
tokenProvider: this.config.tokenProvider,
|
|
148
176
|
headers: this.config.headers,
|
|
149
177
|
timeout: this.config.timeout,
|
|
178
|
+
maxRetries: this.config.maxRetries,
|
|
179
|
+
retryDelay: this.config.retryDelay,
|
|
150
180
|
})
|
|
151
181
|
|
|
152
182
|
// Library uses GraphQL and accepts graphId per-call — pass either
|
|
@@ -159,6 +189,8 @@ export class RoboSystemsClients {
|
|
|
159
189
|
tokenProvider: this.config.tokenProvider,
|
|
160
190
|
headers: this.config.headers,
|
|
161
191
|
timeout: this.config.timeout,
|
|
192
|
+
maxRetries: this.config.maxRetries,
|
|
193
|
+
retryDelay: this.config.retryDelay,
|
|
162
194
|
})
|
|
163
195
|
|
|
164
196
|
// Reports consolidated into LedgerClient — alias for backward compat
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit-aware `fetch` wrapper shared by the facade clients.
|
|
3
|
+
*
|
|
4
|
+
* The API rate-limits per user per endpoint category and answers an
|
|
5
|
+
* exhausted budget with `429` plus `Retry-After` / `X-RateLimit-*`
|
|
6
|
+
* headers. That rejection is raised by a request dependency *before*
|
|
7
|
+
* the endpoint handler runs, so the request had no effect and is always
|
|
8
|
+
* safe to replay — including a `POST` carrying no idempotency key.
|
|
9
|
+
* Nothing other than `429` is retried here, precisely because nothing
|
|
10
|
+
* else carries that guarantee.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `robosystems_client/clients/retry.py` in the Python client.
|
|
13
|
+
*/
|
|
14
|
+
/** The one status a rejected request is known to have had no effect for. */
|
|
15
|
+
export declare const RETRY_STATUS_CODES: Set<number>;
|
|
16
|
+
export declare const DEFAULT_MAX_RETRIES = 5;
|
|
17
|
+
export declare const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
18
|
+
export declare const MAX_BACKOFF_MS = 30000;
|
|
19
|
+
export interface RetryOptions {
|
|
20
|
+
/** Replays after the first attempt. `0` disables retrying entirely. */
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
/** Base of the exponential backoff, in milliseconds. */
|
|
23
|
+
retryDelay?: number;
|
|
24
|
+
/** Underlying fetch to wrap. Defaults to the global one, resolved per call. */
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse `Retry-After` as a delta-seconds value, in milliseconds.
|
|
29
|
+
*
|
|
30
|
+
* The API always sends the numeric form. The HTTP-date form is ignored
|
|
31
|
+
* rather than parsed, since treating an unreadable value as "no hint"
|
|
32
|
+
* degrades to plain backoff instead of to a wrong sleep.
|
|
33
|
+
*/
|
|
34
|
+
export declare function retryAfterMs(response: Response): number | null;
|
|
35
|
+
/**
|
|
36
|
+
* Milliseconds to wait before replaying a rate-limited request.
|
|
37
|
+
*
|
|
38
|
+
* Exponential with full jitter, and `Retry-After` applied as a
|
|
39
|
+
* *ceiling* rather than as the delay itself. The limiter is a sliding
|
|
40
|
+
* window, so `Retry-After` reports the whole window — the worst case
|
|
41
|
+
* for a client that filled its budget instantaneously. A caller that
|
|
42
|
+
* merely ran at the sustained rate has slots freeing up within a second
|
|
43
|
+
* or two, and obeying the header literally would turn a handful of
|
|
44
|
+
* rejections into minutes of idling.
|
|
45
|
+
*/
|
|
46
|
+
export declare function backoffMs(attempt: number, retryDelay: number, retryAfter: number | null): number;
|
|
47
|
+
/**
|
|
48
|
+
* Wrap a `fetch` so rate-limited requests are replayed.
|
|
49
|
+
*
|
|
50
|
+
* Composes over an existing fetch rather than replacing it, so a
|
|
51
|
+
* per-request timeout wrapper stays in place and each attempt gets its
|
|
52
|
+
* own timeout. The global `fetch` is resolved at call time (not
|
|
53
|
+
* captured) so test harnesses that swap `globalThis.fetch` keep
|
|
54
|
+
* working.
|
|
55
|
+
*
|
|
56
|
+
* Set `maxRetries: 0` to opt out — an interactive surface may well
|
|
57
|
+
* prefer to surface the rejection immediately rather than wait.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRetryingFetch(options?: RetryOptions): typeof fetch;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate-limit-aware `fetch` wrapper shared by the facade clients.
|
|
4
|
+
*
|
|
5
|
+
* The API rate-limits per user per endpoint category and answers an
|
|
6
|
+
* exhausted budget with `429` plus `Retry-After` / `X-RateLimit-*`
|
|
7
|
+
* headers. That rejection is raised by a request dependency *before*
|
|
8
|
+
* the endpoint handler runs, so the request had no effect and is always
|
|
9
|
+
* safe to replay — including a `POST` carrying no idempotency key.
|
|
10
|
+
* Nothing other than `429` is retried here, precisely because nothing
|
|
11
|
+
* else carries that guarantee.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors `robosystems_client/clients/retry.py` in the Python client.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.MAX_BACKOFF_MS = exports.DEFAULT_RETRY_DELAY_MS = exports.DEFAULT_MAX_RETRIES = exports.RETRY_STATUS_CODES = void 0;
|
|
17
|
+
exports.retryAfterMs = retryAfterMs;
|
|
18
|
+
exports.backoffMs = backoffMs;
|
|
19
|
+
exports.createRetryingFetch = createRetryingFetch;
|
|
20
|
+
/** The one status a rejected request is known to have had no effect for. */
|
|
21
|
+
exports.RETRY_STATUS_CODES = new Set([429]);
|
|
22
|
+
exports.DEFAULT_MAX_RETRIES = 5;
|
|
23
|
+
exports.DEFAULT_RETRY_DELAY_MS = 1000;
|
|
24
|
+
exports.MAX_BACKOFF_MS = 30000;
|
|
25
|
+
/**
|
|
26
|
+
* Parse `Retry-After` as a delta-seconds value, in milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* The API always sends the numeric form. The HTTP-date form is ignored
|
|
29
|
+
* rather than parsed, since treating an unreadable value as "no hint"
|
|
30
|
+
* degrades to plain backoff instead of to a wrong sleep.
|
|
31
|
+
*/
|
|
32
|
+
function retryAfterMs(response) {
|
|
33
|
+
const raw = response.headers.get('retry-after');
|
|
34
|
+
if (!raw) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const seconds = Number(raw.trim());
|
|
38
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return seconds * 1000;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Milliseconds to wait before replaying a rate-limited request.
|
|
45
|
+
*
|
|
46
|
+
* Exponential with full jitter, and `Retry-After` applied as a
|
|
47
|
+
* *ceiling* rather than as the delay itself. The limiter is a sliding
|
|
48
|
+
* window, so `Retry-After` reports the whole window — the worst case
|
|
49
|
+
* for a client that filled its budget instantaneously. A caller that
|
|
50
|
+
* merely ran at the sustained rate has slots freeing up within a second
|
|
51
|
+
* or two, and obeying the header literally would turn a handful of
|
|
52
|
+
* rejections into minutes of idling.
|
|
53
|
+
*/
|
|
54
|
+
function backoffMs(attempt, retryDelay, retryAfter) {
|
|
55
|
+
let ceiling = Math.min(retryDelay * 2 ** attempt, exports.MAX_BACKOFF_MS);
|
|
56
|
+
if (retryAfter !== null) {
|
|
57
|
+
ceiling = Math.min(ceiling, retryAfter);
|
|
58
|
+
}
|
|
59
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether a request can be sent a second time.
|
|
63
|
+
*
|
|
64
|
+
* A `ReadableStream` body is consumed by the first attempt, so
|
|
65
|
+
* replaying it would send nothing. Strings, `FormData`, `Blob` and
|
|
66
|
+
* typed arrays — everything the SDK and the GraphQL client actually
|
|
67
|
+
* produce — are re-readable.
|
|
68
|
+
*/
|
|
69
|
+
function isReplayable(init) {
|
|
70
|
+
const body = init?.body;
|
|
71
|
+
return !(typeof ReadableStream !== 'undefined' && body instanceof ReadableStream);
|
|
72
|
+
}
|
|
73
|
+
function sleep(ms, signal) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
signal?.removeEventListener('abort', onAbort);
|
|
77
|
+
resolve();
|
|
78
|
+
}, ms);
|
|
79
|
+
function onAbort() {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
82
|
+
}
|
|
83
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a `fetch` so rate-limited requests are replayed.
|
|
88
|
+
*
|
|
89
|
+
* Composes over an existing fetch rather than replacing it, so a
|
|
90
|
+
* per-request timeout wrapper stays in place and each attempt gets its
|
|
91
|
+
* own timeout. The global `fetch` is resolved at call time (not
|
|
92
|
+
* captured) so test harnesses that swap `globalThis.fetch` keep
|
|
93
|
+
* working.
|
|
94
|
+
*
|
|
95
|
+
* Set `maxRetries: 0` to opt out — an interactive surface may well
|
|
96
|
+
* prefer to surface the rejection immediately rather than wait.
|
|
97
|
+
*/
|
|
98
|
+
function createRetryingFetch(options = {}) {
|
|
99
|
+
const maxRetries = Math.max(0, options.maxRetries ?? exports.DEFAULT_MAX_RETRIES);
|
|
100
|
+
const retryDelay = Math.max(1, options.retryDelay ?? exports.DEFAULT_RETRY_DELAY_MS);
|
|
101
|
+
const inner = options.fetch;
|
|
102
|
+
return async (input, init) => {
|
|
103
|
+
const send = () => (inner ?? fetch)(input, init);
|
|
104
|
+
let response = await send();
|
|
105
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
106
|
+
if (!exports.RETRY_STATUS_CODES.has(response.status) || !isReplayable(init)) {
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
if (init?.signal?.aborted) {
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
const delay = backoffMs(attempt, retryDelay, retryAfterMs(response));
|
|
113
|
+
// The rejection body goes unused, but leaving it undrained keeps
|
|
114
|
+
// the connection pinned in some runtimes.
|
|
115
|
+
await response.body?.cancel().catch(() => { });
|
|
116
|
+
await sleep(delay, init?.signal);
|
|
117
|
+
response = await send();
|
|
118
|
+
}
|
|
119
|
+
return response;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limit-aware `fetch` wrapper shared by the facade clients.
|
|
3
|
+
*
|
|
4
|
+
* The API rate-limits per user per endpoint category and answers an
|
|
5
|
+
* exhausted budget with `429` plus `Retry-After` / `X-RateLimit-*`
|
|
6
|
+
* headers. That rejection is raised by a request dependency *before*
|
|
7
|
+
* the endpoint handler runs, so the request had no effect and is always
|
|
8
|
+
* safe to replay — including a `POST` carrying no idempotency key.
|
|
9
|
+
* Nothing other than `429` is retried here, precisely because nothing
|
|
10
|
+
* else carries that guarantee.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `robosystems_client/clients/retry.py` in the Python client.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The one status a rejected request is known to have had no effect for. */
|
|
16
|
+
export const RETRY_STATUS_CODES = new Set([429])
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_MAX_RETRIES = 5
|
|
19
|
+
export const DEFAULT_RETRY_DELAY_MS = 1000
|
|
20
|
+
export const MAX_BACKOFF_MS = 30_000
|
|
21
|
+
|
|
22
|
+
export interface RetryOptions {
|
|
23
|
+
/** Replays after the first attempt. `0` disables retrying entirely. */
|
|
24
|
+
maxRetries?: number
|
|
25
|
+
/** Base of the exponential backoff, in milliseconds. */
|
|
26
|
+
retryDelay?: number
|
|
27
|
+
/** Underlying fetch to wrap. Defaults to the global one, resolved per call. */
|
|
28
|
+
fetch?: typeof fetch
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse `Retry-After` as a delta-seconds value, in milliseconds.
|
|
33
|
+
*
|
|
34
|
+
* The API always sends the numeric form. The HTTP-date form is ignored
|
|
35
|
+
* rather than parsed, since treating an unreadable value as "no hint"
|
|
36
|
+
* degrades to plain backoff instead of to a wrong sleep.
|
|
37
|
+
*/
|
|
38
|
+
export function retryAfterMs(response: Response): number | null {
|
|
39
|
+
const raw = response.headers.get('retry-after')
|
|
40
|
+
if (!raw) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
const seconds = Number(raw.trim())
|
|
44
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
return seconds * 1000
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Milliseconds to wait before replaying a rate-limited request.
|
|
52
|
+
*
|
|
53
|
+
* Exponential with full jitter, and `Retry-After` applied as a
|
|
54
|
+
* *ceiling* rather than as the delay itself. The limiter is a sliding
|
|
55
|
+
* window, so `Retry-After` reports the whole window — the worst case
|
|
56
|
+
* for a client that filled its budget instantaneously. A caller that
|
|
57
|
+
* merely ran at the sustained rate has slots freeing up within a second
|
|
58
|
+
* or two, and obeying the header literally would turn a handful of
|
|
59
|
+
* rejections into minutes of idling.
|
|
60
|
+
*/
|
|
61
|
+
export function backoffMs(attempt: number, retryDelay: number, retryAfter: number | null): number {
|
|
62
|
+
let ceiling = Math.min(retryDelay * 2 ** attempt, MAX_BACKOFF_MS)
|
|
63
|
+
if (retryAfter !== null) {
|
|
64
|
+
ceiling = Math.min(ceiling, retryAfter)
|
|
65
|
+
}
|
|
66
|
+
return ceiling / 2 + Math.random() * (ceiling / 2)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a request can be sent a second time.
|
|
71
|
+
*
|
|
72
|
+
* A `ReadableStream` body is consumed by the first attempt, so
|
|
73
|
+
* replaying it would send nothing. Strings, `FormData`, `Blob` and
|
|
74
|
+
* typed arrays — everything the SDK and the GraphQL client actually
|
|
75
|
+
* produce — are re-readable.
|
|
76
|
+
*/
|
|
77
|
+
function isReplayable(init: RequestInit | undefined): boolean {
|
|
78
|
+
const body = init?.body
|
|
79
|
+
return !(typeof ReadableStream !== 'undefined' && body instanceof ReadableStream)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sleep(ms: number, signal?: AbortSignal | null): Promise<void> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
signal?.removeEventListener('abort', onAbort)
|
|
86
|
+
resolve()
|
|
87
|
+
}, ms)
|
|
88
|
+
function onAbort() {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'))
|
|
91
|
+
}
|
|
92
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a `fetch` so rate-limited requests are replayed.
|
|
98
|
+
*
|
|
99
|
+
* Composes over an existing fetch rather than replacing it, so a
|
|
100
|
+
* per-request timeout wrapper stays in place and each attempt gets its
|
|
101
|
+
* own timeout. The global `fetch` is resolved at call time (not
|
|
102
|
+
* captured) so test harnesses that swap `globalThis.fetch` keep
|
|
103
|
+
* working.
|
|
104
|
+
*
|
|
105
|
+
* Set `maxRetries: 0` to opt out — an interactive surface may well
|
|
106
|
+
* prefer to surface the rejection immediately rather than wait.
|
|
107
|
+
*/
|
|
108
|
+
export function createRetryingFetch(options: RetryOptions = {}): typeof fetch {
|
|
109
|
+
const maxRetries = Math.max(0, options.maxRetries ?? DEFAULT_MAX_RETRIES)
|
|
110
|
+
const retryDelay = Math.max(1, options.retryDelay ?? DEFAULT_RETRY_DELAY_MS)
|
|
111
|
+
const inner = options.fetch
|
|
112
|
+
|
|
113
|
+
return async (input, init) => {
|
|
114
|
+
const send = () => (inner ?? fetch)(input, init)
|
|
115
|
+
let response = await send()
|
|
116
|
+
|
|
117
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
118
|
+
if (!RETRY_STATUS_CODES.has(response.status) || !isReplayable(init)) {
|
|
119
|
+
return response
|
|
120
|
+
}
|
|
121
|
+
if (init?.signal?.aborted) {
|
|
122
|
+
return response
|
|
123
|
+
}
|
|
124
|
+
const delay = backoffMs(attempt, retryDelay, retryAfterMs(response))
|
|
125
|
+
// The rejection body goes unused, but leaving it undrained keeps
|
|
126
|
+
// the connection pinned in some runtimes.
|
|
127
|
+
await response.body?.cancel().catch(() => {})
|
|
128
|
+
await sleep(delay, init?.signal)
|
|
129
|
+
response = await send()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
}
|
|
134
|
+
}
|