@stocklayer/sdk 0.0.0-stage → 0.1.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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +145 -2
- package/dist/index.d.ts +120 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +259 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +377 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -4
- package/src/index.ts +432 -0
- package/src/types.ts +445 -0
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stocklayer/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Official TypeScript SDK for the Stocklayer trading API: tokenized stocks, Robinhood Chain market data, quotes, and checked trade plans",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stocklayer",
|
|
7
|
+
"robinhood-chain",
|
|
8
|
+
"stock-tokens",
|
|
9
|
+
"tokenized-stocks",
|
|
10
|
+
"tokenized-assets",
|
|
11
|
+
"trading-api",
|
|
12
|
+
"trading",
|
|
13
|
+
"sdk",
|
|
14
|
+
"typescript",
|
|
15
|
+
"api-client",
|
|
16
|
+
"rwa",
|
|
17
|
+
"ai-agents"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Stocklayer",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./types": {
|
|
30
|
+
"types": "./dist/types.d.ts",
|
|
31
|
+
"import": "./dist/types.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.3.0"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/stocklayerdev/sdk.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/stocklayerdev/sdk/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://stocklayer.dev/docs",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiFailure,
|
|
3
|
+
ApiIndex,
|
|
4
|
+
ApiSuccess,
|
|
5
|
+
AssetList,
|
|
6
|
+
CanonicalAsset,
|
|
7
|
+
CorporateActionList,
|
|
8
|
+
CorporateActionStatus,
|
|
9
|
+
CorporateActionType,
|
|
10
|
+
HealthReport,
|
|
11
|
+
Portfolio,
|
|
12
|
+
PriceObservation,
|
|
13
|
+
Quote,
|
|
14
|
+
SandboxScenario,
|
|
15
|
+
SimulateRequest,
|
|
16
|
+
SimulationResult,
|
|
17
|
+
TradePlan,
|
|
18
|
+
TradeRequest,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
export type * from './types.js';
|
|
22
|
+
export { ROBINHOOD_CHAIN_ID } from './types.js';
|
|
23
|
+
|
|
24
|
+
/** The API version this SDK release targets. */
|
|
25
|
+
export const API_VERSION = 'v0' as const;
|
|
26
|
+
/** Keep in sync with `package.json`; verified by the test suite. */
|
|
27
|
+
export const SDK_VERSION = '0.1.1';
|
|
28
|
+
|
|
29
|
+
export interface StocklayerRequestOptions {
|
|
30
|
+
/** Cancel this request with an AbortController. */
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
/** Override the client's per-attempt timeout. Set to 0 to disable. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StocklayerTradePlanOptions extends StocklayerRequestOptions {
|
|
37
|
+
/** Reuse this key when retrying the same intent. Generated when omitted. */
|
|
38
|
+
idempotencyKey?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StocklayerClientOptions {
|
|
42
|
+
/** Base URL including the version segment, e.g. `https://host/api/v0`. */
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
/** Project key beginning with `stocklayer_test_` or `stocklayer_live_`. */
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
fetch?: typeof globalThis.fetch;
|
|
47
|
+
/**
|
|
48
|
+
* Force a sandbox scenario on every request. Only valid with test keys;
|
|
49
|
+
* the API rejects it on live keys.
|
|
50
|
+
*/
|
|
51
|
+
scenario?: SandboxScenario;
|
|
52
|
+
/**
|
|
53
|
+
* Retries for safe GET requests and idempotent plan creation when they fail
|
|
54
|
+
* with a retryable error. Other POST requests are never retried. Default 2.
|
|
55
|
+
*/
|
|
56
|
+
retries?: number;
|
|
57
|
+
/** Per-attempt timeout in milliseconds. Set to 0 to disable. Default 30000. */
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface StocklayerResponseInfo {
|
|
62
|
+
requestId: string | null;
|
|
63
|
+
status: number;
|
|
64
|
+
sandbox: boolean;
|
|
65
|
+
plan: string | null;
|
|
66
|
+
environment: 'test' | 'live' | null;
|
|
67
|
+
rateLimit: { limit: number; remaining: number; resetAt: number } | null;
|
|
68
|
+
monthly: { limit: number; remaining: number } | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class StocklayerApiError extends Error {
|
|
72
|
+
readonly status: number;
|
|
73
|
+
readonly code: string;
|
|
74
|
+
readonly retryable: boolean;
|
|
75
|
+
readonly details: unknown;
|
|
76
|
+
readonly requestId: string | null;
|
|
77
|
+
|
|
78
|
+
constructor(options: {
|
|
79
|
+
status: number;
|
|
80
|
+
code: string;
|
|
81
|
+
message: string;
|
|
82
|
+
retryable?: boolean;
|
|
83
|
+
details?: unknown;
|
|
84
|
+
requestId?: string | null;
|
|
85
|
+
}) {
|
|
86
|
+
super(options.message);
|
|
87
|
+
this.name = 'StocklayerApiError';
|
|
88
|
+
this.status = options.status;
|
|
89
|
+
this.code = options.code;
|
|
90
|
+
this.retryable = options.retryable ?? false;
|
|
91
|
+
this.details = options.details ?? null;
|
|
92
|
+
this.requestId = options.requestId ?? null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type Query = Record<string, string | number | boolean | undefined>;
|
|
97
|
+
|
|
98
|
+
function numberHeader(headers: Headers, name: string): number | null {
|
|
99
|
+
const value = headers.get(name);
|
|
100
|
+
if (value === null) return null;
|
|
101
|
+
const parsed = Number(value);
|
|
102
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readResponseInfo(response: Response): StocklayerResponseInfo {
|
|
106
|
+
const headers = response.headers;
|
|
107
|
+
const environment = headers.get('x-stocklayer-environment');
|
|
108
|
+
const limit = numberHeader(headers, 'x-ratelimit-limit');
|
|
109
|
+
const remaining = numberHeader(headers, 'x-ratelimit-remaining');
|
|
110
|
+
const resetAt = numberHeader(headers, 'x-ratelimit-reset');
|
|
111
|
+
const monthlyLimit = numberHeader(headers, 'x-stocklayer-monthly-limit');
|
|
112
|
+
const monthlyRemaining = numberHeader(headers, 'x-stocklayer-monthly-remaining');
|
|
113
|
+
return {
|
|
114
|
+
requestId: headers.get('x-request-id'),
|
|
115
|
+
status: response.status,
|
|
116
|
+
sandbox: headers.get('x-stocklayer-sandbox') === 'on',
|
|
117
|
+
plan: headers.get('x-stocklayer-plan'),
|
|
118
|
+
environment:
|
|
119
|
+
environment === 'test' || environment === 'live' ? environment : null,
|
|
120
|
+
rateLimit:
|
|
121
|
+
limit !== null && remaining !== null && resetAt !== null
|
|
122
|
+
? { limit, remaining, resetAt }
|
|
123
|
+
: null,
|
|
124
|
+
monthly:
|
|
125
|
+
monthlyLimit !== null && monthlyRemaining !== null
|
|
126
|
+
? { limit: monthlyLimit, remaining: monthlyRemaining }
|
|
127
|
+
: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizedTimeout(
|
|
132
|
+
value: number | undefined,
|
|
133
|
+
fallback: number,
|
|
134
|
+
): number {
|
|
135
|
+
const timeout = value ?? fallback;
|
|
136
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) {
|
|
137
|
+
throw new TypeError('timeoutMs must be a non-negative integer');
|
|
138
|
+
}
|
|
139
|
+
return timeout;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
if (signal?.aborted) {
|
|
145
|
+
reject(signal.reason);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
signal?.removeEventListener('abort', onAbort);
|
|
150
|
+
resolve();
|
|
151
|
+
}, ms);
|
|
152
|
+
const onAbort = () => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
reject(signal?.reason);
|
|
155
|
+
};
|
|
156
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class StocklayerClient {
|
|
161
|
+
readonly system: {
|
|
162
|
+
index: (requestOptions?: StocklayerRequestOptions) => Promise<ApiIndex>;
|
|
163
|
+
health: (requestOptions?: StocklayerRequestOptions) => Promise<HealthReport>;
|
|
164
|
+
};
|
|
165
|
+
readonly assets: {
|
|
166
|
+
list: (
|
|
167
|
+
options?: {
|
|
168
|
+
status?: 'active' | 'inactive' | 'all';
|
|
169
|
+
limit?: number;
|
|
170
|
+
after?: string;
|
|
171
|
+
},
|
|
172
|
+
requestOptions?: StocklayerRequestOptions,
|
|
173
|
+
) => Promise<AssetList>;
|
|
174
|
+
get: (
|
|
175
|
+
symbol: string,
|
|
176
|
+
requestOptions?: StocklayerRequestOptions,
|
|
177
|
+
) => Promise<CanonicalAsset>;
|
|
178
|
+
};
|
|
179
|
+
readonly prices: {
|
|
180
|
+
get: (
|
|
181
|
+
symbol: string,
|
|
182
|
+
requestOptions?: StocklayerRequestOptions,
|
|
183
|
+
) => Promise<PriceObservation>;
|
|
184
|
+
};
|
|
185
|
+
readonly corporateActions: {
|
|
186
|
+
list: (
|
|
187
|
+
options?: {
|
|
188
|
+
symbol?: string;
|
|
189
|
+
type?: CorporateActionType;
|
|
190
|
+
status?: CorporateActionStatus;
|
|
191
|
+
limit?: number;
|
|
192
|
+
},
|
|
193
|
+
requestOptions?: StocklayerRequestOptions,
|
|
194
|
+
) => Promise<CorporateActionList>;
|
|
195
|
+
};
|
|
196
|
+
readonly portfolios: {
|
|
197
|
+
get: (
|
|
198
|
+
wallet: string,
|
|
199
|
+
options?: { symbols?: string[]; includeZero?: boolean },
|
|
200
|
+
requestOptions?: StocklayerRequestOptions,
|
|
201
|
+
) => Promise<Portfolio>;
|
|
202
|
+
};
|
|
203
|
+
readonly quotes: {
|
|
204
|
+
create: (
|
|
205
|
+
request: TradeRequest,
|
|
206
|
+
options?: StocklayerRequestOptions,
|
|
207
|
+
) => Promise<Quote>;
|
|
208
|
+
};
|
|
209
|
+
readonly trades: {
|
|
210
|
+
plan: (
|
|
211
|
+
request: TradeRequest,
|
|
212
|
+
options?: StocklayerTradePlanOptions,
|
|
213
|
+
) => Promise<TradePlan>;
|
|
214
|
+
get: (planId: string, options?: StocklayerRequestOptions) => Promise<TradePlan>;
|
|
215
|
+
};
|
|
216
|
+
readonly transactions: {
|
|
217
|
+
simulate: (
|
|
218
|
+
input: SimulateRequest,
|
|
219
|
+
options?: StocklayerRequestOptions,
|
|
220
|
+
) => Promise<SimulationResult>;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Metadata from the most recent response, including limits and sandbox state. */
|
|
224
|
+
lastResponse: StocklayerResponseInfo | null = null;
|
|
225
|
+
|
|
226
|
+
private readonly baseUrl: string;
|
|
227
|
+
private readonly apiKey?: string;
|
|
228
|
+
private readonly scenario?: SandboxScenario;
|
|
229
|
+
private readonly retries: number;
|
|
230
|
+
private readonly timeoutMs: number;
|
|
231
|
+
private readonly requestFetch: typeof globalThis.fetch;
|
|
232
|
+
|
|
233
|
+
constructor(options: StocklayerClientOptions) {
|
|
234
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
235
|
+
this.apiKey = options.apiKey;
|
|
236
|
+
this.scenario = options.scenario;
|
|
237
|
+
this.retries = options.retries ?? 2;
|
|
238
|
+
if (!Number.isSafeInteger(this.retries) || this.retries < 0) {
|
|
239
|
+
throw new TypeError('retries must be a non-negative integer');
|
|
240
|
+
}
|
|
241
|
+
this.timeoutMs = normalizedTimeout(options.timeoutMs, 30_000);
|
|
242
|
+
this.requestFetch = options.fetch ?? globalThis.fetch;
|
|
243
|
+
|
|
244
|
+
this.system = {
|
|
245
|
+
index: (requestOptions) => this.request<ApiIndex>('', { requestOptions }),
|
|
246
|
+
health: (requestOptions) =>
|
|
247
|
+
this.request<HealthReport>('/health', { requestOptions }),
|
|
248
|
+
};
|
|
249
|
+
this.assets = {
|
|
250
|
+
list: (options = {}, requestOptions) =>
|
|
251
|
+
this.request<AssetList>('/assets', { query: options, requestOptions }),
|
|
252
|
+
get: (symbol, requestOptions) =>
|
|
253
|
+
this.request<CanonicalAsset>(`/assets/${encodeURIComponent(symbol)}`, {
|
|
254
|
+
requestOptions,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
this.prices = {
|
|
258
|
+
get: (symbol, requestOptions) =>
|
|
259
|
+
this.request<PriceObservation>(
|
|
260
|
+
`/prices/${encodeURIComponent(symbol)}`,
|
|
261
|
+
{
|
|
262
|
+
requestOptions,
|
|
263
|
+
},
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
this.corporateActions = {
|
|
267
|
+
list: (options = {}, requestOptions) =>
|
|
268
|
+
this.request<CorporateActionList>('/corporate-actions', {
|
|
269
|
+
query: options,
|
|
270
|
+
requestOptions,
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
this.portfolios = {
|
|
274
|
+
get: (wallet, options = {}, requestOptions) =>
|
|
275
|
+
this.request<Portfolio>(`/portfolios/${encodeURIComponent(wallet)}`, {
|
|
276
|
+
query: {
|
|
277
|
+
symbols: options.symbols?.join(','),
|
|
278
|
+
includeZero: options.includeZero,
|
|
279
|
+
},
|
|
280
|
+
requestOptions,
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
this.quotes = {
|
|
284
|
+
create: (body, requestOptions) =>
|
|
285
|
+
this.request<Quote>('/quotes', {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
body,
|
|
288
|
+
requestOptions,
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
this.trades = {
|
|
292
|
+
plan: (body, options = {}) => {
|
|
293
|
+
const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
294
|
+
return this.request<TradePlan>('/trades/plan', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body,
|
|
297
|
+
headers: { 'idempotency-key': idempotencyKey },
|
|
298
|
+
idempotent: true,
|
|
299
|
+
requestOptions: options,
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
get: (planId, requestOptions) =>
|
|
303
|
+
this.request<TradePlan>(`/trade-plans/${encodeURIComponent(planId)}`, {
|
|
304
|
+
requestOptions,
|
|
305
|
+
}),
|
|
306
|
+
};
|
|
307
|
+
this.transactions = {
|
|
308
|
+
simulate: (body, requestOptions) =>
|
|
309
|
+
this.request<SimulationResult>('/transactions/simulate', {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
body,
|
|
312
|
+
requestOptions,
|
|
313
|
+
}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async request<T>(
|
|
318
|
+
path: string,
|
|
319
|
+
options: {
|
|
320
|
+
method?: 'GET' | 'POST';
|
|
321
|
+
body?: unknown;
|
|
322
|
+
query?: Query;
|
|
323
|
+
headers?: Record<string, string>;
|
|
324
|
+
idempotent?: boolean;
|
|
325
|
+
requestOptions?: StocklayerRequestOptions;
|
|
326
|
+
} = {},
|
|
327
|
+
): Promise<T> {
|
|
328
|
+
const method = options.method ?? 'GET';
|
|
329
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
330
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
331
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
332
|
+
}
|
|
333
|
+
const attempts =
|
|
334
|
+
method === 'GET' || options.idempotent ? this.retries + 1 : 1;
|
|
335
|
+
let lastError: StocklayerApiError | null = null;
|
|
336
|
+
const timeoutMs = normalizedTimeout(
|
|
337
|
+
options.requestOptions?.timeoutMs,
|
|
338
|
+
this.timeoutMs,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
342
|
+
const timeoutSignal =
|
|
343
|
+
timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
344
|
+
const signal =
|
|
345
|
+
timeoutSignal && options.requestOptions?.signal
|
|
346
|
+
? AbortSignal.any([timeoutSignal, options.requestOptions.signal])
|
|
347
|
+
: (timeoutSignal ?? options.requestOptions?.signal);
|
|
348
|
+
let response: Response;
|
|
349
|
+
let envelope: Partial<ApiSuccess<T> & ApiFailure> | null = null;
|
|
350
|
+
try {
|
|
351
|
+
response = await this.requestFetch(url, {
|
|
352
|
+
method,
|
|
353
|
+
signal,
|
|
354
|
+
headers: {
|
|
355
|
+
accept: 'application/json',
|
|
356
|
+
...(options.body ? { 'content-type': 'application/json' } : {}),
|
|
357
|
+
...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}),
|
|
358
|
+
...(this.scenario ? { 'x-stocklayer-scenario': this.scenario } : {}),
|
|
359
|
+
...options.headers,
|
|
360
|
+
},
|
|
361
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
362
|
+
});
|
|
363
|
+
this.lastResponse = readResponseInfo(response);
|
|
364
|
+
try {
|
|
365
|
+
const body: unknown = await response.json();
|
|
366
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
367
|
+
envelope = body as Partial<ApiSuccess<T> & ApiFailure>;
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (signal?.aborted) throw error;
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (!signal?.aborted) throw error;
|
|
374
|
+
const cancelled = options.requestOptions?.signal?.aborted === true;
|
|
375
|
+
lastError = new StocklayerApiError({
|
|
376
|
+
status: 0,
|
|
377
|
+
code: cancelled ? 'REQUEST_ABORTED' : 'REQUEST_TIMEOUT',
|
|
378
|
+
message: cancelled
|
|
379
|
+
? 'Stocklayer request was cancelled'
|
|
380
|
+
: 'Stocklayer request timed out',
|
|
381
|
+
retryable: !cancelled,
|
|
382
|
+
});
|
|
383
|
+
const isLast = attempt === attempts - 1;
|
|
384
|
+
if (!lastError.retryable || isLast) throw lastError;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const info = readResponseInfo(response);
|
|
388
|
+
this.lastResponse = info;
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
response.ok &&
|
|
392
|
+
envelope &&
|
|
393
|
+
Object.hasOwn(envelope, 'data') &&
|
|
394
|
+
!envelope.error
|
|
395
|
+
) {
|
|
396
|
+
return envelope.data as T;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
lastError = new StocklayerApiError({
|
|
400
|
+
status: response.status,
|
|
401
|
+
code: envelope?.error?.code ?? 'API_ERROR',
|
|
402
|
+
message:
|
|
403
|
+
envelope?.error?.message ??
|
|
404
|
+
(response.ok
|
|
405
|
+
? 'Stocklayer API returned an invalid response envelope'
|
|
406
|
+
: `Stocklayer API returned HTTP ${response.status}`),
|
|
407
|
+
retryable:
|
|
408
|
+
envelope?.error?.retryable ??
|
|
409
|
+
(response.status === 429 || response.status >= 500),
|
|
410
|
+
details: envelope?.error?.details,
|
|
411
|
+
requestId: envelope?.meta?.requestId ?? info.requestId,
|
|
412
|
+
});
|
|
413
|
+
const isLast = attempt === attempts - 1;
|
|
414
|
+
if (!lastError.retryable || isLast) throw lastError;
|
|
415
|
+
|
|
416
|
+
const retryAfter = numberHeader(response.headers, 'retry-after');
|
|
417
|
+
try {
|
|
418
|
+
await delay(
|
|
419
|
+
retryAfter !== null ? retryAfter * 1000 : 250 * 2 ** attempt,
|
|
420
|
+
options.requestOptions?.signal,
|
|
421
|
+
);
|
|
422
|
+
} catch {
|
|
423
|
+
throw new StocklayerApiError({
|
|
424
|
+
status: 0,
|
|
425
|
+
code: 'REQUEST_ABORTED',
|
|
426
|
+
message: 'Stocklayer request was cancelled',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
throw lastError ?? new Error('unreachable');
|
|
431
|
+
}
|
|
432
|
+
}
|