aspi 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Zero runtime dependencies. Three response modes. Full error-union types.
12
12
  ## Features
13
13
 
14
14
  - **Zero dependencies** — thin wrapper around the platform `fetch` API
15
- - **Three response modes** — tuple `[data, error]`, `Result` monad, or `throwable` (your choice per call)
15
+ - **Three response modes** — tuple `[data, error]`, `Result` monad, or `throwable` (your choice per call, plus explicit `.withTuple()` to reset)
16
16
  - **Typed error unions** — every error variant is tagged and narrowable at compile time
17
17
  - **Custom error mapping** — map any HTTP status code to a structured, typed error object
18
18
  - **Retry with back-off** — fixed or dynamic delay, status-code filtering, custom predicates
@@ -65,7 +65,7 @@ if (data) console.log(data.title);
65
65
 
66
66
  ## Response modes
67
67
 
68
- Every request can be consumed in one of three modes. Switch mode by calling `.withResult()` or `.throwable()` before the body-parser method.
68
+ Every request can be consumed in one of three modes. Switch mode by calling `.withResult()`, `.throwable()`, or `.withTuple()` before the body-parser method.
69
69
 
70
70
  ### 1. Tuple mode (default)
71
71
 
@@ -106,7 +106,7 @@ try {
106
106
  }
107
107
  ```
108
108
 
109
- > `throwable()` and `withResult()` are mutually exclusive — the **last one called wins**.
109
+ > `throwable()`, `withResult()`, and `withTuple()` are mutually exclusive — the **last one called wins**. Use `.withTuple()` to explicitly reset back to the default tuple mode after a previous `.withResult()` or `.throwable()`.
110
110
 
111
111
  ---
112
112
 
@@ -116,12 +116,12 @@ try {
116
116
 
117
117
  Every response mode surfaces the same tagged error variants:
118
118
 
119
- | Tag | When |
120
- | ---------------- | ------------------------------------------------------------ |
121
- | `aspiError` | Any non-2xx response with no matching custom handler |
122
- | `jsonParseError` | Response body could not be parsed as JSON |
123
- | `parseError` | Response failed schema validation (when `.schema()` is used) |
124
- | _custom_ | Any tag you define via `.error()` or a convenience shortcut |
119
+ | Tag | When |
120
+ | ------------------ | ------------------------------------------------------------------------------------------------- |
121
+ | `aspiError` | Any non-2xx response with no matching custom handler |
122
+ | `jsonParseError` | Response body could not be parsed as JSON |
123
+ | `schemaParseError` | Response (or request body) failed schema validation (when `.schema()` or `.bodySchema()` is used) |
124
+ | _custom_ | Any tag you define via `.error()` or a convenience shortcut |
125
125
 
126
126
  ### Custom error mapping
127
127
 
@@ -228,7 +228,7 @@ const [data, error] = await api
228
228
  .bodyJson({ name: 'Alice', email: 'alice@example.com' })
229
229
  .json<User>();
230
230
 
231
- // If bodyJson fails validation, error.tag === 'parseError'
231
+ // If bodyJson fails validation, error.tag === 'schemaParseError'
232
232
  ```
233
233
 
234
234
  ### Query parameters
@@ -309,6 +309,8 @@ Aspi integrates with any library that implements the [StandardSchemaV1](https://
309
309
 
310
310
  Attach a schema with `.schema()` before the body-parser. The inferred output type is used automatically — you don't need to pass a generic.
311
311
 
312
+ Aspi supports both **synchronous and asynchronous** `validate` implementations, so schema libraries that return `Promise<Result>` work out of the box.
313
+
312
314
  ```ts
313
315
  import { z } from 'zod';
314
316
 
@@ -323,7 +325,7 @@ const result = await api.get('/todos/1').withResult().schema(TodoSchema).json();
323
325
  Result.match(result, {
324
326
  onOk: ({ data }) => console.log(data.title), // data: { id: number; title: string; completed: boolean }
325
327
  onErr: (err) => {
326
- if (err.tag === 'parseError') {
328
+ if (err.tag === 'schemaParseError') {
327
329
  console.error('Validation failed:', err.data); // StandardSchemaV1 issue list
328
330
  }
329
331
  },
@@ -530,6 +532,7 @@ These methods are available on the `Aspi` instance and affect all requests creat
530
532
  | `useCapability(cap)` | Register a capability |
531
533
  | `withResult()` | Switch all requests to Result mode |
532
534
  | `throwable()` | Switch all requests to throwable mode |
535
+ | `withTuple()` | Switch all requests back to tuple mode |
533
536
  | `.error(tag, status, cb)` | Map an HTTP status to a typed error |
534
537
 
535
538
  Per-request methods (`api.get('/…').setQueryParams(…)`, `.schema(…)`, `.bodyJson(…)`, etc.) override the global config for that call only.
package/dist/index.cjs CHANGED
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Aspi: () => Aspi,
23
+ Aspi: () => Aspi2,
24
24
  AspiError: () => AspiError,
25
25
  CustomError: () => CustomError,
26
26
  Request: () => Request,
@@ -30,7 +30,7 @@ __export(index_exports, {
30
30
  isAspiError: () => isAspiError,
31
31
  isCustomError: () => isCustomError,
32
32
  isJSONParseError: () => isJSONParseError,
33
- isParseError: () => isParseError
33
+ isSchemaParseError: () => isSchemaParseError
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
@@ -110,12 +110,12 @@ var isAspiError = (error) => {
110
110
  var isCustomError = (error) => {
111
111
  return error instanceof CustomError;
112
112
  };
113
- var isParseError = (error) => {
114
- return error instanceof CustomError && error.tag === "parseError";
115
- };
116
113
  var isJSONParseError = (error) => {
117
114
  return error instanceof CustomError && error.tag === "jsonParseError";
118
115
  };
116
+ var isSchemaParseError = (error) => {
117
+ return error instanceof CustomError && error.tag === "schemaParseError";
118
+ };
119
119
 
120
120
  // src/result.ts
121
121
  var result_exports = {};
@@ -325,6 +325,8 @@ var Request = class {
325
325
  #timeoutMs;
326
326
  #shouldBeResult = false;
327
327
  #bodySchemaIssues = [];
328
+ #bodySchemaAsyncResult = null;
329
+ #bodySchemaAsyncBody = null;
328
330
  #throwOnError = false;
329
331
  #capabilities = [];
330
332
  constructor(method, path, requestOptions, capabilities = []) {
@@ -478,9 +480,9 @@ var Request = class {
478
480
  if (this.#bodySchema) {
479
481
  const data = this.#bodySchema["~standard"].validate(body);
480
482
  if (data instanceof Promise) {
481
- throw new Error("Schema validation should not return a promise");
482
- }
483
- if (data.issues) {
483
+ this.#bodySchemaAsyncResult = data;
484
+ this.#bodySchemaAsyncBody = body;
485
+ } else if (data.issues) {
484
486
  this.#bodySchemaIssues = data.issues;
485
487
  } else {
486
488
  this.#localRequestInit.body = JSON.stringify(data.value);
@@ -1158,6 +1160,49 @@ var Request = class {
1158
1160
  this.#shouldBeResult = true;
1159
1161
  return this;
1160
1162
  }
1163
+ /**
1164
+ * Switches the request into **tuple** mode (the default).
1165
+ *
1166
+ * In tuple mode the response helpers (`json`, `text`, `blob`, …) resolve to a
1167
+ * tuple `[value, error]` where exactly one element is non‑null. This is useful
1168
+ * when you want an explicit opt‑in method to reset from `withResult()` or
1169
+ * `throwable()` back to the default tuple behaviour.
1170
+ *
1171
+ * Calling `withTuple` disables both Result mode and throwable mode.
1172
+ *
1173
+ * @returns {Request<
1174
+ * Method,
1175
+ * TRequest,
1176
+ * Merge<
1177
+ * Omit<Opts, 'withResult' | 'throwable'>,
1178
+ * {
1179
+ * withResult: false;
1180
+ * throwable: false;
1181
+ * }
1182
+ * >
1183
+ * >} The same {@link Request} instance, now typed with `withResult: false` and
1184
+ * `throwable: false` for fluent chaining.
1185
+ *
1186
+ * @example
1187
+ * ```ts
1188
+ * const request = new Request('/users', config);
1189
+ *
1190
+ * const [user, err] = await request
1191
+ * .withTuple() // explicitly use tuple mode
1192
+ * .json<User>();
1193
+ *
1194
+ * if (err) {
1195
+ * console.error(err);
1196
+ * } else {
1197
+ * console.log(user);
1198
+ * }
1199
+ * ```
1200
+ */
1201
+ withTuple() {
1202
+ this.#throwOnError = false;
1203
+ this.#shouldBeResult = false;
1204
+ return this;
1205
+ }
1161
1206
  #mapResponse(value) {
1162
1207
  if (this.#shouldBeResult) {
1163
1208
  return value;
@@ -1175,8 +1220,20 @@ var Request = class {
1175
1220
  return response.ok || response.status >= 300 && response.status < 400;
1176
1221
  }
1177
1222
  async #makeRequest(responseParser, isJson = false) {
1223
+ if (this.#bodySchemaAsyncResult) {
1224
+ const data = await this.#bodySchemaAsyncResult;
1225
+ if (data.issues) {
1226
+ this.#bodySchemaIssues = data.issues;
1227
+ } else {
1228
+ this.#localRequestInit.body = JSON.stringify(data.value);
1229
+ }
1230
+ this.#bodySchemaAsyncResult = null;
1231
+ this.#bodySchemaAsyncBody = null;
1232
+ }
1178
1233
  if (this.#bodySchemaIssues.length) {
1179
- return err(new CustomError("parseError", this.#bodySchemaIssues));
1234
+ return err(
1235
+ new CustomError("schemaParseError", this.#bodySchemaIssues)
1236
+ );
1180
1237
  }
1181
1238
  const request = this.#request();
1182
1239
  const { retries, retryDelay, retryOn, retryWhile, onRetry } = this.#sanitisedRetryConfig();
@@ -1310,12 +1367,9 @@ var Request = class {
1310
1367
  );
1311
1368
  }
1312
1369
  if (isJson && this.#schema) {
1313
- const data = this.#schema["~standard"].validate(responseData);
1314
- if (data instanceof Promise) {
1315
- throw new Error("Schema validation should not return a promise");
1316
- }
1370
+ const data = await this.#schema["~standard"].validate(responseData);
1317
1371
  if (data.issues) {
1318
- return err(new CustomError("parseError", data.issues));
1372
+ return err(new CustomError("schemaParseError", data.issues));
1319
1373
  }
1320
1374
  return ok({
1321
1375
  data: data.value,
@@ -1525,7 +1579,7 @@ var Request = class {
1525
1579
  };
1526
1580
 
1527
1581
  // src/aspi.ts
1528
- var Aspi = class {
1582
+ var Aspi2 = class {
1529
1583
  #globalRequestInit;
1530
1584
  #middlewares = [];
1531
1585
  #retryConfig;
@@ -1885,6 +1939,20 @@ var Aspi = class {
1885
1939
  this.#throwOnError = false;
1886
1940
  return this;
1887
1941
  }
1942
+ /**
1943
+ * Configures all subsequent requests to return the default tuple `[value, error]`.
1944
+ *
1945
+ * This is the default behaviour, but `withTuple()` is provided as an explicit
1946
+ * reset when you have previously called {@link withResult} or {@link throwable}
1947
+ * on the {@link Aspi} instance.
1948
+ *
1949
+ * @returns The Aspi instance with tuple handling enabled.
1950
+ */
1951
+ withTuple() {
1952
+ this.#shouldBeResult = false;
1953
+ this.#throwOnError = false;
1954
+ return this;
1955
+ }
1888
1956
  /**
1889
1957
  * Registers a capability on this {@link Aspi} instance.
1890
1958
  *
@@ -1939,5 +2007,5 @@ var Aspi = class {
1939
2007
  isAspiError,
1940
2008
  isCustomError,
1941
2009
  isJSONParseError,
1942
- isParseError
2010
+ isSchemaParseError
1943
2011
  });
package/dist/index.d.cts CHANGED
@@ -1,3 +1,64 @@
1
+ /**
2
+ * Standard Schema is a common interface designed to be implemented by JavaScript and TypeScript schema libraries.
3
+ * Will support multiple schema validators including Zod, Valibot, and ArkType
4
+ * https://standardschema.dev/
5
+ */
6
+ /** The Standard Schema interface. */
7
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
8
+ /** The Standard Schema properties. */
9
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
10
+ }
11
+ declare namespace StandardSchemaV1 {
12
+ /** The Standard Schema properties interface. */
13
+ interface Props<Input = unknown, Output = Input> {
14
+ /** The version number of the standard. */
15
+ readonly version: 1;
16
+ /** The vendor name of the schema library. */
17
+ readonly vendor: string;
18
+ /** Validates unknown input values. */
19
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
20
+ /** Inferred types associated with the schema. */
21
+ readonly types?: Types<Input, Output> | undefined;
22
+ }
23
+ /** The result interface of the validate function. */
24
+ type Result<Output> = SuccessResult<Output> | FailureResult;
25
+ /** The result interface if validation succeeds. */
26
+ interface SuccessResult<Output> {
27
+ /** The typed output value. */
28
+ readonly value: Output;
29
+ /** The non-existent issues. */
30
+ readonly issues?: undefined;
31
+ }
32
+ /** The result interface if validation fails. */
33
+ interface FailureResult {
34
+ /** The issues of failed validation. */
35
+ readonly issues: ReadonlyArray<Issue>;
36
+ }
37
+ /** The issue interface of the failure output. */
38
+ interface Issue {
39
+ /** The error message of the issue. */
40
+ readonly message: string;
41
+ /** The path of the issue, if any. */
42
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
43
+ }
44
+ /** The path segment interface of the issue. */
45
+ interface PathSegment {
46
+ /** The key representing a path segment. */
47
+ readonly key: PropertyKey;
48
+ }
49
+ /** The Standard Schema types interface. */
50
+ interface Types<Input = unknown, Output = Input> {
51
+ /** The input type of the schema. */
52
+ readonly input: Input;
53
+ /** The output type of the schema. */
54
+ readonly output: Output;
55
+ }
56
+ /** Infers the input type of a Standard Schema. */
57
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
58
+ /** Infers the output type of a Standard Schema. */
59
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
60
+ }
61
+
1
62
  /**
2
63
  * Standard HTTP /**
3
64
  * Common HTTP error status codes with their numeric values.
@@ -364,14 +425,16 @@ interface JSONParseError extends CustomError<'jsonParseError', {
364
425
  }> {
365
426
  }
366
427
  /**
367
- * Type alias for a schema validation parse error.
368
- * Emitted when either the request body schema or the response schema fails validation.
428
+ * Interface representing a schema validation error (body or response).
429
+ * @interface SchemaParseError
430
+ * @extends {CustomError<'schemaParseError', ReadonlyArray<StandardSchemaV1.Issue>>}
369
431
  */
370
- type ParseError = CustomError<'parseError', unknown>;
432
+ interface SchemaParseError extends CustomError<'schemaParseError', ReadonlyArray<StandardSchemaV1.Issue>> {
433
+ }
371
434
  declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
372
435
  declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
373
- declare const isParseError: (error: unknown) => error is ParseError;
374
436
  declare const isJSONParseError: (error: unknown) => error is JSONParseError;
437
+ declare const isSchemaParseError: (error: unknown) => error is SchemaParseError;
375
438
 
376
439
  /**
377
440
  * Arguments passed to a capability factory.
@@ -756,67 +819,6 @@ declare namespace result {
756
819
  export { type result_Err as Err, type result_Ok as Ok, type result_Result as Result, result_catchAllErrors as catchAllErrors, result_catchError as catchError, result_catchErrors as catchErrors, result_err as err, result_getErrorOrNull as getErrorOrNull, result_getOrElse as getOrElse, result_getOrNull as getOrNull, result_getOrThrow as getOrThrow, result_getOrThrowWith as getOrThrowWith, result_isErr as isErr, result_isOk as isOk, result_map as map, result_mapErr as mapErr, result_match as match, result_ok as ok, result_pipe as pipe };
757
820
  }
758
821
 
759
- /**
760
- * Standard Schema is a common interface designed to be implemented by JavaScript and TypeScript schema libraries.
761
- * Will support multiple schema validators including Zod, Valibot, and ArkType
762
- * https://standardschema.dev/
763
- */
764
- /** The Standard Schema interface. */
765
- interface StandardSchemaV1<Input = unknown, Output = Input> {
766
- /** The Standard Schema properties. */
767
- readonly '~standard': StandardSchemaV1.Props<Input, Output>;
768
- }
769
- declare namespace StandardSchemaV1 {
770
- /** The Standard Schema properties interface. */
771
- interface Props<Input = unknown, Output = Input> {
772
- /** The version number of the standard. */
773
- readonly version: 1;
774
- /** The vendor name of the schema library. */
775
- readonly vendor: string;
776
- /** Validates unknown input values. */
777
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
778
- /** Inferred types associated with the schema. */
779
- readonly types?: Types<Input, Output> | undefined;
780
- }
781
- /** The result interface of the validate function. */
782
- type Result<Output> = SuccessResult<Output> | FailureResult;
783
- /** The result interface if validation succeeds. */
784
- interface SuccessResult<Output> {
785
- /** The typed output value. */
786
- readonly value: Output;
787
- /** The non-existent issues. */
788
- readonly issues?: undefined;
789
- }
790
- /** The result interface if validation fails. */
791
- interface FailureResult {
792
- /** The issues of failed validation. */
793
- readonly issues: ReadonlyArray<Issue>;
794
- }
795
- /** The issue interface of the failure output. */
796
- interface Issue {
797
- /** The error message of the issue. */
798
- readonly message: string;
799
- /** The path of the issue, if any. */
800
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
801
- }
802
- /** The path segment interface of the issue. */
803
- interface PathSegment {
804
- /** The key representing a path segment. */
805
- readonly key: PropertyKey;
806
- }
807
- /** The Standard Schema types interface. */
808
- interface Types<Input = unknown, Output = Input> {
809
- /** The input type of the schema. */
810
- readonly input: Input;
811
- /** The output type of the schema. */
812
- readonly output: Output;
813
- }
814
- /** Infers the input type of a Standard Schema. */
815
- type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
816
- /** Infers the output type of a Standard Schema. */
817
- type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
818
- }
819
-
820
822
  /**
821
823
  * A class for building and executing HTTP requests with customizable options and error handling.
822
824
  * @template Method The HTTP method type (GET, POST, etc.)
@@ -937,7 +939,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
937
939
  bodySchema<TSchema extends StandardSchemaV1>(schema: TSchema): Request<Method, TRequest, Omit<Opts, "bodySchema"> & {
938
940
  bodySchema: TSchema;
939
941
  error: Opts["error"] & {
940
- parseError: CustomError<"parseError", StandardSchemaV1.FailureResult["issues"]>;
942
+ schemaParseError: CustomError<"schemaParseError", StandardSchemaV1.FailureResult["issues"]>;
941
943
  };
942
944
  }>;
943
945
  /**
@@ -1209,7 +1211,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1209
1211
  schema<TSchema extends StandardSchemaV1>(schema: TSchema): Request<Method, TRequest, Merge<Omit<Opts, "schema">, {
1210
1212
  schema: TSchema;
1211
1213
  error: Merge<Opts["error"], {
1212
- parseError: CustomError<"parseError", StandardSchemaV1.FailureResult["issues"]>;
1214
+ schemaParseError: CustomError<"schemaParseError", StandardSchemaV1.FailureResult["issues"]>;
1213
1215
  }>;
1214
1216
  }>>;
1215
1217
  /**
@@ -1562,6 +1564,48 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1562
1564
  withResult: true;
1563
1565
  throwable: false;
1564
1566
  }>>;
1567
+ /**
1568
+ * Switches the request into **tuple** mode (the default).
1569
+ *
1570
+ * In tuple mode the response helpers (`json`, `text`, `blob`, …) resolve to a
1571
+ * tuple `[value, error]` where exactly one element is non‑null. This is useful
1572
+ * when you want an explicit opt‑in method to reset from `withResult()` or
1573
+ * `throwable()` back to the default tuple behaviour.
1574
+ *
1575
+ * Calling `withTuple` disables both Result mode and throwable mode.
1576
+ *
1577
+ * @returns {Request<
1578
+ * Method,
1579
+ * TRequest,
1580
+ * Merge<
1581
+ * Omit<Opts, 'withResult' | 'throwable'>,
1582
+ * {
1583
+ * withResult: false;
1584
+ * throwable: false;
1585
+ * }
1586
+ * >
1587
+ * >} The same {@link Request} instance, now typed with `withResult: false` and
1588
+ * `throwable: false` for fluent chaining.
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * const request = new Request('/users', config);
1593
+ *
1594
+ * const [user, err] = await request
1595
+ * .withTuple() // explicitly use tuple mode
1596
+ * .json<User>();
1597
+ *
1598
+ * if (err) {
1599
+ * console.error(err);
1600
+ * } else {
1601
+ * console.log(user);
1602
+ * }
1603
+ * ```
1604
+ */
1605
+ withTuple(): Request<Method, TRequest, Merge<Omit<Opts, "withResult" | "throwable">, {
1606
+ withResult: false;
1607
+ throwable: false;
1608
+ }>>;
1565
1609
  /**
1566
1610
  * Returns the underlying {@link AspiRequest} object that will be used for the fetch call.
1567
1611
  *
@@ -1965,6 +2009,19 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1965
2009
  withResult: true;
1966
2010
  throwable: false;
1967
2011
  }>>;
2012
+ /**
2013
+ * Configures all subsequent requests to return the default tuple `[value, error]`.
2014
+ *
2015
+ * This is the default behaviour, but `withTuple()` is provided as an explicit
2016
+ * reset when you have previously called {@link withResult} or {@link throwable}
2017
+ * on the {@link Aspi} instance.
2018
+ *
2019
+ * @returns The Aspi instance with tuple handling enabled.
2020
+ */
2021
+ withTuple(): Aspi<TRequest, Merge<Omit<Opts, "withResult" | "throwable">, {
2022
+ withResult: false;
2023
+ throwable: false;
2024
+ }>>;
1968
2025
  /**
1969
2026
  * Registers a capability on this {@link Aspi} instance.
1970
2027
  *
@@ -2005,4 +2062,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
2005
2062
  useCapability(capability: Capability<TRequest>): this;
2006
2063
  }
2007
2064
 
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 };
2065
+ 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, type SchemaParseError, getHttpErrorStatus, httpErrors, isAspiError, isCustomError, isJSONParseError, isSchemaParseError };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,64 @@
1
+ /**
2
+ * Standard Schema is a common interface designed to be implemented by JavaScript and TypeScript schema libraries.
3
+ * Will support multiple schema validators including Zod, Valibot, and ArkType
4
+ * https://standardschema.dev/
5
+ */
6
+ /** The Standard Schema interface. */
7
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
8
+ /** The Standard Schema properties. */
9
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
10
+ }
11
+ declare namespace StandardSchemaV1 {
12
+ /** The Standard Schema properties interface. */
13
+ interface Props<Input = unknown, Output = Input> {
14
+ /** The version number of the standard. */
15
+ readonly version: 1;
16
+ /** The vendor name of the schema library. */
17
+ readonly vendor: string;
18
+ /** Validates unknown input values. */
19
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
20
+ /** Inferred types associated with the schema. */
21
+ readonly types?: Types<Input, Output> | undefined;
22
+ }
23
+ /** The result interface of the validate function. */
24
+ type Result<Output> = SuccessResult<Output> | FailureResult;
25
+ /** The result interface if validation succeeds. */
26
+ interface SuccessResult<Output> {
27
+ /** The typed output value. */
28
+ readonly value: Output;
29
+ /** The non-existent issues. */
30
+ readonly issues?: undefined;
31
+ }
32
+ /** The result interface if validation fails. */
33
+ interface FailureResult {
34
+ /** The issues of failed validation. */
35
+ readonly issues: ReadonlyArray<Issue>;
36
+ }
37
+ /** The issue interface of the failure output. */
38
+ interface Issue {
39
+ /** The error message of the issue. */
40
+ readonly message: string;
41
+ /** The path of the issue, if any. */
42
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
43
+ }
44
+ /** The path segment interface of the issue. */
45
+ interface PathSegment {
46
+ /** The key representing a path segment. */
47
+ readonly key: PropertyKey;
48
+ }
49
+ /** The Standard Schema types interface. */
50
+ interface Types<Input = unknown, Output = Input> {
51
+ /** The input type of the schema. */
52
+ readonly input: Input;
53
+ /** The output type of the schema. */
54
+ readonly output: Output;
55
+ }
56
+ /** Infers the input type of a Standard Schema. */
57
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
58
+ /** Infers the output type of a Standard Schema. */
59
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
60
+ }
61
+
1
62
  /**
2
63
  * Standard HTTP /**
3
64
  * Common HTTP error status codes with their numeric values.
@@ -364,14 +425,16 @@ interface JSONParseError extends CustomError<'jsonParseError', {
364
425
  }> {
365
426
  }
366
427
  /**
367
- * Type alias for a schema validation parse error.
368
- * Emitted when either the request body schema or the response schema fails validation.
428
+ * Interface representing a schema validation error (body or response).
429
+ * @interface SchemaParseError
430
+ * @extends {CustomError<'schemaParseError', ReadonlyArray<StandardSchemaV1.Issue>>}
369
431
  */
370
- type ParseError = CustomError<'parseError', unknown>;
432
+ interface SchemaParseError extends CustomError<'schemaParseError', ReadonlyArray<StandardSchemaV1.Issue>> {
433
+ }
371
434
  declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
372
435
  declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
373
- declare const isParseError: (error: unknown) => error is ParseError;
374
436
  declare const isJSONParseError: (error: unknown) => error is JSONParseError;
437
+ declare const isSchemaParseError: (error: unknown) => error is SchemaParseError;
375
438
 
376
439
  /**
377
440
  * Arguments passed to a capability factory.
@@ -756,67 +819,6 @@ declare namespace result {
756
819
  export { type result_Err as Err, type result_Ok as Ok, type result_Result as Result, result_catchAllErrors as catchAllErrors, result_catchError as catchError, result_catchErrors as catchErrors, result_err as err, result_getErrorOrNull as getErrorOrNull, result_getOrElse as getOrElse, result_getOrNull as getOrNull, result_getOrThrow as getOrThrow, result_getOrThrowWith as getOrThrowWith, result_isErr as isErr, result_isOk as isOk, result_map as map, result_mapErr as mapErr, result_match as match, result_ok as ok, result_pipe as pipe };
757
820
  }
758
821
 
759
- /**
760
- * Standard Schema is a common interface designed to be implemented by JavaScript and TypeScript schema libraries.
761
- * Will support multiple schema validators including Zod, Valibot, and ArkType
762
- * https://standardschema.dev/
763
- */
764
- /** The Standard Schema interface. */
765
- interface StandardSchemaV1<Input = unknown, Output = Input> {
766
- /** The Standard Schema properties. */
767
- readonly '~standard': StandardSchemaV1.Props<Input, Output>;
768
- }
769
- declare namespace StandardSchemaV1 {
770
- /** The Standard Schema properties interface. */
771
- interface Props<Input = unknown, Output = Input> {
772
- /** The version number of the standard. */
773
- readonly version: 1;
774
- /** The vendor name of the schema library. */
775
- readonly vendor: string;
776
- /** Validates unknown input values. */
777
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
778
- /** Inferred types associated with the schema. */
779
- readonly types?: Types<Input, Output> | undefined;
780
- }
781
- /** The result interface of the validate function. */
782
- type Result<Output> = SuccessResult<Output> | FailureResult;
783
- /** The result interface if validation succeeds. */
784
- interface SuccessResult<Output> {
785
- /** The typed output value. */
786
- readonly value: Output;
787
- /** The non-existent issues. */
788
- readonly issues?: undefined;
789
- }
790
- /** The result interface if validation fails. */
791
- interface FailureResult {
792
- /** The issues of failed validation. */
793
- readonly issues: ReadonlyArray<Issue>;
794
- }
795
- /** The issue interface of the failure output. */
796
- interface Issue {
797
- /** The error message of the issue. */
798
- readonly message: string;
799
- /** The path of the issue, if any. */
800
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
801
- }
802
- /** The path segment interface of the issue. */
803
- interface PathSegment {
804
- /** The key representing a path segment. */
805
- readonly key: PropertyKey;
806
- }
807
- /** The Standard Schema types interface. */
808
- interface Types<Input = unknown, Output = Input> {
809
- /** The input type of the schema. */
810
- readonly input: Input;
811
- /** The output type of the schema. */
812
- readonly output: Output;
813
- }
814
- /** Infers the input type of a Standard Schema. */
815
- type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
816
- /** Infers the output type of a Standard Schema. */
817
- type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
818
- }
819
-
820
822
  /**
821
823
  * A class for building and executing HTTP requests with customizable options and error handling.
822
824
  * @template Method The HTTP method type (GET, POST, etc.)
@@ -937,7 +939,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
937
939
  bodySchema<TSchema extends StandardSchemaV1>(schema: TSchema): Request<Method, TRequest, Omit<Opts, "bodySchema"> & {
938
940
  bodySchema: TSchema;
939
941
  error: Opts["error"] & {
940
- parseError: CustomError<"parseError", StandardSchemaV1.FailureResult["issues"]>;
942
+ schemaParseError: CustomError<"schemaParseError", StandardSchemaV1.FailureResult["issues"]>;
941
943
  };
942
944
  }>;
943
945
  /**
@@ -1209,7 +1211,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1209
1211
  schema<TSchema extends StandardSchemaV1>(schema: TSchema): Request<Method, TRequest, Merge<Omit<Opts, "schema">, {
1210
1212
  schema: TSchema;
1211
1213
  error: Merge<Opts["error"], {
1212
- parseError: CustomError<"parseError", StandardSchemaV1.FailureResult["issues"]>;
1214
+ schemaParseError: CustomError<"schemaParseError", StandardSchemaV1.FailureResult["issues"]>;
1213
1215
  }>;
1214
1216
  }>>;
1215
1217
  /**
@@ -1562,6 +1564,48 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1562
1564
  withResult: true;
1563
1565
  throwable: false;
1564
1566
  }>>;
1567
+ /**
1568
+ * Switches the request into **tuple** mode (the default).
1569
+ *
1570
+ * In tuple mode the response helpers (`json`, `text`, `blob`, …) resolve to a
1571
+ * tuple `[value, error]` where exactly one element is non‑null. This is useful
1572
+ * when you want an explicit opt‑in method to reset from `withResult()` or
1573
+ * `throwable()` back to the default tuple behaviour.
1574
+ *
1575
+ * Calling `withTuple` disables both Result mode and throwable mode.
1576
+ *
1577
+ * @returns {Request<
1578
+ * Method,
1579
+ * TRequest,
1580
+ * Merge<
1581
+ * Omit<Opts, 'withResult' | 'throwable'>,
1582
+ * {
1583
+ * withResult: false;
1584
+ * throwable: false;
1585
+ * }
1586
+ * >
1587
+ * >} The same {@link Request} instance, now typed with `withResult: false` and
1588
+ * `throwable: false` for fluent chaining.
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * const request = new Request('/users', config);
1593
+ *
1594
+ * const [user, err] = await request
1595
+ * .withTuple() // explicitly use tuple mode
1596
+ * .json<User>();
1597
+ *
1598
+ * if (err) {
1599
+ * console.error(err);
1600
+ * } else {
1601
+ * console.log(user);
1602
+ * }
1603
+ * ```
1604
+ */
1605
+ withTuple(): Request<Method, TRequest, Merge<Omit<Opts, "withResult" | "throwable">, {
1606
+ withResult: false;
1607
+ throwable: false;
1608
+ }>>;
1565
1609
  /**
1566
1610
  * Returns the underlying {@link AspiRequest} object that will be used for the fetch call.
1567
1611
  *
@@ -1965,6 +2009,19 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1965
2009
  withResult: true;
1966
2010
  throwable: false;
1967
2011
  }>>;
2012
+ /**
2013
+ * Configures all subsequent requests to return the default tuple `[value, error]`.
2014
+ *
2015
+ * This is the default behaviour, but `withTuple()` is provided as an explicit
2016
+ * reset when you have previously called {@link withResult} or {@link throwable}
2017
+ * on the {@link Aspi} instance.
2018
+ *
2019
+ * @returns The Aspi instance with tuple handling enabled.
2020
+ */
2021
+ withTuple(): Aspi<TRequest, Merge<Omit<Opts, "withResult" | "throwable">, {
2022
+ withResult: false;
2023
+ throwable: false;
2024
+ }>>;
1968
2025
  /**
1969
2026
  * Registers a capability on this {@link Aspi} instance.
1970
2027
  *
@@ -2005,4 +2062,4 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
2005
2062
  useCapability(capability: Capability<TRequest>): this;
2006
2063
  }
2007
2064
 
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 };
2065
+ 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, type SchemaParseError, getHttpErrorStatus, httpErrors, isAspiError, isCustomError, isJSONParseError, isSchemaParseError };
package/dist/index.js CHANGED
@@ -80,12 +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
83
  var isJSONParseError = (error) => {
87
84
  return error instanceof CustomError && error.tag === "jsonParseError";
88
85
  };
86
+ var isSchemaParseError = (error) => {
87
+ return error instanceof CustomError && error.tag === "schemaParseError";
88
+ };
89
89
 
90
90
  // src/result.ts
91
91
  var result_exports = {};
@@ -295,6 +295,8 @@ var Request = class {
295
295
  #timeoutMs;
296
296
  #shouldBeResult = false;
297
297
  #bodySchemaIssues = [];
298
+ #bodySchemaAsyncResult = null;
299
+ #bodySchemaAsyncBody = null;
298
300
  #throwOnError = false;
299
301
  #capabilities = [];
300
302
  constructor(method, path, requestOptions, capabilities = []) {
@@ -448,9 +450,9 @@ var Request = class {
448
450
  if (this.#bodySchema) {
449
451
  const data = this.#bodySchema["~standard"].validate(body);
450
452
  if (data instanceof Promise) {
451
- throw new Error("Schema validation should not return a promise");
452
- }
453
- if (data.issues) {
453
+ this.#bodySchemaAsyncResult = data;
454
+ this.#bodySchemaAsyncBody = body;
455
+ } else if (data.issues) {
454
456
  this.#bodySchemaIssues = data.issues;
455
457
  } else {
456
458
  this.#localRequestInit.body = JSON.stringify(data.value);
@@ -1128,6 +1130,49 @@ var Request = class {
1128
1130
  this.#shouldBeResult = true;
1129
1131
  return this;
1130
1132
  }
1133
+ /**
1134
+ * Switches the request into **tuple** mode (the default).
1135
+ *
1136
+ * In tuple mode the response helpers (`json`, `text`, `blob`, …) resolve to a
1137
+ * tuple `[value, error]` where exactly one element is non‑null. This is useful
1138
+ * when you want an explicit opt‑in method to reset from `withResult()` or
1139
+ * `throwable()` back to the default tuple behaviour.
1140
+ *
1141
+ * Calling `withTuple` disables both Result mode and throwable mode.
1142
+ *
1143
+ * @returns {Request<
1144
+ * Method,
1145
+ * TRequest,
1146
+ * Merge<
1147
+ * Omit<Opts, 'withResult' | 'throwable'>,
1148
+ * {
1149
+ * withResult: false;
1150
+ * throwable: false;
1151
+ * }
1152
+ * >
1153
+ * >} The same {@link Request} instance, now typed with `withResult: false` and
1154
+ * `throwable: false` for fluent chaining.
1155
+ *
1156
+ * @example
1157
+ * ```ts
1158
+ * const request = new Request('/users', config);
1159
+ *
1160
+ * const [user, err] = await request
1161
+ * .withTuple() // explicitly use tuple mode
1162
+ * .json<User>();
1163
+ *
1164
+ * if (err) {
1165
+ * console.error(err);
1166
+ * } else {
1167
+ * console.log(user);
1168
+ * }
1169
+ * ```
1170
+ */
1171
+ withTuple() {
1172
+ this.#throwOnError = false;
1173
+ this.#shouldBeResult = false;
1174
+ return this;
1175
+ }
1131
1176
  #mapResponse(value) {
1132
1177
  if (this.#shouldBeResult) {
1133
1178
  return value;
@@ -1145,8 +1190,20 @@ var Request = class {
1145
1190
  return response.ok || response.status >= 300 && response.status < 400;
1146
1191
  }
1147
1192
  async #makeRequest(responseParser, isJson = false) {
1193
+ if (this.#bodySchemaAsyncResult) {
1194
+ const data = await this.#bodySchemaAsyncResult;
1195
+ if (data.issues) {
1196
+ this.#bodySchemaIssues = data.issues;
1197
+ } else {
1198
+ this.#localRequestInit.body = JSON.stringify(data.value);
1199
+ }
1200
+ this.#bodySchemaAsyncResult = null;
1201
+ this.#bodySchemaAsyncBody = null;
1202
+ }
1148
1203
  if (this.#bodySchemaIssues.length) {
1149
- return err(new CustomError("parseError", this.#bodySchemaIssues));
1204
+ return err(
1205
+ new CustomError("schemaParseError", this.#bodySchemaIssues)
1206
+ );
1150
1207
  }
1151
1208
  const request = this.#request();
1152
1209
  const { retries, retryDelay, retryOn, retryWhile, onRetry } = this.#sanitisedRetryConfig();
@@ -1280,12 +1337,9 @@ var Request = class {
1280
1337
  );
1281
1338
  }
1282
1339
  if (isJson && this.#schema) {
1283
- const data = this.#schema["~standard"].validate(responseData);
1284
- if (data instanceof Promise) {
1285
- throw new Error("Schema validation should not return a promise");
1286
- }
1340
+ const data = await this.#schema["~standard"].validate(responseData);
1287
1341
  if (data.issues) {
1288
- return err(new CustomError("parseError", data.issues));
1342
+ return err(new CustomError("schemaParseError", data.issues));
1289
1343
  }
1290
1344
  return ok({
1291
1345
  data: data.value,
@@ -1495,7 +1549,7 @@ var Request = class {
1495
1549
  };
1496
1550
 
1497
1551
  // src/aspi.ts
1498
- var Aspi = class {
1552
+ var Aspi2 = class {
1499
1553
  #globalRequestInit;
1500
1554
  #middlewares = [];
1501
1555
  #retryConfig;
@@ -1855,6 +1909,20 @@ var Aspi = class {
1855
1909
  this.#throwOnError = false;
1856
1910
  return this;
1857
1911
  }
1912
+ /**
1913
+ * Configures all subsequent requests to return the default tuple `[value, error]`.
1914
+ *
1915
+ * This is the default behaviour, but `withTuple()` is provided as an explicit
1916
+ * reset when you have previously called {@link withResult} or {@link throwable}
1917
+ * on the {@link Aspi} instance.
1918
+ *
1919
+ * @returns The Aspi instance with tuple handling enabled.
1920
+ */
1921
+ withTuple() {
1922
+ this.#shouldBeResult = false;
1923
+ this.#throwOnError = false;
1924
+ return this;
1925
+ }
1858
1926
  /**
1859
1927
  * Registers a capability on this {@link Aspi} instance.
1860
1928
  *
@@ -1898,7 +1966,7 @@ var Aspi = class {
1898
1966
  }
1899
1967
  };
1900
1968
  export {
1901
- Aspi,
1969
+ Aspi2 as Aspi,
1902
1970
  AspiError,
1903
1971
  CustomError,
1904
1972
  Request,
@@ -1908,5 +1976,5 @@ export {
1908
1976
  isAspiError,
1909
1977
  isCustomError,
1910
1978
  isJSONParseError,
1911
- isParseError
1979
+ isSchemaParseError
1912
1980
  };
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.6.0",
4
+ "version": "2.8.0",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {