aspi 2.3.0 → 2.5.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/dist/index.cjs CHANGED
@@ -284,6 +284,8 @@ function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
284
284
  return bc(ab(a));
285
285
  case 4:
286
286
  return cd(bc(ab(a)));
287
+ case 5:
288
+ return de(cd(bc(ab(a))));
287
289
  case 6:
288
290
  return ef(de(cd(bc(ab(a)))));
289
291
  case 7:
@@ -312,6 +314,7 @@ var Request = class {
312
314
  #schema = null;
313
315
  #bodySchema = null;
314
316
  #retryConfig;
317
+ #timeoutMs;
315
318
  #shouldBeResult = false;
316
319
  #bodySchemaIssues = [];
317
320
  #throwOnError = false;
@@ -361,6 +364,24 @@ var Request = class {
361
364
  };
362
365
  return this;
363
366
  }
367
+ /**
368
+ * Sets a timeout for the request in milliseconds.
369
+ *
370
+ * When the timeout expires, the request is aborted with an `AbortError`.
371
+ * If a signal was already provided, the timeout is chained so that either
372
+ * the external signal or the timeout can abort the request.
373
+ *
374
+ * @param {number} ms - Timeout duration in milliseconds.
375
+ * @returns {this} The current {@link Request} instance for method chaining.
376
+ *
377
+ * @example
378
+ * const request = new Request('/slow-endpoint', config);
379
+ * request.timeout(5000); // abort after 5 seconds
380
+ */
381
+ timeout(ms) {
382
+ this.#timeoutMs = ms;
383
+ return this;
384
+ }
364
385
  /**
365
386
  * Merges the provided headers into the request configuration.
366
387
  *
@@ -944,6 +965,92 @@ var Request = class {
944
965
  const output = await this.#makeRequest((response) => response.blob());
945
966
  return this.#mapResponse(output);
946
967
  }
968
+ /**
969
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
970
+ *
971
+ * The shape of the returned {@link Promise} depends on the request mode:
972
+ *
973
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
974
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
975
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
976
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
977
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
978
+ * is `null`.
979
+ *
980
+ * @returns {Promise<
981
+ * Opts['withResult'] extends true
982
+ * ? Result.Result<
983
+ * AspiResultOk<TRequest, ArrayBuffer>,
984
+ * | AspiError<TRequest>
985
+ * | (Opts extends { error: any }
986
+ * ? Opts['error'][keyof Opts['error']]
987
+ * : never)
988
+ * >
989
+ * : Opts['throwable'] extends true
990
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
991
+ * : [
992
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
993
+ * (
994
+ * | (
995
+ * | AspiError<TRequest>
996
+ * | (Opts extends { error: any }
997
+ * ? Opts['error'][keyof Opts['error']]
998
+ * : never)
999
+ * )
1000
+ * | null
1001
+ * ),
1002
+ * ]
1003
+ * >}
1004
+ */
1005
+ async arrayBuffer() {
1006
+ const output = await this.#makeRequest(
1007
+ (response) => response.arrayBuffer()
1008
+ );
1009
+ return this.#mapResponse(output);
1010
+ }
1011
+ /**
1012
+ * Executes the request and returns the response body as {@link FormData}.
1013
+ *
1014
+ * The shape of the returned {@link Promise} depends on the request mode:
1015
+ *
1016
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1017
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1018
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1019
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1020
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1021
+ * is `null`.
1022
+ *
1023
+ * @returns {Promise<
1024
+ * Opts['withResult'] extends true
1025
+ * ? Result.Result<
1026
+ * AspiResultOk<TRequest, FormData>,
1027
+ * | AspiError<TRequest>
1028
+ * | (Opts extends { error: any }
1029
+ * ? Opts['error'][keyof Opts['error']]
1030
+ * : never)
1031
+ * >
1032
+ * : Opts['throwable'] extends true
1033
+ * ? AspiPlainResponse<TRequest, FormData>
1034
+ * : [
1035
+ * AspiResultOk<TRequest, FormData> | null,
1036
+ * (
1037
+ * | (
1038
+ * | AspiError<TRequest>
1039
+ * | (Opts extends { error: any }
1040
+ * ? Opts['error'][keyof Opts['error']]
1041
+ * : never)
1042
+ * )
1043
+ * | null
1044
+ * ),
1045
+ * ]
1046
+ * >}
1047
+ */
1048
+ async formData() {
1049
+ const output = await this.#makeRequest(
1050
+ (response) => response.formData()
1051
+ );
1052
+ return this.#mapResponse(output);
1053
+ }
947
1054
  #url() {
948
1055
  if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
949
1056
  const absolute = new URL(this.#path);
@@ -961,7 +1068,6 @@ var Request = class {
961
1068
  let path = rawPath || "";
962
1069
  path = path.replace(/^\/+/, "");
963
1070
  path = path.replace(/\/{2,}/g, "/");
964
- path = path.replace(/\/+$/, "");
965
1071
  if (path) {
966
1072
  path = "/" + path.replace(/^\/+/, "");
967
1073
  }
@@ -979,7 +1085,6 @@ var Request = class {
979
1085
  if (fragment) {
980
1086
  url += `#${fragment}`;
981
1087
  }
982
- url = url.replace(/\/+$/, "");
983
1088
  return url;
984
1089
  }
985
1090
  /**
@@ -1077,16 +1182,32 @@ var Request = class {
1077
1182
  });
1078
1183
  let responseData = null;
1079
1184
  while (attempts <= retries) {
1185
+ let timeoutTimer;
1080
1186
  try {
1081
- if (this.#capabilities.length > 0) {
1082
- for (const capability of this.#capabilities) {
1083
- response = await capability({ request }).run(
1084
- () => fetch(url, requestInit)
1187
+ const attemptInit = { ...requestInit };
1188
+ if (this.#timeoutMs && this.#timeoutMs > 0) {
1189
+ const timeoutController = new AbortController();
1190
+ timeoutTimer = setTimeout(
1191
+ () => timeoutController.abort(),
1192
+ this.#timeoutMs
1193
+ );
1194
+ if (attemptInit.signal) {
1195
+ attemptInit.signal.addEventListener(
1196
+ "abort",
1197
+ () => timeoutController.abort(),
1198
+ { once: true }
1085
1199
  );
1086
1200
  }
1087
- } else {
1088
- response = await fetch(url, requestInit);
1201
+ attemptInit.signal = timeoutController.signal;
1202
+ }
1203
+ let runner = () => fetch(url, attemptInit);
1204
+ for (let i = this.#capabilities.length - 1; i >= 0; i--) {
1205
+ const cap = this.#capabilities[i];
1206
+ const next = runner;
1207
+ runner = () => cap({ request }).run(next);
1089
1208
  }
1209
+ response = await runner();
1210
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1090
1211
  responseData = await responseParser(response);
1091
1212
  if (responseData instanceof Error) {
1092
1213
  return err(responseData);
@@ -1098,7 +1219,7 @@ var Request = class {
1098
1219
  if (this.#isSuccessResponse(response) || !retryOn.includes(response.status) && !retryWhileCondition) {
1099
1220
  break;
1100
1221
  }
1101
- if (response.status in this.#customErrorCbs && attempts === retries) {
1222
+ if (response.status in this.#customErrorCbs) {
1102
1223
  const result = this.#customErrorCbs[response.status].cb({
1103
1224
  request,
1104
1225
  response: this.#makeResponse(response, responseData)
@@ -1121,6 +1242,7 @@ var Request = class {
1121
1242
  await this.#abortDelay(delay, request);
1122
1243
  }
1123
1244
  } catch (e) {
1245
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1124
1246
  if (e instanceof Error && e.name === "AbortError") {
1125
1247
  if (500 in this.#customErrorCbs) {
1126
1248
  const result = this.#customErrorCbs[response.status].cb({
@@ -1389,7 +1511,8 @@ var Request = class {
1389
1511
  * ```
1390
1512
  */
1391
1513
  useCapability(capability) {
1392
- return this.#capabilities.push(capability);
1514
+ this.#capabilities.push(capability);
1515
+ return this;
1393
1516
  }
1394
1517
  };
1395
1518
 
package/dist/index.d.cts CHANGED
@@ -858,6 +858,21 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
858
858
  * });
859
859
  */
860
860
  setRetry(retry: AspiRetryConfig<TRequest>): this;
861
+ /**
862
+ * Sets a timeout for the request in milliseconds.
863
+ *
864
+ * When the timeout expires, the request is aborted with an `AbortError`.
865
+ * If a signal was already provided, the timeout is chained so that either
866
+ * the external signal or the timeout can abort the request.
867
+ *
868
+ * @param {number} ms - Timeout duration in milliseconds.
869
+ * @returns {this} The current {@link Request} instance for method chaining.
870
+ *
871
+ * @example
872
+ * const request = new Request('/slow-endpoint', config);
873
+ * request.timeout(5000); // abort after 5 seconds
874
+ */
875
+ timeout(ms: number): this;
861
876
  /**
862
877
  * Merges the provided headers into the request configuration.
863
878
  *
@@ -1390,6 +1405,96 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1390
1405
  error: any;
1391
1406
  } ? Opts['error'][keyof Opts['error']] : never)) | null)
1392
1407
  ]>;
1408
+ /**
1409
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
1410
+ *
1411
+ * The shape of the returned {@link Promise} depends on the request mode:
1412
+ *
1413
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1414
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
1415
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
1416
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1417
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1418
+ * is `null`.
1419
+ *
1420
+ * @returns {Promise<
1421
+ * Opts['withResult'] extends true
1422
+ * ? Result.Result<
1423
+ * AspiResultOk<TRequest, ArrayBuffer>,
1424
+ * | AspiError<TRequest>
1425
+ * | (Opts extends { error: any }
1426
+ * ? Opts['error'][keyof Opts['error']]
1427
+ * : never)
1428
+ * >
1429
+ * : Opts['throwable'] extends true
1430
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
1431
+ * : [
1432
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
1433
+ * (
1434
+ * | (
1435
+ * | AspiError<TRequest>
1436
+ * | (Opts extends { error: any }
1437
+ * ? Opts['error'][keyof Opts['error']]
1438
+ * : never)
1439
+ * )
1440
+ * | null
1441
+ * ),
1442
+ * ]
1443
+ * >}
1444
+ */
1445
+ arrayBuffer(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, ArrayBuffer>, AspiError<TRequest> | (Opts extends {
1446
+ error: any;
1447
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, ArrayBuffer> : [
1448
+ AspiResultOk<TRequest, ArrayBuffer> | null,
1449
+ ((AspiError<TRequest> | (Opts extends {
1450
+ error: any;
1451
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1452
+ ]>;
1453
+ /**
1454
+ * Executes the request and returns the response body as {@link FormData}.
1455
+ *
1456
+ * The shape of the returned {@link Promise} depends on the request mode:
1457
+ *
1458
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1459
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1460
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1461
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1462
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1463
+ * is `null`.
1464
+ *
1465
+ * @returns {Promise<
1466
+ * Opts['withResult'] extends true
1467
+ * ? Result.Result<
1468
+ * AspiResultOk<TRequest, FormData>,
1469
+ * | AspiError<TRequest>
1470
+ * | (Opts extends { error: any }
1471
+ * ? Opts['error'][keyof Opts['error']]
1472
+ * : never)
1473
+ * >
1474
+ * : Opts['throwable'] extends true
1475
+ * ? AspiPlainResponse<TRequest, FormData>
1476
+ * : [
1477
+ * AspiResultOk<TRequest, FormData> | null,
1478
+ * (
1479
+ * | (
1480
+ * | AspiError<TRequest>
1481
+ * | (Opts extends { error: any }
1482
+ * ? Opts['error'][keyof Opts['error']]
1483
+ * : never)
1484
+ * )
1485
+ * | null
1486
+ * ),
1487
+ * ]
1488
+ * >}
1489
+ */
1490
+ formData(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, FormData>, AspiError<TRequest> | (Opts extends {
1491
+ error: any;
1492
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, FormData> : [
1493
+ AspiResultOk<TRequest, FormData> | null,
1494
+ ((AspiError<TRequest> | (Opts extends {
1495
+ error: any;
1496
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1497
+ ]>;
1393
1498
  /**
1394
1499
  * Returns the fully‑qualified URL that will be used for the request.
1395
1500
  *
@@ -1549,7 +1654,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1549
1654
  * .json();
1550
1655
  * ```
1551
1656
  */
1552
- useCapability(capability: Capability<TRequest>): number;
1657
+ useCapability(capability: Capability<TRequest>): this;
1553
1658
  }
1554
1659
 
1555
1660
  /**
@@ -1893,4 +1998,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1893
1998
  useCapability(capability: Capability<TRequest>): this;
1894
1999
  }
1895
2000
 
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 };
2001
+ export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, type Capability, type CapabilityArgs, 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
@@ -858,6 +858,21 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
858
858
  * });
859
859
  */
860
860
  setRetry(retry: AspiRetryConfig<TRequest>): this;
861
+ /**
862
+ * Sets a timeout for the request in milliseconds.
863
+ *
864
+ * When the timeout expires, the request is aborted with an `AbortError`.
865
+ * If a signal was already provided, the timeout is chained so that either
866
+ * the external signal or the timeout can abort the request.
867
+ *
868
+ * @param {number} ms - Timeout duration in milliseconds.
869
+ * @returns {this} The current {@link Request} instance for method chaining.
870
+ *
871
+ * @example
872
+ * const request = new Request('/slow-endpoint', config);
873
+ * request.timeout(5000); // abort after 5 seconds
874
+ */
875
+ timeout(ms: number): this;
861
876
  /**
862
877
  * Merges the provided headers into the request configuration.
863
878
  *
@@ -1390,6 +1405,96 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1390
1405
  error: any;
1391
1406
  } ? Opts['error'][keyof Opts['error']] : never)) | null)
1392
1407
  ]>;
1408
+ /**
1409
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
1410
+ *
1411
+ * The shape of the returned {@link Promise} depends on the request mode:
1412
+ *
1413
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1414
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
1415
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
1416
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1417
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1418
+ * is `null`.
1419
+ *
1420
+ * @returns {Promise<
1421
+ * Opts['withResult'] extends true
1422
+ * ? Result.Result<
1423
+ * AspiResultOk<TRequest, ArrayBuffer>,
1424
+ * | AspiError<TRequest>
1425
+ * | (Opts extends { error: any }
1426
+ * ? Opts['error'][keyof Opts['error']]
1427
+ * : never)
1428
+ * >
1429
+ * : Opts['throwable'] extends true
1430
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
1431
+ * : [
1432
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
1433
+ * (
1434
+ * | (
1435
+ * | AspiError<TRequest>
1436
+ * | (Opts extends { error: any }
1437
+ * ? Opts['error'][keyof Opts['error']]
1438
+ * : never)
1439
+ * )
1440
+ * | null
1441
+ * ),
1442
+ * ]
1443
+ * >}
1444
+ */
1445
+ arrayBuffer(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, ArrayBuffer>, AspiError<TRequest> | (Opts extends {
1446
+ error: any;
1447
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, ArrayBuffer> : [
1448
+ AspiResultOk<TRequest, ArrayBuffer> | null,
1449
+ ((AspiError<TRequest> | (Opts extends {
1450
+ error: any;
1451
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1452
+ ]>;
1453
+ /**
1454
+ * Executes the request and returns the response body as {@link FormData}.
1455
+ *
1456
+ * The shape of the returned {@link Promise} depends on the request mode:
1457
+ *
1458
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1459
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1460
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1461
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1462
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1463
+ * is `null`.
1464
+ *
1465
+ * @returns {Promise<
1466
+ * Opts['withResult'] extends true
1467
+ * ? Result.Result<
1468
+ * AspiResultOk<TRequest, FormData>,
1469
+ * | AspiError<TRequest>
1470
+ * | (Opts extends { error: any }
1471
+ * ? Opts['error'][keyof Opts['error']]
1472
+ * : never)
1473
+ * >
1474
+ * : Opts['throwable'] extends true
1475
+ * ? AspiPlainResponse<TRequest, FormData>
1476
+ * : [
1477
+ * AspiResultOk<TRequest, FormData> | null,
1478
+ * (
1479
+ * | (
1480
+ * | AspiError<TRequest>
1481
+ * | (Opts extends { error: any }
1482
+ * ? Opts['error'][keyof Opts['error']]
1483
+ * : never)
1484
+ * )
1485
+ * | null
1486
+ * ),
1487
+ * ]
1488
+ * >}
1489
+ */
1490
+ formData(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, FormData>, AspiError<TRequest> | (Opts extends {
1491
+ error: any;
1492
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, FormData> : [
1493
+ AspiResultOk<TRequest, FormData> | null,
1494
+ ((AspiError<TRequest> | (Opts extends {
1495
+ error: any;
1496
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1497
+ ]>;
1393
1498
  /**
1394
1499
  * Returns the fully‑qualified URL that will be used for the request.
1395
1500
  *
@@ -1549,7 +1654,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1549
1654
  * .json();
1550
1655
  * ```
1551
1656
  */
1552
- useCapability(capability: Capability<TRequest>): number;
1657
+ useCapability(capability: Capability<TRequest>): this;
1553
1658
  }
1554
1659
 
1555
1660
  /**
@@ -1893,4 +1998,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1893
1998
  useCapability(capability: Capability<TRequest>): this;
1894
1999
  }
1895
2000
 
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 };
2001
+ export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, type Capability, type CapabilityArgs, 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
@@ -256,6 +256,8 @@ function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
256
256
  return bc(ab(a));
257
257
  case 4:
258
258
  return cd(bc(ab(a)));
259
+ case 5:
260
+ return de(cd(bc(ab(a))));
259
261
  case 6:
260
262
  return ef(de(cd(bc(ab(a)))));
261
263
  case 7:
@@ -284,6 +286,7 @@ var Request = class {
284
286
  #schema = null;
285
287
  #bodySchema = null;
286
288
  #retryConfig;
289
+ #timeoutMs;
287
290
  #shouldBeResult = false;
288
291
  #bodySchemaIssues = [];
289
292
  #throwOnError = false;
@@ -333,6 +336,24 @@ var Request = class {
333
336
  };
334
337
  return this;
335
338
  }
339
+ /**
340
+ * Sets a timeout for the request in milliseconds.
341
+ *
342
+ * When the timeout expires, the request is aborted with an `AbortError`.
343
+ * If a signal was already provided, the timeout is chained so that either
344
+ * the external signal or the timeout can abort the request.
345
+ *
346
+ * @param {number} ms - Timeout duration in milliseconds.
347
+ * @returns {this} The current {@link Request} instance for method chaining.
348
+ *
349
+ * @example
350
+ * const request = new Request('/slow-endpoint', config);
351
+ * request.timeout(5000); // abort after 5 seconds
352
+ */
353
+ timeout(ms) {
354
+ this.#timeoutMs = ms;
355
+ return this;
356
+ }
336
357
  /**
337
358
  * Merges the provided headers into the request configuration.
338
359
  *
@@ -916,6 +937,92 @@ var Request = class {
916
937
  const output = await this.#makeRequest((response) => response.blob());
917
938
  return this.#mapResponse(output);
918
939
  }
940
+ /**
941
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
942
+ *
943
+ * The shape of the returned {@link Promise} depends on the request mode:
944
+ *
945
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
946
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
947
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
948
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
949
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
950
+ * is `null`.
951
+ *
952
+ * @returns {Promise<
953
+ * Opts['withResult'] extends true
954
+ * ? Result.Result<
955
+ * AspiResultOk<TRequest, ArrayBuffer>,
956
+ * | AspiError<TRequest>
957
+ * | (Opts extends { error: any }
958
+ * ? Opts['error'][keyof Opts['error']]
959
+ * : never)
960
+ * >
961
+ * : Opts['throwable'] extends true
962
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
963
+ * : [
964
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
965
+ * (
966
+ * | (
967
+ * | AspiError<TRequest>
968
+ * | (Opts extends { error: any }
969
+ * ? Opts['error'][keyof Opts['error']]
970
+ * : never)
971
+ * )
972
+ * | null
973
+ * ),
974
+ * ]
975
+ * >}
976
+ */
977
+ async arrayBuffer() {
978
+ const output = await this.#makeRequest(
979
+ (response) => response.arrayBuffer()
980
+ );
981
+ return this.#mapResponse(output);
982
+ }
983
+ /**
984
+ * Executes the request and returns the response body as {@link FormData}.
985
+ *
986
+ * The shape of the returned {@link Promise} depends on the request mode:
987
+ *
988
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
989
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
990
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
991
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
992
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
993
+ * is `null`.
994
+ *
995
+ * @returns {Promise<
996
+ * Opts['withResult'] extends true
997
+ * ? Result.Result<
998
+ * AspiResultOk<TRequest, FormData>,
999
+ * | AspiError<TRequest>
1000
+ * | (Opts extends { error: any }
1001
+ * ? Opts['error'][keyof Opts['error']]
1002
+ * : never)
1003
+ * >
1004
+ * : Opts['throwable'] extends true
1005
+ * ? AspiPlainResponse<TRequest, FormData>
1006
+ * : [
1007
+ * AspiResultOk<TRequest, FormData> | null,
1008
+ * (
1009
+ * | (
1010
+ * | AspiError<TRequest>
1011
+ * | (Opts extends { error: any }
1012
+ * ? Opts['error'][keyof Opts['error']]
1013
+ * : never)
1014
+ * )
1015
+ * | null
1016
+ * ),
1017
+ * ]
1018
+ * >}
1019
+ */
1020
+ async formData() {
1021
+ const output = await this.#makeRequest(
1022
+ (response) => response.formData()
1023
+ );
1024
+ return this.#mapResponse(output);
1025
+ }
919
1026
  #url() {
920
1027
  if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
921
1028
  const absolute = new URL(this.#path);
@@ -933,7 +1040,6 @@ var Request = class {
933
1040
  let path = rawPath || "";
934
1041
  path = path.replace(/^\/+/, "");
935
1042
  path = path.replace(/\/{2,}/g, "/");
936
- path = path.replace(/\/+$/, "");
937
1043
  if (path) {
938
1044
  path = "/" + path.replace(/^\/+/, "");
939
1045
  }
@@ -951,7 +1057,6 @@ var Request = class {
951
1057
  if (fragment) {
952
1058
  url += `#${fragment}`;
953
1059
  }
954
- url = url.replace(/\/+$/, "");
955
1060
  return url;
956
1061
  }
957
1062
  /**
@@ -1049,16 +1154,32 @@ var Request = class {
1049
1154
  });
1050
1155
  let responseData = null;
1051
1156
  while (attempts <= retries) {
1157
+ let timeoutTimer;
1052
1158
  try {
1053
- if (this.#capabilities.length > 0) {
1054
- for (const capability of this.#capabilities) {
1055
- response = await capability({ request }).run(
1056
- () => fetch(url, requestInit)
1159
+ const attemptInit = { ...requestInit };
1160
+ if (this.#timeoutMs && this.#timeoutMs > 0) {
1161
+ const timeoutController = new AbortController();
1162
+ timeoutTimer = setTimeout(
1163
+ () => timeoutController.abort(),
1164
+ this.#timeoutMs
1165
+ );
1166
+ if (attemptInit.signal) {
1167
+ attemptInit.signal.addEventListener(
1168
+ "abort",
1169
+ () => timeoutController.abort(),
1170
+ { once: true }
1057
1171
  );
1058
1172
  }
1059
- } else {
1060
- response = await fetch(url, requestInit);
1173
+ attemptInit.signal = timeoutController.signal;
1174
+ }
1175
+ let runner = () => fetch(url, attemptInit);
1176
+ for (let i = this.#capabilities.length - 1; i >= 0; i--) {
1177
+ const cap = this.#capabilities[i];
1178
+ const next = runner;
1179
+ runner = () => cap({ request }).run(next);
1061
1180
  }
1181
+ response = await runner();
1182
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1062
1183
  responseData = await responseParser(response);
1063
1184
  if (responseData instanceof Error) {
1064
1185
  return err(responseData);
@@ -1070,7 +1191,7 @@ var Request = class {
1070
1191
  if (this.#isSuccessResponse(response) || !retryOn.includes(response.status) && !retryWhileCondition) {
1071
1192
  break;
1072
1193
  }
1073
- if (response.status in this.#customErrorCbs && attempts === retries) {
1194
+ if (response.status in this.#customErrorCbs) {
1074
1195
  const result = this.#customErrorCbs[response.status].cb({
1075
1196
  request,
1076
1197
  response: this.#makeResponse(response, responseData)
@@ -1093,6 +1214,7 @@ var Request = class {
1093
1214
  await this.#abortDelay(delay, request);
1094
1215
  }
1095
1216
  } catch (e) {
1217
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1096
1218
  if (e instanceof Error && e.name === "AbortError") {
1097
1219
  if (500 in this.#customErrorCbs) {
1098
1220
  const result = this.#customErrorCbs[response.status].cb({
@@ -1361,7 +1483,8 @@ var Request = class {
1361
1483
  * ```
1362
1484
  */
1363
1485
  useCapability(capability) {
1364
- return this.#capabilities.push(capability);
1486
+ this.#capabilities.push(capability);
1487
+ return this;
1365
1488
  }
1366
1489
  };
1367
1490
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aspi",
3
3
  "description": "Rest API client for typescript projects with chain of responsibility design pattern.",
4
- "version": "2.3.0",
4
+ "version": "2.5.0",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {