aspi 2.1.0 → 2.2.1
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 +218 -4
- package/dist/index.cjs +114 -12
- package/dist/index.d.cts +148 -1
- package/dist/index.d.ts +148 -1
- package/dist/index.js +114 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,15 +189,15 @@ try {
|
|
|
189
189
|
// Result mode wins (throwable is ignored)
|
|
190
190
|
const result = await api
|
|
191
191
|
.post('/login')
|
|
192
|
-
.withResult() //
|
|
193
|
-
.throwable() //
|
|
192
|
+
.withResult() // ignored because throwable was called later
|
|
193
|
+
.throwable() // enables throwable mode
|
|
194
194
|
.json<{ token: string }>();
|
|
195
195
|
|
|
196
196
|
// Throwable mode wins (Result is ignored)
|
|
197
197
|
const data = await api
|
|
198
198
|
.get('/profile')
|
|
199
|
-
.throwable() //
|
|
200
|
-
.withResult() //
|
|
199
|
+
.throwable() // ignored because withResult was called later
|
|
200
|
+
.withResult() // enables Result mode
|
|
201
201
|
.json();
|
|
202
202
|
```
|
|
203
203
|
|
|
@@ -537,6 +537,220 @@ class Request<
|
|
|
537
537
|
|
|
538
538
|
---
|
|
539
539
|
|
|
540
|
+
## Result utilities
|
|
541
|
+
|
|
542
|
+
`Result<T, E>` is a small tagged-union helper used throughout Aspi to represent success or failure without throwing.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
type Ok<T> = { __tag: 'ok'; value: T };
|
|
546
|
+
type Err<E> = { __tag: 'err'; error: E };
|
|
547
|
+
type Result<T, E> = Ok<T> | Err<E>;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Creating Results
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
import * as Result from './result';
|
|
554
|
+
|
|
555
|
+
const success = Result.ok(42);
|
|
556
|
+
const failure = Result.err('not found');
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Checking and Extracting
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
if (Result.isOk(success)) {
|
|
563
|
+
console.log(success.value); // 42
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (Result.isErr(failure)) {
|
|
567
|
+
console.error(failure.error); // "not found"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const valueOrNull = Result.getOrNull(success); // 42 | null
|
|
571
|
+
const errorOrNull = Result.getErrorOrNull(failure); // "not found" | null
|
|
572
|
+
|
|
573
|
+
const valueOrFallback = Result.getOrElse(failure, 0); // 0
|
|
574
|
+
|
|
575
|
+
// Throwing
|
|
576
|
+
const mustHaveValue = Result.getOrThrow(success); // 42
|
|
577
|
+
// Result.getOrThrow(failure) throws "not found"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Transforming
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
// map value
|
|
584
|
+
const doubled = Result.map(success, (n) => n * 2); // ok(84)
|
|
585
|
+
|
|
586
|
+
// map error
|
|
587
|
+
const upperError = Result.mapErr(failure, (e) => e.toUpperCase()); // err("NOT FOUND")
|
|
588
|
+
|
|
589
|
+
// pattern matching
|
|
590
|
+
const message = Result.match(success, {
|
|
591
|
+
onOk: (n) => `Got ${n}`,
|
|
592
|
+
onErr: (e) => `Error: ${e}`,
|
|
593
|
+
});
|
|
594
|
+
// "Got 42"
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Tagged error helpers
|
|
598
|
+
When your error type is a union with a tag field, you can use helpers to handle specific variants:
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
type HttpError =
|
|
602
|
+
| { tag: 'BAD_REQUEST'; details?: string }
|
|
603
|
+
| { tag: 'UNAUTHORIZED' }
|
|
604
|
+
| { tag: 'NOT_FOUND' };
|
|
605
|
+
|
|
606
|
+
const result: Result<number, HttpError> = Result.err({
|
|
607
|
+
tag: 'NOT_FOUND',
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Handle a specific tag
|
|
611
|
+
Result.catchError(result, 'NOT_FOUND', (e) => {
|
|
612
|
+
console.log('Missing resource');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Handle multiple tags
|
|
616
|
+
Result.catchErrors(result, {
|
|
617
|
+
BAD_REQUEST: (e) => console.log('Invalid input'),
|
|
618
|
+
UNAUTHORIZED: () => console.log('Please log in'),
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Pipe Utility
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { pipe } from './result';
|
|
626
|
+
|
|
627
|
+
const label = pipe(
|
|
628
|
+
12345,
|
|
629
|
+
(cents) => cents / 100,
|
|
630
|
+
(amount) => amount.toFixed(2),
|
|
631
|
+
(str) => `$${str}`,
|
|
632
|
+
);
|
|
633
|
+
// "$123.45"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Experimental capabilities
|
|
639
|
+
|
|
640
|
+
> **Experimental:** This API is still evolving. Names and behavior may change in minor versions.
|
|
641
|
+
|
|
642
|
+
Capabilities are small plugins that wrap the low‑level fetch call for each request. They let you implement cross‑cutting behavior (logging, retries, token refresh, tracing, etc.) without changing Aspi core.
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
import type { Capability } from './interceptor';
|
|
646
|
+
import { Aspi } from './aspi';
|
|
647
|
+
|
|
648
|
+
// Capability signature
|
|
649
|
+
const myCapability: Capability = ({ request }) => ({
|
|
650
|
+
async run(runner) {
|
|
651
|
+
// Called before fetch
|
|
652
|
+
console.log('→', request.path);
|
|
653
|
+
|
|
654
|
+
const res = await runner(); // performs fetch(url, requestInit)
|
|
655
|
+
|
|
656
|
+
// Called after fetch
|
|
657
|
+
console.log('←', res.status, res.statusText);
|
|
658
|
+
|
|
659
|
+
return res;
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Registering capabilities
|
|
665
|
+
Capabilities are attached at the Aspi client level and apply to all requests created from that instance:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
const api = new Aspi({ baseUrl: 'https://api.example.com' }).useCapability(
|
|
669
|
+
myCapability,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const user = await api.get('/users/1').throwable().json<User>();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
You can register multiple capabilities; they execute in the order they were added, each wrapping the next:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
679
|
+
.useCapability(loggingCapability)
|
|
680
|
+
.useCapability(tracingCapability)
|
|
681
|
+
.useCapability(refreshTokenCapability);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Example: token refresh (simplified)
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
import type { Capability } from './interceptor';
|
|
688
|
+
import { Aspi } from './aspi';
|
|
689
|
+
import * as Result from './result';
|
|
690
|
+
|
|
691
|
+
let tokens: { accessToken: string | null; refreshToken: string | null } = {
|
|
692
|
+
accessToken: null,
|
|
693
|
+
refreshToken: null,
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
async function refreshTokenRequest(refreshToken: string) {
|
|
697
|
+
const res = await new Aspi({ baseUrl: 'https://auth.example.com' })
|
|
698
|
+
.post('/refresh')
|
|
699
|
+
.bodyJson({ refreshToken })
|
|
700
|
+
.withResult()
|
|
701
|
+
.json<{ accessToken: string; refreshToken: string }>();
|
|
702
|
+
|
|
703
|
+
await Result.match(res, {
|
|
704
|
+
onOk: ({ data }) => {
|
|
705
|
+
tokens = data;
|
|
706
|
+
},
|
|
707
|
+
onErr: (err) => {
|
|
708
|
+
tokens = { accessToken: null, refreshToken: null };
|
|
709
|
+
throw err;
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export const refreshTokenCapability: Capability = ({ request }) => {
|
|
715
|
+
let isRefreshing = false;
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
async run(runner) {
|
|
719
|
+
// First try
|
|
720
|
+
const first = await runner();
|
|
721
|
+
|
|
722
|
+
if (first.status !== 401) {
|
|
723
|
+
return first;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// No refresh token → just propagate 401
|
|
727
|
+
if (!tokens.refreshToken || isRefreshing) {
|
|
728
|
+
return first;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
isRefreshing = true;
|
|
732
|
+
try {
|
|
733
|
+
await refreshTokenRequest(tokens.refreshToken);
|
|
734
|
+
} finally {
|
|
735
|
+
isRefreshing = false;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!tokens.accessToken) {
|
|
739
|
+
return first;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Inject new Authorization header and retry once
|
|
743
|
+
request.requestInit.headers = {
|
|
744
|
+
...request.requestInit.headers,
|
|
745
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
return runner();
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
```
|
|
753
|
+
|
|
540
754
|
## License
|
|
541
755
|
|
|
542
756
|
MIT © Aspi contributors
|
package/dist/index.cjs
CHANGED
|
@@ -315,7 +315,8 @@ var Request = class {
|
|
|
315
315
|
#shouldBeResult = false;
|
|
316
316
|
#bodySchemaIssues = [];
|
|
317
317
|
#throwOnError = false;
|
|
318
|
-
|
|
318
|
+
#capabilities = [];
|
|
319
|
+
constructor(method, path, requestOptions, capabilities = []) {
|
|
319
320
|
this.#path = path;
|
|
320
321
|
this.#middlewares = requestOptions.middlewares || [];
|
|
321
322
|
this.#localRequestInit = {
|
|
@@ -326,6 +327,7 @@ var Request = class {
|
|
|
326
327
|
this.#customErrorCbs = { ...requestOptions?.errorCbs || {} };
|
|
327
328
|
this.#throwOnError = requestOptions.throwOnError || false;
|
|
328
329
|
this.#shouldBeResult = requestOptions.shouldBeResult || false;
|
|
330
|
+
this.#capabilities = [...capabilities];
|
|
329
331
|
}
|
|
330
332
|
/**
|
|
331
333
|
* Sets the base URL for the request.
|
|
@@ -1076,7 +1078,15 @@ var Request = class {
|
|
|
1076
1078
|
let responseData = null;
|
|
1077
1079
|
while (attempts <= retries) {
|
|
1078
1080
|
try {
|
|
1079
|
-
|
|
1081
|
+
if (this.#capabilities.length > 0) {
|
|
1082
|
+
for (const capability of this.#capabilities) {
|
|
1083
|
+
response = await capability({ request }).run(
|
|
1084
|
+
() => fetch(url, requestInit)
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
response = await fetch(url, requestInit);
|
|
1089
|
+
}
|
|
1080
1090
|
responseData = await responseParser(response);
|
|
1081
1091
|
if (responseData instanceof Error) {
|
|
1082
1092
|
return err(responseData);
|
|
@@ -1336,6 +1346,51 @@ var Request = class {
|
|
|
1336
1346
|
const cfg = this.#sanitisedRetryConfig();
|
|
1337
1347
|
return { ...cfg };
|
|
1338
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Registers a capability for this request.
|
|
1351
|
+
*
|
|
1352
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1353
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1354
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1355
|
+
* execution of the network call via its `run` method.
|
|
1356
|
+
*
|
|
1357
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1358
|
+
* call, the `Request` will:
|
|
1359
|
+
*
|
|
1360
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1361
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1362
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1363
|
+
* - log or trace requests,
|
|
1364
|
+
* - implement retry/refresh logic,
|
|
1365
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1366
|
+
*
|
|
1367
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1368
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1369
|
+
*
|
|
1370
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* ```ts
|
|
1374
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1375
|
+
*
|
|
1376
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1377
|
+
* async run(runner) {
|
|
1378
|
+
* console.log('→', request.path, request.requestInit);
|
|
1379
|
+
* const res = await runner();
|
|
1380
|
+
* console.log('←', res.status, res.statusText);
|
|
1381
|
+
* return res;
|
|
1382
|
+
* },
|
|
1383
|
+
* }));
|
|
1384
|
+
*
|
|
1385
|
+
* const result = await apiWithLogging
|
|
1386
|
+
* .get('/users')
|
|
1387
|
+
* .withResult()
|
|
1388
|
+
* .json();
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
useCapability(capability) {
|
|
1392
|
+
return this.#capabilities.push(capability);
|
|
1393
|
+
}
|
|
1339
1394
|
};
|
|
1340
1395
|
|
|
1341
1396
|
// src/aspi.ts
|
|
@@ -1346,6 +1401,7 @@ var Aspi = class {
|
|
|
1346
1401
|
#customErrorCbs = {};
|
|
1347
1402
|
#throwOnError = false;
|
|
1348
1403
|
#shouldBeResult = false;
|
|
1404
|
+
#capabilities = [];
|
|
1349
1405
|
constructor(config) {
|
|
1350
1406
|
const { retryConfig, ...requestInit } = config;
|
|
1351
1407
|
this.#globalRequestInit = requestInit;
|
|
@@ -1388,17 +1444,22 @@ var Aspi = class {
|
|
|
1388
1444
|
return this;
|
|
1389
1445
|
}
|
|
1390
1446
|
#createRequest(method, path) {
|
|
1391
|
-
return new Request(
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1447
|
+
return new Request(
|
|
1448
|
+
method,
|
|
1449
|
+
path,
|
|
1450
|
+
{
|
|
1451
|
+
requestConfig: {
|
|
1452
|
+
...this.#globalRequestInit,
|
|
1453
|
+
method
|
|
1454
|
+
},
|
|
1455
|
+
middlewares: this.#middlewares,
|
|
1456
|
+
errorCbs: this.#customErrorCbs,
|
|
1457
|
+
throwOnError: this.#throwOnError,
|
|
1458
|
+
shouldBeResult: this.#shouldBeResult,
|
|
1459
|
+
retryConfig: this.#retryConfig
|
|
1395
1460
|
},
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
throwOnError: this.#throwOnError,
|
|
1399
|
-
shouldBeResult: this.#shouldBeResult,
|
|
1400
|
-
retryConfig: this.#retryConfig
|
|
1401
|
-
});
|
|
1461
|
+
this.#capabilities
|
|
1462
|
+
);
|
|
1402
1463
|
}
|
|
1403
1464
|
/**
|
|
1404
1465
|
* Makes a GET request to the specified path
|
|
@@ -1693,6 +1754,47 @@ var Aspi = class {
|
|
|
1693
1754
|
this.#throwOnError = false;
|
|
1694
1755
|
return this;
|
|
1695
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1759
|
+
*
|
|
1760
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1761
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1762
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1763
|
+
* and can:
|
|
1764
|
+
*
|
|
1765
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1766
|
+
* - inspect the raw {@link Response},
|
|
1767
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1768
|
+
* or token refresh, and
|
|
1769
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1770
|
+
*
|
|
1771
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1772
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1773
|
+
* They are applied in the order they are registered.
|
|
1774
|
+
*
|
|
1775
|
+
* @param capability - The capability factory to install on this client.
|
|
1776
|
+
*
|
|
1777
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1778
|
+
*
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1782
|
+
* .useCapability(({ request }) => ({
|
|
1783
|
+
* async run(runner) {
|
|
1784
|
+
* console.log('→', request.path);
|
|
1785
|
+
* const res = await runner();
|
|
1786
|
+
* console.log('←', res.status);
|
|
1787
|
+
* return res;
|
|
1788
|
+
* },
|
|
1789
|
+
* }));
|
|
1790
|
+
*
|
|
1791
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1792
|
+
* ```
|
|
1793
|
+
*/
|
|
1794
|
+
useCapability(capability) {
|
|
1795
|
+
this.#capabilities.push(capability);
|
|
1796
|
+
return this;
|
|
1797
|
+
}
|
|
1696
1798
|
};
|
|
1697
1799
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1698
1800
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -366,6 +366,72 @@ interface JSONParseError extends CustomError<'jsonParseError', {
|
|
|
366
366
|
declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
|
|
367
367
|
declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Arguments passed to a capability factory.
|
|
371
|
+
*
|
|
372
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
373
|
+
*/
|
|
374
|
+
type CapabilityArgs<T extends AspiRequestInit = AspiRequestInit> = {
|
|
375
|
+
/**
|
|
376
|
+
* The fully constructed AspiRequest that will be used to execute `fetch`.
|
|
377
|
+
*
|
|
378
|
+
* You can:
|
|
379
|
+
* - inspect `request.path`, `request.requestInit`, `request.retryConfig`, etc.
|
|
380
|
+
* - mutate `request.requestInit.headers`, `signal`, or other properties
|
|
381
|
+
* before the network call is made.
|
|
382
|
+
*/
|
|
383
|
+
request: AspiRequest<T>;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* A capability is a small wrapper around the low-level `fetch` call that can
|
|
387
|
+
* intercept outgoing requests and incoming responses.
|
|
388
|
+
*
|
|
389
|
+
* It is a factory function that receives the current {@link AspiRequest}
|
|
390
|
+
* and returns an object exposing a single `run` method. The `run` method is
|
|
391
|
+
* responsible for invoking the provided `runner` (which performs the actual
|
|
392
|
+
* `fetch`) and may:
|
|
393
|
+
*
|
|
394
|
+
* - call `runner()` directly and return its result
|
|
395
|
+
* - call `runner()` multiple times (e.g. retry, refresh token then retry)
|
|
396
|
+
* - short‑circuit by *not* calling `runner()` and returning a synthetic
|
|
397
|
+
* {@link Response} instead
|
|
398
|
+
*
|
|
399
|
+
* Capabilities are composed in the order they are registered via
|
|
400
|
+
* `Aspi.useCapability`, with each capability wrapping the next one in the
|
|
401
|
+
* chain.
|
|
402
|
+
*
|
|
403
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* // Simple logging capability
|
|
408
|
+
* const loggingCapability: Capability = ({ request }) => ({
|
|
409
|
+
* async run(runner) {
|
|
410
|
+
* console.log('→', request.path, request.requestInit);
|
|
411
|
+
* const res = await runner();
|
|
412
|
+
* console.log('←', res.status, res.statusText);
|
|
413
|
+
* return res;
|
|
414
|
+
* },
|
|
415
|
+
* });
|
|
416
|
+
*
|
|
417
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
418
|
+
* .useCapability(loggingCapability);
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
type Capability<T extends AspiRequestInit = AspiRequestInit> = ({ request, }: CapabilityArgs<T>) => {
|
|
422
|
+
/**
|
|
423
|
+
* Executes the next step in the capability chain.
|
|
424
|
+
*
|
|
425
|
+
* @param runner - A function that, when called, performs the actual
|
|
426
|
+
* network request (or the next capability in the chain) and resolves to
|
|
427
|
+
* a {@link Response}.
|
|
428
|
+
*
|
|
429
|
+
* @returns A promise resolving to the final {@link Response} that should be
|
|
430
|
+
* used by Aspi for this request.
|
|
431
|
+
*/
|
|
432
|
+
run: (runner: () => Promise<Response>) => Promise<Response>;
|
|
433
|
+
};
|
|
434
|
+
|
|
369
435
|
type Ok<T> = {
|
|
370
436
|
__tag: 'ok';
|
|
371
437
|
value: T;
|
|
@@ -768,7 +834,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
768
834
|
error: {};
|
|
769
835
|
}> {
|
|
770
836
|
#private;
|
|
771
|
-
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>);
|
|
837
|
+
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>, capabilities?: Capability<TRequest>[]);
|
|
772
838
|
/**
|
|
773
839
|
* Sets the base URL for the request.
|
|
774
840
|
* @param url The base URL to set
|
|
@@ -1441,6 +1507,49 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1441
1507
|
retryWhile: ((request: AspiRequest<TRequest>, response: AspiResponse) => boolean | Promise<boolean>) | undefined;
|
|
1442
1508
|
onRetry: ((request: AspiRequest<TRequest>, response: AspiResponse) => void) | undefined;
|
|
1443
1509
|
};
|
|
1510
|
+
/**
|
|
1511
|
+
* Registers a capability for this request.
|
|
1512
|
+
*
|
|
1513
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1514
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1515
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1516
|
+
* execution of the network call via its `run` method.
|
|
1517
|
+
*
|
|
1518
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1519
|
+
* call, the `Request` will:
|
|
1520
|
+
*
|
|
1521
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1522
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1523
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1524
|
+
* - log or trace requests,
|
|
1525
|
+
* - implement retry/refresh logic,
|
|
1526
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1527
|
+
*
|
|
1528
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1529
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1530
|
+
*
|
|
1531
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* ```ts
|
|
1535
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1536
|
+
*
|
|
1537
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1538
|
+
* async run(runner) {
|
|
1539
|
+
* console.log('→', request.path, request.requestInit);
|
|
1540
|
+
* const res = await runner();
|
|
1541
|
+
* console.log('←', res.status, res.statusText);
|
|
1542
|
+
* return res;
|
|
1543
|
+
* },
|
|
1544
|
+
* }));
|
|
1545
|
+
*
|
|
1546
|
+
* const result = await apiWithLogging
|
|
1547
|
+
* .get('/users')
|
|
1548
|
+
* .withResult()
|
|
1549
|
+
* .json();
|
|
1550
|
+
* ```
|
|
1551
|
+
*/
|
|
1552
|
+
useCapability(capability: Capability<TRequest>): number;
|
|
1444
1553
|
}
|
|
1445
1554
|
|
|
1446
1555
|
/**
|
|
@@ -1744,6 +1853,44 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1744
1853
|
withResult: true;
|
|
1745
1854
|
throwable: false;
|
|
1746
1855
|
}>>;
|
|
1856
|
+
/**
|
|
1857
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1858
|
+
*
|
|
1859
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1860
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1861
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1862
|
+
* and can:
|
|
1863
|
+
*
|
|
1864
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1865
|
+
* - inspect the raw {@link Response},
|
|
1866
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1867
|
+
* or token refresh, and
|
|
1868
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1869
|
+
*
|
|
1870
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1871
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1872
|
+
* They are applied in the order they are registered.
|
|
1873
|
+
*
|
|
1874
|
+
* @param capability - The capability factory to install on this client.
|
|
1875
|
+
*
|
|
1876
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* ```ts
|
|
1880
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1881
|
+
* .useCapability(({ request }) => ({
|
|
1882
|
+
* async run(runner) {
|
|
1883
|
+
* console.log('→', request.path);
|
|
1884
|
+
* const res = await runner();
|
|
1885
|
+
* console.log('←', res.status);
|
|
1886
|
+
* return res;
|
|
1887
|
+
* },
|
|
1888
|
+
* }));
|
|
1889
|
+
*
|
|
1890
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1891
|
+
* ```
|
|
1892
|
+
*/
|
|
1893
|
+
useCapability(capability: Capability<TRequest>): this;
|
|
1747
1894
|
}
|
|
1748
1895
|
|
|
1749
1896
|
export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, CustomError, type CustomErrorCb, type ErrorCallbacks, type HttpErrorCodes, type HttpErrorStatus, type HttpMethods, type JSONParseError, type Merge, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError };
|
package/dist/index.d.ts
CHANGED
|
@@ -366,6 +366,72 @@ interface JSONParseError extends CustomError<'jsonParseError', {
|
|
|
366
366
|
declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
|
|
367
367
|
declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Arguments passed to a capability factory.
|
|
371
|
+
*
|
|
372
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
373
|
+
*/
|
|
374
|
+
type CapabilityArgs<T extends AspiRequestInit = AspiRequestInit> = {
|
|
375
|
+
/**
|
|
376
|
+
* The fully constructed AspiRequest that will be used to execute `fetch`.
|
|
377
|
+
*
|
|
378
|
+
* You can:
|
|
379
|
+
* - inspect `request.path`, `request.requestInit`, `request.retryConfig`, etc.
|
|
380
|
+
* - mutate `request.requestInit.headers`, `signal`, or other properties
|
|
381
|
+
* before the network call is made.
|
|
382
|
+
*/
|
|
383
|
+
request: AspiRequest<T>;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* A capability is a small wrapper around the low-level `fetch` call that can
|
|
387
|
+
* intercept outgoing requests and incoming responses.
|
|
388
|
+
*
|
|
389
|
+
* It is a factory function that receives the current {@link AspiRequest}
|
|
390
|
+
* and returns an object exposing a single `run` method. The `run` method is
|
|
391
|
+
* responsible for invoking the provided `runner` (which performs the actual
|
|
392
|
+
* `fetch`) and may:
|
|
393
|
+
*
|
|
394
|
+
* - call `runner()` directly and return its result
|
|
395
|
+
* - call `runner()` multiple times (e.g. retry, refresh token then retry)
|
|
396
|
+
* - short‑circuit by *not* calling `runner()` and returning a synthetic
|
|
397
|
+
* {@link Response} instead
|
|
398
|
+
*
|
|
399
|
+
* Capabilities are composed in the order they are registered via
|
|
400
|
+
* `Aspi.useCapability`, with each capability wrapping the next one in the
|
|
401
|
+
* chain.
|
|
402
|
+
*
|
|
403
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* // Simple logging capability
|
|
408
|
+
* const loggingCapability: Capability = ({ request }) => ({
|
|
409
|
+
* async run(runner) {
|
|
410
|
+
* console.log('→', request.path, request.requestInit);
|
|
411
|
+
* const res = await runner();
|
|
412
|
+
* console.log('←', res.status, res.statusText);
|
|
413
|
+
* return res;
|
|
414
|
+
* },
|
|
415
|
+
* });
|
|
416
|
+
*
|
|
417
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
418
|
+
* .useCapability(loggingCapability);
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
type Capability<T extends AspiRequestInit = AspiRequestInit> = ({ request, }: CapabilityArgs<T>) => {
|
|
422
|
+
/**
|
|
423
|
+
* Executes the next step in the capability chain.
|
|
424
|
+
*
|
|
425
|
+
* @param runner - A function that, when called, performs the actual
|
|
426
|
+
* network request (or the next capability in the chain) and resolves to
|
|
427
|
+
* a {@link Response}.
|
|
428
|
+
*
|
|
429
|
+
* @returns A promise resolving to the final {@link Response} that should be
|
|
430
|
+
* used by Aspi for this request.
|
|
431
|
+
*/
|
|
432
|
+
run: (runner: () => Promise<Response>) => Promise<Response>;
|
|
433
|
+
};
|
|
434
|
+
|
|
369
435
|
type Ok<T> = {
|
|
370
436
|
__tag: 'ok';
|
|
371
437
|
value: T;
|
|
@@ -768,7 +834,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
768
834
|
error: {};
|
|
769
835
|
}> {
|
|
770
836
|
#private;
|
|
771
|
-
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>);
|
|
837
|
+
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>, capabilities?: Capability<TRequest>[]);
|
|
772
838
|
/**
|
|
773
839
|
* Sets the base URL for the request.
|
|
774
840
|
* @param url The base URL to set
|
|
@@ -1441,6 +1507,49 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1441
1507
|
retryWhile: ((request: AspiRequest<TRequest>, response: AspiResponse) => boolean | Promise<boolean>) | undefined;
|
|
1442
1508
|
onRetry: ((request: AspiRequest<TRequest>, response: AspiResponse) => void) | undefined;
|
|
1443
1509
|
};
|
|
1510
|
+
/**
|
|
1511
|
+
* Registers a capability for this request.
|
|
1512
|
+
*
|
|
1513
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1514
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1515
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1516
|
+
* execution of the network call via its `run` method.
|
|
1517
|
+
*
|
|
1518
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1519
|
+
* call, the `Request` will:
|
|
1520
|
+
*
|
|
1521
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1522
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1523
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1524
|
+
* - log or trace requests,
|
|
1525
|
+
* - implement retry/refresh logic,
|
|
1526
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1527
|
+
*
|
|
1528
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1529
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1530
|
+
*
|
|
1531
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* ```ts
|
|
1535
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1536
|
+
*
|
|
1537
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1538
|
+
* async run(runner) {
|
|
1539
|
+
* console.log('→', request.path, request.requestInit);
|
|
1540
|
+
* const res = await runner();
|
|
1541
|
+
* console.log('←', res.status, res.statusText);
|
|
1542
|
+
* return res;
|
|
1543
|
+
* },
|
|
1544
|
+
* }));
|
|
1545
|
+
*
|
|
1546
|
+
* const result = await apiWithLogging
|
|
1547
|
+
* .get('/users')
|
|
1548
|
+
* .withResult()
|
|
1549
|
+
* .json();
|
|
1550
|
+
* ```
|
|
1551
|
+
*/
|
|
1552
|
+
useCapability(capability: Capability<TRequest>): number;
|
|
1444
1553
|
}
|
|
1445
1554
|
|
|
1446
1555
|
/**
|
|
@@ -1744,6 +1853,44 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1744
1853
|
withResult: true;
|
|
1745
1854
|
throwable: false;
|
|
1746
1855
|
}>>;
|
|
1856
|
+
/**
|
|
1857
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1858
|
+
*
|
|
1859
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1860
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1861
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1862
|
+
* and can:
|
|
1863
|
+
*
|
|
1864
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1865
|
+
* - inspect the raw {@link Response},
|
|
1866
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1867
|
+
* or token refresh, and
|
|
1868
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1869
|
+
*
|
|
1870
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1871
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1872
|
+
* They are applied in the order they are registered.
|
|
1873
|
+
*
|
|
1874
|
+
* @param capability - The capability factory to install on this client.
|
|
1875
|
+
*
|
|
1876
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* ```ts
|
|
1880
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1881
|
+
* .useCapability(({ request }) => ({
|
|
1882
|
+
* async run(runner) {
|
|
1883
|
+
* console.log('→', request.path);
|
|
1884
|
+
* const res = await runner();
|
|
1885
|
+
* console.log('←', res.status);
|
|
1886
|
+
* return res;
|
|
1887
|
+
* },
|
|
1888
|
+
* }));
|
|
1889
|
+
*
|
|
1890
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1891
|
+
* ```
|
|
1892
|
+
*/
|
|
1893
|
+
useCapability(capability: Capability<TRequest>): this;
|
|
1747
1894
|
}
|
|
1748
1895
|
|
|
1749
1896
|
export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, CustomError, type CustomErrorCb, type ErrorCallbacks, type HttpErrorCodes, type HttpErrorStatus, type HttpMethods, type JSONParseError, type Merge, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError };
|
package/dist/index.js
CHANGED
|
@@ -287,7 +287,8 @@ var Request = class {
|
|
|
287
287
|
#shouldBeResult = false;
|
|
288
288
|
#bodySchemaIssues = [];
|
|
289
289
|
#throwOnError = false;
|
|
290
|
-
|
|
290
|
+
#capabilities = [];
|
|
291
|
+
constructor(method, path, requestOptions, capabilities = []) {
|
|
291
292
|
this.#path = path;
|
|
292
293
|
this.#middlewares = requestOptions.middlewares || [];
|
|
293
294
|
this.#localRequestInit = {
|
|
@@ -298,6 +299,7 @@ var Request = class {
|
|
|
298
299
|
this.#customErrorCbs = { ...requestOptions?.errorCbs || {} };
|
|
299
300
|
this.#throwOnError = requestOptions.throwOnError || false;
|
|
300
301
|
this.#shouldBeResult = requestOptions.shouldBeResult || false;
|
|
302
|
+
this.#capabilities = [...capabilities];
|
|
301
303
|
}
|
|
302
304
|
/**
|
|
303
305
|
* Sets the base URL for the request.
|
|
@@ -1048,7 +1050,15 @@ var Request = class {
|
|
|
1048
1050
|
let responseData = null;
|
|
1049
1051
|
while (attempts <= retries) {
|
|
1050
1052
|
try {
|
|
1051
|
-
|
|
1053
|
+
if (this.#capabilities.length > 0) {
|
|
1054
|
+
for (const capability of this.#capabilities) {
|
|
1055
|
+
response = await capability({ request }).run(
|
|
1056
|
+
() => fetch(url, requestInit)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
} else {
|
|
1060
|
+
response = await fetch(url, requestInit);
|
|
1061
|
+
}
|
|
1052
1062
|
responseData = await responseParser(response);
|
|
1053
1063
|
if (responseData instanceof Error) {
|
|
1054
1064
|
return err(responseData);
|
|
@@ -1308,6 +1318,51 @@ var Request = class {
|
|
|
1308
1318
|
const cfg = this.#sanitisedRetryConfig();
|
|
1309
1319
|
return { ...cfg };
|
|
1310
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Registers a capability for this request.
|
|
1323
|
+
*
|
|
1324
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1325
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1326
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1327
|
+
* execution of the network call via its `run` method.
|
|
1328
|
+
*
|
|
1329
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1330
|
+
* call, the `Request` will:
|
|
1331
|
+
*
|
|
1332
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1333
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1334
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1335
|
+
* - log or trace requests,
|
|
1336
|
+
* - implement retry/refresh logic,
|
|
1337
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1338
|
+
*
|
|
1339
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1340
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1341
|
+
*
|
|
1342
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1347
|
+
*
|
|
1348
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1349
|
+
* async run(runner) {
|
|
1350
|
+
* console.log('→', request.path, request.requestInit);
|
|
1351
|
+
* const res = await runner();
|
|
1352
|
+
* console.log('←', res.status, res.statusText);
|
|
1353
|
+
* return res;
|
|
1354
|
+
* },
|
|
1355
|
+
* }));
|
|
1356
|
+
*
|
|
1357
|
+
* const result = await apiWithLogging
|
|
1358
|
+
* .get('/users')
|
|
1359
|
+
* .withResult()
|
|
1360
|
+
* .json();
|
|
1361
|
+
* ```
|
|
1362
|
+
*/
|
|
1363
|
+
useCapability(capability) {
|
|
1364
|
+
return this.#capabilities.push(capability);
|
|
1365
|
+
}
|
|
1311
1366
|
};
|
|
1312
1367
|
|
|
1313
1368
|
// src/aspi.ts
|
|
@@ -1318,6 +1373,7 @@ var Aspi = class {
|
|
|
1318
1373
|
#customErrorCbs = {};
|
|
1319
1374
|
#throwOnError = false;
|
|
1320
1375
|
#shouldBeResult = false;
|
|
1376
|
+
#capabilities = [];
|
|
1321
1377
|
constructor(config) {
|
|
1322
1378
|
const { retryConfig, ...requestInit } = config;
|
|
1323
1379
|
this.#globalRequestInit = requestInit;
|
|
@@ -1360,17 +1416,22 @@ var Aspi = class {
|
|
|
1360
1416
|
return this;
|
|
1361
1417
|
}
|
|
1362
1418
|
#createRequest(method, path) {
|
|
1363
|
-
return new Request(
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1419
|
+
return new Request(
|
|
1420
|
+
method,
|
|
1421
|
+
path,
|
|
1422
|
+
{
|
|
1423
|
+
requestConfig: {
|
|
1424
|
+
...this.#globalRequestInit,
|
|
1425
|
+
method
|
|
1426
|
+
},
|
|
1427
|
+
middlewares: this.#middlewares,
|
|
1428
|
+
errorCbs: this.#customErrorCbs,
|
|
1429
|
+
throwOnError: this.#throwOnError,
|
|
1430
|
+
shouldBeResult: this.#shouldBeResult,
|
|
1431
|
+
retryConfig: this.#retryConfig
|
|
1367
1432
|
},
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
throwOnError: this.#throwOnError,
|
|
1371
|
-
shouldBeResult: this.#shouldBeResult,
|
|
1372
|
-
retryConfig: this.#retryConfig
|
|
1373
|
-
});
|
|
1433
|
+
this.#capabilities
|
|
1434
|
+
);
|
|
1374
1435
|
}
|
|
1375
1436
|
/**
|
|
1376
1437
|
* Makes a GET request to the specified path
|
|
@@ -1665,6 +1726,47 @@ var Aspi = class {
|
|
|
1665
1726
|
this.#throwOnError = false;
|
|
1666
1727
|
return this;
|
|
1667
1728
|
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1731
|
+
*
|
|
1732
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1733
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1734
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1735
|
+
* and can:
|
|
1736
|
+
*
|
|
1737
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1738
|
+
* - inspect the raw {@link Response},
|
|
1739
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1740
|
+
* or token refresh, and
|
|
1741
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1742
|
+
*
|
|
1743
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1744
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1745
|
+
* They are applied in the order they are registered.
|
|
1746
|
+
*
|
|
1747
|
+
* @param capability - The capability factory to install on this client.
|
|
1748
|
+
*
|
|
1749
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```ts
|
|
1753
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1754
|
+
* .useCapability(({ request }) => ({
|
|
1755
|
+
* async run(runner) {
|
|
1756
|
+
* console.log('→', request.path);
|
|
1757
|
+
* const res = await runner();
|
|
1758
|
+
* console.log('←', res.status);
|
|
1759
|
+
* return res;
|
|
1760
|
+
* },
|
|
1761
|
+
* }));
|
|
1762
|
+
*
|
|
1763
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1764
|
+
* ```
|
|
1765
|
+
*/
|
|
1766
|
+
useCapability(capability) {
|
|
1767
|
+
this.#capabilities.push(capability);
|
|
1768
|
+
return this;
|
|
1769
|
+
}
|
|
1668
1770
|
};
|
|
1669
1771
|
export {
|
|
1670
1772
|
Aspi,
|
package/package.json
CHANGED