@vertesia/api-fetch-client 1.3.0 → 1.4.0-dev.20260615.042033Z

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.
Files changed (80) hide show
  1. package/README.md +25 -0
  2. package/lib/base.d.ts +135 -0
  3. package/lib/base.d.ts.map +1 -0
  4. package/lib/base.js +374 -0
  5. package/lib/base.js.map +1 -0
  6. package/lib/{types/client.d.ts → client.d.ts} +7 -8
  7. package/lib/client.d.ts.map +1 -0
  8. package/lib/{esm/client.js → client.js} +24 -11
  9. package/lib/client.js.map +1 -0
  10. package/lib/{types/errors.d.ts → errors.d.ts} +5 -5
  11. package/lib/errors.d.ts.map +1 -0
  12. package/lib/{esm/errors.js → errors.js} +22 -9
  13. package/lib/errors.js.map +1 -0
  14. package/lib/index.d.ts +5 -0
  15. package/lib/index.d.ts.map +1 -0
  16. package/lib/index.js +5 -0
  17. package/lib/index.js.map +1 -0
  18. package/lib/{types/sse → sse}/EventSourceParserStream.d.ts +4 -1
  19. package/lib/sse/EventSourceParserStream.d.ts.map +1 -0
  20. package/lib/{esm/sse → sse}/EventSourceParserStream.js +5 -4
  21. package/lib/sse/EventSourceParserStream.js.map +1 -0
  22. package/lib/sse/TextDecoderStream.d.ts.map +1 -0
  23. package/lib/{esm/sse → sse}/TextDecoderStream.js +2 -2
  24. package/lib/sse/TextDecoderStream.js.map +1 -0
  25. package/lib/{types/sse → sse}/index.d.ts +7 -2
  26. package/lib/sse/index.d.ts.map +1 -0
  27. package/lib/{esm/sse → sse}/index.js +3 -3
  28. package/lib/sse/index.js.map +1 -0
  29. package/lib/{types/utils.d.ts → utils.d.ts} +1 -1
  30. package/lib/utils.d.ts.map +1 -0
  31. package/lib/{esm/utils.js → utils.js} +3 -3
  32. package/lib/utils.js.map +1 -0
  33. package/package.json +21 -25
  34. package/src/base.ts +297 -79
  35. package/src/client.ts +46 -24
  36. package/src/errors.ts +28 -15
  37. package/src/index.ts +4 -4
  38. package/src/sse/EventSourceParserStream.ts +13 -10
  39. package/src/sse/TextDecoderStream.ts +16 -11
  40. package/src/sse/index.ts +14 -8
  41. package/src/utils.ts +5 -6
  42. package/lib/cjs/base.js +0 -240
  43. package/lib/cjs/base.js.map +0 -1
  44. package/lib/cjs/client.js +0 -115
  45. package/lib/cjs/client.js.map +0 -1
  46. package/lib/cjs/errors.js +0 -63
  47. package/lib/cjs/errors.js.map +0 -1
  48. package/lib/cjs/index.js +0 -21
  49. package/lib/cjs/index.js.map +0 -1
  50. package/lib/cjs/package.json +0 -3
  51. package/lib/cjs/sse/EventSourceParserStream.js +0 -41
  52. package/lib/cjs/sse/EventSourceParserStream.js.map +0 -1
  53. package/lib/cjs/sse/TextDecoderStream.js +0 -53
  54. package/lib/cjs/sse/TextDecoderStream.js.map +0 -1
  55. package/lib/cjs/sse/index.js +0 -27
  56. package/lib/cjs/sse/index.js.map +0 -1
  57. package/lib/cjs/utils.js +0 -38
  58. package/lib/cjs/utils.js.map +0 -1
  59. package/lib/esm/base.js +0 -235
  60. package/lib/esm/base.js.map +0 -1
  61. package/lib/esm/client.js.map +0 -1
  62. package/lib/esm/errors.js.map +0 -1
  63. package/lib/esm/index.js +0 -5
  64. package/lib/esm/index.js.map +0 -1
  65. package/lib/esm/sse/EventSourceParserStream.js.map +0 -1
  66. package/lib/esm/sse/TextDecoderStream.js.map +0 -1
  67. package/lib/esm/sse/index.js.map +0 -1
  68. package/lib/esm/utils.js.map +0 -1
  69. package/lib/tsconfig.tsbuildinfo +0 -1
  70. package/lib/types/base.d.ts +0 -83
  71. package/lib/types/base.d.ts.map +0 -1
  72. package/lib/types/client.d.ts.map +0 -1
  73. package/lib/types/errors.d.ts.map +0 -1
  74. package/lib/types/index.d.ts +0 -5
  75. package/lib/types/index.d.ts.map +0 -1
  76. package/lib/types/sse/EventSourceParserStream.d.ts.map +0 -1
  77. package/lib/types/sse/TextDecoderStream.d.ts.map +0 -1
  78. package/lib/types/sse/index.d.ts.map +0 -1
  79. package/lib/types/utils.d.ts.map +0 -1
  80. /package/lib/{types/sse → sse}/TextDecoderStream.d.ts +0 -0
package/src/base.ts CHANGED
@@ -1,10 +1,61 @@
1
- import { ConnectionError, RequestError, ServerError } from "./errors.js";
2
- import { sse, ServerSentEvent } from "./sse/index.js";
3
- import { buildQueryString, join, removeTrailingSlash } from "./utils.js";
1
+ import { ConnectionError, type RequestError, ServerError } from './errors.js';
2
+ import { type ServerSentEvent, sse } from './sse/index.js';
3
+ import { buildQueryString, join, removeTrailingSlash } from './utils.js';
4
4
 
5
5
  export type FETCH_FN = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
6
6
  type IPrimitives = string | number | boolean | null | undefined | string[] | number[] | boolean[];
7
7
 
8
+ export interface IRequestRetryPolicy {
9
+ /**
10
+ * Total attempts, including the first request. Defaults to 3 when a retry policy is enabled.
11
+ */
12
+ attempts?: number;
13
+ /**
14
+ * HTTP methods that may be retried. Defaults to idempotent methods.
15
+ */
16
+ methods?: string[];
17
+ /**
18
+ * HTTP response statuses that should be retried. Defaults to 502, 503, and 504.
19
+ */
20
+ statuses?: number[];
21
+ /**
22
+ * Retry network failures thrown by fetch. Defaults to true.
23
+ */
24
+ retryOnConnectionError?: boolean;
25
+ /**
26
+ * Initial backoff delay in milliseconds. Defaults to 250.
27
+ */
28
+ baseDelayMs?: number;
29
+ /**
30
+ * Maximum backoff delay in milliseconds. Defaults to 4000.
31
+ */
32
+ maxDelayMs?: number;
33
+ /**
34
+ * Use full jitter for backoff delays. Defaults to true.
35
+ */
36
+ jitter?: boolean;
37
+ }
38
+
39
+ type NormalizedRetryPolicy = {
40
+ attempts: number;
41
+ methods: Set<string>;
42
+ statuses: Set<number>;
43
+ retryOnConnectionError: boolean;
44
+ baseDelayMs: number;
45
+ maxDelayMs: number;
46
+ jitter: boolean;
47
+ };
48
+
49
+ const DEFAULT_RETRY_METHODS = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'];
50
+ const DEFAULT_RETRY_STATUSES = [502, 503, 504];
51
+ const DEFAULT_RETRY_ATTEMPTS = 3;
52
+ const DEFAULT_RETRY_BASE_DELAY_MS = 250;
53
+ const DEFAULT_RETRY_MAX_DELAY_MS = 4000;
54
+
55
+ function isRecord(value: unknown): value is Record<string, unknown> {
56
+ return value !== null && typeof value === 'object';
57
+ }
58
+
8
59
  export interface IRequestParams {
9
60
  query?: Record<string, IPrimitives> | null;
10
61
  headers?: Record<string, string> | null;
@@ -17,12 +68,17 @@ export interface IRequestParams {
17
68
  * If set to 'sse' the response will be treated as a server-sent event stream
18
69
  * and the request will return a Promise<ReadableStream<ServerSentEvent>> object
19
70
  */
20
- reader?: 'sse' | ((response: Response) => any);
71
+ reader?: 'sse' | ((response: Response) => unknown);
21
72
  /**
22
73
  * Set to false to disable automatic JSON payload serialization
23
74
  * If you need to post other data than a json payload, set this to false and use the `payload` property to set the desired payload
24
75
  */
25
- jsonPayload?: boolean
76
+ jsonPayload?: boolean;
77
+ /**
78
+ * Opt-in retry policy for this request. Retries are disabled by default.
79
+ * Set to false to disable a client-level retry policy for this request.
80
+ */
81
+ retryPolicy?: IRequestRetryPolicy | false | null;
26
82
  }
27
83
 
28
84
  export interface IRequestParamsWithPayload extends IRequestParams {
@@ -37,21 +93,74 @@ export function fetchPromise(fetchImpl?: FETCH_FN | Promise<FETCH_FN>) {
37
93
  } else {
38
94
  // install an error impl
39
95
  return Promise.resolve(() => {
40
- throw new Error('No Fetch implementation found')
96
+ throw new Error('No Fetch implementation found');
41
97
  });
42
98
  }
43
99
  }
44
100
 
45
- function isInvalidJsonPayload(payload: any) {
46
- return payload?.error === "Not a valid JSON payload" && typeof payload.text === "string";
101
+ function isInvalidJsonPayload(payload: unknown) {
102
+ return isRecord(payload) && payload.error === 'Not a valid JSON payload' && typeof payload.text === 'string';
47
103
  }
48
104
 
49
- export abstract class ClientBase {
105
+ function isReplayableBody(body: BodyInit | undefined) {
106
+ return !body || typeof ReadableStream === 'undefined' || !(body instanceof ReadableStream);
107
+ }
108
+
109
+ function normalizeRetryPolicy(policy: IRequestRetryPolicy): NormalizedRetryPolicy {
110
+ const attempts = Math.max(1, Math.floor(policy.attempts ?? DEFAULT_RETRY_ATTEMPTS));
111
+ const baseDelayMs = Math.max(0, policy.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS);
112
+ const maxDelayMs = Math.max(baseDelayMs, policy.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS);
113
+ return {
114
+ attempts,
115
+ methods: new Set((policy.methods ?? DEFAULT_RETRY_METHODS).map((method) => method.toUpperCase())),
116
+ statuses: new Set(policy.statuses ?? DEFAULT_RETRY_STATUSES),
117
+ retryOnConnectionError: policy.retryOnConnectionError ?? true,
118
+ baseDelayMs,
119
+ maxDelayMs,
120
+ jitter: policy.jitter ?? true,
121
+ };
122
+ }
123
+
124
+ function retryAfterDelayMs(res: Response): number | undefined {
125
+ const retryAfter = res.headers.get('retry-after');
126
+ if (!retryAfter) {
127
+ return undefined;
128
+ }
129
+ const seconds = Number(retryAfter);
130
+ if (Number.isFinite(seconds) && seconds >= 0) {
131
+ return seconds * 1000;
132
+ }
133
+ const retryAt = Date.parse(retryAfter);
134
+ if (!Number.isNaN(retryAt)) {
135
+ return Math.max(0, retryAt - Date.now());
136
+ }
137
+ return undefined;
138
+ }
50
139
 
140
+ function retryDelayMs(policy: NormalizedRetryPolicy, attempt: number, res?: Response): number {
141
+ const retryAfter = res ? retryAfterDelayMs(res) : undefined;
142
+ const delay = retryAfter ?? Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** attempt);
143
+ return policy.jitter ? Math.floor(Math.random() * delay) : delay;
144
+ }
145
+
146
+ function toError(err: unknown): Error {
147
+ return err instanceof Error ? err : new Error(String(err));
148
+ }
149
+
150
+ async function discardBody(res: Response) {
151
+ try {
152
+ await res.body?.cancel();
153
+ } catch {
154
+ // Ignore body cleanup failures while retrying the original request.
155
+ }
156
+ }
157
+
158
+ export abstract class ClientBase {
51
159
  _fetch: Promise<FETCH_FN>;
52
160
  baseUrl: string;
53
161
  errorFactory: (err: RequestError) => Error = (err) => err;
54
162
  verboseErrors = true;
163
+ retryPolicy?: IRequestRetryPolicy;
55
164
 
56
165
  abstract get headers(): Record<string, string>;
57
166
 
@@ -68,6 +177,15 @@ export abstract class ClientBase {
68
177
  throw this.errorFactory(err);
69
178
  }
70
179
 
180
+ withRetryPolicy(policy?: IRequestRetryPolicy | null): this {
181
+ this.retryPolicy = policy || undefined;
182
+ return this;
183
+ }
184
+
185
+ getRetryPolicy(): IRequestRetryPolicy | undefined {
186
+ return this.retryPolicy;
187
+ }
188
+
71
189
  /**
72
190
  * Resolve a path to a full URL. If the path is already an absolute URL
73
191
  * (starts with http:// or https://), it is returned as-is.
@@ -79,24 +197,24 @@ export abstract class ClientBase {
79
197
  return removeTrailingSlash(join(this.baseUrl, path));
80
198
  }
81
199
 
82
- get(path: string, params?: IRequestParams) {
83
- return this.request('GET', path, params);
200
+ get<T = unknown>(path: string, params?: IRequestParams): Promise<T> {
201
+ return this.request<T>('GET', path, params);
84
202
  }
85
203
 
86
- del(path: string, params?: IRequestParams) {
87
- return this.request('DELETE', path, params);
204
+ del<T = unknown>(path: string, params?: IRequestParams): Promise<T> {
205
+ return this.request<T>('DELETE', path, params);
88
206
  }
89
207
 
90
- delete(path: string, params?: IRequestParams) {
208
+ delete(path: string, params?: IRequestParams): Promise<unknown> {
91
209
  return this.request('DELETE', path, params);
92
210
  }
93
211
 
94
- post(path: string, params?: IRequestParamsWithPayload) {
95
- return this.request('POST', path, params);
212
+ post<T = unknown>(path: string, params?: IRequestParamsWithPayload): Promise<T> {
213
+ return this.request<T>('POST', path, params);
96
214
  }
97
215
 
98
- put(path: string, params?: IRequestParamsWithPayload) {
99
- return this.request('PUT', path, params);
216
+ put<T = unknown>(path: string, params?: IRequestParamsWithPayload): Promise<T> {
217
+ return this.request<T>('PUT', path, params);
100
218
  }
101
219
 
102
220
  /**
@@ -104,82 +222,90 @@ export abstract class ClientBase {
104
222
  * @param text
105
223
  * @returns
106
224
  */
107
- jsonParse(text: string) {
225
+ jsonParse(text: string): unknown {
108
226
  return JSON.parse(text);
109
227
  }
110
228
 
111
229
  /**
112
- * Can be overridden to create the request
113
- * @param fetch
114
- * @param url
115
- * @param init
116
- * @returns
117
- */
230
+ * Can be overridden to create the request
231
+ * @param fetch
232
+ * @param url
233
+ * @param init
234
+ * @returns
235
+ */
118
236
  createRequest(url: string, init: RequestInit): Promise<Request> {
119
237
  return Promise.resolve(new Request(url, init));
120
238
  }
121
239
 
122
- createServerError(req: Request, res: Response, payload: any): RequestError {
240
+ handleFetchResponse(_req: Request, _res: Response): void {}
241
+
242
+ createServerError(req: Request, res: Response, payload: unknown): RequestError {
123
243
  const status = res.status;
124
- let message = 'Server Error: ' + status;
244
+ let message = `Server Error: ${status}`;
125
245
  if (payload) {
126
246
  if (isInvalidJsonPayload(payload)) {
127
- message += res.statusText ? ' ' + res.statusText : '';
247
+ message += res.statusText ? ` ${res.statusText}` : '';
128
248
  message += ': non-JSON response';
129
- } else if (payload.message) {
249
+ } else if (isRecord(payload) && payload.message) {
130
250
  message = String(payload.message);
131
- } else if (payload.error) {
251
+ } else if (isRecord(payload) && payload.error) {
132
252
  if (typeof payload.error === 'string') {
133
253
  message = String(payload.error);
134
- } else if (typeof payload.error.message === 'string') {
135
- message = String(payload.error.message);
254
+ } else if (isRecord(payload.error) && typeof payload.error.message === 'string') {
255
+ message = payload.error.message;
136
256
  }
137
257
  }
138
258
  }
139
259
  return new ServerError(message, req, res.status, payload, this.verboseErrors);
140
260
  }
141
261
 
142
-
143
262
  async readJSONPayload(res: Response) {
144
- return res.text().then(text => {
145
- if (!text) {
146
- return undefined;
147
- } else {
148
- try {
149
- return this.jsonParse(text);
150
- } catch (err: any) {
151
- return {
152
- status: res.status,
153
- error: "Not a valid JSON payload",
154
- message: err.message,
155
- text: text,
156
- };
263
+ return res
264
+ .text()
265
+ .then((text) => {
266
+ if (!text) {
267
+ return undefined;
268
+ } else {
269
+ try {
270
+ return this.jsonParse(text);
271
+ } catch (err: unknown) {
272
+ return {
273
+ status: res.status,
274
+ error: 'Not a valid JSON payload',
275
+ message: err instanceof Error ? err.message : String(err),
276
+ text: text,
277
+ };
278
+ }
157
279
  }
158
- }
159
- }).catch((err) => {
160
- return {
161
- status: res.status,
162
- error: "Unable to load response content",
163
- message: err.message,
164
- };
165
- });
280
+ })
281
+ .catch((err: unknown) => {
282
+ return {
283
+ status: res.status,
284
+ error: 'Unable to load response content',
285
+ message: err instanceof Error ? err.message : String(err),
286
+ };
287
+ });
166
288
  }
167
289
 
168
290
  /**
169
291
  * Subclasses You can override this to do something with the response
170
292
  * @param res
171
293
  */
172
- handleResponse(req: Request, res: Response, params: IRequestParamsWithPayload | undefined) {
173
- if (params && params.reader) {
294
+ handleResponse<T = unknown>(
295
+ req: Request,
296
+ res: Response,
297
+ params: IRequestParamsWithPayload | undefined,
298
+ ): T | Promise<T> {
299
+ if (params?.reader) {
174
300
  if (params.reader === 'sse') {
175
- return sse(res);
301
+ return sse(res) as T;
176
302
  } else {
177
- return params.reader.call(this, res);
303
+ return params.reader.call(this, res) as T | Promise<T>;
178
304
  }
179
305
  } else {
180
306
  return this.readJSONPayload(res).then((payload) => {
181
307
  if (res.ok) {
182
- return payload;
308
+ return payload as T;
183
309
  } else {
184
310
  this.throwError(this.createServerError(req, res, payload));
185
311
  }
@@ -187,10 +313,10 @@ export abstract class ClientBase {
187
313
  }
188
314
  }
189
315
 
190
- async request(method: string, path: string, params?: IRequestParamsWithPayload) {
316
+ async request<T = unknown>(method: string, path: string, params?: IRequestParamsWithPayload): Promise<T> {
191
317
  let url = this.getUrl(path);
192
318
  if (params?.query) {
193
- url += '?' + buildQueryString(params.query);
319
+ url += `?${buildQueryString(params.query)}`;
194
320
  }
195
321
  const headers = this.headers ? Object.assign({}, this.headers) : {};
196
322
  const paramsHeaders = params?.headers;
@@ -205,29 +331,122 @@ export abstract class ClientBase {
205
331
  if (params && params.jsonPayload === false) {
206
332
  body = payload as BodyInit;
207
333
  } else {
208
- body = (typeof payload !== 'string') ? JSON.stringify(payload) : payload;
334
+ body = typeof payload !== 'string' ? JSON.stringify(payload) : payload;
209
335
  if (!('content-type' in headers)) {
210
336
  headers['content-type'] = 'application/json';
211
337
  }
212
338
  }
213
339
  }
214
340
  // When using SSE reader, ensure the Accept header requests event-stream
215
- if (params?.reader === 'sse' && !('accept' in headers)) {
216
- headers['accept'] = 'text/event-stream';
341
+ if (params?.reader === 'sse') {
342
+ headers.accept = 'text/event-stream';
217
343
  }
218
344
 
219
- const init: RequestInit = {
220
- method: method,
221
- headers: headers,
222
- body: body,
345
+ const normalizedMethod = method.toUpperCase();
346
+ const createRequestInit = (): RequestInit => {
347
+ return {
348
+ method: normalizedMethod,
349
+ headers: Object.assign({}, headers),
350
+ body: body,
351
+ };
352
+ };
353
+ const retryPolicy = this.resolveRetryPolicy(params);
354
+ const fetch = await this._fetch;
355
+
356
+ if (!retryPolicy) {
357
+ const req = await this.createRequest(url, createRequestInit());
358
+ let res: Response;
359
+ try {
360
+ res = await fetch(req);
361
+ } catch (err: unknown) {
362
+ console.error(`Failed to connect to ${url}`, err);
363
+ this.throwError(new ConnectionError(req, toError(err)));
364
+ }
365
+ this.handleFetchResponse(req, res);
366
+ return this.handleResponse<T>(req, res, params);
223
367
  }
224
- const req = await this.createRequest(url, init);
225
- return this._fetch.then(fetch => fetch(req).catch(err => {
226
- console.error(`Failed to connect to ${url}`, err);
227
- this.throwError(new ConnectionError(req, err));
228
- }).then(res => {
229
- return this.handleResponse(req, res, params);
230
- }));
368
+
369
+ const replayableBody = isReplayableBody(body);
370
+ let lastReq: Request | undefined;
371
+ for (let attempt = 0; attempt < retryPolicy.attempts; attempt++) {
372
+ const req = await this.createRequest(url, createRequestInit());
373
+ lastReq = req;
374
+ let res: Response;
375
+ try {
376
+ res = await fetch(req);
377
+ } catch (err: unknown) {
378
+ if (!this.shouldRetryConnectionError(retryPolicy, normalizedMethod, attempt, replayableBody)) {
379
+ console.error(`Failed to connect to ${url}`, err);
380
+ this.throwError(new ConnectionError(req, toError(err)));
381
+ }
382
+ await this.waitBeforeRetry(retryPolicy, attempt);
383
+ continue;
384
+ }
385
+ this.handleFetchResponse(req, res);
386
+ if (this.shouldRetryResponse(retryPolicy, normalizedMethod, attempt, replayableBody, res)) {
387
+ await discardBody(res);
388
+ await this.waitBeforeRetry(retryPolicy, attempt, res);
389
+ continue;
390
+ }
391
+ return this.handleResponse<T>(req, res, params);
392
+ }
393
+
394
+ if (lastReq) {
395
+ this.throwError(
396
+ new ConnectionError(lastReq, new Error(`Retry attempts exhausted for ${normalizedMethod} ${url}`)),
397
+ );
398
+ }
399
+ throw new Error(`Retry attempts exhausted for ${normalizedMethod} ${url}`);
400
+ }
401
+
402
+ protected resolveRetryPolicy(params: IRequestParamsWithPayload | undefined): NormalizedRetryPolicy | undefined {
403
+ if (params?.reader === 'sse' || params?.retryPolicy === false || params?.retryPolicy === null) {
404
+ return undefined;
405
+ }
406
+ const requestPolicy = params?.retryPolicy;
407
+ const clientPolicy = this.getRetryPolicy();
408
+ if (!requestPolicy && !clientPolicy) {
409
+ return undefined;
410
+ }
411
+ const policy = normalizeRetryPolicy({
412
+ ...clientPolicy,
413
+ ...requestPolicy,
414
+ });
415
+ return policy.attempts > 1 ? policy : undefined;
416
+ }
417
+
418
+ private shouldRetryResponse(
419
+ policy: NormalizedRetryPolicy,
420
+ method: string,
421
+ attempt: number,
422
+ replayableBody: boolean,
423
+ res: Response,
424
+ ) {
425
+ return (
426
+ attempt < policy.attempts - 1 &&
427
+ replayableBody &&
428
+ policy.methods.has(method) &&
429
+ policy.statuses.has(res.status)
430
+ );
431
+ }
432
+
433
+ private shouldRetryConnectionError(
434
+ policy: NormalizedRetryPolicy,
435
+ method: string,
436
+ attempt: number,
437
+ replayableBody: boolean,
438
+ ) {
439
+ return (
440
+ attempt < policy.attempts - 1 &&
441
+ replayableBody &&
442
+ policy.retryOnConnectionError &&
443
+ policy.methods.has(method)
444
+ );
445
+ }
446
+
447
+ private waitBeforeRetry(policy: NormalizedRetryPolicy, attempt: number, res?: Response) {
448
+ const delay = retryDelayMs(policy, attempt, res);
449
+ return new Promise<void>((resolve) => setTimeout(resolve, delay));
231
450
  }
232
451
 
233
452
  /**
@@ -246,10 +465,10 @@ export abstract class ClientBase {
246
465
  params: IRequestParamsWithPayload | undefined,
247
466
  onEvent: (event: ServerSentEvent) => void,
248
467
  ): Promise<ServerSentEvent | undefined> {
249
- const stream = await this.request(method, path, {
468
+ const stream = (await this.request(method, path, {
250
469
  ...params,
251
470
  reader: 'sse',
252
- }) as ReadableStream<ServerSentEvent>;
471
+ })) as ReadableStream<ServerSentEvent>;
253
472
 
254
473
  const reader = stream.getReader();
255
474
  let lastEvent: ServerSentEvent | undefined;
@@ -275,7 +494,6 @@ export abstract class ClientBase {
275
494
  * @returns
276
495
  */
277
496
  fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
278
- return this._fetch.then(fetch => fetch(input, init));
497
+ return this._fetch.then((fetch) => fetch(input, init));
279
498
  }
280
-
281
499
  }
package/src/client.ts CHANGED
@@ -1,13 +1,26 @@
1
- import { ClientBase, FETCH_FN, IRequestParamsWithPayload } from "./base.js";
2
- import { RequestError } from "./errors.js";
1
+ import { ClientBase, type FETCH_FN, type IRequestParamsWithPayload } from './base.js';
2
+ import type { RequestError } from './errors.js';
3
3
 
4
4
  function isAuthorizationHeaderSet(headers: HeadersInit | undefined): boolean {
5
5
  if (!headers) return false;
6
- return "authorization" in headers;
6
+ return 'authorization' in headers;
7
7
  }
8
8
 
9
- export class AbstractFetchClient<T extends AbstractFetchClient<T>> extends ClientBase {
9
+ function isServerFetchRuntime(): boolean {
10
+ const runtime = globalThis as typeof globalThis & {
11
+ Bun?: unknown;
12
+ process?: { versions?: { bun?: string; node?: string } };
13
+ window?: unknown;
14
+ };
15
+ return (
16
+ typeof runtime.window === 'undefined' &&
17
+ (typeof runtime.process?.versions?.node === 'string' ||
18
+ typeof runtime.process?.versions?.bun === 'string' ||
19
+ typeof runtime.Bun !== 'undefined')
20
+ );
21
+ }
10
22
 
23
+ export class AbstractFetchClient<T extends AbstractFetchClient<T>> extends ClientBase {
11
24
  headers: Record<string, string>;
12
25
  _auth?: () => Promise<string>;
13
26
  // callbacks useful to log requests and responses
@@ -23,7 +36,11 @@ export class AbstractFetchClient<T extends AbstractFetchClient<T>> extends Clien
23
36
  }
24
37
 
25
38
  get initialHeaders() {
26
- return { accept: 'application/json' };
39
+ const headers: Record<string, string> = { accept: 'application/json' };
40
+ if (isServerFetchRuntime()) {
41
+ headers['accept-encoding'] = 'br, gzip, deflate';
42
+ }
43
+ return headers;
27
44
  }
28
45
 
29
46
  /**
@@ -75,38 +92,32 @@ export class AbstractFetchClient<T extends AbstractFetchClient<T>> extends Clien
75
92
  init.headers = headers;
76
93
  const auth = await this._auth();
77
94
  if (auth) {
78
- init.headers["authorization"] = auth;
95
+ init.headers.authorization = auth;
79
96
  }
80
97
  }
81
98
  this.response = undefined;
82
99
  const request = await super.createRequest(url, init);
83
- this.onRequest && this.onRequest(request);
100
+ this.onRequest?.(request);
84
101
  return request;
85
102
  }
86
103
 
87
- async handleResponse(req: Request, res: Response, params: IRequestParamsWithPayload | undefined) {
104
+ handleFetchResponse(req: Request, res: Response): void {
88
105
  this.response = res; // store last response
89
- this.onResponse && this.onResponse(res, req);
90
- return super.handleResponse(req, res, params);
106
+ this.onResponse?.(res, req);
91
107
  }
92
-
93
108
  }
94
109
 
95
- export class FetchClient extends AbstractFetchClient<FetchClient> {
96
-
97
- constructor(baseUrl: string, fetchImpl?: FETCH_FN | Promise<FETCH_FN>) {
98
- super(baseUrl, fetchImpl);
99
- }
100
-
101
- }
110
+ export class FetchClient extends AbstractFetchClient<FetchClient> {}
102
111
 
103
112
  export abstract class ApiTopic extends ClientBase {
104
-
105
- constructor(public client: ClientBase, basePath: string) {
113
+ constructor(
114
+ public client: ClientBase,
115
+ basePath: string,
116
+ ) {
106
117
  //TODO we should refactor the way ClientBase and ApiTopic is created
107
118
  // to avoid cloning all customizations
108
119
  super(client.getUrl(basePath), client._fetch);
109
- this.createServerError = client.createServerError
120
+ this.createServerError = client.createServerError;
110
121
  this.errorFactory = client.errorFactory;
111
122
  this.verboseErrors = client.verboseErrors;
112
123
  }
@@ -115,12 +126,23 @@ export abstract class ApiTopic extends ClientBase {
115
126
  return this.client.createRequest(url, init);
116
127
  }
117
128
 
118
- handleResponse(req: Request, res: Response, params: IRequestParamsWithPayload | undefined): Promise<any> {
119
- return this.client.handleResponse(req, res, params);
129
+ handleResponse<T = unknown>(
130
+ req: Request,
131
+ res: Response,
132
+ params: IRequestParamsWithPayload | undefined,
133
+ ): T | Promise<T> {
134
+ return this.client.handleResponse<T>(req, res, params);
135
+ }
136
+
137
+ handleFetchResponse(req: Request, res: Response): void {
138
+ this.client.handleFetchResponse(req, res);
139
+ }
140
+
141
+ getRetryPolicy() {
142
+ return this.client.getRetryPolicy();
120
143
  }
121
144
 
122
145
  get headers() {
123
146
  return this.client.headers;
124
147
  }
125
-
126
148
  }