lissa 1.0.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/lib/index.d.ts ADDED
@@ -0,0 +1,510 @@
1
+ import { RequestInit, HeadersInit, BodyInit, Headers } from 'undici-types';
2
+
3
+ /*
4
+ * General json typing
5
+ */
6
+
7
+ type JsonPrimitive = null | string | number | boolean;
8
+
9
+ type JsonObject = {
10
+ [key: string]: Json;
11
+ };
12
+
13
+ type JsonArray = Json[];
14
+
15
+ type Json = JsonPrimitive | JsonObject | JsonArray;
16
+
17
+ /*
18
+ * Param typing (like json but it supports dates if paramsSerializer is set to extended mode)
19
+ */
20
+
21
+ type ParamPrimitive = null | string | number | bigint | boolean | Date;
22
+
23
+ type ParamObject = {
24
+ [key: string]: ParamValue;
25
+ };
26
+
27
+ type ParamArray = ParamValue[];
28
+
29
+ type ParamValue = ParamPrimitive | ParamObject | ParamArray;
30
+
31
+ type Params = ParamObject;
32
+
33
+ /*
34
+ * Input and output types
35
+ */
36
+
37
+ type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | (string & {});
38
+
39
+ type LissaOptionsInit = Omit<RequestInit, 'method' | 'body'> & {
40
+ adapter?: 'fetch' | 'xhr';
41
+ baseURL?: string;
42
+ url?: string;
43
+ method?: HttpMethod;
44
+ authenticate?: { username: string, password: string };
45
+ params?: Params;
46
+ paramsSerializer?: 'simple' | 'extended' | ((params: Params) => string);
47
+ urlBuilder?: 'simple' | 'extended' | ((url: string, baseURL: string) => string | URL);
48
+ responseType?: 'json' | 'text' | 'file' | 'raw';
49
+ timeout?: number;
50
+ onUploadProgress?: (uploaded: number, total: number) => void;
51
+ onDownloadProgress?: (downloaded: number, total: number) => void;
52
+ body?: BodyInit | JsonObject;
53
+ };
54
+
55
+ type LissaOptions = Omit<LissaOptionsInit, 'headers'> & {
56
+ headers: Headers
57
+ params: Params;
58
+ };
59
+
60
+ type DefaultOptionsInit = LissaOptionsInit & {
61
+ get?: LissaOptionsInit;
62
+ post?: LissaOptionsInit;
63
+ put?: LissaOptionsInit;
64
+ patch?: LissaOptionsInit;
65
+ delete?: LissaOptionsInit;
66
+ };
67
+
68
+ type DefaultOptions = LissaOptions & {
69
+ get: LissaOptions;
70
+ post: LissaOptions;
71
+ put: LissaOptions;
72
+ patch: LissaOptions;
73
+ delete: LissaOptions;
74
+ };
75
+
76
+ type FetchArguments = {
77
+ url: URL,
78
+ options: Omit<RequestInit, 'headers'> & { headers: Headers },
79
+ };
80
+
81
+ type LissaResult = {
82
+ options: LissaOptions;
83
+ request: FetchArguments;
84
+ response: Response;
85
+ headers: Headers;
86
+ status: number;
87
+ data: null | string | Json | File | ReadableStream | Blob;
88
+ };
89
+
90
+ type GeneralErrorResponse = Error & {
91
+ options: LissaOptions;
92
+ request: FetchArguments;
93
+ };
94
+
95
+ type ResultValue = LissaResult | Exclude<any, undefined>;
96
+
97
+ /*
98
+ * Interfaces
99
+ */
100
+
101
+ declare class LissaRequest extends Promise<ResultValue> {
102
+ readonly options: LissaOptions;
103
+
104
+ /**
105
+ * Set a base URL for the request
106
+ */
107
+ baseURL(baseURL: string): LissaRequest;
108
+
109
+ /**
110
+ * Set the request URL
111
+ */
112
+ url(url: string): LissaRequest;
113
+
114
+ /**
115
+ * Set the HTTP method (GET, POST, etc.)
116
+ */
117
+ method(method: HttpMethod): LissaRequest;
118
+
119
+ /**
120
+ * Add or override request headers
121
+ */
122
+ headers(headers: HeadersInit): LissaRequest;
123
+
124
+ /**
125
+ * Provide basic authentication credentials
126
+ *
127
+ * It sets the "Authorization" header to "Basic base64(username:password)"
128
+ */
129
+ authenticate(username: string, password: string): LissaRequest;
130
+
131
+ /**
132
+ * Add or override query string parameters
133
+ *
134
+ * Check the paramsSerializer option to control the serialization of the params
135
+ */
136
+ params(params: Params): LissaRequest;
137
+
138
+ /**
139
+ * Attach or merge a request body
140
+ *
141
+ * The body gets json stringified if it is a plain object
142
+ */
143
+ body(body: BodyInit | JsonObject): LissaRequest;
144
+
145
+ /**
146
+ * Set request timeout in milliseconds
147
+ *
148
+ * Attaches an AbortSignal.timeout(...) signal to the request
149
+ */
150
+ timeout(timeout: number): LissaRequest;
151
+
152
+ /**
153
+ * Attach an AbortSignal to cancel the request
154
+ */
155
+ signal(signal: AbortSignal): LissaRequest;
156
+
157
+ /**
158
+ * Change the expected response type
159
+ */
160
+ responseType(responseType: 'json' | 'text' | 'file' | 'raw'): LissaRequest;
161
+
162
+ /**
163
+ * Add an upload progress listener
164
+ */
165
+ onUploadProgress(onProgress: (uploaded: number, total: number) => void): LissaRequest;
166
+
167
+ /**
168
+ * Add a download progress listener
169
+ */
170
+ onDownloadProgress(onProgress: (downloaded: number, total: number) => void): LissaRequest;
171
+
172
+ readonly status: 'pending' | 'fulfilled' | 'rejected';
173
+ readonly value: void | ResultValue;
174
+ readonly reason: void | Error;
175
+
176
+ on(
177
+ event: 'resolve' | 'reject' | 'settle',
178
+ listener: (arg: ResultValue | Error | {
179
+ status: 'fulfilled' | 'rejected',
180
+ value: void | ResultValue,
181
+ reason: void | Error,
182
+ }) => void,
183
+ ): LissaRequest;
184
+
185
+ off(
186
+ event: 'resolve' | 'reject' | 'settle',
187
+ listener: (arg: ResultValue | Error | {
188
+ status: 'fulfilled' | 'rejected',
189
+ value: void | ResultValue,
190
+ reason: void | Error,
191
+ }) => void,
192
+ ): LissaRequest;
193
+ }
194
+
195
+ interface MakeRequest {
196
+
197
+ /**
198
+ * Perform a GET request
199
+ */
200
+ get(
201
+ url?: string,
202
+ options?: Omit<LissaOptionsInit, 'method' | 'url'>
203
+ ): LissaRequest;
204
+
205
+ /**
206
+ * Perform a POST request with optional body
207
+ */
208
+ post(
209
+ url?: string,
210
+ body?: BodyInit | JsonObject,
211
+ options?: Omit<LissaOptionsInit, 'method' | 'url' | 'body'>
212
+ ): LissaRequest;
213
+
214
+ /**
215
+ * Perform a PUT request with optional body
216
+ */
217
+ put(
218
+ url?: string,
219
+ body?: BodyInit | JsonObject,
220
+ options?: Omit<LissaOptionsInit, 'method' | 'url' | 'body'>
221
+ ): LissaRequest;
222
+
223
+ /**
224
+ * Perform a PATCH request with optional body
225
+ */
226
+ patch(
227
+ url?: string,
228
+ body?: BodyInit | JsonObject,
229
+ options?: Omit<LissaOptionsInit, 'method' | 'url' | 'body'>
230
+ ): LissaRequest;
231
+
232
+ /**
233
+ * Perform a DELETE request
234
+ */
235
+ delete(
236
+ url?: string,
237
+ options?: Omit<LissaOptionsInit, 'method' | 'url'>
238
+ ): LissaRequest;
239
+
240
+ /**
241
+ * Perform a general fetch request.
242
+ *
243
+ * Specify url, method, body, headers and more in the given options object
244
+ */
245
+ request(options?: LissaOptionsInit): LissaRequest;
246
+
247
+ /**
248
+ * Upload a file
249
+ */
250
+ upload(
251
+ file: File,
252
+ url?: string,
253
+ onProgress?: (uploaded: number, total: number) => void,
254
+ options?: Omit<LissaOptionsInit, 'url'>,
255
+ ): LissaRequest;
256
+ upload(
257
+ file: File,
258
+ url?: string,
259
+ options?: Omit<LissaOptionsInit, 'url'>,
260
+ onProgress?: (uploaded: number, total: number) => void,
261
+ ): LissaRequest;
262
+ upload(
263
+ file: File,
264
+ onProgress?: (uploaded: number, total: number) => void,
265
+ options?: LissaOptionsInit,
266
+ ): LissaRequest;
267
+ upload(
268
+ file: File,
269
+ options?: LissaOptionsInit,
270
+ onProgress?: (uploaded: number, total: number) => void,
271
+ ): LissaRequest;
272
+
273
+ /**
274
+ * Download a file
275
+ */
276
+ download(
277
+ url?: string,
278
+ onProgress?: (downloaded: number, total: number) => void,
279
+ options?: Omit<LissaOptionsInit, 'url'>,
280
+ ): LissaRequest;
281
+ download(
282
+ url?: string,
283
+ options?: Omit<LissaOptionsInit, 'url'>,
284
+ onProgress?: (downloaded: number, total: number) => void,
285
+ ): LissaRequest;
286
+ download(
287
+ onProgress?: (downloaded: number, total: number) => void,
288
+ options?: LissaOptionsInit,
289
+ ): LissaRequest;
290
+ download(
291
+ options?: LissaOptionsInit,
292
+ onProgress?: (downloaded: number, total: number) => void,
293
+ ): LissaRequest;
294
+ }
295
+
296
+ type Plugin = (lissa: Lissa) => void;
297
+
298
+ interface Lissa extends MakeRequest {
299
+ /**
300
+ * Modify the base options directly.
301
+ *
302
+ * Keep in mind that removing an option that is still in the defaults defined
303
+ * will get merged back into the final request.
304
+ */
305
+ readonly options: DefaultOptions;
306
+
307
+ /**
308
+ * Register a plugin
309
+ *
310
+ * @example
311
+ * lissa.use(Lissa.retry());
312
+ */
313
+ use(plugin: Plugin): Lissa;
314
+
315
+ /**
316
+ * Add a beforeRequest hook into the request cycle.
317
+ *
318
+ * Modify the given options as argument or return a new options object.
319
+ */
320
+ beforeRequest(hook: (options: LissaOptions) => void | LissaOptions): Lissa;
321
+
322
+ /**
323
+ * Add a beforeFetch hook into the request cycle.
324
+ *
325
+ * Modify the actual fetch arguments or return new arguments.
326
+ */
327
+ beforeFetch(hook: (request: FetchArguments) => void | FetchArguments): Lissa;
328
+
329
+ /**
330
+ * Add an onResponse hook into the request cycle.
331
+ *
332
+ * React to successful responses or modify them. A provided return value will
333
+ * stop looping over existing hooks and instantly returns this value (if it
334
+ * is an instance of Error it will get thrown).
335
+ */
336
+ onResponse(hook: (result: LissaResult) => void | Exclude<any, undefined>): Lissa;
337
+
338
+ /**
339
+ * Add an onError hook into the request cycle.
340
+ *
341
+ * React to errors or modify them. A provided return value will stop looping
342
+ * over existing hooks and instantly returns this value (if it is an instance
343
+ * of Error it will get thrown).
344
+ */
345
+ onError(hook: (error: ResponseError | ConnectionError | GeneralErrorResponse) => void | Exclude<any, undefined>): Lissa;
346
+
347
+ /**
348
+ * Copy the current instance with all its options and hooks.
349
+ */
350
+ extend(options: DefaultOptionsInit): Lissa;
351
+
352
+ /**
353
+ * Provide basic authentication credentials
354
+ *
355
+ * It sets the "Authorization" header to "Basic base64(username:password)"
356
+ */
357
+ authenticate(username: string, password: string): Lissa;
358
+ }
359
+
360
+ declare const LissaLib: MakeRequest & NamedExports & {
361
+ /**
362
+ * Perform a general fetch request.
363
+ *
364
+ * Specify method, body, headers and more in the given options object
365
+ */
366
+ (
367
+ url: string,
368
+ options?: Omit<LissaOptionsInit, 'url'>
369
+ ): LissaRequest;
370
+
371
+ /**
372
+ * Create a Lissa instance with the given base options.
373
+ */
374
+ create(options: DefaultOptionsInit): Lissa;
375
+ create(
376
+ baseURL?: string,
377
+ options?: Omit<DefaultOptionsInit, 'baseURL'>
378
+ ): Lissa;
379
+ };
380
+
381
+ export default LissaLib;
382
+
383
+ /**
384
+ * Global default options.
385
+ */
386
+ export declare const defaults: DefaultOptions;
387
+
388
+
389
+ /**
390
+ * ConnectionError class for instance checking
391
+ *
392
+ * Will get thrown if a network-level error occurs (e.g. DNS resolution, connection lost)
393
+ */
394
+ export declare class ConnectionError extends Error {
395
+ name: 'ConnectionError';
396
+ options: LissaOptions;
397
+ request: FetchArguments;
398
+ }
399
+
400
+ /**
401
+ * ResponseError class for instance checking
402
+ *
403
+ * Will get thrown when the server responds with a status code that indicates a failure (non-2xx status)
404
+ */
405
+ export declare class ResponseError extends Error {
406
+ name: 'ResponseError';
407
+ options: LissaOptions;
408
+ request: FetchArguments;
409
+ response: Response;
410
+ headers: Headers;
411
+ status: number;
412
+ data: null | string | Json | ReadableStream;
413
+ }
414
+
415
+ /**
416
+ * Retry plugin
417
+ *
418
+ * Retry requests on connection errors or server errors
419
+ */
420
+ export declare const retry: (options: RetryOptions) => Plugin;
421
+
422
+ type CustomRetryError = {
423
+ /** custom retry type have to be returned by shouldRetry hook */
424
+ [K in `on${string}`]: number;
425
+ };
426
+
427
+ type RetryOptions = CustomRetryError & {
428
+ onConnectionError: number;
429
+ onGatewayError: number;
430
+ on429: number;
431
+ onServerError: number;
432
+
433
+ /**
434
+ * Decide if the occurred error should trigger a retry.
435
+ *
436
+ * The given errorType helps preselecting error types. Return false to not
437
+ * trigger a retry. Return nothing if the given errorType is correct. Return
438
+ * a string to redefine the errorType or use a custom one. The number of
439
+ * maximum retries can be configured as `on${errorType}`. Return "CustomError"
440
+ * and define the retries as { onCustomError: 3 }
441
+ */
442
+ shouldRetry(
443
+ errorType: void | 'ConnectionError' | 'GatewayError' | '429' | 'ServerError',
444
+ error: ResponseError | ConnectionError | GeneralErrorResponse,
445
+ ): void | false | string;
446
+
447
+ /**
448
+ * Hook into the retry logic after the retry is triggered and before the delay
449
+ * is awaited. Use beforeRetry e. g. if you want to change how long the delay
450
+ * should be or to notify a customer that the connection is lost.
451
+ */
452
+ beforeRetry(
453
+ retry: { attempt: number, delay: number },
454
+ error: ResponseError | ConnectionError | GeneralErrorResponse,
455
+ ): void | { attempt: number, delay: number };
456
+
457
+ /**
458
+ * Hook into the retry logic after the delay is awaited and before the request
459
+ * gets resend. Use onRetry e. g. if you want to log that a retry is running now
460
+ */
461
+ onRetry(
462
+ retry: { attempt: number, delay: number },
463
+ error: ResponseError | ConnectionError | GeneralErrorResponse,
464
+ ): void;
465
+
466
+ /**
467
+ * Hook into the retry logic after a request was successful. Use onSuccess
468
+ * e. g. if you want to dismiss a connection lost notification
469
+ */
470
+ onSuccess(
471
+ retry: { attempt: number, delay: number },
472
+ res: ResultValue,
473
+ ): void;
474
+ };
475
+
476
+ /**
477
+ * Dedupe plugin
478
+ *
479
+ * Aborts leading or trailing requests to the same endpoint (depends on configured strategy [default is leading])
480
+ */
481
+ export declare const dedupe: (options: DedupeOptions) => Plugin;
482
+
483
+ type DedupeOptions = {
484
+ /**
485
+ * Which request methods should be deduped. Defaults to "get"
486
+ */
487
+ methods: HttpMethod[];
488
+
489
+ /**
490
+ * How to build the endpoint identifier. Defaults to url + method.
491
+ * Return false to skip dedupe logic.
492
+ */
493
+ getIdentifier: (options: LissaOptions) => any;
494
+
495
+ /**
496
+ * Define default strategy. Abort leading requests on new request or abort
497
+ * trailing new requests until first finishes. Can be also configured
498
+ * individually by adding a dedupe param to the request options.
499
+ */
500
+ defaultStrategy: 'leading' | 'trailing';
501
+ };
502
+
503
+ // Named exports are also params of the default export
504
+ interface NamedExports {
505
+ defaults: typeof defaults;
506
+ ConnectionError: typeof ConnectionError;
507
+ ResponseError: typeof ResponseError;
508
+ retry: typeof retry;
509
+ dedupe: typeof dedupe;
510
+ }
package/lib/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import * as exports from './exports.js';
2
+ import Lissa from './core/lissa.js';
3
+
4
+ const defaultLissa = Lissa.create();
5
+
6
+ const lib = (url, options = {}) => defaultLissa.request({
7
+ method: 'get',
8
+ ...options,
9
+ url,
10
+ });
11
+
12
+ export default Object.assign(lib, {
13
+ 'create': (...args) => Lissa.create(...args),
14
+
15
+ 'get': (...args) => defaultLissa.get(...args),
16
+ 'post': (...args) => defaultLissa.post(...args),
17
+ 'put': (...args) => defaultLissa.put(...args),
18
+ 'patch': (...args) => defaultLissa.patch(...args),
19
+ 'delete': (...args) => defaultLissa.delete(...args),
20
+ 'request': (...args) => defaultLissa.request(...args),
21
+
22
+ 'upload': (...args) => defaultLissa.upload(...args),
23
+ 'download': (...args) => defaultLissa.download(...args),
24
+
25
+ ...exports,
26
+ });
27
+
28
+ export * from './exports.js';
@@ -0,0 +1,45 @@
1
+ const defaults = {
2
+ methods: ['get'],
3
+ getIdentifier: options => options.method + options.url,
4
+ defaultStrategy: 'leading', // or trailing
5
+ };
6
+
7
+ export default (pluginOptions = {}) => (lissa) => {
8
+ pluginOptions = Object.assign({}, defaults, pluginOptions);
9
+
10
+ const requestMap = new Map();
11
+
12
+ lissa.beforeRequest(function beforeRequest(options) {
13
+ if (!options.dedupe && !pluginOptions.methods.includes(options.method)) return;
14
+
15
+ const dedupeStrategy = options.dedupe || pluginOptions.defaultStrategy;
16
+ if (dedupeStrategy === false) return;
17
+
18
+ const id = pluginOptions.getIdentifier(options);
19
+ if (!id) return;
20
+
21
+ let running = requestMap.get(id);
22
+ if (!running) requestMap.set(id, running = { value: 0, abortController: null });
23
+
24
+ running.value++;
25
+ this.on('settle', () => --running.value);
26
+
27
+ if (dedupeStrategy === 'trailing' && running.value > 1) {
28
+ options.signal = AbortSignal.abort();
29
+ return;
30
+ }
31
+
32
+ running.abortController?.abort();
33
+ running.abortController = new AbortController();
34
+
35
+ if (options.signal) {
36
+ options.signal = AbortSignal.any([
37
+ options.signal,
38
+ running.abortController.signal,
39
+ ]);
40
+ }
41
+ else {
42
+ options.signal = running.abortController.signal;
43
+ }
44
+ });
45
+ };
@@ -0,0 +1,2 @@
1
+ export { default as retry } from './retry.js';
2
+ export { default as dedupe } from './dedupe.js';
@@ -0,0 +1,65 @@
1
+ const browserDefaults = {
2
+ onConnectionError: Infinity,
3
+ onGatewayError: Infinity,
4
+ on429: Infinity,
5
+ onServerError: 0,
6
+ };
7
+
8
+ const nodeDefaults = {
9
+ onConnectionError: 3,
10
+ onGatewayError: 3,
11
+ on429: 3,
12
+ onServerError: 3,
13
+ };
14
+
15
+ // Expecting to connect to own service in browser and vendor services in node
16
+ const defaults = typeof window === 'undefined' ? nodeDefaults : browserDefaults;
17
+
18
+ export default (options = {}) => (lissa) => {
19
+ options = Object.assign({}, defaults, options);
20
+
21
+ lissa.onError(async (error) => {
22
+ let errorType;
23
+ if (error.name === 'ConnectionError') errorType = 'ConnectionError';
24
+ else if (error.status === 429) errorType = '429';
25
+ else if (error.status === 500) errorType = 'ServerError';
26
+ else if (error.status >= 502 && error.status <= 504) errorType = 'GatewayError';
27
+
28
+ if (options.shouldRetry) {
29
+ const shouldRetry = await options.shouldRetry(errorType, error);
30
+ if (shouldRetry === false) return;
31
+ if (typeof shouldRetry === 'string') errorType = shouldRetry;
32
+ }
33
+
34
+ // Return nothing to hand over the error to the next onError hook
35
+ if (!errorType) return;
36
+
37
+ // Initialize
38
+ let retry = error.options.retry || { attempt: 1, delay: 1000 };
39
+
40
+ if (options.beforeRetry) {
41
+ retry = await options.beforeRetry(retry, error) || retry;
42
+ }
43
+
44
+ // Return nothing to hand over the error to the next onError hook
45
+ if (retry.attempt > (options[`on${errorType}`] || 0)) return;
46
+
47
+ await new Promise(resolve => setTimeout(resolve, retry.delay));
48
+
49
+ if (options.onRetry) {
50
+ await options.onRetry({ ...retry }, error);
51
+ }
52
+
53
+ error.options.retry = {
54
+ attempt: retry.attempt + 1,
55
+ // await 1 sec on first retry, 2 sec on second retry ... (but max 5 sec)
56
+ delay: Math.min(1000 * (retry.attempt + 1), 5000),
57
+ };
58
+
59
+ const request = lissa.request(error.options);
60
+
61
+ if (options.onSuccess) request.then(res => options.onSuccess({ ...retry }, res));
62
+
63
+ return request;
64
+ });
65
+ };