@theholocron/http-client 1.56.0 → 1.57.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
@@ -62,6 +62,20 @@ const client = createRestClient({ baseUrl: "...", token: "...", logger, errors }
62
62
  typically expected and already handled by the caller (a 404 from a
63
63
  "does this exist" check, etc.).
64
64
 
65
+ ### `onRetry(listener)`
66
+
67
+ Hear about every retry any client in this process is about to make, so a command line can say "retrying GitHub, attempt 2 of 3" instead of sitting silent. Returns a function that stops listening.
68
+
69
+ ```ts
70
+ const stop = onRetry(({ vendor, attempt, of, code, delayMs }) => {
71
+ console.error(`retrying ${vendor}, attempt ${attempt} of ${of} (${code}, in ${delayMs} ms)`);
72
+ });
73
+ // …requests…
74
+ stop();
75
+ ```
76
+
77
+ It is per process, not per client, so nothing has to be handed to each client. It needs one copy of this package in the process (a workspace pins it); with two, the other copy's retries go unseen. A listener that throws is ignored.
78
+
65
79
  ### `createResolveToken(config)`
66
80
 
67
81
  Factory for the standard token resolution used by all holocron plugins. Resolution order:
package/dist/index.d.mts CHANGED
@@ -117,4 +117,30 @@ interface RestClient {
117
117
  }
118
118
  declare function createRestClient(config: RestClientConfig): RestClient;
119
119
  //#endregion
120
- export { AuthError, ProviderApiError, type RequestOptions, type ResolveTokenConfig, type ResolveTokenInput, type RestClient, type RestClientConfig, createResolveToken, createRestClient };
120
+ //#region src/retry-events.d.ts
121
+ /** A request about to be sent again after a transport failure. */
122
+ interface RetryEvent {
123
+ /** The client's `vendor` label, e.g. `GitHub`. */
124
+ vendor?: string;
125
+ method: string;
126
+ path: string;
127
+ /** What went wrong (`ECONNRESET`, `UND_ERR_CONNECT_TIMEOUT`); a failure with no known cause is never retried. */
128
+ code: string;
129
+ /** The number of the send that is about to happen: 2 for the first retry. */
130
+ attempt: number;
131
+ /** How many sends there can be in all (the retries plus the first). */
132
+ of: number;
133
+ /** How long it waits before sending. */
134
+ delayMs: number;
135
+ }
136
+ /**
137
+ * Hear about every retry any client in this process is about to make, so a command line can say "retrying GitHub,
138
+ * attempt 2 of 3" instead of sitting silent. Returns the way to stop listening. A listener that throws is ignored: a
139
+ * display problem must not fail a request.
140
+ *
141
+ * This is per process, not per client, so a command need not hand a hook to every client it builds. It relies on there
142
+ * being one copy of this package, which the workspace already requires; with two, retries by the other copy go unseen.
143
+ */
144
+ declare function onRetry(listener: (event: RetryEvent) => void): () => void;
145
+ //#endregion
146
+ export { AuthError, ProviderApiError, type RequestOptions, type ResolveTokenConfig, type ResolveTokenInput, type RestClient, type RestClientConfig, type RetryEvent, createResolveToken, createRestClient, onRetry };
package/dist/index.mjs CHANGED
@@ -47,6 +47,26 @@ var ProviderApiError = class extends Error {
47
47
  }
48
48
  };
49
49
  //#endregion
50
+ //#region src/retry-events.ts
51
+ const listeners = /* @__PURE__ */ new Set();
52
+ /**
53
+ * Hear about every retry any client in this process is about to make, so a command line can say "retrying GitHub,
54
+ * attempt 2 of 3" instead of sitting silent. Returns the way to stop listening. A listener that throws is ignored: a
55
+ * display problem must not fail a request.
56
+ *
57
+ * This is per process, not per client, so a command need not hand a hook to every client it builds. It relies on there
58
+ * being one copy of this package, which the workspace already requires; with two, retries by the other copy go unseen.
59
+ */
60
+ function onRetry(listener) {
61
+ listeners.add(listener);
62
+ return () => void listeners.delete(listener);
63
+ }
64
+ function notifyRetry(event) {
65
+ for (const listener of [...listeners]) try {
66
+ listener(event);
67
+ } catch {}
68
+ }
69
+ //#endregion
50
70
  //#region src/transport.ts
51
71
  /**
52
72
  * Node's `fetch` rejects with a bare `TypeError: fetch failed`; the reason (a DNS miss, a connect timeout, a reset) is on
@@ -138,7 +158,17 @@ function createRestClient(config) {
138
158
  code,
139
159
  attempt: attempt + 1
140
160
  }, "retrying");
141
- await new Promise((resolve) => setTimeout(resolve, retryDelay * 3 ** attempt));
161
+ const delayMs = retryDelay * 3 ** attempt;
162
+ notifyRetry({
163
+ ...vendor ? { vendor } : {},
164
+ method: init.method,
165
+ path,
166
+ code,
167
+ attempt: attempt + 2,
168
+ of: maxRetries + 1,
169
+ delayMs
170
+ });
171
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
142
172
  continue;
143
173
  }
144
174
  const apiErr = new ProviderApiError(`${tag} failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}${code ? ` (${code})` : ""}`, 0, void 0);
@@ -165,4 +195,4 @@ function createRestClient(config) {
165
195
  };
166
196
  }
167
197
  //#endregion
168
- export { AuthError, ProviderApiError, createResolveToken, createRestClient };
198
+ export { AuthError, ProviderApiError, createResolveToken, createRestClient, onRetry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/http-client",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "Shared HTTP primitives for theholocron — REST client factory, auth resolver, and error types",
5
5
  "keywords": [
6
6
  "typescript",