aspi 2.4.0 → 2.6.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
@@ -28,7 +28,9 @@ __export(index_exports, {
28
28
  getHttpErrorStatus: () => getHttpErrorStatus,
29
29
  httpErrors: () => httpErrors,
30
30
  isAspiError: () => isAspiError,
31
- isCustomError: () => isCustomError
31
+ isCustomError: () => isCustomError,
32
+ isJSONParseError: () => isJSONParseError,
33
+ isParseError: () => isParseError
32
34
  });
33
35
  module.exports = __toCommonJS(index_exports);
34
36
 
@@ -108,6 +110,12 @@ var isAspiError = (error) => {
108
110
  var isCustomError = (error) => {
109
111
  return error instanceof CustomError;
110
112
  };
113
+ var isParseError = (error) => {
114
+ return error instanceof CustomError && error.tag === "parseError";
115
+ };
116
+ var isJSONParseError = (error) => {
117
+ return error instanceof CustomError && error.tag === "jsonParseError";
118
+ };
111
119
 
112
120
  // src/result.ts
113
121
  var result_exports = {};
@@ -284,6 +292,8 @@ function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
284
292
  return bc(ab(a));
285
293
  case 4:
286
294
  return cd(bc(ab(a)));
295
+ case 5:
296
+ return de(cd(bc(ab(a))));
287
297
  case 6:
288
298
  return ef(de(cd(bc(ab(a)))));
289
299
  case 7:
@@ -312,6 +322,7 @@ var Request = class {
312
322
  #schema = null;
313
323
  #bodySchema = null;
314
324
  #retryConfig;
325
+ #timeoutMs;
315
326
  #shouldBeResult = false;
316
327
  #bodySchemaIssues = [];
317
328
  #throwOnError = false;
@@ -361,6 +372,24 @@ var Request = class {
361
372
  };
362
373
  return this;
363
374
  }
375
+ /**
376
+ * Sets a timeout for the request in milliseconds.
377
+ *
378
+ * When the timeout expires, the request is aborted with an `AbortError`.
379
+ * If a signal was already provided, the timeout is chained so that either
380
+ * the external signal or the timeout can abort the request.
381
+ *
382
+ * @param {number} ms - Timeout duration in milliseconds.
383
+ * @returns {this} The current {@link Request} instance for method chaining.
384
+ *
385
+ * @example
386
+ * const request = new Request('/slow-endpoint', config);
387
+ * request.timeout(5000); // abort after 5 seconds
388
+ */
389
+ timeout(ms) {
390
+ this.#timeoutMs = ms;
391
+ return this;
392
+ }
364
393
  /**
365
394
  * Merges the provided headers into the request configuration.
366
395
  *
@@ -944,6 +973,92 @@ var Request = class {
944
973
  const output = await this.#makeRequest((response) => response.blob());
945
974
  return this.#mapResponse(output);
946
975
  }
976
+ /**
977
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
978
+ *
979
+ * The shape of the returned {@link Promise} depends on the request mode:
980
+ *
981
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
982
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
983
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
984
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
985
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
986
+ * is `null`.
987
+ *
988
+ * @returns {Promise<
989
+ * Opts['withResult'] extends true
990
+ * ? Result.Result<
991
+ * AspiResultOk<TRequest, ArrayBuffer>,
992
+ * | AspiError<TRequest>
993
+ * | (Opts extends { error: any }
994
+ * ? Opts['error'][keyof Opts['error']]
995
+ * : never)
996
+ * >
997
+ * : Opts['throwable'] extends true
998
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
999
+ * : [
1000
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
1001
+ * (
1002
+ * | (
1003
+ * | AspiError<TRequest>
1004
+ * | (Opts extends { error: any }
1005
+ * ? Opts['error'][keyof Opts['error']]
1006
+ * : never)
1007
+ * )
1008
+ * | null
1009
+ * ),
1010
+ * ]
1011
+ * >}
1012
+ */
1013
+ async arrayBuffer() {
1014
+ const output = await this.#makeRequest(
1015
+ (response) => response.arrayBuffer()
1016
+ );
1017
+ return this.#mapResponse(output);
1018
+ }
1019
+ /**
1020
+ * Executes the request and returns the response body as {@link FormData}.
1021
+ *
1022
+ * The shape of the returned {@link Promise} depends on the request mode:
1023
+ *
1024
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1025
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1026
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1027
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1028
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1029
+ * is `null`.
1030
+ *
1031
+ * @returns {Promise<
1032
+ * Opts['withResult'] extends true
1033
+ * ? Result.Result<
1034
+ * AspiResultOk<TRequest, FormData>,
1035
+ * | AspiError<TRequest>
1036
+ * | (Opts extends { error: any }
1037
+ * ? Opts['error'][keyof Opts['error']]
1038
+ * : never)
1039
+ * >
1040
+ * : Opts['throwable'] extends true
1041
+ * ? AspiPlainResponse<TRequest, FormData>
1042
+ * : [
1043
+ * AspiResultOk<TRequest, FormData> | null,
1044
+ * (
1045
+ * | (
1046
+ * | AspiError<TRequest>
1047
+ * | (Opts extends { error: any }
1048
+ * ? Opts['error'][keyof Opts['error']]
1049
+ * : never)
1050
+ * )
1051
+ * | null
1052
+ * ),
1053
+ * ]
1054
+ * >}
1055
+ */
1056
+ async formData() {
1057
+ const output = await this.#makeRequest(
1058
+ (response) => response.formData()
1059
+ );
1060
+ return this.#mapResponse(output);
1061
+ }
947
1062
  #url() {
948
1063
  if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
949
1064
  const absolute = new URL(this.#path);
@@ -961,7 +1076,6 @@ var Request = class {
961
1076
  let path = rawPath || "";
962
1077
  path = path.replace(/^\/+/, "");
963
1078
  path = path.replace(/\/{2,}/g, "/");
964
- path = path.replace(/\/+$/, "");
965
1079
  if (path) {
966
1080
  path = "/" + path.replace(/^\/+/, "");
967
1081
  }
@@ -979,7 +1093,6 @@ var Request = class {
979
1093
  if (fragment) {
980
1094
  url += `#${fragment}`;
981
1095
  }
982
- url = url.replace(/\/+$/, "");
983
1096
  return url;
984
1097
  }
985
1098
  /**
@@ -1077,16 +1190,32 @@ var Request = class {
1077
1190
  });
1078
1191
  let responseData = null;
1079
1192
  while (attempts <= retries) {
1193
+ let timeoutTimer;
1080
1194
  try {
1081
- if (this.#capabilities.length > 0) {
1082
- for (const capability of this.#capabilities) {
1083
- response = await capability({ request }).run(
1084
- () => fetch(url, requestInit)
1195
+ const attemptInit = { ...requestInit };
1196
+ if (this.#timeoutMs && this.#timeoutMs > 0) {
1197
+ const timeoutController = new AbortController();
1198
+ timeoutTimer = setTimeout(
1199
+ () => timeoutController.abort(),
1200
+ this.#timeoutMs
1201
+ );
1202
+ if (attemptInit.signal) {
1203
+ attemptInit.signal.addEventListener(
1204
+ "abort",
1205
+ () => timeoutController.abort(),
1206
+ { once: true }
1085
1207
  );
1086
1208
  }
1087
- } else {
1088
- response = await fetch(url, requestInit);
1209
+ attemptInit.signal = timeoutController.signal;
1210
+ }
1211
+ let runner = () => fetch(url, attemptInit);
1212
+ for (let i = this.#capabilities.length - 1; i >= 0; i--) {
1213
+ const cap = this.#capabilities[i];
1214
+ const next = runner;
1215
+ runner = () => cap({ request }).run(next);
1089
1216
  }
1217
+ response = await runner();
1218
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1090
1219
  responseData = await responseParser(response);
1091
1220
  if (responseData instanceof Error) {
1092
1221
  return err(responseData);
@@ -1098,7 +1227,7 @@ var Request = class {
1098
1227
  if (this.#isSuccessResponse(response) || !retryOn.includes(response.status) && !retryWhileCondition) {
1099
1228
  break;
1100
1229
  }
1101
- if (response.status in this.#customErrorCbs && attempts === retries) {
1230
+ if (response.status in this.#customErrorCbs) {
1102
1231
  const result = this.#customErrorCbs[response.status].cb({
1103
1232
  request,
1104
1233
  response: this.#makeResponse(response, responseData)
@@ -1121,6 +1250,7 @@ var Request = class {
1121
1250
  await this.#abortDelay(delay, request);
1122
1251
  }
1123
1252
  } catch (e) {
1253
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1124
1254
  if (e instanceof Error && e.name === "AbortError") {
1125
1255
  if (500 in this.#customErrorCbs) {
1126
1256
  const result = this.#customErrorCbs[response.status].cb({
@@ -1389,7 +1519,8 @@ var Request = class {
1389
1519
  * ```
1390
1520
  */
1391
1521
  useCapability(capability) {
1392
- return this.#capabilities.push(capability);
1522
+ this.#capabilities.push(capability);
1523
+ return this;
1393
1524
  }
1394
1525
  };
1395
1526
 
@@ -1806,5 +1937,7 @@ var Aspi = class {
1806
1937
  getHttpErrorStatus,
1807
1938
  httpErrors,
1808
1939
  isAspiError,
1809
- isCustomError
1940
+ isCustomError,
1941
+ isJSONParseError,
1942
+ isParseError
1810
1943
  });
package/dist/index.d.cts CHANGED
@@ -363,8 +363,15 @@ interface JSONParseError extends CustomError<'jsonParseError', {
363
363
  message: string;
364
364
  }> {
365
365
  }
366
+ /**
367
+ * Type alias for a schema validation parse error.
368
+ * Emitted when either the request body schema or the response schema fails validation.
369
+ */
370
+ type ParseError = CustomError<'parseError', unknown>;
366
371
  declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
367
372
  declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
373
+ declare const isParseError: (error: unknown) => error is ParseError;
374
+ declare const isJSONParseError: (error: unknown) => error is JSONParseError;
368
375
 
369
376
  /**
370
377
  * Arguments passed to a capability factory.
@@ -858,6 +865,21 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
858
865
  * });
859
866
  */
860
867
  setRetry(retry: AspiRetryConfig<TRequest>): this;
868
+ /**
869
+ * Sets a timeout for the request in milliseconds.
870
+ *
871
+ * When the timeout expires, the request is aborted with an `AbortError`.
872
+ * If a signal was already provided, the timeout is chained so that either
873
+ * the external signal or the timeout can abort the request.
874
+ *
875
+ * @param {number} ms - Timeout duration in milliseconds.
876
+ * @returns {this} The current {@link Request} instance for method chaining.
877
+ *
878
+ * @example
879
+ * const request = new Request('/slow-endpoint', config);
880
+ * request.timeout(5000); // abort after 5 seconds
881
+ */
882
+ timeout(ms: number): this;
861
883
  /**
862
884
  * Merges the provided headers into the request configuration.
863
885
  *
@@ -1390,6 +1412,96 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1390
1412
  error: any;
1391
1413
  } ? Opts['error'][keyof Opts['error']] : never)) | null)
1392
1414
  ]>;
1415
+ /**
1416
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
1417
+ *
1418
+ * The shape of the returned {@link Promise} depends on the request mode:
1419
+ *
1420
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1421
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
1422
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
1423
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1424
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1425
+ * is `null`.
1426
+ *
1427
+ * @returns {Promise<
1428
+ * Opts['withResult'] extends true
1429
+ * ? Result.Result<
1430
+ * AspiResultOk<TRequest, ArrayBuffer>,
1431
+ * | AspiError<TRequest>
1432
+ * | (Opts extends { error: any }
1433
+ * ? Opts['error'][keyof Opts['error']]
1434
+ * : never)
1435
+ * >
1436
+ * : Opts['throwable'] extends true
1437
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
1438
+ * : [
1439
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
1440
+ * (
1441
+ * | (
1442
+ * | AspiError<TRequest>
1443
+ * | (Opts extends { error: any }
1444
+ * ? Opts['error'][keyof Opts['error']]
1445
+ * : never)
1446
+ * )
1447
+ * | null
1448
+ * ),
1449
+ * ]
1450
+ * >}
1451
+ */
1452
+ arrayBuffer(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, ArrayBuffer>, AspiError<TRequest> | (Opts extends {
1453
+ error: any;
1454
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, ArrayBuffer> : [
1455
+ AspiResultOk<TRequest, ArrayBuffer> | null,
1456
+ ((AspiError<TRequest> | (Opts extends {
1457
+ error: any;
1458
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1459
+ ]>;
1460
+ /**
1461
+ * Executes the request and returns the response body as {@link FormData}.
1462
+ *
1463
+ * The shape of the returned {@link Promise} depends on the request mode:
1464
+ *
1465
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1466
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1467
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1468
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1469
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1470
+ * is `null`.
1471
+ *
1472
+ * @returns {Promise<
1473
+ * Opts['withResult'] extends true
1474
+ * ? Result.Result<
1475
+ * AspiResultOk<TRequest, FormData>,
1476
+ * | AspiError<TRequest>
1477
+ * | (Opts extends { error: any }
1478
+ * ? Opts['error'][keyof Opts['error']]
1479
+ * : never)
1480
+ * >
1481
+ * : Opts['throwable'] extends true
1482
+ * ? AspiPlainResponse<TRequest, FormData>
1483
+ * : [
1484
+ * AspiResultOk<TRequest, FormData> | null,
1485
+ * (
1486
+ * | (
1487
+ * | AspiError<TRequest>
1488
+ * | (Opts extends { error: any }
1489
+ * ? Opts['error'][keyof Opts['error']]
1490
+ * : never)
1491
+ * )
1492
+ * | null
1493
+ * ),
1494
+ * ]
1495
+ * >}
1496
+ */
1497
+ formData(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, FormData>, AspiError<TRequest> | (Opts extends {
1498
+ error: any;
1499
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, FormData> : [
1500
+ AspiResultOk<TRequest, FormData> | null,
1501
+ ((AspiError<TRequest> | (Opts extends {
1502
+ error: any;
1503
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1504
+ ]>;
1393
1505
  /**
1394
1506
  * Returns the fully‑qualified URL that will be used for the request.
1395
1507
  *
@@ -1549,7 +1661,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1549
1661
  * .json();
1550
1662
  * ```
1551
1663
  */
1552
- useCapability(capability: Capability<TRequest>): number;
1664
+ useCapability(capability: Capability<TRequest>): this;
1553
1665
  }
1554
1666
 
1555
1667
  /**
@@ -1893,4 +2005,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1893
2005
  useCapability(capability: Capability<TRequest>): this;
1894
2006
  }
1895
2007
 
1896
- 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 };
2008
+ 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 ParseError, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError, isJSONParseError, isParseError };
package/dist/index.d.ts CHANGED
@@ -363,8 +363,15 @@ interface JSONParseError extends CustomError<'jsonParseError', {
363
363
  message: string;
364
364
  }> {
365
365
  }
366
+ /**
367
+ * Type alias for a schema validation parse error.
368
+ * Emitted when either the request body schema or the response schema fails validation.
369
+ */
370
+ type ParseError = CustomError<'parseError', unknown>;
366
371
  declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
367
372
  declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
373
+ declare const isParseError: (error: unknown) => error is ParseError;
374
+ declare const isJSONParseError: (error: unknown) => error is JSONParseError;
368
375
 
369
376
  /**
370
377
  * Arguments passed to a capability factory.
@@ -858,6 +865,21 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
858
865
  * });
859
866
  */
860
867
  setRetry(retry: AspiRetryConfig<TRequest>): this;
868
+ /**
869
+ * Sets a timeout for the request in milliseconds.
870
+ *
871
+ * When the timeout expires, the request is aborted with an `AbortError`.
872
+ * If a signal was already provided, the timeout is chained so that either
873
+ * the external signal or the timeout can abort the request.
874
+ *
875
+ * @param {number} ms - Timeout duration in milliseconds.
876
+ * @returns {this} The current {@link Request} instance for method chaining.
877
+ *
878
+ * @example
879
+ * const request = new Request('/slow-endpoint', config);
880
+ * request.timeout(5000); // abort after 5 seconds
881
+ */
882
+ timeout(ms: number): this;
861
883
  /**
862
884
  * Merges the provided headers into the request configuration.
863
885
  *
@@ -1390,6 +1412,96 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1390
1412
  error: any;
1391
1413
  } ? Opts['error'][keyof Opts['error']] : never)) | null)
1392
1414
  ]>;
1415
+ /**
1416
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
1417
+ *
1418
+ * The shape of the returned {@link Promise} depends on the request mode:
1419
+ *
1420
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1421
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
1422
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
1423
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1424
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1425
+ * is `null`.
1426
+ *
1427
+ * @returns {Promise<
1428
+ * Opts['withResult'] extends true
1429
+ * ? Result.Result<
1430
+ * AspiResultOk<TRequest, ArrayBuffer>,
1431
+ * | AspiError<TRequest>
1432
+ * | (Opts extends { error: any }
1433
+ * ? Opts['error'][keyof Opts['error']]
1434
+ * : never)
1435
+ * >
1436
+ * : Opts['throwable'] extends true
1437
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
1438
+ * : [
1439
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
1440
+ * (
1441
+ * | (
1442
+ * | AspiError<TRequest>
1443
+ * | (Opts extends { error: any }
1444
+ * ? Opts['error'][keyof Opts['error']]
1445
+ * : never)
1446
+ * )
1447
+ * | null
1448
+ * ),
1449
+ * ]
1450
+ * >}
1451
+ */
1452
+ arrayBuffer(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, ArrayBuffer>, AspiError<TRequest> | (Opts extends {
1453
+ error: any;
1454
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, ArrayBuffer> : [
1455
+ AspiResultOk<TRequest, ArrayBuffer> | null,
1456
+ ((AspiError<TRequest> | (Opts extends {
1457
+ error: any;
1458
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1459
+ ]>;
1460
+ /**
1461
+ * Executes the request and returns the response body as {@link FormData}.
1462
+ *
1463
+ * The shape of the returned {@link Promise} depends on the request mode:
1464
+ *
1465
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
1466
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
1467
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
1468
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
1469
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
1470
+ * is `null`.
1471
+ *
1472
+ * @returns {Promise<
1473
+ * Opts['withResult'] extends true
1474
+ * ? Result.Result<
1475
+ * AspiResultOk<TRequest, FormData>,
1476
+ * | AspiError<TRequest>
1477
+ * | (Opts extends { error: any }
1478
+ * ? Opts['error'][keyof Opts['error']]
1479
+ * : never)
1480
+ * >
1481
+ * : Opts['throwable'] extends true
1482
+ * ? AspiPlainResponse<TRequest, FormData>
1483
+ * : [
1484
+ * AspiResultOk<TRequest, FormData> | null,
1485
+ * (
1486
+ * | (
1487
+ * | AspiError<TRequest>
1488
+ * | (Opts extends { error: any }
1489
+ * ? Opts['error'][keyof Opts['error']]
1490
+ * : never)
1491
+ * )
1492
+ * | null
1493
+ * ),
1494
+ * ]
1495
+ * >}
1496
+ */
1497
+ formData(): Promise<Opts['withResult'] extends true ? Result<AspiResultOk<TRequest, FormData>, AspiError<TRequest> | (Opts extends {
1498
+ error: any;
1499
+ } ? Opts['error'][keyof Opts['error']] : never)> : Opts['throwable'] extends true ? AspiPlainResponse<TRequest, FormData> : [
1500
+ AspiResultOk<TRequest, FormData> | null,
1501
+ ((AspiError<TRequest> | (Opts extends {
1502
+ error: any;
1503
+ } ? Opts['error'][keyof Opts['error']] : never)) | null)
1504
+ ]>;
1393
1505
  /**
1394
1506
  * Returns the fully‑qualified URL that will be used for the request.
1395
1507
  *
@@ -1549,7 +1661,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1549
1661
  * .json();
1550
1662
  * ```
1551
1663
  */
1552
- useCapability(capability: Capability<TRequest>): number;
1664
+ useCapability(capability: Capability<TRequest>): this;
1553
1665
  }
1554
1666
 
1555
1667
  /**
@@ -1893,4 +2005,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1893
2005
  useCapability(capability: Capability<TRequest>): this;
1894
2006
  }
1895
2007
 
1896
- 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 };
2008
+ 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 ParseError, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError, isJSONParseError, isParseError };
package/dist/index.js CHANGED
@@ -80,6 +80,12 @@ var isAspiError = (error) => {
80
80
  var isCustomError = (error) => {
81
81
  return error instanceof CustomError;
82
82
  };
83
+ var isParseError = (error) => {
84
+ return error instanceof CustomError && error.tag === "parseError";
85
+ };
86
+ var isJSONParseError = (error) => {
87
+ return error instanceof CustomError && error.tag === "jsonParseError";
88
+ };
83
89
 
84
90
  // src/result.ts
85
91
  var result_exports = {};
@@ -256,6 +262,8 @@ function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
256
262
  return bc(ab(a));
257
263
  case 4:
258
264
  return cd(bc(ab(a)));
265
+ case 5:
266
+ return de(cd(bc(ab(a))));
259
267
  case 6:
260
268
  return ef(de(cd(bc(ab(a)))));
261
269
  case 7:
@@ -284,6 +292,7 @@ var Request = class {
284
292
  #schema = null;
285
293
  #bodySchema = null;
286
294
  #retryConfig;
295
+ #timeoutMs;
287
296
  #shouldBeResult = false;
288
297
  #bodySchemaIssues = [];
289
298
  #throwOnError = false;
@@ -333,6 +342,24 @@ var Request = class {
333
342
  };
334
343
  return this;
335
344
  }
345
+ /**
346
+ * Sets a timeout for the request in milliseconds.
347
+ *
348
+ * When the timeout expires, the request is aborted with an `AbortError`.
349
+ * If a signal was already provided, the timeout is chained so that either
350
+ * the external signal or the timeout can abort the request.
351
+ *
352
+ * @param {number} ms - Timeout duration in milliseconds.
353
+ * @returns {this} The current {@link Request} instance for method chaining.
354
+ *
355
+ * @example
356
+ * const request = new Request('/slow-endpoint', config);
357
+ * request.timeout(5000); // abort after 5 seconds
358
+ */
359
+ timeout(ms) {
360
+ this.#timeoutMs = ms;
361
+ return this;
362
+ }
336
363
  /**
337
364
  * Merges the provided headers into the request configuration.
338
365
  *
@@ -916,6 +943,92 @@ var Request = class {
916
943
  const output = await this.#makeRequest((response) => response.blob());
917
944
  return this.#mapResponse(output);
918
945
  }
946
+ /**
947
+ * Executes the request and returns the response body as an {@link ArrayBuffer}.
948
+ *
949
+ * The shape of the returned {@link Promise} depends on the request mode:
950
+ *
951
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
952
+ * either an {@link AspiResultOk} with `ArrayBuffer` data or an error variant.
953
+ * - **Throwable mode** (`throwable()`): resolves directly to an {@link ArrayBuffer}
954
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
955
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
956
+ * is `null`.
957
+ *
958
+ * @returns {Promise<
959
+ * Opts['withResult'] extends true
960
+ * ? Result.Result<
961
+ * AspiResultOk<TRequest, ArrayBuffer>,
962
+ * | AspiError<TRequest>
963
+ * | (Opts extends { error: any }
964
+ * ? Opts['error'][keyof Opts['error']]
965
+ * : never)
966
+ * >
967
+ * : Opts['throwable'] extends true
968
+ * ? AspiPlainResponse<TRequest, ArrayBuffer>
969
+ * : [
970
+ * AspiResultOk<TRequest, ArrayBuffer> | null,
971
+ * (
972
+ * | (
973
+ * | AspiError<TRequest>
974
+ * | (Opts extends { error: any }
975
+ * ? Opts['error'][keyof Opts['error']]
976
+ * : never)
977
+ * )
978
+ * | null
979
+ * ),
980
+ * ]
981
+ * >}
982
+ */
983
+ async arrayBuffer() {
984
+ const output = await this.#makeRequest(
985
+ (response) => response.arrayBuffer()
986
+ );
987
+ return this.#mapResponse(output);
988
+ }
989
+ /**
990
+ * Executes the request and returns the response body as {@link FormData}.
991
+ *
992
+ * The shape of the returned {@link Promise} depends on the request mode:
993
+ *
994
+ * - **Result mode** (`withResult()`): resolves to a {@link Result.Result} containing
995
+ * either an {@link AspiResultOk} with `FormData` or an error variant.
996
+ * - **Throwable mode** (`throwable()`): resolves directly to {@link FormData}
997
+ * (wrapped in {@link AspiPlainResponse}) and throws on failure.
998
+ * - **Default mode**: resolves to a tuple `[value, error]` where exactly one element
999
+ * is `null`.
1000
+ *
1001
+ * @returns {Promise<
1002
+ * Opts['withResult'] extends true
1003
+ * ? Result.Result<
1004
+ * AspiResultOk<TRequest, FormData>,
1005
+ * | AspiError<TRequest>
1006
+ * | (Opts extends { error: any }
1007
+ * ? Opts['error'][keyof Opts['error']]
1008
+ * : never)
1009
+ * >
1010
+ * : Opts['throwable'] extends true
1011
+ * ? AspiPlainResponse<TRequest, FormData>
1012
+ * : [
1013
+ * AspiResultOk<TRequest, FormData> | null,
1014
+ * (
1015
+ * | (
1016
+ * | AspiError<TRequest>
1017
+ * | (Opts extends { error: any }
1018
+ * ? Opts['error'][keyof Opts['error']]
1019
+ * : never)
1020
+ * )
1021
+ * | null
1022
+ * ),
1023
+ * ]
1024
+ * >}
1025
+ */
1026
+ async formData() {
1027
+ const output = await this.#makeRequest(
1028
+ (response) => response.formData()
1029
+ );
1030
+ return this.#mapResponse(output);
1031
+ }
919
1032
  #url() {
920
1033
  if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
921
1034
  const absolute = new URL(this.#path);
@@ -933,7 +1046,6 @@ var Request = class {
933
1046
  let path = rawPath || "";
934
1047
  path = path.replace(/^\/+/, "");
935
1048
  path = path.replace(/\/{2,}/g, "/");
936
- path = path.replace(/\/+$/, "");
937
1049
  if (path) {
938
1050
  path = "/" + path.replace(/^\/+/, "");
939
1051
  }
@@ -951,7 +1063,6 @@ var Request = class {
951
1063
  if (fragment) {
952
1064
  url += `#${fragment}`;
953
1065
  }
954
- url = url.replace(/\/+$/, "");
955
1066
  return url;
956
1067
  }
957
1068
  /**
@@ -1049,16 +1160,32 @@ var Request = class {
1049
1160
  });
1050
1161
  let responseData = null;
1051
1162
  while (attempts <= retries) {
1163
+ let timeoutTimer;
1052
1164
  try {
1053
- if (this.#capabilities.length > 0) {
1054
- for (const capability of this.#capabilities) {
1055
- response = await capability({ request }).run(
1056
- () => fetch(url, requestInit)
1165
+ const attemptInit = { ...requestInit };
1166
+ if (this.#timeoutMs && this.#timeoutMs > 0) {
1167
+ const timeoutController = new AbortController();
1168
+ timeoutTimer = setTimeout(
1169
+ () => timeoutController.abort(),
1170
+ this.#timeoutMs
1171
+ );
1172
+ if (attemptInit.signal) {
1173
+ attemptInit.signal.addEventListener(
1174
+ "abort",
1175
+ () => timeoutController.abort(),
1176
+ { once: true }
1057
1177
  );
1058
1178
  }
1059
- } else {
1060
- response = await fetch(url, requestInit);
1179
+ attemptInit.signal = timeoutController.signal;
1180
+ }
1181
+ let runner = () => fetch(url, attemptInit);
1182
+ for (let i = this.#capabilities.length - 1; i >= 0; i--) {
1183
+ const cap = this.#capabilities[i];
1184
+ const next = runner;
1185
+ runner = () => cap({ request }).run(next);
1061
1186
  }
1187
+ response = await runner();
1188
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1062
1189
  responseData = await responseParser(response);
1063
1190
  if (responseData instanceof Error) {
1064
1191
  return err(responseData);
@@ -1070,7 +1197,7 @@ var Request = class {
1070
1197
  if (this.#isSuccessResponse(response) || !retryOn.includes(response.status) && !retryWhileCondition) {
1071
1198
  break;
1072
1199
  }
1073
- if (response.status in this.#customErrorCbs && attempts === retries) {
1200
+ if (response.status in this.#customErrorCbs) {
1074
1201
  const result = this.#customErrorCbs[response.status].cb({
1075
1202
  request,
1076
1203
  response: this.#makeResponse(response, responseData)
@@ -1093,6 +1220,7 @@ var Request = class {
1093
1220
  await this.#abortDelay(delay, request);
1094
1221
  }
1095
1222
  } catch (e) {
1223
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1096
1224
  if (e instanceof Error && e.name === "AbortError") {
1097
1225
  if (500 in this.#customErrorCbs) {
1098
1226
  const result = this.#customErrorCbs[response.status].cb({
@@ -1361,7 +1489,8 @@ var Request = class {
1361
1489
  * ```
1362
1490
  */
1363
1491
  useCapability(capability) {
1364
- return this.#capabilities.push(capability);
1492
+ this.#capabilities.push(capability);
1493
+ return this;
1365
1494
  }
1366
1495
  };
1367
1496
 
@@ -1777,5 +1906,7 @@ export {
1777
1906
  getHttpErrorStatus,
1778
1907
  httpErrors,
1779
1908
  isAspiError,
1780
- isCustomError
1909
+ isCustomError,
1910
+ isJSONParseError,
1911
+ isParseError
1781
1912
  };
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.4.0",
4
+ "version": "2.6.0",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {