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/README.md ADDED
@@ -0,0 +1,715 @@
1
+ # Lissa
2
+
3
+ > A lightweight, extensible HTTP client for modern JavaScript applications
4
+
5
+ Lissa is a powerful yet minimal HTTP library that brings simplicity back to API interactions. Built on the native Fetch API, it offers a fluent, promise-based interface with zero dependencies while providing advanced features like intelligent retries, request deduplication, and progress tracking.
6
+
7
+ Whether you're building a complex web application or a simple Node.js service, Lissa adapts to your needs with its plugin-driven architecture and universal compatibility.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [API](#api)
15
+ - [Fluent API](#fluent-api)
16
+ - [Built-in Plugins](#built-in-plugins)
17
+ - [Error Types](#error-types)
18
+ - [Examples](#examples)
19
+ - [Browser Support](#browser-support)
20
+ - [License](#license)
21
+
22
+ ## Features
23
+
24
+ - **Promise-based fluent API**: Modern async/await support with clean, intuitive syntax
25
+ - **Universal**: Works seamlessly in both Node.js and browser environments
26
+ - **Plugin Architecture**: Easily extend functionality with plugins (dedupe and retry are built-in)
27
+ - **Request/Response Hooks**: Powerful hooks for customizing request/response handling
28
+ - **File Operations**: Built-in support for file uploads and downloads with progress tracking
29
+ - **Error Handling**: Custom error classes for robust error management
30
+ - **TypeScript Support**: Full TypeScript definitions included
31
+ - **Lightweight**: No dependencies, built on native Fetch API
32
+ - **Flexible Configuration**: Instance-based configuration with inheritance and extension
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install lissa
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ In this documentation we reference the Lissa class as `Lissa` and a reference to a Lissa instance as `lissa`.
43
+
44
+ ### Basic Example
45
+
46
+ ```js
47
+ import Lissa from "lissa";
48
+
49
+ // Direct function call
50
+ const { data } = await Lissa("https://api.example.com/data");
51
+
52
+ // Use HTTP methods
53
+ const { data: users } = await Lissa.get("https://api.example.com/users");
54
+ const { data: newUser } = await Lissa.post("https://api.example.com/users", {
55
+ name: "John Doe",
56
+ email: "john@example.com"
57
+ });
58
+
59
+ // Create a configured instance
60
+ const lissa = Lissa.create({
61
+ baseURL: "https://api.example.com",
62
+ headers: { "Authorization": "Bearer token" }
63
+ });
64
+
65
+ // Use HTTP methods on the instance
66
+ const { data: user } = await lissa.get("/users/1");
67
+ const { data: newPost } = await lissa.post("/posts", { title: "Hello", body: "World" });
68
+ ```
69
+
70
+ ### Using Plugins
71
+
72
+ ```js
73
+ import Lissa from "lissa";
74
+
75
+ const lissa = Lissa.create("https://api.example.com");
76
+
77
+ lissa.use(Lissa.retry());
78
+
79
+ const { data } = await lissa.get("/data");
80
+ ```
81
+
82
+ ### Advanced Example
83
+
84
+ ```js
85
+ import Lissa from "lissa";
86
+
87
+ const lissa = Lissa.create({
88
+ baseURL: CONFIG.API_ADDRESS,
89
+ headers: {
90
+ "X-Api-Key": CONFIG.API_KEY,
91
+ },
92
+ paramsSerializer: "extended",
93
+ credentials: "include",
94
+ });
95
+
96
+ lissa.use(Lissa.dedupe());
97
+
98
+ lissa.use(Lissa.retry({
99
+ beforeRetry({ attempt }) {
100
+ if (attempt !== 2) return;
101
+ Notify.error("The connection to the server was interrupted!");
102
+ },
103
+
104
+ onSuccess() {
105
+ dismissDisconnectError();
106
+ },
107
+ }));
108
+
109
+ lissa.onError(async (error) => {
110
+ if (error.name === "ResponseError" && error.status === 401) {
111
+ await Session.logout();
112
+ Notify.info("Your session has expired! Please log in again.");
113
+ error.handled = true;
114
+ throw error;
115
+ }
116
+ });
117
+
118
+ ```
119
+
120
+ ## API
121
+
122
+ ### `options` Object
123
+
124
+ Any property that is not listed below will get handed over to the underlying fetch call as is.
125
+ Checkout https://developer.mozilla.org/en-US/docs/Web/API/RequestInit for available properties.
126
+
127
+ | Property | Type | Default Value | Description |
128
+ |------------|--------|------------------|-------------------------------------|
129
+ | baseURL | string | "" | Will be prepended to "url".
130
+ | url | string | "" | The resource to fetch.
131
+ | method | string | "get" | A http request method
132
+ | authenticate | { username, password } | undefined | Basic authentication
133
+ | headers | Headers | {} | HTTP headers
134
+ | params | object | {} | Query params
135
+ | paramsSerializer | "simple", "extended" or Function | "simple" | How to serialize the query params
136
+ | urlBuilder | "simple", "extended" or Function | "simple" | How to build the final fetch url from the defined "baseURL" and "url"
137
+ | responseType | "json", "text", "file" or "raw" | "json" | The type of data that the server will respond with
138
+ | timeout | number | undefined | Specify the number of milliseconds before the request gets aborted
139
+ | signal | AbortSignal | undefined | Cancel/Abort running requests
140
+ | body | object, buffer, stream, file, etc. | undefined | Request body
141
+
142
+ #### paramsSerializer option
143
+ Set the paramsSerializer option to "simple", "extended" or a custom query string params serializer function. Inspired by [express](https://www.npmjs.org/package/express) "simple" and "extended" serializes the params like ["node:querystring"](http://nodejs.org/api/querystring.html) or [qs](https://www.npmjs.org/package/qs). It is set to "simple" by default. A custom function will receive an object of query param keys and their values, and must return the complete query string.
144
+
145
+ #### urlBuilder option
146
+ Set the urlBuilder option to "simple", "extended" or a custom build function.
147
+ - "simple" simply concatenates baseURL and url as strings (default)
148
+ - "extended" is using the URL constructor new URL(url, baseURL);
149
+ - A custom function will receive url and baseURL, and must return the complete url as string or URL instance
150
+
151
+ Make sure to not forget a needed slash using "simple". If using "extended" be careful with leading and trailing slashes in urls, the baseURL and also with sub paths in the baseURL. For example `new URL("todos", "http://api.example.com/v2")` and `new URL("/todos", "http://api.example.com/v2/")` both results in a fetch to `"http://api.example.com/todos"`. Only `new URL("todos", "http://api.example.com/v2/")` will result in a fetch to the expected `"http://api.example.com/v2/todos"`.
152
+
153
+ ### `result` Object/Error
154
+
155
+ Every request returns a promise which gets fulfilled into a result object or rejected into a result error.
156
+ Both provide the following properties:
157
+
158
+ | Property | Type | Description |
159
+ |------------|---------|-----------------------------------------------|
160
+ | options | object | The options used to make the request |
161
+ | request | object | The arguments with which the fetch got called |
162
+ | response | object | The underlying fetch response |
163
+ | headers | Headers | The response headers |
164
+ | status | number | The response status code |
165
+ | data | object | The response data |
166
+
167
+
168
+ ### Call Lissa as Function - Lissa()
169
+ Performs a general fetch request. Specify method, body, headers and more in the given options object.
170
+
171
+ #### Syntax
172
+ ```js
173
+ Lissa(url);
174
+ Lissa(url, options);
175
+ ```
176
+
177
+ #### Returns
178
+
179
+ A Promise that resolves to a result object or rejects into a result error.
180
+
181
+ ### Create a Lissa instance - Lissa.create()
182
+ A Lissa instance can be created with base options that apply to or get merged into every request.
183
+
184
+ #### Syntax
185
+ ```js
186
+ Lissa.create();
187
+ Lissa.create(baseURL);
188
+ Lissa.create(options);
189
+ Lissa.create(baseURL, options);
190
+ ```
191
+
192
+ #### Returns
193
+
194
+ A new lissa instance.
195
+
196
+ ### GET Request - Lissa.get() - lissa.get()
197
+
198
+ ```js
199
+ lissa.get(url);
200
+ lissa.get(url, options);
201
+
202
+ // Can also be called as static method
203
+ Lissa.get()
204
+ ```
205
+
206
+ ### POST Request - Lissa.post() - lissa.post()
207
+
208
+ ```js
209
+ lissa.post(url);
210
+ lissa.post(url, body);
211
+ lissa.post(url, body, options);
212
+
213
+ // Can also be called as static method
214
+ Lissa.post()
215
+ ```
216
+
217
+ ### PUT Request - Lissa.put() - lissa.put()
218
+
219
+ ```js
220
+ lissa.put(url);
221
+ lissa.put(url, body);
222
+ lissa.put(url, body, options);
223
+
224
+ // Can also be called as static method
225
+ Lissa.put()
226
+ ```
227
+
228
+ ### PATCH Request - Lissa.patch() - lissa.patch()
229
+
230
+ ```js
231
+ lissa.patch(url);
232
+ lissa.patch(url, body);
233
+ lissa.patch(url, body, options);
234
+
235
+ // Can also be called as static method
236
+ Lissa.patch()
237
+ ```
238
+
239
+ ### DELETE Request - Lissa.delete() - lissa.delete()
240
+
241
+ ```js
242
+ lissa.delete(url);
243
+ lissa.delete(url, options);
244
+
245
+ // Can also be called as static method
246
+ Lissa.delete()
247
+ ```
248
+
249
+ ### General Request - Lissa.request() - lissa.request()
250
+
251
+ ```js
252
+ lissa.request(options);
253
+
254
+ // Can also be called as static method
255
+ Lissa.request()
256
+ ```
257
+
258
+ ### Upload Files - Lissa.upload() - lissa.upload()
259
+ Upload files with optional progress tracking
260
+
261
+ #### Syntax
262
+ ```js
263
+ lissa.upload(file, url);
264
+ lissa.upload(file, url, onProgress);
265
+ lissa.upload(file, url, onProgress, options);
266
+
267
+ // Can also be called as static method
268
+ Lissa.upload()
269
+ ```
270
+
271
+ #### Example
272
+ ```js
273
+ // Basic upload
274
+ await lissa.upload(file, "/upload");
275
+
276
+ // Upload with progress tracking
277
+ await lissa.upload(file, "/upload", (uploaded, total) => {
278
+ console.log(`Upload progress: ${Math.round(uploaded / total * 100)} %`);
279
+ });
280
+ ```
281
+
282
+ ### Download Files - Lissa.download() - lissa.download()
283
+ Download files with optional progress tracking
284
+
285
+ #### Syntax
286
+ ```js
287
+ lissa.download(url);
288
+ lissa.download(url, onProgress);
289
+ lissa.download(url, onProgress, options);
290
+
291
+ // Can also be called as static method
292
+ Lissa.download()
293
+ ```
294
+
295
+ #### Example
296
+ ```js
297
+ // Basic download
298
+ const { data: file } = await lissa.download("/file.pdf");
299
+
300
+ // Download with progress tracking
301
+ const { data: file } = await lissa.download("/file.pdf", (downloaded, total) => {
302
+ console.log(`Download progress: ${Math.round(downloaded / total * 100)} %`);
303
+ });
304
+ ```
305
+
306
+ ### Register a plugin - lissa.use()
307
+ Easily add functionality with plugins like `Lissa.dedupe()` and `Lissa.retry()`
308
+
309
+ ```js
310
+ lissa.use(plugin);
311
+ ```
312
+
313
+ ### Before Request Hook - lissa.beforeRequest()
314
+ Modify options before a request is processed. Returning a new options object is also possible, keep in mind a new options object will not get merged with defaults again.
315
+
316
+ ```js
317
+ lissa.beforeRequest((options) => {
318
+ options.headers.set("X-Timestamp", Date.now());
319
+ });
320
+ ```
321
+
322
+ ### Before Fetch Hook - lissa.beforeFetch()
323
+ Modify the final fetch arguments for special edge cases. Returning a new object is also possible.
324
+
325
+ ```js
326
+ lissa.beforeFetch(({ url, options }) => {
327
+ console.log("Calling fetch(url, options)", { url, options });
328
+ });
329
+ ```
330
+
331
+ ### Response Hook - lissa.onResponse()
332
+ Handle successful responses. If a value gets returned in this hook, every hook registered after this hook getting skipped and the request promise fulfills with this return value. If an error gets thrown or returned the request promise rejects with this error.
333
+
334
+ ```js
335
+ lissa.onResponse((result) => {
336
+ console.log("Response received:", result.status);
337
+ });
338
+ ```
339
+
340
+ ### Error Hook - lissa.onError()
341
+ Handle connection, response or abort errors. If a value gets returned in this hook, every hook registered after this hook getting skipped and the request promise fulfills with this return value. If an error gets thrown or returned the request promise rejects with this error.
342
+
343
+ ```js
344
+ lissa.onError((error) => {
345
+ if (error.name === "ResponseError" && error.status === 401) {
346
+ redirectToLogin();
347
+ }
348
+ });
349
+ ```
350
+
351
+ ### Extend an Instance - lissa.extend()
352
+ Creates a new instance with merged options
353
+
354
+ ```js
355
+ const apiClient = lissa.extend({
356
+ headers: { "Authorization": "Bearer token" }
357
+ });
358
+ ```
359
+
360
+ ### Authentication - lissa.authenticate()
361
+ Creates a new instance with added basic authentication
362
+
363
+ ```js
364
+ const authenticatedClient = lissa.authenticate("username", "password");
365
+ ```
366
+
367
+ ## Fluent API
368
+
369
+ The promises returned by the requests aren't just promises. They are instances of the LissaRequest class which allows a fluent API syntax. An instance of LissaRequest is referenced as `request`.
370
+
371
+ ### Set the base URL - request.baseURL()
372
+
373
+ ```js
374
+ await lissa.get("/data").baseURL("https://api2.example.com");
375
+ ```
376
+
377
+ ### Set the request URL - request.url()
378
+
379
+ ```js
380
+ await lissa.request(options).url("/data2");
381
+ ```
382
+
383
+ ### Set the HTTP method - request.method()
384
+
385
+ ```js
386
+ await lissa.get("/data").method("delete"); // :D
387
+ ```
388
+
389
+ ### Add or override request headers - request.headers()
390
+
391
+ ```js
392
+ await lissa.get("/data").headers({ "X-Foo": "bar" });
393
+ ```
394
+
395
+ ### Set basic authentication - request.authenticate()
396
+
397
+ ```js
398
+ await lissa.get("/data").authenticate("username", "password");
399
+ ```
400
+
401
+ ### Add or override query string parameters - request.params()
402
+
403
+ ```js
404
+ await lissa.get("/data").params({ foo: "bar" });
405
+ ```
406
+
407
+ ### Attach or merge request body - request.body()
408
+
409
+ ```js
410
+ await lissa.post("/data").body({ foo: "bar" });
411
+ ```
412
+
413
+ ### Set a request timeout in milliseconds - request.timeout()
414
+ Attaches an AbortSignal.timeout(...) signal to the request
415
+
416
+ ```js
417
+ await lissa.get("/data").timeout(30 * 1000);
418
+ ```
419
+
420
+ ### Attach an AbortSignal - request.signal()
421
+ Attaches an AbortSignal to the request
422
+
423
+ ```js
424
+ await lissa.get("/data").signal(abortController.signal);
425
+ ```
426
+
427
+ ### Change the expected response type - request.responseType()
428
+ By default every response gets parsed as json
429
+
430
+ ```js
431
+ await lissa.get("/plain-text").responseType("text");
432
+ ```
433
+
434
+ ### Access the options directly - request.options
435
+ For special edge cases you can access the options directly
436
+
437
+ ```js
438
+ const request = lissa.get("/data");
439
+ request.options.headers.delete("X-Foo");
440
+ const result = await request;
441
+ ```
442
+
443
+ ### Other
444
+ A LissaRequest provides some other maybe useful properties and events for more special edge cases.
445
+
446
+ ```js
447
+ const request = lissa.get("/data");
448
+
449
+ request.status; // "pending", "fulfilled" or "rejected"
450
+ request.value; // The result object if promise fulfills
451
+ request.reason; // The result error if promise rejects
452
+
453
+ request.on("resolve", (value) => console.log(value));
454
+ request.on("reject", (reason) => console.log(reason));
455
+ request.on("settle", ({ status, value, reason }) => console.log({ status, value, reason }));
456
+
457
+ const result = await request;
458
+ ```
459
+
460
+
461
+ ## Built-in Plugins
462
+
463
+ ### Retry Plugin - Lissa.retry()
464
+ Automatically retry failed requests. The retry delay is 1 sec on first retry, 2 sec on second retry, etc., but max 5 sec. The default options are different for browsers and node. It is most likely that we want to connect to our own service in a browser and to vendor services in node. The node default is 3 retries on every error type. The browser default is Infinity for all error types except for server errors which is 0 (no retries).
465
+
466
+ ```js
467
+ lissa.use(Lissa.retry({
468
+ onConnectionError: Infinity,
469
+ onGatewayError: Infinity,
470
+ on429: Infinity,
471
+ onServerError: 0,
472
+ }));
473
+ ```
474
+
475
+ #### shouldRetry Option
476
+ Decide if the occurred error should trigger a retry.
477
+
478
+ The given errorType helps preselecting error types. Return false to not
479
+ trigger a retry. Return nothing if the given errorType is correct. Return
480
+ a string to redefine the errorType or use a custom one. The number of
481
+ maximum retries can be configured as `on${errorType}`. Return "CustomError"
482
+ and define the retries as { onCustomError: 3 }
483
+
484
+ ```js
485
+ Lissa.retry({
486
+ onCustomError: 5,
487
+
488
+ shouldRetry(errorType, error) {
489
+ if (error.status === 999) return "CustomError";
490
+ if (errorType === "429" && !error.headers.has("Retry-After")) return false;
491
+ return errorType; // optional
492
+ },
493
+ })
494
+ ```
495
+
496
+ #### beforeRetry Option
497
+ Hook into the retry logic after the retry is triggered and before the delay
498
+ is awaited. Use beforeRetry e.g. if you want to change how long the delay
499
+ should be or to notify a customer that the connection is lost.
500
+
501
+ ```js
502
+ Lissa.retry({
503
+ // Return new object
504
+ beforeRetry({ attempt, delay }, error) {
505
+ if (error.status === 429) return { attempt, delay: delay * attempt };
506
+ },
507
+
508
+ // Or change existing object
509
+ beforeRetry(retry, error) {
510
+ if (error.status === 429) retry.delay = retry.attempt * 1234;
511
+ },
512
+ })
513
+ ```
514
+
515
+ #### onRetry Option
516
+ Hook into the retry logic after the delay is awaited and before the request
517
+ gets resent. Use onRetry e.g. if you want to log that a retry is running now
518
+
519
+ ```js
520
+ Lissa.retry({
521
+ onRetry({ attempt, delay }, error) {
522
+ console.log("Retry attempt", attempt, "for", error.options.method, error.options.url);
523
+ },
524
+ })
525
+ ```
526
+
527
+ #### onSuccess Option
528
+ Hook into the retry logic after a request was successful. Use onSuccess
529
+ e.g. if you want to dismiss a connection lost notification
530
+
531
+ ```js
532
+ Lissa.retry({
533
+ onSuccess({ attempt, delay }, result) {
534
+ console.log("Retry successful after attempt", attempt, "for", result.options.method, result.options.url, "-", result.status);
535
+ },
536
+ })
537
+ ```
538
+
539
+
540
+ ### Dedupe Plugin - Lissa.dedupe()
541
+ Prevent duplicate requests by aborting leading or trailing identical requests. By default it only aborts leading get requests to the same endpoint ignoring query string params. Dedupe can be forced or disabled per request by adding dedupe to the request options setting the strategy or false.
542
+
543
+ ```js
544
+ lissa.use(Lissa.dedupe({
545
+ methods: ["get"], // Pre-filter by HTTP method
546
+ getIdentifier: options => options.method + options.url, // Identify request
547
+ defaultStrategy: "leading", // or trailing
548
+ }));
549
+
550
+ lissa.get("/data"); // Getting aborted on dedupe strategy "leading" (default)
551
+ lissa.get("/data"); // Getting aborted on dedupe strategy "trailing"
552
+
553
+ lissa.get("/data", { dedupe: false }); // Dedupe logic getting skipped
554
+
555
+ lissa.get("/data", { dedupe: "trailing" }); // Force dedupe with the given strategy
556
+ ```
557
+
558
+ #### Dedupe by origin example
559
+
560
+ ```js
561
+ lissa.use(Lissa.dedupe({
562
+ // We use our own property to identify requests. A falsy identifier results in skipping dedupe logic
563
+ getIdentifier: options => options.origin,
564
+
565
+ // Abort trailing requests / Only the first request gets through
566
+ defaultStrategy: "trailing",
567
+ }));
568
+
569
+ lissa.get("/todos", {
570
+ origin: "todo_table_retry_btn",
571
+ dedupe: "trailing", // Can be omitted - Already applied by defaultStrategy here
572
+ });
573
+
574
+ lissa.get("/todos", {
575
+ origin: "todo_table_filter_input",
576
+ dedupe: "leading", // Override defaultStrategy - abort previous requests
577
+
578
+ // Example filter with params
579
+ params: {
580
+ search: "a search string",
581
+ status: "done",
582
+ orderBy: "created",
583
+ },
584
+ });
585
+ ```
586
+
587
+ ## Error Types
588
+
589
+ Error types for different failure scenarios:
590
+
591
+ - **ResponseError**: HTTP errors (4xx, 5xx status codes)
592
+ - **TimeoutError**: Request timed out
593
+ - **AbortError**: Request got aborted
594
+ - **ConnectionError**: Network connectivity issues
595
+
596
+ All errors include the original request options. Response errors include response data and status information.
597
+
598
+ ## Examples
599
+
600
+ ### Query Parameters
601
+ ```js
602
+ // Using params option
603
+ const { data } = await lissa.get("/posts", {
604
+ params: { userId: 1, limit: 10 }
605
+ });
606
+
607
+ // Using fluent API
608
+ const { data } = await lissa.get("/posts").params({ userId: 1 });
609
+ ```
610
+
611
+ ### Request/Response Hooks
612
+ ```js
613
+ // Add dynamic header to all requests
614
+ lissa.beforeRequest((options) => {
615
+ options.headers.set("X-NOW", Date.now());
616
+ });
617
+
618
+ // Log all responses
619
+ lissa.onResponse(({ options, status }) => {
620
+ console.log(`${options.method.toUpperCase()} ${options.url} - ${status}`);
621
+ });
622
+
623
+ // Handle errors globally
624
+ lissa.onError((error) => {
625
+ if (error.status === 500) {
626
+ Notify.error("An unexpected error occurred! Please try again later.");
627
+ }
628
+ });
629
+ ```
630
+
631
+ ### File Upload with Progress
632
+ ```js
633
+ const fileInput = document.querySelector("#file-input");
634
+ const file = fileInput.files[0];
635
+
636
+ await lissa.upload(file, "/upload", (uploaded, total) => {
637
+ const percent = Math.round(uploaded / total * 100);
638
+ console.log(`Upload progress: ${percent} %`);
639
+ updateProgressBar(percent);
640
+ });
641
+ ```
642
+
643
+ ### Error Handling
644
+ ```js
645
+ try {
646
+ const { data } = await lissa.get("/data");
647
+ console.log(data);
648
+ } catch (error) {
649
+ if (error.name === "ResponseError") {
650
+ console.error(`HTTP Error ${error.status}: ${error.data}`);
651
+ } else if (error.name === "TimeoutError") {
652
+ console.error("Request timed out");
653
+ } else if (error.name === "AbortError") {
654
+ console.error("Request got aborted");
655
+ } else if (error.name === "ConnectionError") {
656
+ console.error("Could not connect to target server");
657
+ } else {
658
+ console.error("Most likely a TypeError:", error);
659
+ }
660
+ }
661
+ ```
662
+
663
+ ### Creating Multiple Clients
664
+ ```js
665
+ // API client with authentication
666
+ const apiClient = Lissa.create({
667
+ baseURL: "https://api.example.com",
668
+ headers: {
669
+ "Authorization": "Bearer token",
670
+ }
671
+ });
672
+
673
+ // Public client without authentication
674
+ const publicClient = Lissa.create({
675
+ baseURL: "https://public-api.example.com"
676
+ });
677
+
678
+ apiClient.use(Lissa.retry());
679
+ publicClient.use(Lissa.dedupe());
680
+ ```
681
+
682
+ See the `examples/` directory for more usage examples, including browser-specific code and advanced plugin usage.
683
+
684
+ ## Browser Support
685
+
686
+ Lissa works in all modern browsers that support the following APIs:
687
+
688
+ ### Core Requirements
689
+ - **Fetch API** - For making HTTP requests
690
+ - **Promises** - For async operations
691
+ - **Headers API** - For request/response header manipulation
692
+ - **URL API** - For URL construction and parsing
693
+ - **AbortController/AbortSignal** - For request cancellation and timeouts
694
+ - **TextDecoderStream** - For body processing
695
+
696
+ ### File Operations Requirements
697
+ - **File API** - For file handling, uploads and downloads with proper metadata
698
+ - **FormData API** - For multipart form uploads
699
+
700
+ ### Progress Tracking Requirements (Optional)
701
+ - **TransformStream** - For download progress tracking with fetch
702
+
703
+ ### Browser Compatibility
704
+ - **Chrome**: 124+ (full support)
705
+ - **Firefox**: 124+ (full support)
706
+ - **Safari**: 17.4+ (full support)
707
+ - **Edge**: 124+ (full support)
708
+
709
+ ### Node.js Support
710
+
711
+ Requires Node.js 20.3+.
712
+
713
+ ## License
714
+
715
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.