@zerotal/client 1.8.1 → 1.10.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/CHANGELOG.md +10 -0
- package/api-surface.md +86 -86
- package/package.json +2 -2
- package/src/ApiClient.ts +28 -26
- package/src/CircuitBreaker.ts +3 -3
- package/src/Socket.ts +35 -27
- package/src/config.ts +1 -1
- package/src/http.ts +13 -9
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,16 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.9.0] — 2026-08-29
|
|
12
|
+
|
|
13
|
+
### Documented
|
|
14
|
+
|
|
15
|
+
- **Every promised export is documented.** The `docs-coverage` gate reads `maturity: stable` as a
|
|
16
|
+
promise about a package's exports, and measures how much of that promise is written down. It
|
|
17
|
+
was 798 gaps across the suite; it is now zero. This package's share is covered on its own
|
|
18
|
+
pages — types named, options shapes described, and the decisions behind them recorded where
|
|
19
|
+
somebody looking for them will find them.
|
|
20
|
+
|
|
11
21
|
## [1.7.2] — 2026-08-18
|
|
12
22
|
|
|
13
23
|
### Fixed
|
package/api-surface.md
CHANGED
|
@@ -127,64 +127,64 @@ function ClientConfig = (options?: Partial<ClientConfigShape>) => Partial<Client
|
|
|
127
127
|
function createApiClient = <Routes extends ApiRouteMap>(config?: ApiClientConfig) => ApiClient<Routes>
|
|
128
128
|
|
|
129
129
|
interface ApiClientConfig = {
|
|
130
|
-
baseUrl?: string
|
|
131
|
-
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions
|
|
132
|
-
csrf?: boolean | { cookie?: string; header?: string;}
|
|
133
|
-
headers?: Record<string, string>
|
|
134
|
-
onError?: (error: ApiClientError) => void | Promise<void>
|
|
135
|
-
onForbidden?: (error: ApiClientError) => void | Promise<void>
|
|
136
|
-
onRequest?: RequestInterceptor | RequestInterceptor[]
|
|
137
|
-
onResponse?: ResponseInterceptor | ResponseInterceptor[]
|
|
138
|
-
onUnauthorized?: (error: ApiClientError, retry: (headerOverrides?: Record<string, string>) => Promise<unknown>) => Promise<unknown> | void
|
|
139
|
-
retry?: number | RetryOptions
|
|
140
|
-
timeout?: number
|
|
141
|
-
token?: TokenSource
|
|
142
|
-
withCredentials?: boolean
|
|
130
|
+
baseUrl?: string | undefined
|
|
131
|
+
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions | undefined
|
|
132
|
+
csrf?: boolean | { cookie?: string; header?: string;} | undefined
|
|
133
|
+
headers?: Record<string, string> | undefined
|
|
134
|
+
onError?: ((error: ApiClientError) => void | Promise<void>) | undefined
|
|
135
|
+
onForbidden?: ((error: ApiClientError) => void | Promise<void>) | undefined
|
|
136
|
+
onRequest?: RequestInterceptor | RequestInterceptor[] | undefined
|
|
137
|
+
onResponse?: ResponseInterceptor | ResponseInterceptor[] | undefined
|
|
138
|
+
onUnauthorized?: ((error: ApiClientError, retry: (headerOverrides?: Record<string, string>) => Promise<unknown>) => Promise<unknown> | void) | undefined
|
|
139
|
+
retry?: number | RetryOptions | undefined
|
|
140
|
+
timeout?: number | undefined
|
|
141
|
+
token?: TokenSource | undefined
|
|
142
|
+
withCredentials?: boolean | undefined
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
interface CircuitBreakerOptions = {
|
|
146
|
-
isFailure?: (error: unknown) => boolean
|
|
147
|
-
resetTimeout?: number
|
|
148
|
-
threshold?: number
|
|
146
|
+
isFailure?: ((error: unknown) => boolean) | undefined
|
|
147
|
+
resetTimeout?: number | undefined
|
|
148
|
+
threshold?: number | undefined
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
interface ClientConfigShape = {
|
|
152
|
-
baseUrl?: string
|
|
153
|
-
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions
|
|
154
|
-
csrf?: boolean | { cookie?: string; header?: string;}
|
|
155
|
-
headers?: Record<string, string>
|
|
156
|
-
onError?: (error: ApiClientError) => void | Promise<void>
|
|
157
|
-
onForbidden?: (error: ApiClientError) => void | Promise<void>
|
|
158
|
-
onRequest?: RequestInterceptor | RequestInterceptor[]
|
|
159
|
-
onResponse?: ResponseInterceptor | ResponseInterceptor[]
|
|
160
|
-
onUnauthorized?: (error: ApiClientError, retry: (headerOverrides?: Record<string, string>) => Promise<unknown>) => Promise<unknown> | void
|
|
161
|
-
retry?: number | RetryOptions
|
|
162
|
-
timeout?: number
|
|
163
|
-
token?: TokenSource
|
|
164
|
-
withCredentials?: boolean
|
|
152
|
+
baseUrl?: string | undefined
|
|
153
|
+
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions | undefined
|
|
154
|
+
csrf?: boolean | { cookie?: string; header?: string;} | undefined
|
|
155
|
+
headers?: Record<string, string> | undefined
|
|
156
|
+
onError?: ((error: ApiClientError) => void | Promise<void>) | undefined
|
|
157
|
+
onForbidden?: ((error: ApiClientError) => void | Promise<void>) | undefined
|
|
158
|
+
onRequest?: RequestInterceptor | RequestInterceptor[] | undefined
|
|
159
|
+
onResponse?: ResponseInterceptor | ResponseInterceptor[] | undefined
|
|
160
|
+
onUnauthorized?: ((error: ApiClientError, retry: (headerOverrides?: Record<string, string>) => Promise<unknown>) => Promise<unknown> | void) | undefined
|
|
161
|
+
retry?: number | RetryOptions | undefined
|
|
162
|
+
timeout?: number | undefined
|
|
163
|
+
token?: TokenSource | undefined
|
|
164
|
+
withCredentials?: boolean | undefined
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
interface GetOptions = {
|
|
168
|
-
headers?: Record<string, string>
|
|
169
|
-
init?: Omit<RequestInit, 'method' | 'headers' | 'body'>
|
|
170
|
-
meta?: (meta: ResponseMeta) => void
|
|
171
|
-
query?: Q
|
|
172
|
-
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer'
|
|
173
|
-
retry?: number | false | RetryOptions
|
|
174
|
-
signal?: AbortSignal
|
|
175
|
-
timeout?: number
|
|
168
|
+
headers?: Record<string, string> | undefined
|
|
169
|
+
init?: Omit<RequestInit, 'method' | 'headers' | 'body'> | undefined
|
|
170
|
+
meta?: ((meta: ResponseMeta) => void) | undefined
|
|
171
|
+
query?: Q | undefined
|
|
172
|
+
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer' | undefined
|
|
173
|
+
retry?: number | false | RetryOptions | undefined
|
|
174
|
+
signal?: AbortSignal | undefined
|
|
175
|
+
timeout?: number | undefined
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
interface MutationOptions = {
|
|
179
|
-
headers?: Record<string, string>
|
|
180
|
-
init?: Omit<RequestInit, 'method' | 'headers' | 'body'>
|
|
181
|
-
meta?: (meta: ResponseMeta) => void
|
|
182
|
-
params?: Record<string, string | number>
|
|
183
|
-
query?: Q
|
|
184
|
-
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer'
|
|
185
|
-
retry?: number | false | RetryOptions
|
|
186
|
-
signal?: AbortSignal
|
|
187
|
-
timeout?: number
|
|
179
|
+
headers?: Record<string, string> | undefined
|
|
180
|
+
init?: Omit<RequestInit, 'method' | 'headers' | 'body'> | undefined
|
|
181
|
+
meta?: ((meta: ResponseMeta) => void) | undefined
|
|
182
|
+
params?: Record<string, string | number> | undefined
|
|
183
|
+
query?: Q | undefined
|
|
184
|
+
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer' | undefined
|
|
185
|
+
retry?: number | false | RetryOptions | undefined
|
|
186
|
+
signal?: AbortSignal | undefined
|
|
187
|
+
timeout?: number | undefined
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
interface PresenceMember = {
|
|
@@ -200,13 +200,13 @@ interface RequestConfig = {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
interface RequestOptions = {
|
|
203
|
-
headers?: Record<string, string>
|
|
204
|
-
init?: Omit<RequestInit, 'method' | 'headers' | 'body'>
|
|
205
|
-
meta?: (meta: ResponseMeta) => void
|
|
206
|
-
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer'
|
|
207
|
-
retry?: number | false | RetryOptions
|
|
208
|
-
signal?: AbortSignal
|
|
209
|
-
timeout?: number
|
|
203
|
+
headers?: Record<string, string> | undefined
|
|
204
|
+
init?: Omit<RequestInit, 'method' | 'headers' | 'body'> | undefined
|
|
205
|
+
meta?: ((meta: ResponseMeta) => void) | undefined
|
|
206
|
+
responseType?: 'text' | 'json' | 'auto' | 'blob' | 'arrayBuffer' | undefined
|
|
207
|
+
retry?: number | false | RetryOptions | undefined
|
|
208
|
+
signal?: AbortSignal | undefined
|
|
209
|
+
timeout?: number | undefined
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
interface ResponseContext = {
|
|
@@ -222,11 +222,11 @@ interface ResponseMeta = {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
interface RetryOptions = {
|
|
225
|
-
attempts?: number
|
|
226
|
-
backoff?: (attempt: number, retryAfterMs: number | null) => number
|
|
227
|
-
methods?: string[]
|
|
228
|
-
respectRetryAfter?: boolean
|
|
229
|
-
statuses?: number[]
|
|
225
|
+
attempts?: number | undefined
|
|
226
|
+
backoff?: ((attempt: number, retryAfterMs: number | null) => number) | undefined
|
|
227
|
+
methods?: string[] | undefined
|
|
228
|
+
respectRetryAfter?: boolean | undefined
|
|
229
|
+
statuses?: number[] | undefined
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
interface RouteShape = {
|
|
@@ -247,20 +247,20 @@ interface SocketLike = {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
interface SocketOptions = {
|
|
250
|
-
WebSocket?: new (url: string) => SocketLike
|
|
251
|
-
auth?: { params?: Record<string, string>; headers?: Record<string, string>;}
|
|
252
|
-
authEndpoint?: string | false
|
|
253
|
-
autoConnect?: boolean
|
|
254
|
-
fetch?: typeof fetch
|
|
255
|
-
host?: string
|
|
256
|
-
maxReconnectDelay?: number
|
|
257
|
-
path?: string
|
|
258
|
-
pingInterval?: number
|
|
259
|
-
port?: string | number
|
|
260
|
-
reconnect?: boolean
|
|
261
|
-
reconnectDelay?: number
|
|
262
|
-
scheme?: 'ws' | 'wss'
|
|
263
|
-
url?: string
|
|
250
|
+
WebSocket?: (new (url: string) => SocketLike) | undefined
|
|
251
|
+
auth?: { params?: Record<string, string>; headers?: Record<string, string>;} | undefined
|
|
252
|
+
authEndpoint?: string | false | undefined
|
|
253
|
+
autoConnect?: boolean | undefined
|
|
254
|
+
fetch?: typeof fetch | undefined
|
|
255
|
+
host?: string | undefined
|
|
256
|
+
maxReconnectDelay?: number | undefined
|
|
257
|
+
path?: string | undefined
|
|
258
|
+
pingInterval?: number | undefined
|
|
259
|
+
port?: string | number | undefined
|
|
260
|
+
reconnect?: boolean | undefined
|
|
261
|
+
reconnectDelay?: number | undefined
|
|
262
|
+
scheme?: 'ws' | 'wss' | undefined
|
|
263
|
+
url?: string | undefined
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
type ApiRouteMap = { [x: `GET ${string}`]: RouteShape | undefined; [x: `POST ${string}`]: RouteShape | undefined; [x: `PUT ${string}`]: RouteShape | undefined; [x: `PATCH ${string}`]: RouteShape | undefined; [x: `DELETE ${string}`]: RouteShape | undefined;}
|
|
@@ -373,20 +373,20 @@ interface SocketLike = {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
interface SocketOptions = {
|
|
376
|
-
WebSocket?: new (url: string) => SocketLike
|
|
377
|
-
auth?: { params?: Record<string, string>; headers?: Record<string, string>;}
|
|
378
|
-
authEndpoint?: string | false
|
|
379
|
-
autoConnect?: boolean
|
|
380
|
-
fetch?: typeof fetch
|
|
381
|
-
host?: string
|
|
382
|
-
maxReconnectDelay?: number
|
|
383
|
-
path?: string
|
|
384
|
-
pingInterval?: number
|
|
385
|
-
port?: string | number
|
|
386
|
-
reconnect?: boolean
|
|
387
|
-
reconnectDelay?: number
|
|
388
|
-
scheme?: 'ws' | 'wss'
|
|
389
|
-
url?: string
|
|
376
|
+
WebSocket?: (new (url: string) => SocketLike) | undefined
|
|
377
|
+
auth?: { params?: Record<string, string>; headers?: Record<string, string>;} | undefined
|
|
378
|
+
authEndpoint?: string | false | undefined
|
|
379
|
+
autoConnect?: boolean | undefined
|
|
380
|
+
fetch?: typeof fetch | undefined
|
|
381
|
+
host?: string | undefined
|
|
382
|
+
maxReconnectDelay?: number | undefined
|
|
383
|
+
path?: string | undefined
|
|
384
|
+
pingInterval?: number | undefined
|
|
385
|
+
port?: string | number | undefined
|
|
386
|
+
reconnect?: boolean | undefined
|
|
387
|
+
reconnectDelay?: number | undefined
|
|
388
|
+
scheme?: 'ws' | 'wss' | undefined
|
|
389
|
+
url?: string | undefined
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
type SocketState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'error'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zerotal/core": "1.
|
|
38
|
+
"@zerotal/core": "1.10.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0"
|
package/src/ApiClient.ts
CHANGED
|
@@ -73,31 +73,31 @@ export interface ResponseMeta {
|
|
|
73
73
|
|
|
74
74
|
export interface RequestOptions {
|
|
75
75
|
/** Extra headers merged on top of the client's default headers. */
|
|
76
|
-
headers?: Record<string, string
|
|
76
|
+
headers?: Record<string, string> | undefined;
|
|
77
77
|
/** Additional `RequestInit` fields (e.g. `credentials`, `mode`, `cache`). */
|
|
78
|
-
init?: Omit<RequestInit, "method" | "headers" | "body"
|
|
78
|
+
init?: Omit<RequestInit, "method" | "headers" | "body"> | undefined;
|
|
79
79
|
/** Abort signal for cancellation (combined with any `timeout`). */
|
|
80
|
-
signal?: AbortSignal;
|
|
80
|
+
signal?: AbortSignal | undefined;
|
|
81
81
|
/** Per-request timeout in ms (overrides the client default; `0` disables). */
|
|
82
|
-
timeout?: number;
|
|
82
|
+
timeout?: number | undefined;
|
|
83
83
|
/** Per-request retry policy (overrides the client default; `false` disables). */
|
|
84
|
-
retry?: number | RetryOptions | false;
|
|
84
|
+
retry?: number | RetryOptions | false | undefined;
|
|
85
85
|
/** How to read the response body. Default `"auto"` (JSON with text fallback). */
|
|
86
|
-
responseType?: "auto" | "json" | "text" | "blob" | "arrayBuffer";
|
|
86
|
+
responseType?: "auto" | "json" | "text" | "blob" | "arrayBuffer" | undefined;
|
|
87
87
|
/** Read response metadata (status, headers) without a global interceptor. */
|
|
88
|
-
meta?: (meta: ResponseMeta) => void;
|
|
88
|
+
meta?: ((meta: ResponseMeta) => void) | undefined;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export interface GetOptions<Q> extends RequestOptions {
|
|
92
92
|
/** Query-string parameters (nested arrays/objects are serialized with bracket notation). */
|
|
93
|
-
query?: Q;
|
|
93
|
+
query?: Q | undefined;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export interface MutationOptions<Q> extends RequestOptions {
|
|
97
97
|
/** URL path parameters for routes like `/api/users/{id}/action`. */
|
|
98
|
-
params?: Record<string, string | number
|
|
98
|
+
params?: Record<string, string | number> | undefined;
|
|
99
99
|
/** Query-string parameters. */
|
|
100
|
-
query?: Q;
|
|
100
|
+
query?: Q | undefined;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ── Client config ─────────────────────────────────────────────────────────────
|
|
@@ -108,39 +108,39 @@ export type TokenSource =
|
|
|
108
108
|
|
|
109
109
|
export interface ApiClientConfig {
|
|
110
110
|
/** Base URL prepended to every request path, e.g. `'https://api.example.com'`. */
|
|
111
|
-
baseUrl?: string;
|
|
111
|
+
baseUrl?: string | undefined;
|
|
112
112
|
/** Default headers sent with every request (e.g. `Authorization`). */
|
|
113
|
-
headers?: Record<string, string
|
|
113
|
+
headers?: Record<string, string> | undefined;
|
|
114
114
|
/**
|
|
115
115
|
* Bearer token attached as `Authorization: Bearer <token>` when no `Authorization` header is
|
|
116
116
|
* already present. May be a string or a (possibly async) resolver. Update at runtime with
|
|
117
117
|
* {@link ApiClient.setToken}.
|
|
118
118
|
*/
|
|
119
|
-
token?: TokenSource;
|
|
119
|
+
token?: TokenSource | undefined;
|
|
120
120
|
/** Send credentials (cookies) with every request — sets `credentials: 'include'`. */
|
|
121
|
-
withCredentials?: boolean;
|
|
121
|
+
withCredentials?: boolean | undefined;
|
|
122
122
|
/**
|
|
123
123
|
* Attach the CSRF/XSRF token to mutating requests: reads the `XSRF-TOKEN` cookie and sends it as
|
|
124
124
|
* `X-XSRF-TOKEN` (the SPA/session flow). Defaults to `true` when `withCredentials` is set.
|
|
125
125
|
* Customize the cookie/header names with an object.
|
|
126
126
|
*/
|
|
127
|
-
csrf?: boolean | { cookie?: string; header?: string };
|
|
127
|
+
csrf?: boolean | { cookie?: string; header?: string } | undefined;
|
|
128
128
|
/** Default per-request timeout in ms (`0`/omitted = no timeout). */
|
|
129
|
-
timeout?: number;
|
|
129
|
+
timeout?: number | undefined;
|
|
130
130
|
/**
|
|
131
131
|
* Default retry policy. `2` retries idempotent requests on network errors / 5xx / 429 with
|
|
132
132
|
* exponential backoff (honoring `Retry-After`); pass {@link RetryOptions} to tune, or omit to
|
|
133
133
|
* disable. Per-request `retry` overrides this.
|
|
134
134
|
*/
|
|
135
|
-
retry?: number | RetryOptions;
|
|
135
|
+
retry?: number | RetryOptions | undefined;
|
|
136
136
|
/** Called for every non-2xx response before the error is thrown. */
|
|
137
|
-
onError?: (error: ApiClientError) => void | Promise<void
|
|
137
|
+
onError?: ((error: ApiClientError) => void | Promise<void>) | undefined;
|
|
138
138
|
/** Called when a request receives `403 Forbidden`. */
|
|
139
|
-
onForbidden?: (error: ApiClientError) => void | Promise<void
|
|
139
|
+
onForbidden?: ((error: ApiClientError) => void | Promise<void>) | undefined;
|
|
140
140
|
/** One or more request interceptors executed in order before every fetch. */
|
|
141
|
-
onRequest?: RequestInterceptor | RequestInterceptor[];
|
|
141
|
+
onRequest?: RequestInterceptor | RequestInterceptor[] | undefined;
|
|
142
142
|
/** One or more response interceptors executed in order after every successful (2xx) response. */
|
|
143
|
-
onResponse?: ResponseInterceptor | ResponseInterceptor[];
|
|
143
|
+
onResponse?: ResponseInterceptor | ResponseInterceptor[] | undefined;
|
|
144
144
|
/**
|
|
145
145
|
* Called when a request receives `401 Unauthorized`. Receives the error and a `retry` function
|
|
146
146
|
* (optionally with header overrides) to re-execute the request once.
|
|
@@ -148,12 +148,14 @@ export interface ApiClientConfig {
|
|
|
148
148
|
* @example
|
|
149
149
|
* onUnauthorized: async (err, retry) => retry({ Authorization: `Bearer ${await refresh()}` }),
|
|
150
150
|
*/
|
|
151
|
-
onUnauthorized?:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
onUnauthorized?:
|
|
152
|
+
| ((
|
|
153
|
+
error: ApiClientError,
|
|
154
|
+
retry: (headerOverrides?: Record<string, string>) => Promise<unknown>,
|
|
155
|
+
) => Promise<unknown> | void)
|
|
156
|
+
| undefined;
|
|
155
157
|
/** Attach a circuit breaker (instance to share, or options to create a dedicated one). */
|
|
156
|
-
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions;
|
|
158
|
+
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions | undefined;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
// ── ApiClient<Routes> ───────────────────────────────────────────────────────────
|
package/src/CircuitBreaker.ts
CHANGED
|
@@ -10,19 +10,19 @@ export interface CircuitBreakerOptions {
|
|
|
10
10
|
* Number of consecutive failures required to open the circuit.
|
|
11
11
|
* Default: `5`.
|
|
12
12
|
*/
|
|
13
|
-
threshold?: number;
|
|
13
|
+
threshold?: number | undefined;
|
|
14
14
|
/**
|
|
15
15
|
* Milliseconds to wait in the open state before allowing a single probe
|
|
16
16
|
* request through (transition to half-open). Default: `30_000` (30 s).
|
|
17
17
|
*/
|
|
18
|
-
resetTimeout?: number;
|
|
18
|
+
resetTimeout?: number | undefined;
|
|
19
19
|
/**
|
|
20
20
|
* Predicate that decides whether a thrown error counts as a failure.
|
|
21
21
|
* By default, network errors and `ApiClientError` responses with status ≥ 500
|
|
22
22
|
* are failures; 4xx errors are not (they indicate client mistakes, not
|
|
23
23
|
* upstream outages).
|
|
24
24
|
*/
|
|
25
|
-
isFailure?: (error: unknown) => boolean;
|
|
25
|
+
isFailure?: ((error: unknown) => boolean) | undefined;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// ── Error thrown when the circuit is open ─────────────────────────────────────
|
package/src/Socket.ts
CHANGED
|
@@ -50,42 +50,42 @@ export interface SocketOptions {
|
|
|
50
50
|
* Full WebSocket URL. When omitted it's derived from the page location (browser) as
|
|
51
51
|
* `ws(s)://<host>:<port><path>`. Required in non-browser environments.
|
|
52
52
|
*/
|
|
53
|
-
url?: string;
|
|
53
|
+
url?: string | undefined;
|
|
54
54
|
/** WebSocket upgrade path. Defaults to `/app/ws` (matches the broadcasting config default). */
|
|
55
|
-
path?: string;
|
|
55
|
+
path?: string | undefined;
|
|
56
56
|
/** Host override (defaults to `location.hostname`). */
|
|
57
|
-
host?: string;
|
|
57
|
+
host?: string | undefined;
|
|
58
58
|
/** Port override (defaults to `location.port`). */
|
|
59
|
-
port?: string | number;
|
|
59
|
+
port?: string | number | undefined;
|
|
60
60
|
/** Force a scheme. Defaults to `wss` on https pages, `ws` otherwise. */
|
|
61
|
-
scheme?: "ws" | "wss";
|
|
61
|
+
scheme?: "ws" | "wss" | undefined;
|
|
62
62
|
/**
|
|
63
63
|
* Authentication for the connection and for private/presence channels:
|
|
64
64
|
* - `params` — query params appended to the WS URL (browsers can't set WS headers), read
|
|
65
65
|
* by the server on upgrade.
|
|
66
66
|
* - `headers` — sent on the `POST /broadcasting/auth` request (e.g. a CSRF token).
|
|
67
67
|
*/
|
|
68
|
-
auth?: { params?: Record<string, string>; headers?: Record<string, string> };
|
|
68
|
+
auth?: { params?: Record<string, string>; headers?: Record<string, string> } | undefined;
|
|
69
69
|
/**
|
|
70
70
|
* Endpoint that signs a private/presence subscription (Pusher-style). Defaults to
|
|
71
71
|
* `/broadcasting/auth`. Set to `false` to skip the signature fetch and rely on
|
|
72
72
|
* connection-level authorization instead.
|
|
73
73
|
*/
|
|
74
|
-
authEndpoint?: string | false;
|
|
74
|
+
authEndpoint?: string | false | undefined;
|
|
75
75
|
/** Inject a `fetch` implementation for the auth request (tests / non-DOM runtimes). */
|
|
76
|
-
fetch?: typeof fetch;
|
|
76
|
+
fetch?: typeof fetch | undefined;
|
|
77
77
|
/** Connect immediately on construction. Default: true. */
|
|
78
|
-
autoConnect?: boolean;
|
|
78
|
+
autoConnect?: boolean | undefined;
|
|
79
79
|
/** Reconnect automatically after an unexpected close. Default: true. */
|
|
80
|
-
reconnect?: boolean;
|
|
80
|
+
reconnect?: boolean | undefined;
|
|
81
81
|
/** Initial reconnect delay in ms (doubles each attempt up to `maxReconnectDelay`). Default: 1000. */
|
|
82
|
-
reconnectDelay?: number;
|
|
82
|
+
reconnectDelay?: number | undefined;
|
|
83
83
|
/** Cap on the reconnect backoff in ms. Default: 30000. */
|
|
84
|
-
maxReconnectDelay?: number;
|
|
84
|
+
maxReconnectDelay?: number | undefined;
|
|
85
85
|
/** Heartbeat ping interval in ms (0 disables). Default: 30000. */
|
|
86
|
-
pingInterval?: number;
|
|
86
|
+
pingInterval?: number | undefined;
|
|
87
87
|
/** Inject a WebSocket constructor (tests / non-DOM runtimes). Defaults to the global. */
|
|
88
|
-
WebSocket?: new (url: string) => SocketLike;
|
|
88
|
+
WebSocket?: (new (url: string) => SocketLike) | undefined;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
type Listener = (payload: unknown) => void;
|
|
@@ -217,6 +217,16 @@ export class PresenceChannel extends Channel {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/** The options {@link Socket}'s constructor always fills in. */
|
|
221
|
+
type GuaranteedOption =
|
|
222
|
+
| "path"
|
|
223
|
+
| "autoConnect"
|
|
224
|
+
| "reconnect"
|
|
225
|
+
| "reconnectDelay"
|
|
226
|
+
| "maxReconnectDelay"
|
|
227
|
+
| "pingInterval"
|
|
228
|
+
| "authEndpoint";
|
|
229
|
+
|
|
220
230
|
/**
|
|
221
231
|
* Realtime client for Zerotal broadcasting (native `ws` / `redis` drivers). Exposes a
|
|
222
232
|
* familiar realtime-client API over the native protocol — see the module header for usage.
|
|
@@ -231,19 +241,17 @@ export class Socket {
|
|
|
231
241
|
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
232
242
|
private _pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
233
243
|
private _closedByUser = false;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
> &
|
|
246
|
-
SocketOptions;
|
|
244
|
+
/**
|
|
245
|
+
* The seven fields the constructor guarantees, plus whatever else was passed.
|
|
246
|
+
*
|
|
247
|
+
* `Omit` rather than `& SocketOptions`: intersecting with the whole shape puts the
|
|
248
|
+
* optional declaration of each guaranteed field back alongside the required one, so
|
|
249
|
+
* `pingInterval` read as possibly `undefined` in the very code that had just given
|
|
250
|
+
* it a default.
|
|
251
|
+
*/
|
|
252
|
+
private readonly _opts: {
|
|
253
|
+
[K in GuaranteedOption]-?: Exclude<SocketOptions[K], undefined>;
|
|
254
|
+
} & Omit<SocketOptions, GuaranteedOption>;
|
|
247
255
|
|
|
248
256
|
constructor(options: SocketOptions = {}) {
|
|
249
257
|
// The caller's options go FIRST, then the defaults fill the gaps.
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { deepMerge } from "@zerotal/core/helpers";
|
|
|
2
2
|
import type { ApiClientConfig } from "./ApiClient.ts";
|
|
3
3
|
|
|
4
4
|
export interface ClientConfigShape extends ApiClientConfig {
|
|
5
|
-
baseUrl?: string;
|
|
5
|
+
baseUrl?: string | undefined;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// The client carries no framework-level defaults of its own — `ApiClient` applies
|
package/src/http.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definedOnly, type Resolved } from "@zerotal/core/helpers";
|
|
1
2
|
import { ApiClientError } from "./errors.ts";
|
|
2
3
|
import { CircuitBreakerOpenError } from "./CircuitBreaker.ts";
|
|
3
4
|
|
|
@@ -109,18 +110,18 @@ export function readCookie(name: string): string | undefined {
|
|
|
109
110
|
|
|
110
111
|
export interface RetryOptions {
|
|
111
112
|
/** Max retry attempts after the first try. Default: 2. */
|
|
112
|
-
attempts?: number;
|
|
113
|
+
attempts?: number | undefined;
|
|
113
114
|
/** HTTP methods eligible for retry. Default: idempotent verbs (GET/PUT/DELETE/HEAD/OPTIONS). */
|
|
114
|
-
methods?: string[];
|
|
115
|
+
methods?: string[] | undefined;
|
|
115
116
|
/** HTTP statuses that should be retried. Default: 408, 425, 429, 500, 502, 503, 504. */
|
|
116
|
-
statuses?: number[];
|
|
117
|
+
statuses?: number[] | undefined;
|
|
117
118
|
/** Honor a `Retry-After` header when present. Default: true. */
|
|
118
|
-
respectRetryAfter?: boolean;
|
|
119
|
+
respectRetryAfter?: boolean | undefined;
|
|
119
120
|
/** Compute the delay (ms) before attempt N. Default: exponential backoff + jitter. */
|
|
120
|
-
backoff?: (attempt: number, retryAfterMs: number | null) => number;
|
|
121
|
+
backoff?: ((attempt: number, retryAfterMs: number | null) => number) | undefined;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
const DEFAULT_RETRY:
|
|
124
|
+
const DEFAULT_RETRY: Resolved<Omit<RetryOptions, "backoff">> & Pick<RetryOptions, "backoff"> = {
|
|
124
125
|
attempts: 2,
|
|
125
126
|
methods: ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"],
|
|
126
127
|
statuses: [408, 425, 429, 500, 502, 503, 504],
|
|
@@ -131,12 +132,15 @@ const DEFAULT_RETRY: Required<Omit<RetryOptions, "backoff">> & Pick<RetryOptions
|
|
|
131
132
|
export function resolveRetry(
|
|
132
133
|
retry: number | RetryOptions | false | undefined,
|
|
133
134
|
method: string,
|
|
134
|
-
):
|
|
135
|
+
): Resolved<Omit<RetryOptions, "backoff">> & Pick<RetryOptions, "backoff"> {
|
|
135
136
|
if (retry === undefined || retry === false || retry === 0) {
|
|
136
137
|
return { ...DEFAULT_RETRY, attempts: 0 };
|
|
137
138
|
}
|
|
138
139
|
const opts = typeof retry === "number" ? { attempts: retry } : retry;
|
|
139
|
-
|
|
140
|
+
// `definedOnly`, not a bare spread: these options accept an explicit `undefined`
|
|
141
|
+
// so that `{ attempts: cfg.attempts }` compiles, and a bare spread would then let
|
|
142
|
+
// that `undefined` overwrite the default rather than leave it standing.
|
|
143
|
+
const merged = { ...DEFAULT_RETRY, ...definedOnly(opts) };
|
|
140
144
|
// Methods are matched case-insensitively.
|
|
141
145
|
if (!merged.methods.some((m) => m.toUpperCase() === method.toUpperCase())) {
|
|
142
146
|
return { ...merged, attempts: 0 };
|
|
@@ -173,7 +177,7 @@ function _isRetryable(
|
|
|
173
177
|
/** Run `fn`, retrying transient failures per `policy`. */
|
|
174
178
|
export async function withRetry<T>(
|
|
175
179
|
fn: () => Promise<T>,
|
|
176
|
-
policy:
|
|
180
|
+
policy: Resolved<Omit<RetryOptions, "backoff">> & Pick<RetryOptions, "backoff">,
|
|
177
181
|
): Promise<T> {
|
|
178
182
|
const backoff = policy.backoff ?? _defaultBackoff;
|
|
179
183
|
let attempt = 0;
|