getta 0.2.3 → 0.4.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/main.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  DEFAULT_MAX_REDIRECTS,
13
13
  DEFAULT_MAX_RETRIES,
14
14
  DEFAULT_PATH_TEMPLATE_REGEX,
15
+ DEFAULT_RATE_LIMIT,
15
16
  DEFAULT_REQUEST_RETRY_WAIT,
16
17
  DELETE_METHOD,
17
18
  ETAG_HEADER,
@@ -31,7 +32,9 @@ import {
31
32
  POST_METHOD,
32
33
  PUT_METHOD,
33
34
  REDIRECTION_REPSONSE,
35
+ REQUEST_SENT,
34
36
  RESOURCE_NOT_FOUND_ERROR,
37
+ RESPONSE_RECEIVED,
35
38
  SERVER_ERROR_REPSONSE,
36
39
  } from "./constants";
37
40
  import buildEndpoint from "./helpers/build-endpoint";
@@ -44,10 +47,13 @@ import {
44
47
  FetchOptions,
45
48
  FetchRedirectHandlerOptions,
46
49
  FetchResponse,
50
+ Log,
47
51
  PathTemplateCallback,
48
52
  PendingRequestResolver,
49
53
  PendingRequestResolvers,
54
+ Performance,
50
55
  RequestOptions,
56
+ RequestQueue,
51
57
  RequestTracker,
52
58
  ShortcutProperties,
53
59
  Shortcuts,
@@ -61,12 +67,18 @@ export class Getta {
61
67
  private _conditionalRequestsEnabled: boolean;
62
68
  private _fetchTimeout: number;
63
69
  private _headers: StringObject;
70
+ private _log: Log | undefined;
64
71
  private _maxRedirects: number;
65
72
  private _maxRetries: number;
66
73
  private _optionalPathTemplateRegExp: RegExp;
67
74
  private _pathTemplateCallback: PathTemplateCallback;
68
75
  private _pathTemplateRegExp: RegExp;
76
+ private _performance: Performance;
69
77
  private _queryParams: PlainObject;
78
+ private _rateLimitCount: number = 0;
79
+ private _rateLimitedRequestQueue: RequestQueue = [];
80
+ private _rateLimitPerSecond: number;
81
+ private _rateLimitTimer: NodeJS.Timer | null = null;
70
82
  private _requestRetryWait: number;
71
83
  private _requestTracker: RequestTracker = { active: [], pending: new Map() };
72
84
  private _streamReader: StreamReader;
@@ -79,12 +91,15 @@ export class Getta {
79
91
  enableConditionalRequests = true,
80
92
  fetchTimeout = DEFAULT_FETCH_TIMEOUT,
81
93
  headers,
94
+ log,
82
95
  maxRedirects = DEFAULT_MAX_REDIRECTS,
83
96
  maxRetries = DEFAULT_MAX_RETRIES,
84
97
  optionalPathTemplateRegExp = OPTIONAL_PATH_TEMPLATE_REGEX,
85
98
  pathTemplateCallback = defaultPathTemplateCallback,
86
99
  pathTemplateRegExp = DEFAULT_PATH_TEMPLATE_REGEX,
100
+ performance,
87
101
  queryParams = {},
102
+ rateLimitPerSecond = DEFAULT_RATE_LIMIT,
88
103
  requestRetryWait = DEFAULT_REQUEST_RETRY_WAIT,
89
104
  streamReader = JSON_FORMAT,
90
105
  } = options;
@@ -99,12 +114,15 @@ export class Getta {
99
114
  this._conditionalRequestsEnabled = enableConditionalRequests;
100
115
  this._fetchTimeout = fetchTimeout;
101
116
  this._headers = { ...DEFAULT_HEADERS, ...(headers || {}) };
117
+ this._log = log;
102
118
  this._maxRedirects = maxRedirects;
103
119
  this._maxRetries = maxRetries;
104
120
  this._optionalPathTemplateRegExp = optionalPathTemplateRegExp;
105
121
  this._pathTemplateCallback = pathTemplateCallback;
106
122
  this._pathTemplateRegExp = pathTemplateRegExp;
123
+ this._performance = performance;
107
124
  this._queryParams = queryParams;
125
+ this._rateLimitPerSecond = rateLimitPerSecond;
108
126
  this._requestRetryWait = requestRetryWait;
109
127
  this._streamReader = streamReader;
110
128
  }
@@ -124,20 +142,26 @@ export class Getta {
124
142
  this[requestMethod ?? method](path, merge({}, rest, requestRest)) as Promise<FetchResponse<Resource>>;
125
143
  }
126
144
 
127
- public async delete(path: string, options: Omit<RequestOptions, "method"> = {}) {
128
- return this._delete(path, options);
145
+ public async delete(path: string, options: Omit<RequestOptions, "method"> = {}, context?: PlainObject) {
146
+ return this._delete(path, options, context);
129
147
  }
130
148
 
131
- public async get(path: string, options: Omit<RequestOptions, "method"> = {}) {
132
- return this._get(path, options);
149
+ public async get(path: string, options: Omit<RequestOptions, "method"> = {}, context?: PlainObject) {
150
+ return this._get(path, options, context);
133
151
  }
134
152
 
135
- public async post(path: string, options: Omit<Required<RequestOptions, "body">, "method">) {
136
- return this._request(path, { ...options, method: POST_METHOD });
153
+ public async post(path: string, options: Omit<Required<RequestOptions, "body">, "method">, context?: PlainObject) {
154
+ return this._request(path, { ...options, method: POST_METHOD }, context);
137
155
  }
138
156
 
139
- public async put(path: string, options: Omit<Required<RequestOptions, "body">, "methood">) {
140
- return this._request(path, { ...options, method: PUT_METHOD });
157
+ public async put(path: string, options: Omit<Required<RequestOptions, "body">, "methood">, context?: PlainObject) {
158
+ return this._request(path, { ...options, method: PUT_METHOD }, context);
159
+ }
160
+
161
+ private _addRequestToRateLimitedQueue(endpoint: string, options: FetchOptions) {
162
+ return new Promise((resolve: (value: FetchResponse) => void) => {
163
+ this._rateLimitedRequestQueue.push([resolve, endpoint, options]);
164
+ });
141
165
  }
142
166
 
143
167
  private async _cacheEntryDelete(requestHash: string): Promise<boolean> {
@@ -183,6 +207,7 @@ export class Getta {
183
207
  private async _delete(
184
208
  path: string,
185
209
  { headers = {}, pathTemplateData, queryParams = {}, ...rest }: Omit<RequestOptions, "method">,
210
+ context?: PlainObject,
186
211
  ) {
187
212
  const endpoint = buildEndpoint(this._basePath, path, {
188
213
  optionalPathTemplateRegExp: this._optionalPathTemplateRegExp,
@@ -199,22 +224,52 @@ export class Getta {
199
224
  this._cacheEntryDelete(requestHash);
200
225
  }
201
226
 
202
- return this._fetch(endpoint, {
203
- headers: { ...this._headers, ...headers },
204
- method: DELETE_METHOD,
205
- ...rest,
206
- });
227
+ return this._fetch(
228
+ endpoint,
229
+ {
230
+ headers: { ...this._headers, ...headers },
231
+ method: DELETE_METHOD,
232
+ ...rest,
233
+ },
234
+ context,
235
+ );
207
236
  }
208
237
 
209
- private async _fetch(endpoint: string, { redirects, retries, ...rest }: FetchOptions): Promise<FetchResponse> {
238
+ private async _fetch(
239
+ endpoint: string,
240
+ { redirects, retries, ...rest }: FetchOptions,
241
+ context: PlainObject = {},
242
+ ): Promise<FetchResponse> {
210
243
  try {
211
244
  return new Promise(async (resolve: (value: FetchResponse) => void, reject) => {
212
245
  const fetchTimer = setTimeout(() => {
213
246
  reject(new Error(`${FETCH_TIMEOUT_ERROR} ${this._fetchTimeout}ms.`));
214
247
  }, this._fetchTimeout);
215
248
 
249
+ this._rateLimit();
250
+
251
+ if (!(this._rateLimitCount < this._rateLimitPerSecond)) {
252
+ resolve(await this._addRequestToRateLimitedQueue(endpoint, { redirects, retries, ...rest }));
253
+ return;
254
+ }
255
+
256
+ const startTime = this._performance.now();
257
+
258
+ this._log?.(REQUEST_SENT, {
259
+ context: { redirects, retries, url: endpoint, ...rest, ...context },
260
+ stats: { startTime },
261
+ });
262
+
216
263
  const res = await fetch(endpoint, rest);
217
264
 
265
+ const endTime = this._performance.now();
266
+ const duration = endTime - startTime;
267
+
268
+ this._log?.(RESPONSE_RECEIVED, {
269
+ context: { redirects, retries, url: endpoint, ...rest, ...context },
270
+ stats: { duration, endTime, startTime },
271
+ });
272
+
218
273
  clearTimeout(fetchTimer);
219
274
 
220
275
  const { headers, status } = res;
@@ -228,6 +283,8 @@ export class Getta {
228
283
  ...rest,
229
284
  }),
230
285
  );
286
+
287
+ return;
231
288
  }
232
289
 
233
290
  if (responseGroup === SERVER_ERROR_REPSONSE) {
@@ -237,27 +294,17 @@ export class Getta {
237
294
  ...rest,
238
295
  })) as FetchResponse,
239
296
  );
297
+
298
+ return;
240
299
  }
241
300
 
242
301
  const fetchRes = res as FetchResponse;
243
- const resClone = res.clone();
244
302
 
245
303
  try {
246
304
  fetchRes.data = res.body ? this._bodyParser(await res[this._streamReader]()) : undefined;
247
305
  resolve(fetchRes);
248
306
  } catch (e) {
249
- try {
250
- if (this._streamReader === "json" && res.body) {
251
- const fetchResClone = resClone as FetchResponse;
252
- const text = await resClone.text();
253
- fetchResClone.data = JSON.parse(text);
254
- resolve(fetchResClone);
255
- } else {
256
- reject([e, new Error(`Unable to ${rest.method} ${endpoint} due to previous error`)]);
257
- }
258
- } catch {
259
- reject([e, new Error(`Unable to ${rest.method} ${endpoint} due to previous error`)]);
260
- }
307
+ reject([e, new Error(`Unable to ${rest.method} ${endpoint} due to previous error`)]);
261
308
  }
262
309
  });
263
310
  } catch (error) {
@@ -298,6 +345,7 @@ export class Getta {
298
345
  private async _get(
299
346
  path: string,
300
347
  { headers = {}, pathTemplateData, queryParams = {} }: Omit<RequestOptions, "method">,
348
+ context?: PlainObject,
301
349
  ) {
302
350
  const endpoint = buildEndpoint(this._basePath, path, {
303
351
  optionalPathTemplateRegExp: this._optionalPathTemplateRegExp,
@@ -329,7 +377,7 @@ export class Getta {
329
377
 
330
378
  return this._getResolve(
331
379
  requestHash,
332
- await this._fetch(endpoint, { headers: { ...this._headers, ...headers }, method: GET_METHOD }),
380
+ await this._fetch(endpoint, { headers: { ...this._headers, ...headers }, method: GET_METHOD }, context),
333
381
  );
334
382
  }
335
383
 
@@ -367,9 +415,34 @@ export class Getta {
367
415
  return res;
368
416
  }
369
417
 
418
+ private _rateLimit() {
419
+ if (!this._rateLimitTimer) {
420
+ this._rateLimitTimer = setTimeout(() => {
421
+ this._rateLimitTimer = null;
422
+ this._rateLimitCount = 0;
423
+
424
+ if (this._rateLimitedRequestQueue.length) {
425
+ this._releaseRateLimitedRequestQueue();
426
+ }
427
+ }, 1000);
428
+ }
429
+
430
+ this._rateLimitCount += 1;
431
+ }
432
+
433
+ private _releaseRateLimitedRequestQueue() {
434
+ this._rateLimitedRequestQueue.forEach(async ([resolve, endpoint, options]) => {
435
+ // @ts-ignore
436
+ resolve(await this._fetch(endpoint, options));
437
+ });
438
+
439
+ this._rateLimitedRequestQueue = [];
440
+ }
441
+
370
442
  private async _request(
371
443
  path: string,
372
444
  { body, headers, method, pathTemplateData, queryParams, ...rest }: Required<RequestOptions, "method">,
445
+ context?: PlainObject,
373
446
  ) {
374
447
  const endpoint = buildEndpoint(this._basePath, path, {
375
448
  optionalPathTemplateRegExp: this._optionalPathTemplateRegExp,
@@ -379,12 +452,16 @@ export class Getta {
379
452
  queryParams: { ...this._queryParams, ...queryParams },
380
453
  });
381
454
 
382
- return this._fetch(endpoint, {
383
- body,
384
- headers: { ...this._headers, ...headers },
385
- method,
386
- ...rest,
387
- });
455
+ return this._fetch(
456
+ endpoint,
457
+ {
458
+ body,
459
+ headers: { ...this._headers, ...headers },
460
+ method,
461
+ ...rest,
462
+ },
463
+ context,
464
+ );
388
465
  }
389
466
 
390
467
  private _resolvePendingRequests(requestHash: string, responseData: FetchResponse) {
package/src/types.ts CHANGED
@@ -17,12 +17,15 @@ export interface ConstructorOptions {
17
17
  enableConditionalRequests?: boolean;
18
18
  fetchTimeout?: number;
19
19
  headers?: StringObject;
20
+ log?: Log;
20
21
  maxRedirects?: number;
21
22
  maxRetries?: number;
22
23
  optionalPathTemplateRegExp?: RegExp;
23
24
  pathTemplateCallback?: PathTemplateCallback;
24
25
  pathTemplateRegExp?: RegExp;
26
+ performance: Performance;
25
27
  queryParams?: PlainObject;
28
+ rateLimitPerSecond?: number;
26
29
  requestRetryWait?: number;
27
30
  streamReader?: StreamReader;
28
31
  }
@@ -41,6 +44,14 @@ export interface FetchRedirectHandlerOptions extends FetchOptions {
41
44
  status: number;
42
45
  }
43
46
 
47
+ export type Log = (message: string, data: PlainObject, logLevel?: LogLevel) => void;
48
+
49
+ export type LogLevel = "error" | "warn" | "info" | "http" | "verbose" | "debug" | "silly";
50
+
51
+ export interface Performance {
52
+ now(): number;
53
+ }
54
+
44
55
  export interface RequestOptions {
45
56
  body?: BodyInit;
46
57
  headers?: StringObject;
@@ -49,6 +60,8 @@ export interface RequestOptions {
49
60
  queryParams?: PlainObject;
50
61
  }
51
62
 
63
+ export type RequestQueue = [(value: FetchResponse) => void, string, FetchOptions][];
64
+
52
65
  export interface ResponseDataWithErrors<Resource = PlainObject> {
53
66
  data?: Resource;
54
67
  errors?: Error[];