@robosystems/client 1.12.2 → 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.
@@ -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
- const timeoutFetch = createTimeoutFetch(config.timeout ?? exports.DEFAULT_GRAPHQL_TIMEOUT_MS);
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
- const timeoutFetch = createTimeoutFetch(config.timeout ?? DEFAULT_GRAPHQL_TIMEOUT_MS)
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
@@ -45,6 +45,7 @@ function useQuery(graphId) {
45
45
  credentials: sdkConfig.credentials,
46
46
  headers: sdkConfig.headers,
47
47
  token,
48
+ tokenProvider: sdkConfig.tokenProvider,
48
49
  });
49
50
  return () => {
50
51
  clientRef.current?.close();
@@ -122,6 +123,7 @@ function useStreamingQuery(graphId) {
122
123
  credentials: sdkConfig.credentials,
123
124
  headers: sdkConfig.headers,
124
125
  token: sdkConfig.token,
126
+ tokenProvider: sdkConfig.tokenProvider,
125
127
  });
126
128
  return () => {
127
129
  clientRef.current?.close();
@@ -201,6 +203,7 @@ function useOperation(operationId) {
201
203
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
202
204
  credentials: sdkConfig.credentials,
203
205
  token,
206
+ tokenProvider: sdkConfig.tokenProvider,
204
207
  maxRetries: sdkConfig.maxRetries,
205
208
  retryDelay: sdkConfig.retryDelay,
206
209
  });
@@ -300,6 +303,7 @@ function useMultipleOperations() {
300
303
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
301
304
  credentials: sdkConfig.credentials,
302
305
  token,
306
+ tokenProvider: sdkConfig.tokenProvider,
303
307
  maxRetries: sdkConfig.maxRetries,
304
308
  retryDelay: sdkConfig.retryDelay,
305
309
  });
@@ -368,6 +372,7 @@ function useSDKClients() {
368
372
  credentials: sdkConfig.credentials,
369
373
  headers: sdkConfig.headers,
370
374
  token,
375
+ tokenProvider: sdkConfig.tokenProvider,
371
376
  };
372
377
  const queryClient = new QueryClient_1.QueryClient(baseConfig);
373
378
  const operationsClient = new OperationClient_1.OperationClient(baseConfig);
@@ -46,6 +46,7 @@ export function useQuery(graphId: string) {
46
46
  credentials: sdkConfig.credentials,
47
47
  headers: sdkConfig.headers,
48
48
  token,
49
+ tokenProvider: sdkConfig.tokenProvider,
49
50
  })
50
51
 
51
52
  return () => {
@@ -143,6 +144,7 @@ export function useStreamingQuery(graphId: string) {
143
144
  credentials: sdkConfig.credentials,
144
145
  headers: sdkConfig.headers,
145
146
  token: sdkConfig.token,
147
+ tokenProvider: sdkConfig.tokenProvider,
146
148
  })
147
149
 
148
150
  return () => {
@@ -240,6 +242,7 @@ export function useOperation<T = any>(operationId?: string) {
240
242
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
241
243
  credentials: sdkConfig.credentials,
242
244
  token,
245
+ tokenProvider: sdkConfig.tokenProvider,
243
246
  maxRetries: sdkConfig.maxRetries,
244
247
  retryDelay: sdkConfig.retryDelay,
245
248
  })
@@ -353,6 +356,7 @@ export function useMultipleOperations<T = any>() {
353
356
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
354
357
  credentials: sdkConfig.credentials,
355
358
  token,
359
+ tokenProvider: sdkConfig.tokenProvider,
356
360
  maxRetries: sdkConfig.maxRetries,
357
361
  retryDelay: sdkConfig.retryDelay,
358
362
  })
@@ -439,6 +443,7 @@ export function useSDKClients() {
439
443
  credentials: sdkConfig.credentials,
440
444
  headers: sdkConfig.headers,
441
445
  token,
446
+ tokenProvider: sdkConfig.tokenProvider,
442
447
  }
443
448
 
444
449
  const queryClient = new QueryClient(baseConfig)
@@ -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';
@@ -22,9 +23,12 @@ export interface RoboSystemsClientConfig {
22
23
  */
23
24
  token?: string;
24
25
  /**
25
- * Dynamic credential callback invoked on every GraphQL request.
26
- * When set, JWT refreshes are picked up automatically — no need
27
- * to rebuild or clear cached clients after a refresh.
26
+ * Dynamic credential callback invoked on every GraphQL request and
27
+ * every SSE connect. When set, JWT refreshes are picked up
28
+ * automatically — no need to rebuild or clear cached clients after a
29
+ * refresh. Browser sessions need this: the backend revokes the previous
30
+ * JWT on each refresh, so a static `token` captured at construction
31
+ * stops opening progress streams the moment the session rotates.
28
32
  */
29
33
  tokenProvider?: TokenProvider;
30
34
  headers?: Record<string, string>;
@@ -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
@@ -60,27 +82,33 @@ class RoboSystemsClients {
60
82
  retryDelay: config.retryDelay || 1000,
61
83
  timeout: config.timeout,
62
84
  };
85
+ // Every client that opens an SSE stream gets the tokenProvider too:
86
+ // the stream endpoint authenticates the JWT from the URL, and a
87
+ // captured static token is dead after the first session refresh.
63
88
  this.query = new QueryClient_1.QueryClient({
64
89
  baseUrl: this.config.baseUrl,
65
90
  credentials: this.config.credentials,
66
91
  token: this.config.token,
92
+ tokenProvider: this.config.tokenProvider,
67
93
  headers: this.config.headers,
68
94
  });
69
95
  this.operator = new OperatorClient_1.OperatorClient({
70
96
  baseUrl: this.config.baseUrl,
71
97
  credentials: this.config.credentials,
72
98
  token: this.config.token,
99
+ tokenProvider: this.config.tokenProvider,
73
100
  headers: this.config.headers,
74
101
  });
75
102
  this.operations = new OperationClient_1.OperationClient({
76
103
  baseUrl: this.config.baseUrl,
77
104
  credentials: this.config.credentials,
78
105
  token: this.config.token,
106
+ tokenProvider: this.config.tokenProvider,
79
107
  maxRetries: this.config.maxRetries,
80
108
  retryDelay: this.config.retryDelay,
81
109
  });
82
- // LedgerClient / InvestorClient use GraphQL internally, so they
83
- // get the tokenProvider — REST-only clients do not.
110
+ // LedgerClient / InvestorClient / LibraryClient use GraphQL internally
111
+ // and consult the tokenProvider on every request.
84
112
  this.ledger = new LedgerClient_1.LedgerClient({
85
113
  baseUrl: this.config.baseUrl,
86
114
  credentials: this.config.credentials,
@@ -88,6 +116,8 @@ class RoboSystemsClients {
88
116
  tokenProvider: this.config.tokenProvider,
89
117
  headers: this.config.headers,
90
118
  timeout: this.config.timeout,
119
+ maxRetries: this.config.maxRetries,
120
+ retryDelay: this.config.retryDelay,
91
121
  });
92
122
  this.investor = new InvestorClient_1.InvestorClient({
93
123
  baseUrl: this.config.baseUrl,
@@ -96,6 +126,8 @@ class RoboSystemsClients {
96
126
  tokenProvider: this.config.tokenProvider,
97
127
  headers: this.config.headers,
98
128
  timeout: this.config.timeout,
129
+ maxRetries: this.config.maxRetries,
130
+ retryDelay: this.config.retryDelay,
99
131
  });
100
132
  // Library uses GraphQL and accepts graphId per-call — pass either
101
133
  // the `"library"` sentinel (canonical) or any tenant graph_id
@@ -107,6 +139,8 @@ class RoboSystemsClients {
107
139
  tokenProvider: this.config.tokenProvider,
108
140
  headers: this.config.headers,
109
141
  timeout: this.config.timeout,
142
+ maxRetries: this.config.maxRetries,
143
+ retryDelay: this.config.retryDelay,
110
144
  });
111
145
  // Reports consolidated into LedgerClient — alias for backward compat
112
146
  this.reports = this.ledger;
@@ -125,6 +159,7 @@ class RoboSystemsClients {
125
159
  baseUrl: this.config.baseUrl,
126
160
  credentials: this.config.credentials,
127
161
  token: this.config.token,
162
+ tokenProvider: this.config.tokenProvider,
128
163
  headers: this.config.headers,
129
164
  maxRetries: this.config.maxRetries,
130
165
  retryDelay: this.config.retryDelay,
@@ -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'
@@ -34,9 +47,12 @@ export interface RoboSystemsClientConfig {
34
47
  */
35
48
  token?: string
36
49
  /**
37
- * Dynamic credential callback invoked on every GraphQL request.
38
- * When set, JWT refreshes are picked up automatically — no need
39
- * to rebuild or clear cached clients after a refresh.
50
+ * Dynamic credential callback invoked on every GraphQL request and
51
+ * every SSE connect. When set, JWT refreshes are picked up
52
+ * automatically — no need to rebuild or clear cached clients after a
53
+ * refresh. Browser sessions need this: the backend revokes the previous
54
+ * JWT on each refresh, so a static `token` captured at construction
55
+ * stops opening progress streams the moment the session rotates.
40
56
  */
41
57
  tokenProvider?: TokenProvider
42
58
  headers?: Record<string, string>
@@ -79,6 +95,19 @@ export class RoboSystemsClients {
79
95
  // Get base URL from SDK client config or use provided/default
80
96
  const sdkConfig = client.getConfig()
81
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
+
82
111
  // Extract JWT token using centralized logic
83
112
  const token = config.token || extractTokenFromSDKClient()
84
113
 
@@ -98,10 +127,14 @@ export class RoboSystemsClients {
98
127
  timeout: config.timeout,
99
128
  }
100
129
 
130
+ // Every client that opens an SSE stream gets the tokenProvider too:
131
+ // the stream endpoint authenticates the JWT from the URL, and a
132
+ // captured static token is dead after the first session refresh.
101
133
  this.query = new QueryClient({
102
134
  baseUrl: this.config.baseUrl,
103
135
  credentials: this.config.credentials,
104
136
  token: this.config.token,
137
+ tokenProvider: this.config.tokenProvider,
105
138
  headers: this.config.headers,
106
139
  })
107
140
 
@@ -109,6 +142,7 @@ export class RoboSystemsClients {
109
142
  baseUrl: this.config.baseUrl,
110
143
  credentials: this.config.credentials,
111
144
  token: this.config.token,
145
+ tokenProvider: this.config.tokenProvider,
112
146
  headers: this.config.headers,
113
147
  })
114
148
 
@@ -116,12 +150,13 @@ export class RoboSystemsClients {
116
150
  baseUrl: this.config.baseUrl,
117
151
  credentials: this.config.credentials,
118
152
  token: this.config.token,
153
+ tokenProvider: this.config.tokenProvider,
119
154
  maxRetries: this.config.maxRetries,
120
155
  retryDelay: this.config.retryDelay,
121
156
  })
122
157
 
123
- // LedgerClient / InvestorClient use GraphQL internally, so they
124
- // get the tokenProvider — REST-only clients do not.
158
+ // LedgerClient / InvestorClient / LibraryClient use GraphQL internally
159
+ // and consult the tokenProvider on every request.
125
160
  this.ledger = new LedgerClient({
126
161
  baseUrl: this.config.baseUrl,
127
162
  credentials: this.config.credentials,
@@ -129,6 +164,8 @@ export class RoboSystemsClients {
129
164
  tokenProvider: this.config.tokenProvider,
130
165
  headers: this.config.headers,
131
166
  timeout: this.config.timeout,
167
+ maxRetries: this.config.maxRetries,
168
+ retryDelay: this.config.retryDelay,
132
169
  })
133
170
 
134
171
  this.investor = new InvestorClient({
@@ -138,6 +175,8 @@ export class RoboSystemsClients {
138
175
  tokenProvider: this.config.tokenProvider,
139
176
  headers: this.config.headers,
140
177
  timeout: this.config.timeout,
178
+ maxRetries: this.config.maxRetries,
179
+ retryDelay: this.config.retryDelay,
141
180
  })
142
181
 
143
182
  // Library uses GraphQL and accepts graphId per-call — pass either
@@ -150,6 +189,8 @@ export class RoboSystemsClients {
150
189
  tokenProvider: this.config.tokenProvider,
151
190
  headers: this.config.headers,
152
191
  timeout: this.config.timeout,
192
+ maxRetries: this.config.maxRetries,
193
+ retryDelay: this.config.retryDelay,
153
194
  })
154
195
 
155
196
  // Reports consolidated into LedgerClient — alias for backward compat
@@ -171,6 +212,7 @@ export class RoboSystemsClients {
171
212
  baseUrl: this.config.baseUrl,
172
213
  credentials: this.config.credentials,
173
214
  token: this.config.token,
215
+ tokenProvider: this.config.tokenProvider,
174
216
  headers: this.config.headers,
175
217
  maxRetries: this.config.maxRetries,
176
218
  retryDelay: this.config.retryDelay,
@@ -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
+ }