@molecule/app-http 1.2.1 → 1.4.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
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-18T17:46:02.937Z
6
+ Generated: 2026-10-19T22:40:45.495Z
7
7
  -->
8
8
 
9
9
  # @molecule/app-http
@@ -187,6 +187,58 @@ interface HttpClientConfig {
187
187
  * it returns.
188
188
  */
189
189
  authScheme?: string
190
+
191
+ /**
192
+ * Default retry policy for every request this client makes, or `false` to
193
+ * disable retries. A request's own `retry` overrides it.
194
+ */
195
+ retry?: RetryOptions | false
196
+ }
197
+ ```
198
+
199
+ #### `RetryOptions`
200
+
201
+ Tuning for the retry policy.
202
+
203
+ ```typescript
204
+ interface RetryOptions {
205
+ /**
206
+ * Maximum retries AFTER the first attempt. Defaults to `2` (three attempts
207
+ * in total).
208
+ */
209
+ maxRetries?: number
210
+
211
+ /** Base backoff in milliseconds. Defaults to `300`. */
212
+ baseDelayMs?: number
213
+
214
+ /** Maximum backoff (before `Retry-After` can extend it). Defaults to `5000`. */
215
+ maxDelayMs?: number
216
+
217
+ /** Statuses treated as transient. Defaults to `[502, 503, 504]`. */
218
+ retryStatuses?: number[]
219
+ }
220
+ ```
221
+
222
+ #### `RetryDecisionInput`
223
+
224
+ The request fields the policy decides from.
225
+
226
+ ```typescript
227
+ interface RetryDecisionInput {
228
+ /** The request method. */
229
+ method: HttpMethod
230
+ /** The request headers (lower-cased lookup is done here). */
231
+ headers: Record<string, string | undefined>
232
+ /** The HTTP status, or `0` when no response arrived. */
233
+ status: number
234
+ /** How many retries have already been made (0 for the first failure). */
235
+ attempt: number
236
+ /** The error message, used to recognize a connection reset/refusal. */
237
+ errorMessage?: string
238
+ /** `true` when the request was aborted before a response. */
239
+ aborted?: boolean
240
+ /** Overrides layered over {@link DEFAULT_RETRY_OPTIONS}. */
241
+ options?: RetryOptions | false
190
242
  }
191
243
  ```
192
244
 
@@ -264,6 +316,17 @@ interface RequestConfig {
264
316
  * Custom request options (implementation-specific).
265
317
  */
266
318
  options?: Record<string, unknown>
319
+
320
+ /**
321
+ * Retry policy for this request, or `false` to disable retries.
322
+ *
323
+ * The default policy retries only IDEMPOTENT requests (`GET`/`HEAD`/`PUT`/
324
+ * `DELETE`/`OPTIONS`, and `POST`/`PATCH` carrying an `Idempotency-Key`) on
325
+ * `502`/`503`/`504` or a reset/refused connection, twice, with exponential
326
+ * backoff and jitter, honoring `Retry-After`. See `@molecule/app-http`'s
327
+ * `shouldRetryRequest`.
328
+ */
329
+ retry?: RetryOptions | false
267
330
  }
268
331
  ````
269
332
 
@@ -401,14 +464,38 @@ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTION
401
464
 
402
465
  HTTP error with response details.
403
466
 
467
+ **`status` is an HTTP status when the server answered, and `0` when it did not.**
468
+ A `0` means NO response exists: the request was aborted (an `AbortController`, a
469
+ component's effect cleanup, a route change tearing the request down) or the
470
+ network failed before anything came back. It is not an HTTP status at all, so it
471
+ says nothing about the session — an app must never treat `status < 500` as
472
+ "signed out", because every aborted request on a route change would clear the
473
+ session. The correct check is {@link isAuthFailure} (`status === 401 || status === 403`);
474
+ `isNoResponse` (`status === 0`) identifies this case.
475
+
404
476
  ```typescript
405
477
  class HttpError extends Error {
478
+ /**
479
+ * Builds a failed-request error.
480
+ *
481
+ * @param message - Human-readable message (already translated where a key exists).
482
+ * @param status - HTTP status, or `0` when no response was received (aborted / network failure).
483
+ * @param response - Parsed response when the server answered, otherwise undefined.
484
+ * @param config - The request config that failed.
485
+ * @param errorKey - Translation key for the failure (`http.error.aborted` for an
486
+ * aborted request, `http.error.networkError` for a real network failure).
487
+ * @param aborted - Whether the request was aborted before a response: an
488
+ * `AbortController` signal (component cleanup, a cancelled request) or the
489
+ * browser tearing the fetch down during a navigation. Distinguishes an abort
490
+ * from a network outage on the same `status === 0`, without parsing the message.
491
+ */
406
492
  constructor(
407
493
  message: string,
408
494
  public status: number,
409
495
  public response?: HttpResponse,
410
496
  public config?: FullRequestConfig,
411
497
  public errorKey?: string,
498
+ public aborted = false,
412
499
  ) {
413
500
  super(message)
414
501
  this.name = 'HttpError'
@@ -468,6 +555,66 @@ function awaitAuthGate(request?: AuthGateRequest): Promise<void>
468
555
 
469
556
  **Returns:** A promise that settles when the gate has released the request.
470
557
 
558
+ #### `isHttpError(err)`
559
+
560
+ Whether `err` is an {@link HttpError} from this package.
561
+
562
+ ```typescript
563
+ function isHttpError(err: unknown): boolean
564
+ ```
565
+
566
+ - `err` — Any thrown value.
567
+
568
+ **Returns:** True for an `HttpError`.
569
+
570
+ #### `isAuthFailure(err)`
571
+
572
+ Whether the server REJECTED the request's credential — the only failure that
573
+ justifies clearing a session.
574
+
575
+ `401` (unauthenticated/expired token) and `403` (authenticated but refused) are
576
+ the two statuses that say "this credential no longer authorizes this session".
577
+ Everything else — `0` (aborted or offline), `404` (wrong endpoint), `5xx` (server
578
+ fault), a rate limit — says nothing about the credential.
579
+
580
+ ```typescript
581
+ function isAuthFailure(err: unknown): boolean
582
+ ```
583
+
584
+ - `err` — Any thrown value.
585
+
586
+ **Returns:** True only when `err` is an `HttpError` with status 401 or 403.
587
+
588
+ #### `isNoResponse(err)`
589
+
590
+ Whether no response was received at all: the request was aborted or the network
591
+ failed. `status === 0` is not an HTTP status — it must never be read as
592
+ "the user is not signed in".
593
+
594
+ ```typescript
595
+ function isNoResponse(err: unknown): boolean
596
+ ```
597
+
598
+ - `err` — Any thrown value.
599
+
600
+ **Returns:** True when `err` is an `HttpError` with status `0`.
601
+
602
+ #### `isAborted(err)`
603
+
604
+ Whether the request was ABORTED (an `AbortController` signal, or the browser
605
+ cancelling the fetch during a navigation) rather than failing on the network.
606
+
607
+ Both arrive as `status === 0`; this distinguishes them, so a UI can stay silent
608
+ for a cancelled request while still reporting a real outage.
609
+
610
+ ```typescript
611
+ function isAborted(err: unknown): boolean
612
+ ```
613
+
614
+ - `err` — Any thrown value.
615
+
616
+ **Returns:** True when `err` is an `HttpError` flagged `aborted`.
617
+
471
618
  #### `createFetchClient(config)`
472
619
 
473
620
  Creates a fetch-based HTTP client using the native Fetch API.
@@ -484,6 +631,83 @@ function createFetchClient(config?: HttpClientConfig): HttpClient
484
631
 
485
632
  **Returns:** A fully configured `HttpClient` instance.
486
633
 
634
+ #### `isIdempotentRequest(method, headers)`
635
+
636
+ Whether a request may be repeated at all.
637
+
638
+ ```typescript
639
+ function isIdempotentRequest(
640
+ method: HttpMethod,
641
+ headers: Record<string, string | undefined>,
642
+ ): boolean
643
+ ```
644
+
645
+ - `method` — The HTTP method.
646
+ - `headers` — The request headers (an `Idempotency-Key` makes POST/PATCH repeatable).
647
+
648
+ **Returns:** `true` when repeating the request cannot duplicate a side effect.
649
+
650
+ #### `isTransientConnectionError(message)`
651
+
652
+ Whether a connection-level error is transient (a restarted process refuses
653
+ or resets connections; a DNS or TLS configuration error does not).
654
+
655
+ ```typescript
656
+ function isTransientConnectionError(message: string | undefined): boolean
657
+ ```
658
+
659
+ - `message` — The error message.
660
+
661
+ **Returns:** `true` for reset/refused/closed-early/timeout shapes.
662
+
663
+ #### `parseRetryAfter(value, now)`
664
+
665
+ Parse a `Retry-After` header into milliseconds from now.
666
+
667
+ ```typescript
668
+ function parseRetryAfter(value: string | undefined, now?: number): number | undefined
669
+ ```
670
+
671
+ - `value` — The header value (`seconds` or an HTTP date).
672
+ - `now` — The clock, injected for tests.
673
+
674
+ **Returns:** Milliseconds to wait, or undefined when absent/unparseable.
675
+
676
+ #### `shouldRetryRequest(input)`
677
+
678
+ Decide whether one failed request should be retried.
679
+
680
+ ```typescript
681
+ function shouldRetryRequest(input: RetryDecisionInput): boolean
682
+ ```
683
+
684
+ - `input` — The method, headers, status/error and attempt count.
685
+
686
+ **Returns:** `true` when the request may be repeated.
687
+
688
+ #### `retryDelayMs(retryAfter, attempt, options, random)`
689
+
690
+ Compute the wait before the next attempt.
691
+
692
+ `Retry-After` wins when the server sent one (it knows when the instance will
693
+ be back); otherwise exponential backoff with full jitter.
694
+
695
+ ```typescript
696
+ function retryDelayMs(
697
+ retryAfter: string | undefined,
698
+ attempt: number,
699
+ options?: false | RetryOptions,
700
+ random?: () => number,
701
+ ): number
702
+ ```
703
+
704
+ - `retryAfter` — The raw `Retry-After` header value, when present.
705
+ - `attempt` — How many retries have already been made (0 for the first).
706
+ - `options` — Policy overrides.
707
+ - `random` — Jitter source, injected for tests (defaults to `Math.random`).
708
+
709
+ **Returns:** Milliseconds to wait before retrying.
710
+
487
711
  #### `unwrapList(res)`
488
712
 
489
713
  Normalize an unknown response body into a typed array.
@@ -507,6 +731,11 @@ r.json())`) or the `HttpResponse` returned by `useHttpClient().get(...)`.
507
731
  Both shapes are handled here so pages don't have to remember to call
508
732
  `unwrapList(res.data)` vs `unwrapList(res)`.
509
733
 
734
+ A custom envelope such as `{ items: [...], count: 10 }` is not a `data`
735
+ envelope and returns `[]`. Read its named array directly: `body.items` for
736
+ raw JSON, or `response.data.items` for an `HttpResponse`. Passing that
737
+ array to `unwrapList` is also valid; passing the enclosing object is not.
738
+
510
739
  ```typescript
511
740
  function unwrapList(res: unknown): T[]
512
741
  ```
@@ -665,6 +894,14 @@ function del(url: string, config?: RequestConfig): Promise<HttpResponse<T>>
665
894
 
666
895
  ### Constants
667
896
 
897
+ #### `DEFAULT_RETRY_OPTIONS`
898
+
899
+ The resolved policy used when a caller passes no options.
900
+
901
+ ```typescript
902
+ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions>
903
+ ```
904
+
668
905
  #### `fetchClient`
669
906
 
670
907
  Pre-created fetch client for environments where `fetch` is available.
@@ -751,6 +988,17 @@ skipAuthGate: true })`. Without the opt-out the gate waits on itself, the restor
751
988
  settles, and every request the gate holds stalls until it times out (3 s).
752
989
  `@molecule/app-auth`'s own restore does not need it (it calls `fetch`, not this client).
753
990
 
991
+ **A failed request's `status` is `0` when NO response arrived — never read it as
992
+ "signed out".** An aborted request (an `AbortController` signal, a component's
993
+ effect cleanup, a route change tearing the fetch down) and a network outage both
994
+ throw an `HttpError` with `status === 0`; the aborted ones also carry
995
+ `aborted: true` and the `http.error.aborted` key, the rest `http.error.networkError`.
996
+ Neither says anything about the session. To decide whether a session ended, use
997
+ `isAuthFailure(err)` (401/403 only); `isNoResponse(err)` identifies the no-response
998
+ case so a caller can stay silent instead of reporting an outage. **Do not write
999
+ `err instanceof HttpError && err.status < 500` as a sign-out check** — it clears
1000
+ the session on every aborted navigation request.
1001
+
754
1002
  ## Translations
755
1003
 
756
1004
  Translation strings are provided by `@molecule/app-locales-http`.
package/dist/client.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * @module
7
7
  */
8
8
  import type { ErrorInterceptor, RequestInterceptor, ResponseInterceptor } from './interceptors.js';
9
+ import type { RetryOptions } from './retry.js';
9
10
  import type { FullRequestConfig, HttpResponse, RequestConfig } from './types.js';
10
11
  /**
11
12
  * HTTP client interface.
@@ -109,5 +110,10 @@ export interface HttpClientConfig {
109
110
  * it returns.
110
111
  */
111
112
  authScheme?: string;
113
+ /**
114
+ * Default retry policy for every request this client makes, or `false` to
115
+ * disable retries. A request's own `retry` overrides it.
116
+ */
117
+ retry?: RetryOptions | false;
112
118
  }
113
119
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhF;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEtC;;;OAGG;IACH,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEzE;;OAEG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAE/E;;;;OAIG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhG;;;OAGG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAE/F;;;OAGG;IACH,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEjG;;OAEG;IACH,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAElF;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAA;IAElE;;;OAGG;IACH,sBAAsB,CAAC,WAAW,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAAA;IAEpE;;;OAGG;IACH,mBAAmB,CAAC,WAAW,EAAE,gBAAgB,GAAG,MAAM,IAAI,CAAA;IAE9D;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAExC;;OAEG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI,CAAA;IAE7B;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC7C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEvC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAClG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhF;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEtC;;;OAGG;IACH,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEzE;;OAEG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAE/E;;;;OAIG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhG;;;OAGG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAE/F;;;OAGG;IACH,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAEjG;;OAEG;IACH,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAElF;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAA;IAElE;;;OAGG;IACH,sBAAsB,CAAC,WAAW,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAAA;IAEpE;;;OAGG;IACH,mBAAmB,CAAC,WAAW,EAAE,gBAAgB,GAAG,MAAM,IAAI,CAAA;IAE9D;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAExC;;OAEG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI,CAAA;IAE7B;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC7C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEvC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,KAAK,CAAC,EAAE,YAAY,GAAG,KAAK,CAAA;CAC7B"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Classify a failed request instead of reading its `status` by hand.
3
+ *
4
+ * A failure from this package is an {@link HttpError} whose `status` is either an
5
+ * HTTP status (the server answered) or `0` (it did not — the request was aborted or
6
+ * the network failed). Code that clears a session must ask "did the server REJECT
7
+ * the credential?", which is `status === 401 || status === 403` and nothing else:
8
+ *
9
+ * ```typescript
10
+ * try {
11
+ * await http.get('/projects')
12
+ * } catch (err) {
13
+ * if (isAuthFailure(err)) clearSession()
14
+ * else if (isNoResponse(err)) return // aborted / offline — keep the session
15
+ * else showError(err)
16
+ * }
17
+ * ```
18
+ *
19
+ * `err instanceof HttpError && err.status < 500` is the trap these helpers exist to
20
+ * replace: an aborted request (a component unmounting, a route change cancelling an
21
+ * in-flight fetch) and a network outage both arrive with `status === 0`, so that
22
+ * check signs the user out on every navigation that cancels a request.
23
+ *
24
+ * @module
25
+ */
26
+ import { HttpError } from './types.js';
27
+ /**
28
+ * Whether `err` is an {@link HttpError} from this package.
29
+ *
30
+ * @param err - Any thrown value.
31
+ * @returns True for an `HttpError`.
32
+ */
33
+ export declare function isHttpError(err: unknown): err is HttpError;
34
+ /**
35
+ * Whether the server REJECTED the request's credential — the only failure that
36
+ * justifies clearing a session.
37
+ *
38
+ * `401` (unauthenticated/expired token) and `403` (authenticated but refused) are
39
+ * the two statuses that say "this credential no longer authorizes this session".
40
+ * Everything else — `0` (aborted or offline), `404` (wrong endpoint), `5xx` (server
41
+ * fault), a rate limit — says nothing about the credential.
42
+ *
43
+ * @param err - Any thrown value.
44
+ * @returns True only when `err` is an `HttpError` with status 401 or 403.
45
+ */
46
+ export declare function isAuthFailure(err: unknown): boolean;
47
+ /**
48
+ * Whether no response was received at all: the request was aborted or the network
49
+ * failed. `status === 0` is not an HTTP status — it must never be read as
50
+ * "the user is not signed in".
51
+ *
52
+ * @param err - Any thrown value.
53
+ * @returns True when `err` is an `HttpError` with status `0`.
54
+ */
55
+ export declare function isNoResponse(err: unknown): boolean;
56
+ /**
57
+ * Whether the request was ABORTED (an `AbortController` signal, or the browser
58
+ * cancelling the fetch during a navigation) rather than failing on the network.
59
+ *
60
+ * Both arrive as `status === 0`; this distinguishes them, so a UI can stay silent
61
+ * for a cancelled request while still reporting a real outage.
62
+ *
63
+ * @param err - Any thrown value.
64
+ * @returns True when `err` is an `HttpError` flagged `aborted`.
65
+ */
66
+ export declare function isAborted(err: unknown): boolean;
67
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtC;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,SAAS,CAE1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAElD;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE/C"}
package/dist/errors.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Classify a failed request instead of reading its `status` by hand.
3
+ *
4
+ * A failure from this package is an {@link HttpError} whose `status` is either an
5
+ * HTTP status (the server answered) or `0` (it did not — the request was aborted or
6
+ * the network failed). Code that clears a session must ask "did the server REJECT
7
+ * the credential?", which is `status === 401 || status === 403` and nothing else:
8
+ *
9
+ * ```typescript
10
+ * try {
11
+ * await http.get('/projects')
12
+ * } catch (err) {
13
+ * if (isAuthFailure(err)) clearSession()
14
+ * else if (isNoResponse(err)) return // aborted / offline — keep the session
15
+ * else showError(err)
16
+ * }
17
+ * ```
18
+ *
19
+ * `err instanceof HttpError && err.status < 500` is the trap these helpers exist to
20
+ * replace: an aborted request (a component unmounting, a route change cancelling an
21
+ * in-flight fetch) and a network outage both arrive with `status === 0`, so that
22
+ * check signs the user out on every navigation that cancels a request.
23
+ *
24
+ * @module
25
+ */
26
+ import { HttpError } from './types.js';
27
+ /**
28
+ * Whether `err` is an {@link HttpError} from this package.
29
+ *
30
+ * @param err - Any thrown value.
31
+ * @returns True for an `HttpError`.
32
+ */
33
+ export function isHttpError(err) {
34
+ return err instanceof HttpError;
35
+ }
36
+ /**
37
+ * Whether the server REJECTED the request's credential — the only failure that
38
+ * justifies clearing a session.
39
+ *
40
+ * `401` (unauthenticated/expired token) and `403` (authenticated but refused) are
41
+ * the two statuses that say "this credential no longer authorizes this session".
42
+ * Everything else — `0` (aborted or offline), `404` (wrong endpoint), `5xx` (server
43
+ * fault), a rate limit — says nothing about the credential.
44
+ *
45
+ * @param err - Any thrown value.
46
+ * @returns True only when `err` is an `HttpError` with status 401 or 403.
47
+ */
48
+ export function isAuthFailure(err) {
49
+ return isHttpError(err) && (err.status === 401 || err.status === 403);
50
+ }
51
+ /**
52
+ * Whether no response was received at all: the request was aborted or the network
53
+ * failed. `status === 0` is not an HTTP status — it must never be read as
54
+ * "the user is not signed in".
55
+ *
56
+ * @param err - Any thrown value.
57
+ * @returns True when `err` is an `HttpError` with status `0`.
58
+ */
59
+ export function isNoResponse(err) {
60
+ return isHttpError(err) && err.status === 0;
61
+ }
62
+ /**
63
+ * Whether the request was ABORTED (an `AbortController` signal, or the browser
64
+ * cancelling the fetch during a navigation) rather than failing on the network.
65
+ *
66
+ * Both arrive as `status === 0`; this distinguishes them, so a UI can stay silent
67
+ * for a cancelled request while still reporting a real outage.
68
+ *
69
+ * @param err - Any thrown value.
70
+ * @returns True when `err` is an `HttpError` flagged `aborted`.
71
+ */
72
+ export function isAborted(err) {
73
+ return isHttpError(err) && err.aborted;
74
+ }
75
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,OAAO,GAAG,YAAY,SAAS,CAAA;AACjC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,CAAA;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,CAAA;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,GAAY;IACpC,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAA;AACxC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -88,12 +88,25 @@
88
88
  * settles, and every request the gate holds stalls until it times out (3 s).
89
89
  * `@molecule/app-auth`'s own restore does not need it (it calls `fetch`, not this client).
90
90
  *
91
+ * **A failed request's `status` is `0` when NO response arrived — never read it as
92
+ * "signed out".** An aborted request (an `AbortController` signal, a component's
93
+ * effect cleanup, a route change tearing the fetch down) and a network outage both
94
+ * throw an `HttpError` with `status === 0`; the aborted ones also carry
95
+ * `aborted: true` and the `http.error.aborted` key, the rest `http.error.networkError`.
96
+ * Neither says anything about the session. To decide whether a session ended, use
97
+ * `isAuthFailure(err)` (401/403 only); `isNoResponse(err)` identifies the no-response
98
+ * case so a caller can stay silent instead of reporting an outage. **Do not write
99
+ * `err instanceof HttpError && err.status < 500` as a sign-out check** — it clears
100
+ * the session on every aborted navigation request.
101
+ *
91
102
  * @module
92
103
  */
93
104
  export * from './auth-gate.js';
94
105
  export * from './client.js';
106
+ export * from './errors.js';
95
107
  export * from './interceptors.js';
96
108
  export * from './provider.js';
109
+ export * from './retry.js';
97
110
  export * from './types.js';
98
111
  export * from './unwrap.js';
99
112
  export * from './utilities.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -88,12 +88,25 @@
88
88
  * settles, and every request the gate holds stalls until it times out (3 s).
89
89
  * `@molecule/app-auth`'s own restore does not need it (it calls `fetch`, not this client).
90
90
  *
91
+ * **A failed request's `status` is `0` when NO response arrived — never read it as
92
+ * "signed out".** An aborted request (an `AbortController` signal, a component's
93
+ * effect cleanup, a route change tearing the fetch down) and a network outage both
94
+ * throw an `HttpError` with `status === 0`; the aborted ones also carry
95
+ * `aborted: true` and the `http.error.aborted` key, the rest `http.error.networkError`.
96
+ * Neither says anything about the session. To decide whether a session ended, use
97
+ * `isAuthFailure(err)` (401/403 only); `isNoResponse(err)` identifies the no-response
98
+ * case so a caller can stay silent instead of reporting an outage. **Do not write
99
+ * `err instanceof HttpError && err.status < 500` as a sign-out check** — it clears
100
+ * the session on every aborted navigation request.
101
+ *
91
102
  * @module
92
103
  */
93
104
  export * from './auth-gate.js';
94
105
  export * from './client.js';
106
+ export * from './errors.js';
95
107
  export * from './interceptors.js';
96
108
  export * from './provider.js';
109
+ export * from './retry.js';
97
110
  export * from './types.js';
98
111
  export * from './unwrap.js';
99
112
  export * from './utilities.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAQ/D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAQ,gBAAqB,KAAG,UAwXjE,CAAA"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAc/D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAQ,gBAAqB,KAAG,UA8ajE,CAAA"}