@compassdigital/sdk.typescript 3.68.0 → 3.69.0

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/src/base.ts CHANGED
@@ -1,29 +1,32 @@
1
-
2
- import fetch from "cross-fetch";
1
+ /* eslint-disable max-classes-per-file */
2
+ import fetch from 'cross-fetch';
3
3
 
4
4
  export interface RequestOptions {
5
- // environment to make requests to
6
- stage?: string;
7
- // authentication token
8
- token?: string;
9
- // additional headers
10
- headers?: Record<string, string>;
11
- // query parameters
12
- query?: { _provider?: string };
13
- // log all requests and responses
14
- debug?: boolean;
15
- // the number of times to retry a request
16
- retry?: number;
17
- // make requests against this base url.
18
- // per-service base urls are configured as an object
19
- // eg. { order: 'http://localhost:3000', shoppingcart: 'http://localhost:4000' }
20
- // note: the stage property is ignored if base_url is set
21
- base_url?: string | Record<string, string>
22
- // intercept outgoing http requests
23
- intercept?: InterceptFn;
24
- // throw this error value instead of a ServiceError
25
- // note: the ServiceError will be set as the cause property.
26
- throws?: any;
5
+ // environment to make requests to
6
+ stage?: string;
7
+ // authentication token
8
+ token?: string;
9
+ // additional headers
10
+ headers?: Record<string, string>;
11
+ // query parameters
12
+ query?: { _provider?: string };
13
+ // log all requests and responses
14
+ debug?: boolean;
15
+ // the number of times to retry a request
16
+ retry?: number;
17
+ // make requests against this base url.
18
+ // per-service base urls are configured as an object
19
+ // eg. { order: 'http://localhost:3000', shoppingcart: 'http://localhost:4000' }
20
+ // note: the stage property is ignored if base_url is set
21
+ base_url?: string | Record<string, string>;
22
+ // intercept outgoing http requests
23
+ intercept?: InterceptFn;
24
+ // throw this error value instead of a ServiceError
25
+ // note: the ServiceError will be set as the cause property.
26
+ throws?: any;
27
+ // An http.Agent instance in NodeJS.
28
+ // This is not typed because this package must remain browser compatible.
29
+ agent?: any;
27
30
  }
28
31
 
29
32
  /**
@@ -31,12 +34,12 @@ export interface RequestOptions {
31
34
  * functions.
32
35
  */
33
36
  export interface RequestData {
34
- name: string;
35
- service: string;
36
- url: string;
37
- method: string;
38
- headers: Record<string, string>;
39
- body?: string;
37
+ name: string;
38
+ service: string;
39
+ url: string;
40
+ method: string;
41
+ headers: Record<string, string>;
42
+ body?: string;
40
43
  }
41
44
 
42
45
  /**
@@ -44,10 +47,10 @@ export interface RequestData {
44
47
  * intercept functions.
45
48
  */
46
49
  export interface ResponseData {
47
- ok: boolean;
48
- status: number;
49
- body: string;
50
- err?: any; // network error
50
+ ok: boolean;
51
+ status: number;
52
+ body: string;
53
+ err?: any; // network error
51
54
  }
52
55
 
53
56
  /**
@@ -60,196 +63,200 @@ export type FetchFn = (req: RequestData) => Promise<ResponseData>;
60
63
  */
61
64
  export type InterceptFn = (req: RequestData, fetch: FetchFn) => Promise<ResponseData>;
62
65
 
63
- /**
64
- * We have to re-implement the promise methods instead of just existing
65
- * the existing Promise class due to: https://github.com/microsoft/TypeScript/issues/15202
66
- */
67
- export class ResponsePromise<T> implements Promise<T> {
68
-
69
- /**
70
- * Implements Promise interface.
71
- */
72
- [Symbol.toStringTag] = "[object ResponsePromise]";
73
-
74
- constructor(private promise: Promise<T>) {}
75
-
76
- /**
77
- * Implements Promise interface.
78
- */
79
- then<TResult1 = T, TResult2 = never>(
80
- onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
81
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
82
- ): ResponsePromise<TResult1 | TResult2> {
83
- return new ResponsePromise(this.promise.then(onfulfilled, onrejected));
84
- }
85
-
86
- /**
87
- * Implements Promise interface.
88
- */
89
- catch<TResult = never>(
90
- onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
91
- ): ResponsePromise<T | TResult> {
92
- return new ResponsePromise(this.promise.catch(onrejected));
93
- }
94
-
95
- /**
96
- * Implements Promise interface.
97
- */
98
- finally(onfinally?: (() => void) | null): ResponsePromise<T> {
99
- return new ResponsePromise(this.promise.finally(onfinally));
100
- }
101
-
102
- /**
103
- * Returns a promise that resolves to null if http status code
104
- * or service error code matches one of the provided codes.
105
- * If no codes are specified, all codes are ignored.
106
- */
107
- ignore(...codes: number[]): ResponsePromise<T | null> {
108
- return new ResponsePromise(ServiceError.ignore(codes, this.promise));
109
- }
110
-
111
- /**
112
- * Returns a promise that resolves to either the response body
113
- * or a ServiceError.
114
- */
115
- combine(...codes: number[]): Promise<EitherResponse<T>> {
116
- return new ResponsePromise(ServiceError.combine<T>(codes, this.promise));
117
- }
118
- }
119
-
120
66
  /**
121
67
  * Either a response body or a service error.
122
68
  */
123
- export type EitherResponse<T> = { ok: true; data: T; err: null } | { ok: false; data: null, err: ServiceError; };
69
+ export type EitherResponse<T> =
70
+ | { ok: true; data: T; err: null }
71
+ | { ok: false; data: null; err: ServiceError };
124
72
 
125
73
  /**
126
74
  * This is a hack to make captureStackTrace visible.
127
75
  */
76
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
128
77
  interface ErrorConstructor {
129
- captureStackTrace?(target: object, constructorOpt?: Function): void;
78
+ captureStackTrace?(target: object, constructorOpt?: Function): void;
130
79
  }
131
80
 
132
81
  /**
133
82
  * An error returned from an api service.
134
83
  */
135
84
  export class ServiceError extends Error {
136
- constructor(
137
- public status: number, // http status
138
- public code: number, // service error code
139
- message: string // service error message
140
- ) {
141
- super(message);
142
- this.name = `ServiceError(${code})`;
143
-
144
- // TODO: remove this if we change compilation target > es5
145
- Object.setPrototypeOf(this, ServiceError.prototype);
146
- }
147
-
148
- /**
149
- * Returns the http status code from err.
150
- * If err isn't an instance of ServiceError, -1 is returned.
151
- */
152
- static status(err: any): number {
153
- if (err instanceof ServiceError) {
154
- return err.status;
155
- }
156
- return -1;
157
- }
158
-
159
- /**
160
- * Returns the service error code from err.
161
- * If err isn't an instance of ServiceError, -1 is returned.
162
- */
163
- static code(err: any): number {
164
- if (err instanceof ServiceError) {
165
- return err.code;
166
- }
167
- return -1;
168
- }
169
-
170
- /**
171
- * Returns a promise that resolves to null if http status code
172
- * or service error code matches one of the provided codes.
173
- * If no codes are specified, all codes are ignored.
174
- */
175
- static async ignore<T>(codes: number[], response: Promise<T>): Promise<T | null> {
176
- try {
177
- return await response;
178
- } catch (err) {
179
- if (ServiceError.unwrap(err, codes)) {
180
- return null;
181
- }
182
- throw err;
183
- }
184
- }
185
-
186
- /**
187
- * Returns a promise that resolves to either T or a ServiceError.
188
- */
189
- static async combine<T>(codes: number[], response: Promise<T>): Promise<EitherResponse<T>> {
190
- try {
191
- const data = await response;
192
- return { ok: true, data, err: null };
193
- } catch (err) {
194
- const match = ServiceError.unwrap(err, codes);
195
- if (match) {
196
- return { ok: false, data: null, err: match };
197
- }
198
- throw err;
199
- }
200
- }
201
-
202
- /**
203
- * Returns the first ServiceError in the cause chain matches the provided codes.
204
- * If no codes were provided, the first ServiceError is returned.
205
- */
206
- private static unwrap(err: any, codes: number[]): ServiceError|null {
207
- while (err) {
208
- if (err instanceof ServiceError) {
209
- if (codes.length === 0) {
210
- return err
211
- }
212
- for (const code of codes) {
213
- if (err.status === code || err.code === code) {
214
- return err
215
- }
216
- }
217
- }
218
- err = err?.cause;
219
- }
220
- return null;
221
- }
222
-
223
- /**
224
- * Constructs a ServiceError from the provided response data.
225
- */
226
- static from_res(res: ResponseData): ServiceError {
227
- let message = res.body;
228
- let code = res.status;
229
- if (res.err) {
230
- // network error
231
- if (res.err instanceof Error) {
232
- message = res.err.message;
233
- } else {
234
- message = "Network Error";
235
- }
236
- code = 0;
237
- } else {
238
- try {
239
- let data = JSON.parse(res.body);
240
- if (data.code && (data.message || data.error)) {
241
- code = data.code;
242
- message = data.message || data.error;
243
- }
244
- } catch (_) {}
245
- }
246
- const err = new ServiceError(res.status, code, message);
247
- // override the stack trace so that it doesn't include the from_res helper.
248
- if (Error.captureStackTrace) {
249
- Error.captureStackTrace(err, ServiceError.from_res);
250
- }
251
- return err;
252
- }
85
+ constructor(
86
+ public status: number, // http status
87
+ public code: number, // service error code
88
+ message: string, // service error message
89
+ ) {
90
+ super(message);
91
+ this.name = `ServiceError(${code})`;
92
+
93
+ // TODO: remove this if we change compilation target > es5
94
+ Object.setPrototypeOf(this, ServiceError.prototype);
95
+ }
96
+
97
+ /**
98
+ * Returns the http status code from err.
99
+ * If err isn't an instance of ServiceError, -1 is returned.
100
+ */
101
+ static status(err: any): number {
102
+ if (err instanceof ServiceError) {
103
+ return err.status;
104
+ }
105
+ return -1;
106
+ }
107
+
108
+ /**
109
+ * Returns the service error code from err.
110
+ * If err isn't an instance of ServiceError, -1 is returned.
111
+ */
112
+ static code(err: any): number {
113
+ if (err instanceof ServiceError) {
114
+ return err.code;
115
+ }
116
+ return -1;
117
+ }
118
+
119
+ /**
120
+ * Returns a promise that resolves to null if http status code
121
+ * or service error code matches one of the provided codes.
122
+ * If no codes are specified, all codes are ignored.
123
+ */
124
+ static async ignore<T>(codes: number[], response: Promise<T>): Promise<T | null> {
125
+ try {
126
+ return await response;
127
+ } catch (err) {
128
+ if (ServiceError.unwrap(err, codes)) {
129
+ return null;
130
+ }
131
+ throw err;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Returns a promise that resolves to either T or a ServiceError.
137
+ */
138
+ static async combine<T>(codes: number[], response: Promise<T>): Promise<EitherResponse<T>> {
139
+ try {
140
+ const data = await response;
141
+ return { ok: true, data, err: null };
142
+ } catch (err) {
143
+ const match = ServiceError.unwrap(err, codes);
144
+ if (match) {
145
+ return { ok: false, data: null, err: match };
146
+ }
147
+ throw err;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Returns the first ServiceError in the cause chain matches the provided codes.
153
+ * If no codes were provided, the first ServiceError is returned.
154
+ */
155
+ private static unwrap(err: any, codes: number[]): ServiceError | null {
156
+ while (err) {
157
+ if (err instanceof ServiceError) {
158
+ if (codes.length === 0) {
159
+ return err;
160
+ }
161
+ for (const code of codes) {
162
+ if (err.status === code || err.code === code) {
163
+ return err;
164
+ }
165
+ }
166
+ }
167
+ err = err?.cause;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Constructs a ServiceError from the provided response data.
174
+ */
175
+ static from_res(res: ResponseData): ServiceError {
176
+ let message = res.body;
177
+ let code = res.status;
178
+ if (res.err) {
179
+ // network error
180
+ if (res.err instanceof Error) {
181
+ message = res.err.message;
182
+ } else {
183
+ message = 'Network Error';
184
+ }
185
+ code = 0;
186
+ } else {
187
+ try {
188
+ const data = JSON.parse(res.body);
189
+ if (data.code && (data.message || data.error)) {
190
+ code = data.code;
191
+ message = data.message || data.error;
192
+ }
193
+ } catch {
194
+ // ignore
195
+ }
196
+ }
197
+ const err = new ServiceError(res.status, code, message);
198
+ // override the stack trace so that it doesn't include the from_res helper.
199
+ if (Error.captureStackTrace) {
200
+ Error.captureStackTrace(err, ServiceError.from_res);
201
+ }
202
+ return err;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * We have to re-implement the promise methods instead of just existing
208
+ * the existing Promise class due to: https://github.com/microsoft/TypeScript/issues/15202
209
+ */
210
+ export class ResponsePromise<T> implements Promise<T> {
211
+ /**
212
+ * Implements Promise interface.
213
+ */
214
+ [Symbol.toStringTag] = '[object ResponsePromise]';
215
+
216
+ constructor(private promise: Promise<T>) {}
217
+
218
+ /**
219
+ * Implements Promise interface.
220
+ */
221
+ then<TResult1 = T, TResult2 = never>(
222
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
223
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
224
+ ): ResponsePromise<TResult1 | TResult2> {
225
+ return new ResponsePromise(this.promise.then(onfulfilled, onrejected));
226
+ }
227
+
228
+ /**
229
+ * Implements Promise interface.
230
+ */
231
+ catch<TResult = never>(
232
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
233
+ ): ResponsePromise<T | TResult> {
234
+ return new ResponsePromise(this.promise.catch(onrejected));
235
+ }
236
+
237
+ /**
238
+ * Implements Promise interface.
239
+ */
240
+ finally(onfinally?: (() => void) | null): ResponsePromise<T> {
241
+ return new ResponsePromise(this.promise.finally(onfinally));
242
+ }
243
+
244
+ /**
245
+ * Returns a promise that resolves to null if http status code
246
+ * or service error code matches one of the provided codes.
247
+ * If no codes are specified, all codes are ignored.
248
+ */
249
+ ignore(...codes: number[]): ResponsePromise<T | null> {
250
+ return new ResponsePromise(ServiceError.ignore(codes, this.promise));
251
+ }
252
+
253
+ /**
254
+ * Returns a promise that resolves to either the response body
255
+ * or a ServiceError.
256
+ */
257
+ combine(...codes: number[]): Promise<EitherResponse<T>> {
258
+ return new ResponsePromise(ServiceError.combine<T>(codes, this.promise));
259
+ }
253
260
  }
254
261
 
255
262
  /**
@@ -257,204 +264,212 @@ export class ServiceError extends Error {
257
264
  * This class is meant to be extended by the generated code.
258
265
  */
259
266
  export abstract class BaseServiceClient {
260
- private options: RequestOptions;
261
-
262
- constructor(options?: RequestOptions) {
263
- this.options = {
264
- ...this.env_options(),
265
- ...options,
266
- };
267
- }
268
-
269
- /**
270
- * Parse request options from environment variables.
271
- */
272
- private env_options(): RequestOptions {
273
- const options: RequestOptions = {};
274
- const stage = process.env.P2_SDK_STAGE;
275
- if (stage) {
276
- options.stage = stage;
277
- }
278
- const token = process.env.P2_SDK_TOKEN;
279
- if (token) {
280
- options.token = token;
281
- }
282
- const debug = process.env.P2_SDK_DEBUG;
283
- if (debug && debug !== "0" && `${debug}`.toLowerCase() !== "false") {
284
- options.debug = true;
285
- }
286
- const base_url = process.env.P2_SDK_BASE_URL;
287
- if (base_url) {
288
- if (base_url.includes('=')) {
289
- options.base_url = {};
290
- for (const custom_service_url of base_url.split(',')) {
291
- const [service, custom_url] = custom_service_url.split('=');
292
- options.base_url[service] = custom_url
293
- }
294
- } else {
295
- options.base_url = base_url;
296
- }
297
- }
298
- return options;
299
- }
300
-
301
- /**
302
- * Combine the route and query parameters into a full url.
303
- */
304
- private build_url(route: string, service: string, { base_url, stage = "dev" }: RequestOptions, params: any): string {
305
- let url = `https://api.compassdigital.org/${stage}${route}`;
306
- // use the base url if one was provided.
307
- if (base_url) {
308
- if (typeof base_url === 'object' && base_url[service]) base_url = base_url[service];
309
- if (typeof base_url === 'string') url = `${this.clean_url(base_url)}${route}`;
310
- }
311
- // add query parameters.
312
- const query: string[] = [];
313
- for (const [name, values] of Object.entries(params)) {
314
- for (const value of Array.isArray(values) ? values : [values]) {
315
- query.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
316
- }
317
- }
318
- if (query.length > 0) {
319
- url += "?" + query.join("&");
320
- }
321
- return url;
322
- }
323
-
324
- private clean_url(url: string): string {
325
- if (url.endsWith("/")) {
326
- url = url.slice(0, -1);
327
- }
328
- return url;
329
- }
330
-
331
- /**
332
- * Perform the http request or pass it to the intercept function
333
- * if one was configured.
334
- */
335
- private async fetch(req: RequestData, options?: RequestOptions): Promise<ResponseData> {
336
- if (options?.intercept) {
337
- return options.intercept(req, this.fetch.bind(this));
338
- }
339
- try {
340
- const res = await fetch(req.url, {
341
- method: req.method,
342
- body: req.body,
343
- headers: req.headers,
344
- });
345
- return {
346
- ok: res.ok,
347
- status: res.status,
348
- body: await res.text(),
349
- };
350
- } catch (err) {
351
- return {
352
- ok: false,
353
- status: 0,
354
- body: "",
355
- err: err,
356
- }
357
- }
358
- }
359
-
360
- /**
361
- * Returns true if the status code represents a transient error
362
- * or it's a network error.
363
- */
364
- private should_retry(res: ResponseData): boolean {
365
- if (res.err) {
366
- return true;
367
- }
368
- switch (res.status) {
369
- case 408: // timeout
370
- case 413: // too large
371
- case 429: // too many requests
372
- case 500: // generic error response
373
- case 502: // invalid upstream response
374
- case 503: // service unavailable
375
- case 504: // gateway timeout
376
- case 522: // tcp connection timeout
377
- return true;
378
- default:
379
- return false;
380
- }
381
- }
382
-
383
- /**
384
- * Returns the merged options.
385
- */
386
- private get_options(options: RequestOptions): RequestOptions {
387
- return {
388
- ...this.options,
389
- ...options,
390
- headers: {
391
- ...this.options.headers,
392
- ...options.headers,
393
- }
394
- };
395
- }
396
-
397
- /**
398
- * A delegate to _request that wraps the response in a ResponsePromise.
399
- */
400
- protected request(
401
- service: string,
402
- name: string,
403
- method: string,
404
- route: string,
405
- body: any,
406
- options: { query?: any } & RequestOptions = {}
407
- ): ResponsePromise<any> {
408
- const promise = this._request(service, name, method, route, body, options);
409
- return new ResponsePromise(promise);
410
- }
411
-
412
- /**
413
- * The main logic for fulfilling a request as perscribed by
414
- * the request options.
415
- */
416
- private async _request(
417
- service: string,
418
- name: string,
419
- method: string,
420
- route: string,
421
- body: any,
422
- options: { query?: any } & RequestOptions = {}
423
- ): Promise<any> {
424
- options = this.get_options(options);
425
- const url = this.build_url(route, service, options, options.query ?? {});
426
- const headers = Object.assign({
427
- "User-Agent": "CDL/ServiceClient"
428
- }, options.headers);
429
- if (options.token) {
430
- headers["Authorization"] = `Bearer ${options.token}`;
431
- }
432
- const req: RequestData = { name, service, url, method, headers };
433
- if (body) {
434
- req.body = JSON.stringify(body);
435
- }
436
- if (options.debug) {
437
- console.log(`REQ(${method.toUpperCase()})`, url, JSON.stringify(req, null, 2));
438
- }
439
- const res = await this.fetch(req, options);
440
- if (!res.ok) {
441
- const err = ServiceError.from_res(res);
442
- if (options.debug) {
443
- console.log(`ERR(${res.status})`, url, err.message);
444
- }
445
- if (typeof options.retry === "number" && options.retry > 0 && this.should_retry(res)) {
446
- options.retry--;
447
- return this.request(service, name, method, route, body, options);
448
- }
449
- if (options.throws) {
450
- throw Object.assign(options.throws, { cause: err });
451
- }
452
- throw err;
453
- }
454
- const data = res.body ? JSON.parse(res.body) : res.body;
455
- if (options.debug) {
456
- console.log(`RES(${res.status})`, url, JSON.stringify(data, null, 2));
457
- }
458
- return data;
459
- }
267
+ private options: RequestOptions;
268
+
269
+ constructor(options?: RequestOptions) {
270
+ this.options = {
271
+ ...this.env_options(),
272
+ ...options,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Parse request options from environment variables.
278
+ */
279
+ private env_options(): RequestOptions {
280
+ const options: RequestOptions = {};
281
+ const stage = process.env.P2_SDK_STAGE;
282
+ if (stage) {
283
+ options.stage = stage;
284
+ }
285
+ const token = process.env.P2_SDK_TOKEN;
286
+ if (token) {
287
+ options.token = token;
288
+ }
289
+ const debug = process.env.P2_SDK_DEBUG;
290
+ if (debug && debug !== '0' && `${debug}`.toLowerCase() !== 'false') {
291
+ options.debug = true;
292
+ }
293
+ const base_url = process.env.P2_SDK_BASE_URL;
294
+ if (base_url) {
295
+ if (base_url.includes('=')) {
296
+ options.base_url = {};
297
+ for (const custom_service_url of base_url.split(',')) {
298
+ const [service, custom_url] = custom_service_url.split('=');
299
+ options.base_url[service] = custom_url;
300
+ }
301
+ } else {
302
+ options.base_url = base_url;
303
+ }
304
+ }
305
+ return options;
306
+ }
307
+
308
+ /**
309
+ * Combine the route and query parameters into a full url.
310
+ */
311
+ private build_url(
312
+ route: string,
313
+ service: string,
314
+ { base_url, stage = 'dev' }: RequestOptions,
315
+ params: any,
316
+ ): string {
317
+ let url = `https://api.compassdigital.org/${stage}${route}`;
318
+ // use the base url if one was provided.
319
+ if (base_url) {
320
+ if (typeof base_url === 'object' && base_url[service]) base_url = base_url[service];
321
+ if (typeof base_url === 'string') url = `${this.clean_url(base_url)}${route}`;
322
+ }
323
+ // add query parameters.
324
+ const query: string[] = [];
325
+ for (const [name, values] of Object.entries(params)) {
326
+ for (const value of Array.isArray(values) ? values : [values]) {
327
+ query.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
328
+ }
329
+ }
330
+ if (query.length > 0) {
331
+ url += `?${query.join('&')}`;
332
+ }
333
+ return url;
334
+ }
335
+
336
+ private clean_url(url: string): string {
337
+ if (url.endsWith('/')) {
338
+ url = url.slice(0, -1);
339
+ }
340
+ return url;
341
+ }
342
+
343
+ /**
344
+ * Perform the http request or pass it to the intercept function
345
+ * if one was configured.
346
+ */
347
+ private async fetch(req: RequestData, options?: RequestOptions): Promise<ResponseData> {
348
+ if (options?.intercept) {
349
+ return options.intercept(req, this.fetch.bind(this));
350
+ }
351
+ try {
352
+ const res = await fetch(req.url, {
353
+ method: req.method,
354
+ body: req.body,
355
+ headers: req.headers,
356
+ // @ts-ignore
357
+ agent: options?.agent,
358
+ });
359
+ return {
360
+ ok: res.ok,
361
+ status: res.status,
362
+ body: await res.text(),
363
+ };
364
+ } catch (err) {
365
+ return {
366
+ ok: false,
367
+ status: 0,
368
+ body: '',
369
+ err,
370
+ };
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Returns true if the status code represents a transient error
376
+ * or it's a network error.
377
+ */
378
+ private should_retry(res: ResponseData): boolean {
379
+ if (res.err) {
380
+ return true;
381
+ }
382
+ switch (res.status) {
383
+ case 408: // timeout
384
+ case 413: // too large
385
+ case 429: // too many requests
386
+ case 500: // generic error response
387
+ case 502: // invalid upstream response
388
+ case 503: // service unavailable
389
+ case 504: // gateway timeout
390
+ case 522: // tcp connection timeout
391
+ return true;
392
+ default:
393
+ return false;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Returns the merged options.
399
+ */
400
+ private get_options(options: RequestOptions): RequestOptions {
401
+ return {
402
+ ...this.options,
403
+ ...options,
404
+ headers: {
405
+ ...this.options.headers,
406
+ ...options.headers,
407
+ },
408
+ };
409
+ }
410
+
411
+ /**
412
+ * A delegate to _request that wraps the response in a ResponsePromise.
413
+ */
414
+ protected request(
415
+ service: string,
416
+ name: string,
417
+ method: string,
418
+ route: string,
419
+ body: any,
420
+ options: { query?: any } & RequestOptions = {},
421
+ ): ResponsePromise<any> {
422
+ const promise = this._request(service, name, method, route, body, options);
423
+ return new ResponsePromise(promise);
424
+ }
425
+
426
+ /**
427
+ * The main logic for fulfilling a request as perscribed by
428
+ * the request options.
429
+ */
430
+ private async _request(
431
+ service: string,
432
+ name: string,
433
+ method: string,
434
+ route: string,
435
+ body: any,
436
+ options: { query?: any } & RequestOptions = {},
437
+ ): Promise<any> {
438
+ options = this.get_options(options);
439
+ const url = this.build_url(route, service, options, options.query ?? {});
440
+ const headers: Record<string, string> = {
441
+ 'User-Agent': 'CDL/ServiceClient',
442
+ ...options.headers,
443
+ };
444
+ if (options.token) {
445
+ headers.Authorization = `Bearer ${options.token}`;
446
+ }
447
+ const req: RequestData = { name, service, url, method, headers };
448
+ if (body) {
449
+ req.body = JSON.stringify(body);
450
+ }
451
+ if (options.debug) {
452
+ console.log(`REQ(${method.toUpperCase()})`, url, JSON.stringify(req, null, 2));
453
+ }
454
+ const res = await this.fetch(req, options);
455
+ if (!res.ok) {
456
+ const err = ServiceError.from_res(res);
457
+ if (options.debug) {
458
+ console.log(`ERR(${res.status})`, url, err.message);
459
+ }
460
+ if (typeof options.retry === 'number' && options.retry > 0 && this.should_retry(res)) {
461
+ options.retry -= 1;
462
+ return this.request(service, name, method, route, body, options);
463
+ }
464
+ if (options.throws) {
465
+ throw Object.assign(options.throws, { cause: err });
466
+ }
467
+ throw err;
468
+ }
469
+ const data = res.body ? JSON.parse(res.body) : res.body;
470
+ if (options.debug) {
471
+ console.log(`RES(${res.status})`, url, JSON.stringify(data, null, 2));
472
+ }
473
+ return data;
474
+ }
460
475
  }