@zerotal/client 1.6.3 → 1.7.2
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 +29 -0
- package/api-surface.md +367 -0
- package/package.json +9 -4
- package/src/CircuitBreaker.ts +1 -1
- package/src/Socket.ts +3 -3
- package/src/browser.ts +54 -0
- package/src/config.ts +1 -1
- package/src/createApiClient.ts +36 -0
- package/src/errors.ts +1 -1
- package/src/index.ts +1 -30
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,35 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **The documented import could not be bundled for a browser.** `import { Socket } from
|
|
14
|
+
"@zerotal/client"` — the line the broadcasting guide taught — failed with `Browser build cannot
|
|
15
|
+
import() Bun builtin: "bun"`. The root entry also exports `ClientProvider`, which extends core's
|
|
16
|
+
`ServiceProvider`, which reaches `CommandRunner`, which reaches the built-in CLI commands, one of
|
|
17
|
+
which does `await import("bun")`. That is a *resolution*-time error, so tree-shaking never got the
|
|
18
|
+
chance to drop the half nobody in a browser wanted: one server-only export made the whole package
|
|
19
|
+
unusable in the environment it exists for.
|
|
20
|
+
|
|
21
|
+
`@zerotal/client` now resolves to a browser-safe entry under the `browser` export condition —
|
|
22
|
+
`Socket`, `ApiClient`, `CircuitBreaker`, `createApiClient` and their types, and none of the
|
|
23
|
+
server-side exports. Bun and Node consumers still get the full barrel, `ClientProvider` included,
|
|
24
|
+
so nothing about registering the package changes. `@zerotal/client/Socket` still works and is
|
|
25
|
+
still the leanest import when `Socket` is all you need; `ClientProvider` also gained an explicit
|
|
26
|
+
`./provider` subpath.
|
|
27
|
+
|
|
28
|
+
Two supporting changes: `createApiClient` moved out of `index.ts` into its own module, because a
|
|
29
|
+
browser entry re-exporting it from the barrel pulls the barrel back in — the first attempt at this
|
|
30
|
+
fix did exactly that and bundled straight back into the same error. And the modules a browser can
|
|
31
|
+
reach now import from narrow core subpaths (`@zerotal/core/errors`, `@zerotal/core/helpers`)
|
|
32
|
+
rather than the root, because importing *anything at all* from core's root is enough to poison a
|
|
33
|
+
browser bundle.
|
|
34
|
+
|
|
35
|
+
Guarded by a test that runs `Bun.build` with `target: "browser"` — no unit test can see this,
|
|
36
|
+
since every module imports fine under Bun — plus one that holds the browser entry in step with
|
|
37
|
+
the barrel, since two entry points means two places to remember to add an export to.
|
|
38
|
+
|
|
39
|
+
|
|
11
40
|
## [1.0.3] — 2026-08-07
|
|
12
41
|
|
|
13
42
|
### Changed
|
package/api-surface.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# @zerotal/client — public API surface
|
|
2
|
+
|
|
3
|
+
<!-- AUTO-GENERATED by scripts/api-surface.ts. Do not edit by hand.
|
|
4
|
+
Run `bun run api:surface` to regenerate after an intentional API change. -->
|
|
5
|
+
|
|
6
|
+
## . `(./src/index.ts)`
|
|
7
|
+
|
|
8
|
+
class ApiClient = {
|
|
9
|
+
new <Routes extends ApiRouteMap>(config?: ApiClientConfig): ApiClient<Routes>
|
|
10
|
+
delete: <Path extends PathsFor<Routes, 'DELETE'>>(path: Path, params?: ParamRecord<Path & string>, options?: RequestOptions) => Promise<ResponseOf<Routes, 'DELETE', Path & string>>
|
|
11
|
+
get: <Path extends PathsFor<Routes, 'GET'>>(path: Path, params?: ParamRecord<Path & string>, options?: GetOptions<QueryOf<Routes, 'GET', Path & string>>) => Promise<ResponseOf<Routes, 'GET', Path & string>>
|
|
12
|
+
patch: <Path extends PathsFor<Routes, 'PATCH'>>(path: Path, body?: BodyOf<Routes, 'PATCH', Path & string>, options?: MutationOptions<QueryOf<Routes, 'PATCH', Path & string>>) => Promise<ResponseOf<Routes, 'PATCH', Path & string>>
|
|
13
|
+
post: <Path extends PathsFor<Routes, 'POST'>>(path: Path, body?: BodyOf<Routes, 'POST', Path & string>, options?: MutationOptions<QueryOf<Routes, 'POST', Path & string>>) => Promise<ResponseOf<Routes, 'POST', Path & string>>
|
|
14
|
+
put: <Path extends PathsFor<Routes, 'PUT'>>(path: Path, body?: BodyOf<Routes, 'PUT', Path & string>, options?: MutationOptions<QueryOf<Routes, 'PUT', Path & string>>) => Promise<ResponseOf<Routes, 'PUT', Path & string>>
|
|
15
|
+
setToken: (token: TokenSource | null) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class ApiClientError = {
|
|
19
|
+
new (status: number, statusText: string, body: string, headers?: Headers): ApiClientError
|
|
20
|
+
readonly body: string
|
|
21
|
+
readonly code: string
|
|
22
|
+
readonly context?: Record<string, unknown> | undefined
|
|
23
|
+
readonly headers: Headers | undefined
|
|
24
|
+
readonly status: number
|
|
25
|
+
readonly statusText: string
|
|
26
|
+
retryAfterMs: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class Channel = {
|
|
30
|
+
new (name: string, _socket: Socket): Channel
|
|
31
|
+
_dispatch: (frame: ServerFrame) => void
|
|
32
|
+
error: (cb: Listener) => Channel
|
|
33
|
+
listen: (event: string, cb: Listener) => Channel
|
|
34
|
+
readonly name: string
|
|
35
|
+
stopListening: (event: string, cb?: Listener) => Channel
|
|
36
|
+
subscribed: (cb: Listener) => Channel
|
|
37
|
+
unsubscribe: () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class CircuitBreaker = {
|
|
41
|
+
new (options?: CircuitBreakerOptions): CircuitBreaker
|
|
42
|
+
call: <T>(fn: () => Promise<T>) => Promise<T>
|
|
43
|
+
failures: number
|
|
44
|
+
reset: () => void
|
|
45
|
+
state: CircuitState
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class CircuitBreakerOpenError = {
|
|
49
|
+
new (): CircuitBreakerOpenError
|
|
50
|
+
readonly code: string
|
|
51
|
+
readonly context?: Record<string, unknown> | undefined
|
|
52
|
+
readonly status: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class ClientProvider = {
|
|
56
|
+
new (app: Application): ClientProvider
|
|
57
|
+
static dependsOn?: (new (app: Application) => ServiceProvider)[]
|
|
58
|
+
static environments: AppEnvironment[]
|
|
59
|
+
static priority?: number
|
|
60
|
+
static provides: readonly ['client']
|
|
61
|
+
devProcesses: () => DevProcessDefinition[]
|
|
62
|
+
doctorChecks: () => DoctorCheck[]
|
|
63
|
+
onBooted: () => Promise<void>
|
|
64
|
+
onBooting: () => Promise<void>
|
|
65
|
+
onRegister: () => void
|
|
66
|
+
onRequestProcessed: (_ctx: HttpContext) => Promise<void>
|
|
67
|
+
onRequestReceived: (_ctx: HttpContext) => Promise<void>
|
|
68
|
+
onResponseSent: (_ctx: HttpContext) => Promise<void>
|
|
69
|
+
onStarted: () => Promise<void>
|
|
70
|
+
onStarting: () => Promise<void>
|
|
71
|
+
onStopped: () => Promise<void>
|
|
72
|
+
onStopping: () => Promise<void>
|
|
73
|
+
replContext: () => Record<string, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class PresenceChannel = {
|
|
77
|
+
new (name: string, _socket: Socket): PresenceChannel
|
|
78
|
+
_dispatch: (frame: ServerFrame) => void
|
|
79
|
+
error: (cb: Listener) => PresenceChannel
|
|
80
|
+
here: (cb: (members: PresenceMember[]) => void) => PresenceChannel
|
|
81
|
+
joining: (cb: (member: PresenceMember) => void) => PresenceChannel
|
|
82
|
+
leaving: (cb: (member: PresenceMember) => void) => PresenceChannel
|
|
83
|
+
listen: (event: string, cb: Listener) => PresenceChannel
|
|
84
|
+
readonly name: string
|
|
85
|
+
stopListening: (event: string, cb?: Listener) => PresenceChannel
|
|
86
|
+
subscribed: (cb: Listener) => PresenceChannel
|
|
87
|
+
unsubscribe: () => void
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class Socket = {
|
|
91
|
+
new (options?: SocketOptions): Socket
|
|
92
|
+
channel: (name: string) => Channel
|
|
93
|
+
channels: () => Channel[]
|
|
94
|
+
connect: () => void
|
|
95
|
+
disconnect: () => void
|
|
96
|
+
join: (name: string) => PresenceChannel
|
|
97
|
+
leave: (name: string) => void
|
|
98
|
+
leaveChannel: (wireName: string) => void
|
|
99
|
+
on: (state: SocketState, cb: (detail?: unknown) => void) => () => void
|
|
100
|
+
presence: (name: string) => PresenceChannel
|
|
101
|
+
private: (name: string) => Channel
|
|
102
|
+
socketId: () => string | null
|
|
103
|
+
state: SocketState
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class ValidationError = {
|
|
107
|
+
new (statusText: string, body: string, errors: Record<string, string[]>, validationMessage: string, headers?: Headers): ValidationError
|
|
108
|
+
all: () => Record<string, string[]>
|
|
109
|
+
fields: () => string[]
|
|
110
|
+
first: (field: string) => string | undefined
|
|
111
|
+
has: (field: string) => boolean
|
|
112
|
+
readonly body: string
|
|
113
|
+
readonly code: string
|
|
114
|
+
readonly context?: Record<string, unknown> | undefined
|
|
115
|
+
readonly errors: Record<string, string[]>
|
|
116
|
+
readonly headers: Headers | undefined
|
|
117
|
+
readonly status: number
|
|
118
|
+
readonly statusText: string
|
|
119
|
+
readonly validationMessage: string
|
|
120
|
+
retryAfterMs: number | null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const Client = ApiClient<Partial<Record<`GET ${string}` | `POST ${string}` | `PUT ${string}` | `PATCH ${string}` | `DELETE ${string}`, RouteShape>>>
|
|
124
|
+
|
|
125
|
+
function ClientConfig = (options?: Partial<ClientConfigShape>) => Partial<ClientConfigShape>
|
|
126
|
+
|
|
127
|
+
function createApiClient = <Routes extends ApiRouteMap>(config?: ApiClientConfig) => ApiClient<Routes>
|
|
128
|
+
|
|
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
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface CircuitBreakerOptions = {
|
|
146
|
+
isFailure?: (error: unknown) => boolean
|
|
147
|
+
resetTimeout?: number
|
|
148
|
+
threshold?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
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
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
|
176
|
+
}
|
|
177
|
+
|
|
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
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface PresenceMember = {
|
|
191
|
+
id: string | number
|
|
192
|
+
info: Record<string, unknown>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface RequestConfig = {
|
|
196
|
+
body: BodyInit | undefined
|
|
197
|
+
headers: Record<string, string>
|
|
198
|
+
method: string
|
|
199
|
+
url: string
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface ResponseContext = {
|
|
213
|
+
data: unknown
|
|
214
|
+
headers: Headers
|
|
215
|
+
request: RequestConfig
|
|
216
|
+
status: number
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
interface ResponseMeta = {
|
|
220
|
+
headers: Headers
|
|
221
|
+
status: number
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface RetryOptions = {
|
|
225
|
+
attempts?: number
|
|
226
|
+
backoff?: (attempt: number, retryAfterMs: number | null) => number
|
|
227
|
+
methods?: string[]
|
|
228
|
+
respectRetryAfter?: boolean
|
|
229
|
+
statuses?: number[]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface RouteShape = {
|
|
233
|
+
body?: Record<string, unknown>
|
|
234
|
+
params?: Record<string, string | number>
|
|
235
|
+
query?: Record<string, string | number | boolean | undefined>
|
|
236
|
+
response?: unknown
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface SocketLike = {
|
|
240
|
+
close: (code?: number, reason?: string) => void
|
|
241
|
+
onclose: ((ev: { code?: number; reason?: string;}) => void) | null
|
|
242
|
+
onerror: ((ev: unknown) => void) | null
|
|
243
|
+
onmessage: ((ev: { data: unknown;}) => void) | null
|
|
244
|
+
onopen: ((ev: unknown) => void) | null
|
|
245
|
+
readyState: number
|
|
246
|
+
send: (data: string) => void
|
|
247
|
+
}
|
|
248
|
+
|
|
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
|
|
264
|
+
}
|
|
265
|
+
|
|
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;}
|
|
267
|
+
|
|
268
|
+
type BodyOf = `${Method} ${Path}` extends keyof Routes ? Routes[`${Method} ${Path}`] extends { body: infer B;} ? B : Record<string, unknown> : Record<string, unknown>
|
|
269
|
+
|
|
270
|
+
type CircuitState = 'closed' | 'open' | 'half-open'
|
|
271
|
+
|
|
272
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
273
|
+
|
|
274
|
+
type ParamRecord = [PathParams<Path>] extends [never] ? undefined : { [K in PathParams<Path>]: string | number; }
|
|
275
|
+
|
|
276
|
+
type PathParams = Path extends `${string}{${infer Param}}${infer Tail}` ? Param | PathParams<Tail> : never
|
|
277
|
+
|
|
278
|
+
type PathsFor = { [K in keyof Routes & string]: K extends `${Method} ${infer Path}` ? Path : never; }[keyof Routes & string]
|
|
279
|
+
|
|
280
|
+
type QueryOf = `${Method} ${Path}` extends keyof Routes ? Routes[`${Method} ${Path}`] extends { query: infer Q;} ? Q : Record<string, string | number | boolean | undefined> : Record<string, string | number | boolean | undefined>
|
|
281
|
+
|
|
282
|
+
type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>
|
|
283
|
+
|
|
284
|
+
type ResponseInterceptor = (ctx: ResponseContext) => ResponseContext | Promise<ResponseContext>
|
|
285
|
+
|
|
286
|
+
type ResponseOf = `${Method} ${Path}` extends keyof Routes ? Routes[`${Method} ${Path}`] extends { response: infer R;} ? R : unknown : unknown
|
|
287
|
+
|
|
288
|
+
type SocketState = 'error' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
|
|
289
|
+
|
|
290
|
+
type TokenSource = string | (() => string | null | undefined | Promise<string | null | undefined>)
|
|
291
|
+
|
|
292
|
+
## ./Socket `(./src/Socket.ts)`
|
|
293
|
+
|
|
294
|
+
class Channel = {
|
|
295
|
+
new (name: string, _socket: Socket): Channel
|
|
296
|
+
_dispatch: (frame: ServerFrame) => void
|
|
297
|
+
error: (cb: Listener) => Channel
|
|
298
|
+
listen: (event: string, cb: Listener) => Channel
|
|
299
|
+
readonly name: string
|
|
300
|
+
stopListening: (event: string, cb?: Listener) => Channel
|
|
301
|
+
subscribed: (cb: Listener) => Channel
|
|
302
|
+
unsubscribe: () => void
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
class PresenceChannel = {
|
|
306
|
+
new (name: string, _socket: Socket): PresenceChannel
|
|
307
|
+
_dispatch: (frame: ServerFrame) => void
|
|
308
|
+
error: (cb: Listener) => PresenceChannel
|
|
309
|
+
here: (cb: (members: PresenceMember[]) => void) => PresenceChannel
|
|
310
|
+
joining: (cb: (member: PresenceMember) => void) => PresenceChannel
|
|
311
|
+
leaving: (cb: (member: PresenceMember) => void) => PresenceChannel
|
|
312
|
+
listen: (event: string, cb: Listener) => PresenceChannel
|
|
313
|
+
readonly name: string
|
|
314
|
+
stopListening: (event: string, cb?: Listener) => PresenceChannel
|
|
315
|
+
subscribed: (cb: Listener) => PresenceChannel
|
|
316
|
+
unsubscribe: () => void
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
class Socket = {
|
|
320
|
+
new (options?: SocketOptions): Socket
|
|
321
|
+
channel: (name: string) => Channel
|
|
322
|
+
channels: () => Channel[]
|
|
323
|
+
connect: () => void
|
|
324
|
+
disconnect: () => void
|
|
325
|
+
join: (name: string) => PresenceChannel
|
|
326
|
+
leave: (name: string) => void
|
|
327
|
+
leaveChannel: (wireName: string) => void
|
|
328
|
+
on: (state: SocketState, cb: (detail?: unknown) => void) => () => void
|
|
329
|
+
presence: (name: string) => PresenceChannel
|
|
330
|
+
private: (name: string) => Channel
|
|
331
|
+
socketId: () => string | null
|
|
332
|
+
state: SocketState
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface PresenceMember = {
|
|
336
|
+
id: string | number
|
|
337
|
+
info: Record<string, unknown>
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface SocketLike = {
|
|
341
|
+
close: (code?: number, reason?: string) => void
|
|
342
|
+
onclose: ((ev: { code?: number; reason?: string;}) => void) | null
|
|
343
|
+
onerror: ((ev: unknown) => void) | null
|
|
344
|
+
onmessage: ((ev: { data: unknown;}) => void) | null
|
|
345
|
+
onopen: ((ev: unknown) => void) | null
|
|
346
|
+
readyState: number
|
|
347
|
+
send: (data: string) => void
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface SocketOptions = {
|
|
351
|
+
WebSocket?: new (url: string) => SocketLike
|
|
352
|
+
auth?: { params?: Record<string, string>; headers?: Record<string, string>;}
|
|
353
|
+
authEndpoint?: string | false
|
|
354
|
+
autoConnect?: boolean
|
|
355
|
+
fetch?: typeof fetch
|
|
356
|
+
host?: string
|
|
357
|
+
maxReconnectDelay?: number
|
|
358
|
+
path?: string
|
|
359
|
+
pingInterval?: number
|
|
360
|
+
port?: string | number
|
|
361
|
+
reconnect?: boolean
|
|
362
|
+
reconnectDelay?: number
|
|
363
|
+
scheme?: 'ws' | 'wss'
|
|
364
|
+
url?: string
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
type SocketState = 'error' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./src/index.ts",
|
|
10
10
|
"exports": {
|
|
11
|
-
".":
|
|
12
|
-
|
|
11
|
+
".": {
|
|
12
|
+
"browser": "./src/browser.ts",
|
|
13
|
+
"default": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./Socket": "./src/Socket.ts",
|
|
16
|
+
"./provider": "./src/provider/ClientProvider.ts"
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
15
19
|
"CHANGELOG.md",
|
|
20
|
+
"api-surface.md",
|
|
16
21
|
"src",
|
|
17
22
|
"!src/**/*.test.ts",
|
|
18
23
|
"!src/**/*.test.tsx",
|
|
@@ -30,7 +35,7 @@
|
|
|
30
35
|
"typecheck": "tsc --noEmit"
|
|
31
36
|
},
|
|
32
37
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.
|
|
38
|
+
"@zerotal/core": "1.7.2"
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
41
|
"typescript": "^5.8.0"
|
package/src/CircuitBreaker.ts
CHANGED
package/src/Socket.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// broadcasting protocol (the `ws` / `redis` drivers) and exposes a familiar
|
|
5
5
|
// realtime-client API — the same `channel()/private()/join()/leave()` +
|
|
6
6
|
// `listen()/here()/joining()/leaving()` surface — so it works with `@zerotal/flow`'s
|
|
7
|
-
// `@on('
|
|
7
|
+
// `@on('socket:…')` listeners with zero external dependencies.
|
|
8
8
|
//
|
|
9
9
|
// import { Socket } from "@zerotal/client";
|
|
10
10
|
//
|
|
@@ -335,14 +335,14 @@ export class Socket {
|
|
|
335
335
|
return this._subscribe(wire, () => new PresenceChannel(wire, this)) as PresenceChannel;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/**
|
|
338
|
+
/** Alias for {@link presence} (`window.Socket.join(...)`). */
|
|
339
339
|
join(name: string): PresenceChannel {
|
|
340
340
|
return this.presence(name);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/**
|
|
344
344
|
* Leave a channel by its base name — unsubscribes the public, `private-`, and `presence-`
|
|
345
|
-
* variants
|
|
345
|
+
* variants, which is what `leave()` means here.
|
|
346
346
|
*/
|
|
347
347
|
leave(name: string): void {
|
|
348
348
|
this.leaveChannel(name);
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-safe half of this package.
|
|
3
|
+
*
|
|
4
|
+
* Resolved for `import … from "@zerotal/client"` under the `browser` export
|
|
5
|
+
* condition, so a bundler targeting a browser gets this file and a Bun/Node
|
|
6
|
+
* import gets `index.ts` unchanged. Nobody has to know: the documented import
|
|
7
|
+
* is the same either way.
|
|
8
|
+
*
|
|
9
|
+
* What it leaves out is `ClientProvider` and `ClientConfig` — the two things
|
|
10
|
+
* here that are genuinely server-side. `ClientProvider` extends core's
|
|
11
|
+
* `ServiceProvider`, which reaches `CommandRunner`, which reaches the built-in
|
|
12
|
+
* CLI commands, one of which does `await import("bun")`. That is a hard error in
|
|
13
|
+
* a browser build and it happens during *resolution*, so tree-shaking never gets
|
|
14
|
+
* a chance to drop the unused half. The whole package became unbundleable
|
|
15
|
+
* because of one export nobody in a browser wanted.
|
|
16
|
+
*
|
|
17
|
+
* The rule this encodes: `@zerotal/core`'s root entry is server-only. A module
|
|
18
|
+
* that can run in a browser imports from a narrow subpath — `@zerotal/core/errors`,
|
|
19
|
+
* `@zerotal/core/helpers` — and never from the root.
|
|
20
|
+
*/
|
|
21
|
+
export type {
|
|
22
|
+
ApiRouteMap,
|
|
23
|
+
RouteShape,
|
|
24
|
+
HttpMethod,
|
|
25
|
+
PathParams,
|
|
26
|
+
ParamRecord,
|
|
27
|
+
PathsFor,
|
|
28
|
+
ResponseOf,
|
|
29
|
+
BodyOf,
|
|
30
|
+
QueryOf,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
|
|
33
|
+
export { ApiClient, ApiClientError, ValidationError } from "./ApiClient.ts";
|
|
34
|
+
export type {
|
|
35
|
+
ApiClientConfig,
|
|
36
|
+
RequestConfig,
|
|
37
|
+
RequestInterceptor,
|
|
38
|
+
ResponseContext,
|
|
39
|
+
ResponseInterceptor,
|
|
40
|
+
ResponseMeta,
|
|
41
|
+
RequestOptions,
|
|
42
|
+
GetOptions,
|
|
43
|
+
MutationOptions,
|
|
44
|
+
TokenSource,
|
|
45
|
+
RetryOptions,
|
|
46
|
+
} from "./ApiClient.ts";
|
|
47
|
+
|
|
48
|
+
export { CircuitBreaker, CircuitBreakerOpenError } from "./CircuitBreaker.ts";
|
|
49
|
+
export type { CircuitBreakerOptions, CircuitState } from "./CircuitBreaker.ts";
|
|
50
|
+
|
|
51
|
+
export { Socket, Channel, PresenceChannel } from "./Socket.ts";
|
|
52
|
+
export type { SocketOptions, SocketState, SocketLike, PresenceMember } from "./Socket.ts";
|
|
53
|
+
|
|
54
|
+
export { createApiClient } from "./createApiClient.ts";
|
package/src/config.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ── Factory function ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Its own module rather than a member of `index.ts`, so the browser entry can
|
|
4
|
+
// re-export it without pulling the server half of the barrel back in. That is
|
|
5
|
+
// not hypothetical tidiness: `browser.ts` originally took it from `index.ts`,
|
|
6
|
+
// which re-imported `ClientProvider` and put the bundle straight back into the
|
|
7
|
+
// error this split exists to remove.
|
|
8
|
+
|
|
9
|
+
import { ApiClient } from "./ApiClient.ts";
|
|
10
|
+
import type { ApiRouteMap } from "./types.ts";
|
|
11
|
+
import type { ApiClientConfig } from "./ApiClient.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a type-safe API client bound to your route map.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // frontend/api.ts
|
|
18
|
+
* import { createApiClient } from '@zerotal/client';
|
|
19
|
+
* import type { Routes } from './routes';
|
|
20
|
+
*
|
|
21
|
+
* export const api = createApiClient<Routes>({
|
|
22
|
+
* baseUrl: '/api',
|
|
23
|
+
* headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Anywhere in your UI:
|
|
27
|
+
* const user = await api.get('/users/{id}', { id: 1 });
|
|
28
|
+
* // ^? UserResource (inferred from Routes)
|
|
29
|
+
*
|
|
30
|
+
* const newUser = await api.post('/users', { name: 'Alice', email: 'alice@ex.com' });
|
|
31
|
+
*/
|
|
32
|
+
export function createApiClient<Routes extends ApiRouteMap>(
|
|
33
|
+
config: ApiClientConfig = {},
|
|
34
|
+
): ApiClient<Routes> {
|
|
35
|
+
return new ApiClient<Routes>(config);
|
|
36
|
+
}
|
package/src/errors.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -36,33 +36,4 @@ export { ClientProvider, Client } from "./provider/ClientProvider.ts";
|
|
|
36
36
|
export { ClientConfig } from "./config.ts";
|
|
37
37
|
export type { ClientConfigShape } from "./config.ts";
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import { ApiClient } from "./ApiClient.ts";
|
|
42
|
-
import type { ApiRouteMap } from "./types.ts";
|
|
43
|
-
import type { ApiClientConfig } from "./ApiClient.ts";
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create a type-safe API client bound to your route map.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* // frontend/api.ts
|
|
50
|
-
* import { createApiClient } from '@zerotal/client';
|
|
51
|
-
* import type { Routes } from './routes';
|
|
52
|
-
*
|
|
53
|
-
* export const api = createApiClient<Routes>({
|
|
54
|
-
* baseUrl: '/api',
|
|
55
|
-
* headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
56
|
-
* });
|
|
57
|
-
*
|
|
58
|
-
* // Anywhere in your UI:
|
|
59
|
-
* const user = await api.get('/users/{id}', { id: 1 });
|
|
60
|
-
* // ^? UserResource (inferred from Routes)
|
|
61
|
-
*
|
|
62
|
-
* const newUser = await api.post('/users', { name: 'Alice', email: 'alice@ex.com' });
|
|
63
|
-
*/
|
|
64
|
-
export function createApiClient<Routes extends ApiRouteMap>(
|
|
65
|
-
config: ApiClientConfig = {},
|
|
66
|
-
): ApiClient<Routes> {
|
|
67
|
-
return new ApiClient<Routes>(config);
|
|
68
|
-
}
|
|
39
|
+
export { createApiClient } from "./createApiClient.ts";
|