aspi 2.7.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,
@@ -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,6 +1220,16 @@ 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
1234
  return err(
1180
1235
  new CustomError("schemaParseError", this.#bodySchemaIssues)
@@ -1312,10 +1367,7 @@ var Request = class {
1312
1367
  );
1313
1368
  }
1314
1369
  if (isJson && this.#schema) {
1315
- const data = this.#schema["~standard"].validate(responseData);
1316
- if (data instanceof Promise) {
1317
- throw new Error("Schema validation should not return a promise");
1318
- }
1370
+ const data = await this.#schema["~standard"].validate(responseData);
1319
1371
  if (data.issues) {
1320
1372
  return err(new CustomError("schemaParseError", data.issues));
1321
1373
  }
@@ -1527,7 +1579,7 @@ var Request = class {
1527
1579
  };
1528
1580
 
1529
1581
  // src/aspi.ts
1530
- var Aspi = class {
1582
+ var Aspi2 = class {
1531
1583
  #globalRequestInit;
1532
1584
  #middlewares = [];
1533
1585
  #retryConfig;
@@ -1887,6 +1939,20 @@ var Aspi = class {
1887
1939
  this.#throwOnError = false;
1888
1940
  return this;
1889
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
+ }
1890
1956
  /**
1891
1957
  * Registers a capability on this {@link Aspi} instance.
1892
1958
  *
package/dist/index.d.cts CHANGED
@@ -1564,6 +1564,48 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1564
1564
  withResult: true;
1565
1565
  throwable: false;
1566
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
+ }>>;
1567
1609
  /**
1568
1610
  * Returns the underlying {@link AspiRequest} object that will be used for the fetch call.
1569
1611
  *
@@ -1967,6 +2009,19 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1967
2009
  withResult: true;
1968
2010
  throwable: false;
1969
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
+ }>>;
1970
2025
  /**
1971
2026
  * Registers a capability on this {@link Aspi} instance.
1972
2027
  *
package/dist/index.d.ts CHANGED
@@ -1564,6 +1564,48 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
1564
1564
  withResult: true;
1565
1565
  throwable: false;
1566
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
+ }>>;
1567
1609
  /**
1568
1610
  * Returns the underlying {@link AspiRequest} object that will be used for the fetch call.
1569
1611
  *
@@ -1967,6 +2009,19 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
1967
2009
  withResult: true;
1968
2010
  throwable: false;
1969
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
+ }>>;
1970
2025
  /**
1971
2026
  * Registers a capability on this {@link Aspi} instance.
1972
2027
  *
package/dist/index.js CHANGED
@@ -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,6 +1190,16 @@ 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
1204
  return err(
1150
1205
  new CustomError("schemaParseError", this.#bodySchemaIssues)
@@ -1282,10 +1337,7 @@ var Request = class {
1282
1337
  );
1283
1338
  }
1284
1339
  if (isJson && this.#schema) {
1285
- const data = this.#schema["~standard"].validate(responseData);
1286
- if (data instanceof Promise) {
1287
- throw new Error("Schema validation should not return a promise");
1288
- }
1340
+ const data = await this.#schema["~standard"].validate(responseData);
1289
1341
  if (data.issues) {
1290
1342
  return err(new CustomError("schemaParseError", data.issues));
1291
1343
  }
@@ -1497,7 +1549,7 @@ var Request = class {
1497
1549
  };
1498
1550
 
1499
1551
  // src/aspi.ts
1500
- var Aspi = class {
1552
+ var Aspi2 = class {
1501
1553
  #globalRequestInit;
1502
1554
  #middlewares = [];
1503
1555
  #retryConfig;
@@ -1857,6 +1909,20 @@ var Aspi = class {
1857
1909
  this.#throwOnError = false;
1858
1910
  return this;
1859
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
+ }
1860
1926
  /**
1861
1927
  * Registers a capability on this {@link Aspi} instance.
1862
1928
  *
@@ -1900,7 +1966,7 @@ var Aspi = class {
1900
1966
  }
1901
1967
  };
1902
1968
  export {
1903
- Aspi,
1969
+ Aspi2 as Aspi,
1904
1970
  AspiError,
1905
1971
  CustomError,
1906
1972
  Request,
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.7.0",
4
+ "version": "2.8.0",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {