@emeryld/rrroutes-client 2.2.10 → 2.2.12

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
@@ -1,6 +1,13 @@
1
+ <!--
2
+ Summary:
3
+ - Added comprehensive usage for `createRouteClient`, built endpoints (GET/feeds/mutations), cache helpers, invalidation, debug modes, custom fetchers, and React Query integration.
4
+ - Added new sections for router helpers, infinite/feeds, FormData uploads, and socket utilities (SocketClient, provider hooks, socketed routes).
5
+ - Removed the sparse README and outdated publishing script focus; future additions should cover server-side Socket.IO envelope expectations and testing recipes for hooks.
6
+ -->
7
+
1
8
  # @emeryld/rrroutes-client
2
9
 
3
- React Query helpers that sit on top of an RRRoutes contract. This package consumes finalized leaves from `@emeryld/rrroutes-contract` and produces typed fetch helpers (`useEndpoint`, `useInfinite`, `useMutation`, cache key utilities, etc.).
10
+ Typed React Query + Socket.IO helpers that sit on top of RRRoutes contracts. Build endpoints directly from finalized leaves, get strongly-typed hooks/fetchers, ready-to-use cache keys, debug logging, and optional socket utilities (client + React provider + socketed routes).
4
11
 
5
12
  ## Installation
6
13
 
@@ -10,63 +17,429 @@ pnpm add @emeryld/rrroutes-client @tanstack/react-query
10
17
  npm install @emeryld/rrroutes-client @tanstack/react-query
11
18
  ```
12
19
 
13
- The client already depends on `@emeryld/rrroutes-contract` and `zod`. You only need to provide React Query as a peer dependency.
20
+ `@emeryld/rrroutes-contract` and `zod` come along as dependencies; you supply React Query.
14
21
 
15
- ## Usage
22
+ ## Quick start (typed GET with React Query)
16
23
 
17
24
  ```ts
18
- import { createRouteClient } from '@emeryld/rrroutes-client';
19
25
  import { QueryClient } from '@tanstack/react-query';
20
- import { registry } from '../routes';
26
+ import { createRouteClient } from '@emeryld/rrroutes-client';
27
+ import { registry } from '../routes'; // from @emeryld/rrroutes-contract + finalize(...)
21
28
 
22
29
  const routeClient = createRouteClient({
23
- queryClient: new QueryClient(),
24
- baseUrl: '/api',
30
+ baseUrl: '/api', // prepended to all paths
31
+ queryClient: new QueryClient(), // shared React Query instance
25
32
  });
26
33
 
27
- const listUsers = routeClient.build(registry.byKey['GET /v1/users']);
34
+ const listUsers = routeClient.build(registry.byKey['GET /v1/users'], {
35
+ staleTime: 60_000,
36
+ onReceive: (data) => console.log('fresh users', data),
37
+ });
28
38
 
29
- function Users() {
39
+ export function Users() {
30
40
  const { data, isLoading } = listUsers.useEndpoint({ query: { search: 'emery' } });
31
41
  if (isLoading) return <p>Loading…</p>;
32
- return <pre>{JSON.stringify(data)}</pre>;
42
+ return <pre>{JSON.stringify(data, null, 2)}</pre>;
33
43
  }
34
44
  ```
35
45
 
36
- `createRouteClient` passes your `QueryClient` instance directly into every `useQuery`, `useInfiniteQuery`, and `useMutation` call. That means you can skip wrapping your app with `QueryClientProvider` if you prefer manual wiring (though it's still supported if you need context for devtools or hydration).
46
+ ## How it works
47
+
48
+ - `createRouteClient` wires your `QueryClient`, base URL, optional custom fetcher, and debug settings.
49
+ - `build(leaf, options?, meta?)` returns a helper that exposes:
50
+ - `useEndpoint(args?)` — React hook for GET/feeds/mutations (typed params/query/body/output).
51
+ - `fetch(...)` — direct fetcher (no cache). Mutations require the body as the last argument.
52
+ - `getQueryKeys(...)` — deterministic cache key used by React Query + invalidation.
53
+ - `invalidate(...)` — invalidate this exact endpoint instance.
54
+ - `setData(updater, args?)` — mutate cache (infinite-aware).
55
+ - For feed endpoints (`cfg.feed === true`), cursors are handled automatically; cache keys omit the cursor so pages merge correctly.
37
56
 
38
- ### Debug logging
57
+ ---
39
58
 
40
- You can opt into verbose logging by passing `debug` when constructing the client:
59
+ ## Detailed usage
60
+
61
+ ### 1) Configure the client
41
62
 
42
63
  ```ts
43
- const routeClient = createRouteClient({
64
+ import { QueryClient } from '@tanstack/react-query';
65
+ import { createRouteClient, defaultFetcher } from '@emeryld/rrroutes-client';
66
+
67
+ const queryClient = new QueryClient();
68
+
69
+ const client = createRouteClient({
70
+ baseUrl: 'https://api.example.com',
71
+ queryClient,
72
+ fetcher: async (req) => {
73
+ // Attach auth headers, reuse defaultFetcher for JSON parsing + error handling
74
+ return defaultFetcher({
75
+ ...req,
76
+ headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
77
+ });
78
+ },
79
+ cursorParam: 'cursor', // optional; default is "cursor"
80
+ getNextCursor: (page) => page?.links?.next ?? page?.nextCursor, // optional override
81
+ environment: process.env.NODE_ENV, // disables debug when "production"
82
+ debug: {
83
+ fetch: true,
84
+ invalidate: true,
85
+ verbose: true, // include params/query/output in debug events
86
+ },
87
+ });
88
+ ```
89
+
90
+ ### 2) Build endpoints from your registry
91
+
92
+ ```ts
93
+ import { registry } from '../routes';
94
+
95
+ // Plain GET
96
+ const getUser = client.build(registry.byKey['GET /v1/users/:userId'], {
97
+ staleTime: 30_000,
98
+ });
99
+
100
+ // Infinite/feed GET (cfg.feed === true)
101
+ const listFeed = client.build(registry.byKey['GET /v1/posts'], {
102
+ getNextPageParam: (last) => last.nextCursor, // React Query option override
103
+ });
104
+
105
+ // Mutation
106
+ const updateUser = client.build(registry.byKey['PATCH /v1/users/:userId'], {
107
+ onSuccess: () => client.invalidate(['get', 'v1', 'users']), // prefix invalidate
108
+ });
109
+ ```
110
+
111
+ ### 3) Use GET hooks (with params/query/body)
112
+
113
+ ```ts
114
+ type User = Awaited<ReturnType<typeof getUser.fetch>>; // fully typed output
115
+
116
+ function Profile({ userId }: { userId: string }) {
117
+ const result = getUser.useEndpoint({ params: { userId } });
118
+ if (result.isLoading) return <p>Loading…</p>;
119
+ if (result.error) return <p>Failed: {String(result.error)}</p>;
120
+
121
+ // Register a listener for push-based updates (e.g., sockets) against this hook
122
+ result.onReceive((freshUser) => {
123
+ console.log('pushed update', freshUser);
124
+ });
125
+
126
+ return <div>{result.data.name}</div>;
127
+ }
128
+ ```
129
+
130
+ - For GET leaves that define a `bodySchema`, pass the body after the args tuple:
131
+
132
+ ```ts
133
+ const auditStatus = client.build(registry.byKey['GET /v1/audit']);
134
+ await auditStatus.fetch({}, { includeExternal: true }); // body matches the leaf's bodySchema
135
+ ```
136
+
137
+ ### 4) Use infinite feeds
138
+
139
+ ```ts
140
+ function PostFeed() {
141
+ const feed = listFeed.useEndpoint({ query: { cursor: undefined, limit: 20 } });
142
+
143
+ return (
144
+ <>
145
+ {(feed.data?.pages ?? []).map((page) =>
146
+ page.items.map((post) => <article key={post.id}>{post.title}</article>),
147
+ )}
148
+ <button
149
+ disabled={!feed.hasNextPage || feed.isFetchingNextPage}
150
+ onClick={() => feed.fetchNextPage()}
151
+ >
152
+ Load more
153
+ </button>
154
+ </>
155
+ );
156
+ }
157
+ ```
158
+
159
+ - Cursor params are stripped from cache keys automatically so pages share the same base key.
160
+
161
+ ### 5) Use mutations (with optimistic cache helpers)
162
+
163
+ ```ts
164
+ async function rename(userId: string, name: string) {
165
+ // Direct fetch (server action / non-React usage)
166
+ await updateUser.fetch({ params: { userId } }, { name });
167
+ }
168
+
169
+ export function RenameForm({ userId }: { userId: string }) {
170
+ const mutation = updateUser.useEndpoint({ params: { userId } });
171
+
172
+ async function submit(e: React.FormEvent) {
173
+ e.preventDefault();
174
+ const name = new FormData(e.currentTarget).get('name') as string;
175
+
176
+ // Optimistically update cache for both the detail and list
177
+ updateUser.setData((prev) => (prev ? { ...prev, name } : prev), { params: { userId } });
178
+ client.invalidate(['get', 'v1', 'users']);
179
+
180
+ await mutation.mutateAsync({ name });
181
+ }
182
+
183
+ return (
184
+ <form onSubmit={submit}>
185
+ <input name="name" defaultValue="" />
186
+ <button disabled={mutation.isLoading}>Save</button>
187
+ {mutation.error && <p>Error: {String(mutation.error)}</p>}
188
+ </form>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ### 6) Cache keys, invalidation, and manual cache writes
194
+
195
+ ```ts
196
+ const keys = getUser.getQueryKeys({ params: { userId: 'u_1' } }); // ['get','v1','users','u_1', {}]
197
+ await getUser.invalidate({ params: { userId: 'u_1' } }); // invalidate exact detail
198
+ await client.invalidate(['get', 'v1', 'users']); // invalidate any users endpoints
199
+
200
+ getUser.setData((prev) => (prev ? { ...prev, status: 'online' } : prev), {
201
+ params: { userId: 'u_1' },
202
+ });
203
+ ```
204
+
205
+ `setData` respects feeds (updates `InfiniteData` shape when `cfg.feed === true`).
206
+
207
+ ### 7) Router helper (build by name instead of leaf)
208
+
209
+ ```ts
210
+ import { buildRouter } from '@emeryld/rrroutes-client';
211
+ import { registry } from '../routes';
212
+
213
+ const routes = {
214
+ listUsers: registry.byKey['GET /v1/users'],
215
+ updateUser: registry.byKey['PATCH /v1/users/:userId'],
216
+ } as const;
217
+
218
+ const buildRoute = buildRouter(client, routes);
219
+
220
+ const listUsers = buildRoute('listUsers'); // builds from routes.listUsers
221
+ const updateUser = buildRoute('updateUser', {}, { name: 'profile' }); // debug name filtering
222
+ ```
223
+
224
+ ### 8) File uploads (FormData)
225
+
226
+ If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData`:
227
+
228
+ ```ts
229
+ const uploadAvatar = client.build(registry.byKey['PUT /v1/users/:userId/avatar']);
230
+
231
+ await uploadAvatar.fetch(
232
+ { params: { userId: 'u_1' } },
233
+ { avatar: new File([blob], 'avatar.png', { type: 'image/png' }) },
234
+ );
235
+ ```
236
+
237
+ ### 9) Debug logging
238
+
239
+ ```ts
240
+ const client = createRouteClient({
44
241
  baseUrl: '/api',
45
- queryClient: new QueryClient(),
46
- debug: process.env.NODE_ENV !== 'production',
47
- // or customize the logger:
48
- // debug: {
49
- // enabled: true,
50
- // logger: (event) => console.info('[rrroutes-client]', event),
51
- // },
242
+ queryClient,
243
+ debug: {
244
+ build: true,
245
+ fetch: true,
246
+ invalidate: true,
247
+ setData: true,
248
+ useEndpoint: true,
249
+ verbose: true,
250
+ // Limit to specific endpoints by name (third arg to build)
251
+ only: ['profile', 'feed'],
252
+ logger: (e) => console.info('[rrroutes-client]', e),
253
+ },
52
254
  });
255
+
256
+ const profile = client.build(registry.byKey['GET /v1/me'], {}, { name: 'profile' });
53
257
  ```
54
258
 
55
- Events cover fetch lifecycles (`start`/`success`/`error`), cache updates, and invalidations so you can trace how RRRoutes interacts with React Query.
259
+ Set `environment: 'production'` to silence all debug output regardless of the `debug` option.
56
260
 
57
- ## Scripts
261
+ ---
58
262
 
59
- ```sh
60
- pnpm install
61
- pnpm --filter @emeryld/rrroutes-client build
62
- pnpm --filter @emeryld/rrroutes-client test
263
+ ## Socket utilities (optional)
264
+
265
+ The package also ships a typed Socket.IO client, React provider hooks, and a helper to merge socket events into React Query caches.
266
+
267
+ ### Define events + config
268
+
269
+ ```ts
270
+ import { z } from 'zod';
271
+ import { defineSocketEvents } from '@emeryld/rrroutes-contract';
272
+
273
+ const { events, config } = defineSocketEvents(
274
+ {
275
+ joinMetaMessage: z.object({ source: z.string().optional() }),
276
+ leaveMetaMessage: z.object({ source: z.string().optional() }),
277
+ pingPayload: z.object({ clientEcho: z.object({ sentAt: z.string() }).optional() }),
278
+ pongPayload: z.object({
279
+ clientEcho: z.object({ sentAt: z.string() }).optional(),
280
+ sinceMs: z.number().optional(),
281
+ }),
282
+ },
283
+ {
284
+ 'chat:message': { message: z.object({ roomId: z.string(), text: z.string(), userId: z.string() }) },
285
+ },
286
+ );
63
287
  ```
64
288
 
65
- ## Publishing
289
+ ### Vanilla SocketClient
66
290
 
67
- ```sh
68
- cd packages/client
69
- npm publish --access public
291
+ ```ts
292
+ import { io } from 'socket.io-client';
293
+ import { SocketClient } from '@emeryld/rrroutes-client';
294
+ import { events, config } from './socketContract';
295
+
296
+ const socket = io('https://socket.example.com', { transports: ['websocket'] });
297
+
298
+ const client = new SocketClient(events, {
299
+ socket,
300
+ config,
301
+ sys: {
302
+ 'sys:ping': async () => ({
303
+ clientEcho: { sentAt: new Date().toISOString() },
304
+ }),
305
+ 'sys:pong': async ({ payload }) => {
306
+ console.log('pong latency', payload.sinceMs);
307
+ },
308
+ 'sys:room_join': async ({ rooms }) => {
309
+ console.log('joining rooms', rooms);
310
+ return true; // allow join
311
+ },
312
+ 'sys:room_leave': async ({ rooms }) => {
313
+ console.log('leaving rooms', rooms);
314
+ return true;
315
+ },
316
+ },
317
+ heartbeat: { intervalMs: 15_000, timeoutMs: 7_500 },
318
+ debug: { receive: true, emit: true, verbose: true, logger: console.log },
319
+ });
320
+
321
+ client.on('chat:message', (payload, { ctx }) => {
322
+ console.log('socket message', payload.text, 'latency', ctx.latencyMs);
323
+ });
324
+
325
+ void client.emit('chat:message', { roomId: 'general', text: 'hi', userId: 'u_1' });
326
+ ```
327
+
328
+ Key methods: `emit`, `on`, `joinRooms` / `leaveRooms`, `startHeartbeat` / `stopHeartbeat`, `connect` / `disconnect`, `stats()`, `destroy()`.
329
+
330
+ ### React provider + hooks
331
+
332
+ ```ts
333
+ import { buildSocketProvider } from '@emeryld/rrroutes-client';
334
+ import { io } from 'socket.io-client';
335
+ import { events, config } from './socketContract';
336
+
337
+ const { SocketProvider, useSocketClient, useSocketConnection } = buildSocketProvider({
338
+ events,
339
+ options: {
340
+ config,
341
+ sys: {
342
+ 'sys:ping': async () => ({ clientEcho: { sentAt: new Date().toISOString() } }),
343
+ 'sys:pong': async () => {},
344
+ 'sys:room_join': async () => true,
345
+ 'sys:room_leave': async () => true,
346
+ },
347
+ heartbeat: { intervalMs: 10_000 },
348
+ debug: { receive: true, hook: true, logger: console.log },
349
+ },
350
+ });
351
+
352
+ function App({ children }: { children: React.ReactNode }) {
353
+ return (
354
+ <SocketProvider
355
+ getSocket={() => io('https://socket.example.com')}
356
+ destroyLeaveMeta={{ source: 'app:unmount' }}
357
+ fallback={<p>Connecting…</p>}
358
+ >
359
+ {children}
360
+ </SocketProvider>
361
+ );
362
+ }
363
+
364
+ function RoomMessages({ roomId }: { roomId: string }) {
365
+ const client = useSocketClient<typeof events, typeof config>();
366
+
367
+ useSocketConnection({
368
+ event: 'chat:message',
369
+ rooms: roomId,
370
+ joinMeta: { source: 'room-hydration' },
371
+ leaveMeta: { source: 'room-hydration' },
372
+ onMessage: (payload) => console.log('message for room', payload.text),
373
+ });
374
+
375
+ return (
376
+ <button onClick={() => client.emit('chat:message', { roomId, text: 'ping', userId: 'me' })}>
377
+ Send
378
+ </button>
379
+ );
380
+ }
381
+ ```
382
+
383
+ ### Socket + React Query: `buildSocketedRoute`
384
+
385
+ Automatically join rooms based on fetched data and patch the cache when socket messages arrive.
386
+
387
+ ```ts
388
+ import { buildSocketedRoute } from '@emeryld/rrroutes-client';
389
+ import { useSocketClient } from './socketProvider';
390
+
391
+ const listRooms = client.build(registry.byKey['GET /v1/rooms'], { staleTime: 120_000 });
392
+
393
+ const useSocketedRooms = buildSocketedRoute({
394
+ built: listRooms,
395
+ event: 'chat:message',
396
+ toRooms: (page) => page.items.map((r) => r.id), // derive rooms from data (feeds supported)
397
+ joinMeta: { source: 'rooms:list' },
398
+ leaveMeta: { source: 'rooms:list' },
399
+ useSocketClient,
400
+ applyMessage: (prev, payload) => {
401
+ if (!prev) return prev;
402
+ // Example: bump unread count in cache
403
+ const apply = (items: any[]) =>
404
+ items.map((room) =>
405
+ room.id === payload.roomId ? { ...room, unread: (room.unread ?? 0) + 1 } : room,
406
+ );
407
+ return 'pages' in prev
408
+ ? { ...prev, pages: prev.pages.map((p) => ({ ...p, items: apply(p.items) })) }
409
+ : { ...prev, items: apply(prev.items) };
410
+ },
411
+ });
412
+
413
+ function RoomList() {
414
+ const { data, rooms } = useSocketedRooms();
415
+ return (
416
+ <>
417
+ <p>Subscribed rooms: {rooms.join(', ')}</p>
418
+ <ul>{data?.items.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
419
+ </>
420
+ );
421
+ }
70
422
  ```
71
423
 
72
- Always bump `packages/client/package.json` before publishing so consumers can pick up the new release.
424
+ ---
425
+
426
+ ## Edge cases & notes
427
+
428
+ - Mutation `fetch` requires a body argument; GET `fetch` only requires a body when the leaf defines one.
429
+ - Path and query params are validated with the contract schemas before fetch; missing params throw synchronously.
430
+ - Query objects that contain arrays/objects are JSON-stringified in the URL query string.
431
+ - Feed cache keys omit the cursor so `invalidate(['get','v1','posts'])` clears all pages.
432
+ - `setData` runs your updater against the current cache value; return `undefined` to leave the cache untouched.
433
+ - When `environment` is `'production'`, debug logs are disabled even if `debug` is set.
434
+
435
+ ---
436
+
437
+ ## Scripts (monorepo)
438
+
439
+ Run from the repo root:
440
+
441
+ ```sh
442
+ pnpm --filter @emeryld/rrroutes-client build
443
+ pnpm --filter @emeryld/rrroutes-client typecheck
444
+ pnpm --filter @emeryld/rrroutes-client test
445
+ ```
package/dist/index.cjs CHANGED
@@ -71,8 +71,17 @@ var defaultFetcher = async (req) => {
71
71
  // src/routesV3.client.index.ts
72
72
  var import_react = require("react");
73
73
  var import_react_query = require("@tanstack/react-query");
74
+ var import_zod = require("zod");
74
75
  var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
75
76
  var toUpper = (m) => m.toUpperCase();
77
+ var defaultFeedQuerySchema = import_zod.z.object({
78
+ cursor: import_zod.z.string().optional(),
79
+ limit: import_zod.z.coerce.number().min(1).max(100).default(20)
80
+ });
81
+ var defaultFeedOutputSchema = import_zod.z.object({
82
+ items: import_zod.z.array(import_zod.z.unknown()),
83
+ nextCursor: import_zod.z.string().optional()
84
+ });
76
85
  function zParse(value, schema) {
77
86
  return schema ? schema.parse(value) : value;
78
87
  }
@@ -174,6 +183,37 @@ function extractArgs(args) {
174
183
  function toArgsTuple(args) {
175
184
  return typeof args === "undefined" ? [] : [args];
176
185
  }
186
+ function augmentFeedQuerySchema(schema) {
187
+ if (!schema) return defaultFeedQuerySchema;
188
+ if (schema instanceof import_zod.z.ZodObject) {
189
+ const shape = schema.shape ? schema.shape : schema._def?.shape?.();
190
+ return schema.extend({
191
+ ...shape ?? {},
192
+ cursor: defaultFeedQuerySchema.shape.cursor,
193
+ limit: defaultFeedQuerySchema.shape.limit
194
+ });
195
+ }
196
+ return import_zod.z.intersection(schema, defaultFeedQuerySchema);
197
+ }
198
+ function augmentFeedOutputSchema(schema) {
199
+ if (!schema) return defaultFeedOutputSchema;
200
+ if (schema instanceof import_zod.z.ZodObject) {
201
+ const shape = schema.shape ? schema.shape : schema._def?.shape?.();
202
+ const hasItems = Boolean(shape?.items);
203
+ if (hasItems) return schema.extend({ nextCursor: import_zod.z.string().optional() });
204
+ return import_zod.z.object({
205
+ items: import_zod.z.array(schema),
206
+ nextCursor: import_zod.z.string().optional()
207
+ });
208
+ }
209
+ if (schema instanceof import_zod.z.ZodArray) {
210
+ return import_zod.z.object({
211
+ items: schema,
212
+ nextCursor: import_zod.z.string().optional()
213
+ });
214
+ }
215
+ return defaultFeedOutputSchema;
216
+ }
177
217
  function buildUrl(leaf, baseUrl, params, query) {
178
218
  const normalizedParams = zParse(params, leaf.cfg.paramsSchema);
179
219
  const normalizedQuery = zParse(query, leaf.cfg.querySchema);
@@ -202,8 +242,13 @@ function createRouteClient(opts) {
202
242
  function buildInternal(leaf, rqOpts, meta) {
203
243
  const isGet = leaf.method === "get";
204
244
  const isFeed = !!leaf.cfg.feed;
245
+ const leafCfg = isFeed ? {
246
+ ...leaf.cfg,
247
+ querySchema: augmentFeedQuerySchema(leaf.cfg.querySchema),
248
+ outputSchema: augmentFeedOutputSchema(leaf.cfg.outputSchema)
249
+ } : leaf.cfg;
205
250
  const method = toUpper(leaf.method);
206
- const expectsArgs = Boolean(leaf.cfg.paramsSchema || leaf.cfg.querySchema);
251
+ const expectsArgs = Boolean(leafCfg.paramsSchema || leafCfg.querySchema);
207
252
  const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
208
253
  const debugName = meta?.name;
209
254
  const emit = (event) => emitDebug(event, debugName);
@@ -247,13 +292,18 @@ function createRouteClient(opts) {
247
292
  const a = extractArgs(tuple);
248
293
  const params = a?.params;
249
294
  const query = options?.queryOverride ?? a?.query;
250
- const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
295
+ const { url, normalizedQuery, normalizedParams } = buildUrl(
296
+ { ...leaf, cfg: leafCfg },
297
+ baseUrl,
298
+ params,
299
+ query
300
+ );
251
301
  let payload;
252
- const acceptsBody = Boolean(leaf.cfg.bodySchema);
302
+ const acceptsBody = Boolean(leafCfg.bodySchema);
253
303
  const requiresBody = options?.requireBody ?? (!isGet && acceptsBody);
254
304
  if (typeof options?.body !== "undefined") {
255
- const normalizedBody = zParse(options.body, leaf.cfg.bodySchema);
256
- const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
305
+ const normalizedBody = zParse(options.body, leafCfg.bodySchema);
306
+ const isMultipart = Array.isArray(leafCfg.bodyFiles) && leafCfg.bodyFiles.length > 0;
257
307
  payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
258
308
  } else if (requiresBody) {
259
309
  throw new Error("Body is required when invoking a mutation fetch.");
@@ -277,7 +327,7 @@ function createRouteClient(opts) {
277
327
  const out = await fetcher(
278
328
  payload === void 0 ? { url, method } : { url, method, body: payload }
279
329
  );
280
- const parsed = zParse(out, leaf.cfg.outputSchema);
330
+ const parsed = zParse(out, leafCfg.outputSchema);
281
331
  emit(
282
332
  decorateDebugEvent(
283
333
  {
@@ -313,7 +363,7 @@ function createRouteClient(opts) {
313
363
  }
314
364
  };
315
365
  const fetchGet = (...tupleWithBody) => {
316
- const acceptsBody = Boolean(leaf.cfg.bodySchema);
366
+ const acceptsBody = Boolean(leafCfg.bodySchema);
317
367
  const tupleLength = tupleWithBody.length;
318
368
  const maybeBodyIndex = expectsArgs ? 1 : 0;
319
369
  const hasBodyCandidate = acceptsBody && tupleLength > maybeBodyIndex;
@@ -346,7 +396,12 @@ function createRouteClient(opts) {
346
396
  },
347
397
  []
348
398
  );
349
- const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
399
+ const { normalizedQuery, normalizedParams } = buildUrl(
400
+ { ...leaf, cfg: leafCfg },
401
+ baseUrl,
402
+ params,
403
+ query
404
+ );
350
405
  const queryResult = (0, import_react_query.useInfiniteQuery)({
351
406
  ...buildOptions,
352
407
  queryKey: getQueryKeys(...tuple),
@@ -400,7 +455,7 @@ function createRouteClient(opts) {
400
455
  },
401
456
  []
402
457
  );
403
- buildUrl(leaf, baseUrl, params, query);
458
+ buildUrl({ ...leaf, cfg: leafCfg }, baseUrl, params, query);
404
459
  const queryResult = (0, import_react_query.useQuery)({
405
460
  ...buildOptions,
406
461
  queryKey: getQueryKeys(...tuple),
@@ -488,9 +543,9 @@ function toFormData(body) {
488
543
  }
489
544
 
490
545
  // src/sockets/socket.client.sys.ts
491
- var import_zod = require("zod");
492
- var roomValueSchema = import_zod.z.union([import_zod.z.array(import_zod.z.string()), import_zod.z.string()]);
493
- var buildRoomPayloadSchema = (metaSchema) => import_zod.z.object({
546
+ var import_zod2 = require("zod");
547
+ var roomValueSchema = import_zod2.z.union([import_zod2.z.array(import_zod2.z.string()), import_zod2.z.string()]);
548
+ var buildRoomPayloadSchema = (metaSchema) => import_zod2.z.object({
494
549
  rooms: roomValueSchema,
495
550
  meta: metaSchema
496
551
  });