@robosystems/client 1.13.0 → 1.13.2

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.
@@ -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
- 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
@@ -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';
@@ -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;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/client",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
4
4
  "description": "TypeScript client library for the RoboSystems financial intelligence platform",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -584,6 +584,10 @@ export type AvailableExtension = {
584
584
  * Name
585
585
  */
586
586
  name: string;
587
+ /**
588
+ * Display Name
589
+ */
590
+ display_name?: string | null;
587
591
  /**
588
592
  * Description
589
593
  */
@@ -4927,7 +4931,7 @@ export type EventBlockEnvelope = {
4927
4931
  /**
4928
4932
  * Status
4929
4933
  *
4930
- * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
4934
+ * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged — retractable while its ledger rows are still drafts), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
4931
4935
  */
4932
4936
  status: string;
4933
4937
  /**
@@ -16455,7 +16459,7 @@ export type UpdateEventBlockRequest = {
16455
16459
  /**
16456
16460
  * Transition To
16457
16461
  *
16458
- * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded. Terminal states (fulfilled, voided, superseded) accept no further transitions. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16462
+ * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded; fulfilled → voided | superseded. A retraction (voided, superseded) is final and is refused from any status once the event's ledger rows have posted or it has published to QuickBooks — reverse the posted entries instead. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16459
16463
  */
16460
16464
  transition_to?: 'committed' | 'pending' | 'fulfilled' | 'voided' | 'superseded' | null;
16461
16465
  /**
package/sdk/types.gen.ts CHANGED
@@ -602,6 +602,10 @@ export type AvailableExtension = {
602
602
  * Name
603
603
  */
604
604
  name: string;
605
+ /**
606
+ * Display Name
607
+ */
608
+ display_name?: string | null;
605
609
  /**
606
610
  * Description
607
611
  */
@@ -5057,7 +5061,7 @@ export type EventBlockEnvelope = {
5057
5061
  /**
5058
5062
  * Status
5059
5063
  *
5060
- * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
5064
+ * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged — retractable while its ledger rows are still drafts), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
5061
5065
  */
5062
5066
  status: string;
5063
5067
  /**
@@ -16868,7 +16872,7 @@ export type UpdateEventBlockRequest = {
16868
16872
  /**
16869
16873
  * Transition To
16870
16874
  *
16871
- * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded. Terminal states (fulfilled, voided, superseded) accept no further transitions. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16875
+ * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded; fulfilled → voided | superseded. A retraction (voided, superseded) is final and is refused from any status once the event's ledger rows have posted or it has published to QuickBooks — reverse the posted entries instead. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16872
16876
  */
16873
16877
  transition_to?: 'committed' | 'pending' | 'fulfilled' | 'voided' | 'superseded' | null;
16874
16878
  /**
package/types.gen.d.ts CHANGED
@@ -584,6 +584,10 @@ export type AvailableExtension = {
584
584
  * Name
585
585
  */
586
586
  name: string;
587
+ /**
588
+ * Display Name
589
+ */
590
+ display_name?: string | null;
587
591
  /**
588
592
  * Description
589
593
  */
@@ -4927,7 +4931,7 @@ export type EventBlockEnvelope = {
4927
4931
  /**
4928
4932
  * Status
4929
4933
  *
4930
- * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
4934
+ * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged — retractable while its ledger rows are still drafts), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
4931
4935
  */
4932
4936
  status: string;
4933
4937
  /**
@@ -16455,7 +16459,7 @@ export type UpdateEventBlockRequest = {
16455
16459
  /**
16456
16460
  * Transition To
16457
16461
  *
16458
- * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded. Terminal states (fulfilled, voided, superseded) accept no further transitions. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16462
+ * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded; fulfilled → voided | superseded. A retraction (voided, superseded) is final and is refused from any status once the event's ledger rows have posted or it has published to QuickBooks — reverse the posted entries instead. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16459
16463
  */
16460
16464
  transition_to?: 'committed' | 'pending' | 'fulfilled' | 'voided' | 'superseded' | null;
16461
16465
  /**
package/types.gen.ts CHANGED
@@ -602,6 +602,10 @@ export type AvailableExtension = {
602
602
  * Name
603
603
  */
604
604
  name: string;
605
+ /**
606
+ * Display Name
607
+ */
608
+ display_name?: string | null;
605
609
  /**
606
610
  * Description
607
611
  */
@@ -5057,7 +5061,7 @@ export type EventBlockEnvelope = {
5057
5061
  /**
5058
5062
  * Status
5059
5063
  *
5060
- * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
5064
+ * Lifecycle state. One of: `captured` (raw, pre-classification), `classified` (handler ran, GL pending), `committed` (GL entries posted), `pending` (committed but awaiting fulfillment of an obligation), `fulfilled` (obligation discharged — retractable while its ledger rows are still drafts), `voided` (canceled — terminal), `superseded` (replaced by a corrected event — terminal). See `UpdateEventBlockRequest.transition_to` for the valid transition graph.
5061
5065
  */
5062
5066
  status: string;
5063
5067
  /**
@@ -16868,7 +16872,7 @@ export type UpdateEventBlockRequest = {
16868
16872
  /**
16869
16873
  * Transition To
16870
16874
  *
16871
- * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded. Terminal states (fulfilled, voided, superseded) accept no further transitions. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16875
+ * Status transition. Valid moves depend on current status: captured → committed | voided | superseded; classified → committed | pending | fulfilled | voided | superseded; committed → pending | fulfilled | voided | superseded; pending → fulfilled | voided | superseded; fulfilled → voided | superseded. A retraction (voided, superseded) is final and is refused from any status once the event's ledger rows have posted or it has published to QuickBooks — reverse the posted entries instead. Note: classified and fulfilled are usually set by handlers, not by callers, but the transition is allowed for corrections.
16872
16876
  */
16873
16877
  transition_to?: 'committed' | 'pending' | 'fulfilled' | 'voided' | 'superseded' | null;
16874
16878
  /**