@emeryld/rrroutes-client 1.3.2 → 1.3.4

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/dist/index.cjs ADDED
@@ -0,0 +1,454 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ createRouteClient: () => createRouteClient,
25
+ defaultFetcher: () => defaultFetcher
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/routesV3.client.fetch.ts
30
+ var defaultFetcher = async (req) => {
31
+ const headers = { ...req.headers ?? {} };
32
+ const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
33
+ if (!isFormData) {
34
+ headers["Content-Type"] || (headers["Content-Type"] = "application/json");
35
+ headers["Accept"] || (headers["Accept"] = "application/json");
36
+ }
37
+ const res = await fetch(req.url, {
38
+ method: req.method,
39
+ headers,
40
+ body: isFormData ? req.body : req.body == null ? void 0 : JSON.stringify(req.body)
41
+ });
42
+ const text = await res.text();
43
+ if (!res.ok) {
44
+ const snippet = text.slice(0, 400);
45
+ throw new Error(`[${res.status}] ${res.statusText} \u2014 ${snippet}`);
46
+ }
47
+ try {
48
+ return JSON.parse(text);
49
+ } catch {
50
+ return text;
51
+ }
52
+ };
53
+
54
+ // src/routesV3.client.index.ts
55
+ var import_react_query = require("@tanstack/react-query");
56
+ var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
57
+ var toUpper = (m) => m.toUpperCase();
58
+ function zParse(value, schema) {
59
+ return schema ? schema.parse(value) : value;
60
+ }
61
+ function toSearchString(query) {
62
+ if (!query) return "";
63
+ const params = new URLSearchParams();
64
+ for (const [k, v] of Object.entries(query)) {
65
+ if (v == null) continue;
66
+ if (Array.isArray(v)) {
67
+ v.forEach((x) => {
68
+ if (x == null) return;
69
+ if (typeof x === "object") {
70
+ params.append(k, JSON.stringify(x));
71
+ } else {
72
+ params.append(k, String(x));
73
+ }
74
+ });
75
+ continue;
76
+ }
77
+ if (typeof v === "object") {
78
+ params.set(k, JSON.stringify(v));
79
+ continue;
80
+ }
81
+ params.set(k, String(v));
82
+ }
83
+ const s = params.toString();
84
+ return s ? `?${s}` : "";
85
+ }
86
+ function stripKey(obj, key) {
87
+ if (!obj) return obj;
88
+ const { [key]: _omit, ...rest } = obj;
89
+ return rest;
90
+ }
91
+ var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
92
+ var defaultDebugLogger = (event) => {
93
+ if (typeof console === "undefined") return;
94
+ const fn = console.debug ?? console.log;
95
+ fn?.call(console, "[rrroutes-client]", event);
96
+ };
97
+ var debugEventTypes = [
98
+ "fetch",
99
+ "invalidate",
100
+ "setData",
101
+ "build",
102
+ "useEndpoint"
103
+ ];
104
+ var noopEmit = () => {
105
+ };
106
+ function createDebugEmitter(option, environment) {
107
+ const disabled = { emit: noopEmit, mode: "minimal" };
108
+ if (environment && environment.toLowerCase() === "production") {
109
+ return disabled;
110
+ }
111
+ if (!option) {
112
+ return disabled;
113
+ }
114
+ if (option === true || option === "minimal") {
115
+ return {
116
+ emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),
117
+ mode: "minimal"
118
+ };
119
+ }
120
+ if (option === "complete") {
121
+ return {
122
+ emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),
123
+ mode: "complete"
124
+ };
125
+ }
126
+ if (typeof option === "function") {
127
+ return {
128
+ emit: (event, name) => option(name ? { ...event, name } : event),
129
+ mode: "minimal"
130
+ };
131
+ }
132
+ if (typeof option === "object") {
133
+ const toggles = option;
134
+ const verbose = Boolean(toggles.verbose);
135
+ const enabledTypes = debugEventTypes.filter((type) => toggles[type]);
136
+ if (enabledTypes.length === 0) {
137
+ return { emit: noopEmit, mode: verbose ? "complete" : "minimal" };
138
+ }
139
+ const whitelist = new Set(enabledTypes);
140
+ const onlySet = toggles.only && toggles.only.length > 0 ? new Set(toggles.only) : void 0;
141
+ const logger = toggles.logger ?? defaultDebugLogger;
142
+ const emit = (event, name) => {
143
+ if (!whitelist.has(event.type)) return;
144
+ if (onlySet) {
145
+ if (!name || !onlySet.has(name)) return;
146
+ }
147
+ logger(name ? { ...event, name } : event);
148
+ };
149
+ return { emit, mode: verbose ? "complete" : "minimal" };
150
+ }
151
+ return disabled;
152
+ }
153
+ function extractArgs(args) {
154
+ return args[0];
155
+ }
156
+ function buildUrl(leaf, baseUrl, params, query) {
157
+ const normalizedParams = zParse(params, leaf.cfg.paramsSchema);
158
+ const normalizedQuery = zParse(query, leaf.cfg.querySchema);
159
+ const path = (0, import_rrroutes_contract.compilePath)(leaf.path, normalizedParams ?? {});
160
+ const url = `${baseUrl ?? ""}${path}${toSearchString(normalizedQuery)}`;
161
+ return { url, normalizedQuery, normalizedParams };
162
+ }
163
+ function createRouteClient(opts) {
164
+ const queryClient = opts.queryClient;
165
+ const fetcher = opts.fetcher ?? defaultFetcher;
166
+ const baseUrl = opts.baseUrl;
167
+ const cursorParam = opts.cursorParam ?? "cursor";
168
+ const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
169
+ const environment = opts.environment ?? (typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0);
170
+ const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug, environment);
171
+ const isVerboseDebug = debugMode === "complete";
172
+ const decorateDebugEvent = (event, details) => {
173
+ if (!isVerboseDebug || !details) return event;
174
+ return { ...event, ...details };
175
+ };
176
+ async function invalidate(prefix, exact = false) {
177
+ const queryKey = prefix;
178
+ await queryClient.invalidateQueries({ queryKey, exact });
179
+ emitDebug({ type: "invalidate", key: queryKey, exact });
180
+ }
181
+ function buildInternal(leaf, rqOpts, meta) {
182
+ const isGet = leaf.method === "get";
183
+ const isFeed = !!leaf.cfg.feed;
184
+ const method = toUpper(leaf.method);
185
+ const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
186
+ const debugName = meta?.name;
187
+ const emit = (event) => emitDebug(event, debugName);
188
+ emit({ type: "build", leaf: leafLabel });
189
+ const key = (...tuple) => {
190
+ const a = extractArgs(tuple);
191
+ const params = a?.params;
192
+ const query = a?.query;
193
+ const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
194
+ return (0, import_rrroutes_contract.buildCacheKey)({ leaf, params, query: qForKey });
195
+ };
196
+ const invalidateExact = async (...tuple) => {
197
+ const queryKey = key(...tuple);
198
+ await queryClient.invalidateQueries({ queryKey, exact: true });
199
+ emit({ type: "invalidate", key: queryKey, exact: true });
200
+ };
201
+ const setData = (...args) => {
202
+ const [updater, ...rest] = args;
203
+ const k = key(...rest);
204
+ if (isGet && isFeed) {
205
+ queryClient.setQueryData(
206
+ k,
207
+ (prev) => typeof updater === "function" ? updater(prev) : updater
208
+ );
209
+ } else {
210
+ queryClient.setQueryData(
211
+ k,
212
+ (prev) => typeof updater === "function" ? updater(prev) : updater
213
+ );
214
+ }
215
+ emit({ type: "setData", key: k });
216
+ };
217
+ if (isGet && isFeed) {
218
+ const useEndpoint2 = (...tuple) => {
219
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "infiniteGet" });
220
+ const a = extractArgs(tuple);
221
+ const params = a?.params;
222
+ const query = a?.query;
223
+ const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
224
+ return (0, import_react_query.useInfiniteQuery)({
225
+ ...rqOpts,
226
+ queryKey: key(...tuple),
227
+ initialPageParam: void 0,
228
+ getNextPageParam: (lastPage) => getNextCursor(lastPage),
229
+ placeholderData: import_react_query.keepPreviousData,
230
+ queryFn: async ({ pageParam }) => {
231
+ const pageQuery = {
232
+ ...normalizedQuery,
233
+ ...pageParam ? { [cursorParam]: pageParam } : {}
234
+ };
235
+ const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
236
+ const startedAt = Date.now();
237
+ const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : void 0;
238
+ emit(
239
+ decorateDebugEvent(
240
+ { type: "fetch", stage: "start", method, url, leaf: leafLabel },
241
+ detail
242
+ )
243
+ );
244
+ try {
245
+ const out = await fetcher({ url, method });
246
+ const parsed = zParse(out, leaf.cfg.outputSchema);
247
+ emit(
248
+ decorateDebugEvent(
249
+ {
250
+ type: "fetch",
251
+ stage: "success",
252
+ method,
253
+ url,
254
+ leaf: leafLabel,
255
+ durationMs: Date.now() - startedAt
256
+ },
257
+ isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : void 0
258
+ )
259
+ );
260
+ return parsed;
261
+ } catch (error) {
262
+ emit(
263
+ decorateDebugEvent(
264
+ {
265
+ type: "fetch",
266
+ stage: "error",
267
+ method,
268
+ url,
269
+ leaf: leafLabel,
270
+ durationMs: Date.now() - startedAt,
271
+ error
272
+ },
273
+ detail
274
+ )
275
+ );
276
+ throw error;
277
+ }
278
+ }
279
+ // NOTE: TData is InfiniteData<T>, so we don't need a select here.
280
+ }, queryClient);
281
+ };
282
+ return {
283
+ key,
284
+ invalidate: invalidateExact,
285
+ setData,
286
+ useEndpoint: useEndpoint2
287
+ };
288
+ }
289
+ if (isGet) {
290
+ const useEndpoint2 = (...tuple) => {
291
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "get" });
292
+ const a = extractArgs(tuple);
293
+ const params = a?.params;
294
+ const query = a?.query;
295
+ const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
296
+ return (0, import_react_query.useQuery)({
297
+ ...rqOpts,
298
+ queryKey: key(...tuple),
299
+ placeholderData: import_react_query.keepPreviousData,
300
+ queryFn: async () => {
301
+ const startedAt = Date.now();
302
+ const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
303
+ emit(
304
+ decorateDebugEvent(
305
+ { type: "fetch", stage: "start", method, url, leaf: leafLabel },
306
+ detail
307
+ )
308
+ );
309
+ try {
310
+ const out = await fetcher({ url, method });
311
+ const parsed = zParse(out, leaf.cfg.outputSchema);
312
+ emit(
313
+ decorateDebugEvent(
314
+ {
315
+ type: "fetch",
316
+ stage: "success",
317
+ method,
318
+ url,
319
+ leaf: leafLabel,
320
+ durationMs: Date.now() - startedAt
321
+ },
322
+ isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
323
+ )
324
+ );
325
+ return parsed;
326
+ } catch (error) {
327
+ emit(
328
+ decorateDebugEvent(
329
+ {
330
+ type: "fetch",
331
+ stage: "error",
332
+ method,
333
+ url,
334
+ leaf: leafLabel,
335
+ durationMs: Date.now() - startedAt,
336
+ error
337
+ },
338
+ detail
339
+ )
340
+ );
341
+ throw error;
342
+ }
343
+ }
344
+ }, queryClient);
345
+ };
346
+ return {
347
+ key,
348
+ invalidate: invalidateExact,
349
+ setData,
350
+ useEndpoint: useEndpoint2
351
+ };
352
+ }
353
+ const fetchEndpoint = async (...tupleWithBody) => {
354
+ if (tupleWithBody.length === 0) {
355
+ throw new Error("Body is required when invoking a mutation fetch.");
356
+ }
357
+ const bodyIndex = tupleWithBody.length - 1;
358
+ const tuple = tupleWithBody.slice(0, bodyIndex);
359
+ const body = tupleWithBody[bodyIndex];
360
+ const args = extractArgs(tuple);
361
+ const params = args?.params;
362
+ const query = args?.query;
363
+ const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
364
+ const normalizedBody = zParse(body, leaf.cfg.bodySchema);
365
+ const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
366
+ const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
367
+ const startedAt = Date.now();
368
+ const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
369
+ emit(
370
+ decorateDebugEvent(
371
+ {
372
+ type: "fetch",
373
+ stage: "start",
374
+ method,
375
+ url,
376
+ leaf: leafLabel,
377
+ body: payload
378
+ },
379
+ detail
380
+ )
381
+ );
382
+ try {
383
+ const out = await fetcher({ url, method, body: payload });
384
+ const parsed = zParse(out, leaf.cfg.outputSchema);
385
+ emit(
386
+ decorateDebugEvent(
387
+ {
388
+ type: "fetch",
389
+ stage: "success",
390
+ method,
391
+ url,
392
+ leaf: leafLabel,
393
+ durationMs: Date.now() - startedAt
394
+ },
395
+ isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
396
+ )
397
+ );
398
+ return parsed;
399
+ } catch (error) {
400
+ emit(
401
+ decorateDebugEvent(
402
+ {
403
+ type: "fetch",
404
+ stage: "error",
405
+ method,
406
+ url,
407
+ leaf: leafLabel,
408
+ durationMs: Date.now() - startedAt,
409
+ body: payload,
410
+ error
411
+ },
412
+ detail
413
+ )
414
+ );
415
+ throw error;
416
+ }
417
+ };
418
+ const useEndpoint = (...tuple) => {
419
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "mutation" });
420
+ return (0, import_react_query.useMutation)({
421
+ ...rqOpts,
422
+ mutationKey: key(...tuple),
423
+ mutationFn: (body) => fetchEndpoint(...[...tuple, body])
424
+ }, queryClient);
425
+ };
426
+ return {
427
+ key,
428
+ invalidate: invalidateExact,
429
+ setData,
430
+ useEndpoint,
431
+ fetch: fetchEndpoint
432
+ };
433
+ }
434
+ return {
435
+ queryClient,
436
+ invalidate,
437
+ build: buildInternal
438
+ };
439
+ }
440
+ function toFormData(body) {
441
+ const fd = new FormData();
442
+ for (const [k, v] of Object.entries(body ?? {})) {
443
+ if (v == null) continue;
444
+ if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item));
445
+ else fd.append(k, v);
446
+ }
447
+ return fd;
448
+ }
449
+ // Annotate the CommonJS export names for ESM import in node:
450
+ 0 && (module.exports = {
451
+ createRouteClient,
452
+ defaultFetcher
453
+ });
454
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["/**\n * This package exports React Query hooks, so we mark it as client-only to keep Next.js happy.\n */\n'use client';\n\nexport * from './routesV3.client.types';\nexport * from './routesV3.client.fetch';\nexport * from './routesV3.client.index';\n","// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuildMeta,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n RouteClientDebugEvent,\n RouteClientDebugLogger,\n RouteClientDebugMode,\n RouteClientDebugOptions,\n RouteClientDebugToggleOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Debug logging --------------------------------------------------------------\nconst noopDebugLogger: RouteClientDebugLogger = () => {};\n\nconst defaultDebugLogger: RouteClientDebugLogger = (event: RouteClientDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-client]', event);\n};\n\nconst debugEventTypes: RouteClientDebugEvent['type'][] = [\n 'fetch',\n 'invalidate',\n 'setData',\n 'build',\n 'useEndpoint',\n];\n\ntype DebugEmitter<Names extends string> = {\n emit: (event: RouteClientDebugEvent, name?: Names) => void;\n mode: RouteClientDebugMode;\n};\n\nconst noopEmit = () => {};\n\nfunction createDebugEmitter<Names extends string>(\n option?: RouteClientDebugOptions<Names>,\n environment?: string,\n): DebugEmitter<Names> {\n const disabled: DebugEmitter<Names> = { emit: noopEmit, mode: 'minimal' };\n\n if (environment && environment.toLowerCase() === 'production') {\n return disabled;\n }\n\n if (!option) {\n return disabled;\n }\n if (option === true || option === 'minimal') {\n return {\n emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),\n mode: 'minimal',\n };\n }\n if (option === 'complete') {\n return {\n emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),\n mode: 'complete',\n };\n }\n if (typeof option === 'function') {\n return {\n emit: (event, name) => option(name ? { ...event, name } : event),\n mode: 'minimal',\n };\n }\n if (typeof option === 'object') {\n const toggles = option as RouteClientDebugToggleOptions<Names>;\n const verbose = Boolean(toggles.verbose);\n const enabledTypes = debugEventTypes.filter((type) => toggles[type]);\n if (enabledTypes.length === 0) {\n return { emit: noopEmit, mode: verbose ? 'complete' : 'minimal' };\n }\n const whitelist = new Set<RouteClientDebugEvent['type']>(enabledTypes);\n const onlySet =\n toggles.only && toggles.only.length > 0 ? new Set<Names>(toggles.only) : undefined;\n const logger = toggles.logger ?? defaultDebugLogger;\n const emit: DebugEmitter<Names>['emit'] = (event, name) => {\n if (!whitelist.has(event.type)) return;\n if (onlySet) {\n if (!name || !onlySet.has(name)) return;\n }\n logger(name ? { ...event, name } : event);\n };\n return { emit, mode: verbose ? 'complete' : 'minimal' };\n }\n\n return disabled;\n}\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL plus normalized params/query payloads.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery, normalizedParams };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient<Names extends string = string>(\n opts: RouteClientOptions<Names>,\n): RouteClient<Names> {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n const environment =\n opts.environment ??\n (typeof process !== 'undefined' && process.env ? process.env.NODE_ENV : undefined);\n const { emit: emitDebug, mode: debugMode } = createDebugEmitter<Names>(opts.debug, environment);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteClientDebugEvent>(\n event: T,\n details?: Partial<RouteClientDebugEvent>,\n ): RouteClientDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteClientDebugEvent;\n };\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n const queryKey = prefix as unknown as QueryKey;\n await queryClient.invalidateQueries({ queryKey, exact });\n emitDebug({ type: 'invalidate', key: queryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n meta?: BuildMeta<Names>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n const method = toUpper(leaf.method);\n const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;\n const debugName = meta?.name;\n const emit = (event: RouteClientDebugEvent) => emitDebug(event, debugName);\n emit({ type: 'build', leaf: leafLabel });\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = async (...tuple: ArgsTuple<L>) => {\n const queryKey = key(...tuple);\n await queryClient.invalidateQueries({ queryKey, exact: true });\n emit({ type: 'invalidate', key: queryKey, exact: true });\n };\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n emit({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'infiniteGet' });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const startedAt = Date.now();\n const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : undefined;\n emit(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'get' });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emit(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const fetchEndpoint: BuiltMutation<L>['fetch'] = async (\n ...tupleWithBody: [...ArgsTuple<L>, InferBody<L>]\n ) => {\n if (tupleWithBody.length === 0) {\n throw new Error('Body is required when invoking a mutation fetch.');\n }\n const bodyIndex = tupleWithBody.length - 1;\n const tuple = tupleWithBody.slice(0, bodyIndex) as ArgsTuple<L>;\n const body = tupleWithBody[bodyIndex] as InferBody<L>;\n const args = extractArgs<L>(tuple);\n const params = (args as any)?.params as InferParams<L> | undefined;\n const query = (args as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n body: payload,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n };\n\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'mutation' });\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: (body: InferBody<L>) =>\n fetchEndpoint(...([...tuple, body] as [...ArgsTuple<L>, InferBody<L>])),\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n fetch: fetchEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient<Names>['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
@@ -0,0 +1,3 @@
1
+ export * from './routesV3.client.types';
2
+ export * from './routesV3.client.fetch';
3
+ export * from './routesV3.client.index';
package/dist/index.mjs ADDED
@@ -0,0 +1,435 @@
1
+ "use client";
2
+
3
+ // src/routesV3.client.fetch.ts
4
+ var defaultFetcher = async (req) => {
5
+ const headers = { ...req.headers ?? {} };
6
+ const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
7
+ if (!isFormData) {
8
+ headers["Content-Type"] || (headers["Content-Type"] = "application/json");
9
+ headers["Accept"] || (headers["Accept"] = "application/json");
10
+ }
11
+ const res = await fetch(req.url, {
12
+ method: req.method,
13
+ headers,
14
+ body: isFormData ? req.body : req.body == null ? void 0 : JSON.stringify(req.body)
15
+ });
16
+ const text = await res.text();
17
+ if (!res.ok) {
18
+ const snippet = text.slice(0, 400);
19
+ throw new Error(`[${res.status}] ${res.statusText} \u2014 ${snippet}`);
20
+ }
21
+ try {
22
+ return JSON.parse(text);
23
+ } catch {
24
+ return text;
25
+ }
26
+ };
27
+
28
+ // src/routesV3.client.index.ts
29
+ import {
30
+ keepPreviousData,
31
+ useInfiniteQuery,
32
+ useMutation,
33
+ useQuery
34
+ } from "@tanstack/react-query";
35
+ import {
36
+ buildCacheKey,
37
+ compilePath
38
+ } from "@emeryld/rrroutes-contract";
39
+ var toUpper = (m) => m.toUpperCase();
40
+ function zParse(value, schema) {
41
+ return schema ? schema.parse(value) : value;
42
+ }
43
+ function toSearchString(query) {
44
+ if (!query) return "";
45
+ const params = new URLSearchParams();
46
+ for (const [k, v] of Object.entries(query)) {
47
+ if (v == null) continue;
48
+ if (Array.isArray(v)) {
49
+ v.forEach((x) => {
50
+ if (x == null) return;
51
+ if (typeof x === "object") {
52
+ params.append(k, JSON.stringify(x));
53
+ } else {
54
+ params.append(k, String(x));
55
+ }
56
+ });
57
+ continue;
58
+ }
59
+ if (typeof v === "object") {
60
+ params.set(k, JSON.stringify(v));
61
+ continue;
62
+ }
63
+ params.set(k, String(v));
64
+ }
65
+ const s = params.toString();
66
+ return s ? `?${s}` : "";
67
+ }
68
+ function stripKey(obj, key) {
69
+ if (!obj) return obj;
70
+ const { [key]: _omit, ...rest } = obj;
71
+ return rest;
72
+ }
73
+ var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
74
+ var defaultDebugLogger = (event) => {
75
+ if (typeof console === "undefined") return;
76
+ const fn = console.debug ?? console.log;
77
+ fn?.call(console, "[rrroutes-client]", event);
78
+ };
79
+ var debugEventTypes = [
80
+ "fetch",
81
+ "invalidate",
82
+ "setData",
83
+ "build",
84
+ "useEndpoint"
85
+ ];
86
+ var noopEmit = () => {
87
+ };
88
+ function createDebugEmitter(option, environment) {
89
+ const disabled = { emit: noopEmit, mode: "minimal" };
90
+ if (environment && environment.toLowerCase() === "production") {
91
+ return disabled;
92
+ }
93
+ if (!option) {
94
+ return disabled;
95
+ }
96
+ if (option === true || option === "minimal") {
97
+ return {
98
+ emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),
99
+ mode: "minimal"
100
+ };
101
+ }
102
+ if (option === "complete") {
103
+ return {
104
+ emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),
105
+ mode: "complete"
106
+ };
107
+ }
108
+ if (typeof option === "function") {
109
+ return {
110
+ emit: (event, name) => option(name ? { ...event, name } : event),
111
+ mode: "minimal"
112
+ };
113
+ }
114
+ if (typeof option === "object") {
115
+ const toggles = option;
116
+ const verbose = Boolean(toggles.verbose);
117
+ const enabledTypes = debugEventTypes.filter((type) => toggles[type]);
118
+ if (enabledTypes.length === 0) {
119
+ return { emit: noopEmit, mode: verbose ? "complete" : "minimal" };
120
+ }
121
+ const whitelist = new Set(enabledTypes);
122
+ const onlySet = toggles.only && toggles.only.length > 0 ? new Set(toggles.only) : void 0;
123
+ const logger = toggles.logger ?? defaultDebugLogger;
124
+ const emit = (event, name) => {
125
+ if (!whitelist.has(event.type)) return;
126
+ if (onlySet) {
127
+ if (!name || !onlySet.has(name)) return;
128
+ }
129
+ logger(name ? { ...event, name } : event);
130
+ };
131
+ return { emit, mode: verbose ? "complete" : "minimal" };
132
+ }
133
+ return disabled;
134
+ }
135
+ function extractArgs(args) {
136
+ return args[0];
137
+ }
138
+ function buildUrl(leaf, baseUrl, params, query) {
139
+ const normalizedParams = zParse(params, leaf.cfg.paramsSchema);
140
+ const normalizedQuery = zParse(query, leaf.cfg.querySchema);
141
+ const path = compilePath(leaf.path, normalizedParams ?? {});
142
+ const url = `${baseUrl ?? ""}${path}${toSearchString(normalizedQuery)}`;
143
+ return { url, normalizedQuery, normalizedParams };
144
+ }
145
+ function createRouteClient(opts) {
146
+ const queryClient = opts.queryClient;
147
+ const fetcher = opts.fetcher ?? defaultFetcher;
148
+ const baseUrl = opts.baseUrl;
149
+ const cursorParam = opts.cursorParam ?? "cursor";
150
+ const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
151
+ const environment = opts.environment ?? (typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0);
152
+ const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug, environment);
153
+ const isVerboseDebug = debugMode === "complete";
154
+ const decorateDebugEvent = (event, details) => {
155
+ if (!isVerboseDebug || !details) return event;
156
+ return { ...event, ...details };
157
+ };
158
+ async function invalidate(prefix, exact = false) {
159
+ const queryKey = prefix;
160
+ await queryClient.invalidateQueries({ queryKey, exact });
161
+ emitDebug({ type: "invalidate", key: queryKey, exact });
162
+ }
163
+ function buildInternal(leaf, rqOpts, meta) {
164
+ const isGet = leaf.method === "get";
165
+ const isFeed = !!leaf.cfg.feed;
166
+ const method = toUpper(leaf.method);
167
+ const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
168
+ const debugName = meta?.name;
169
+ const emit = (event) => emitDebug(event, debugName);
170
+ emit({ type: "build", leaf: leafLabel });
171
+ const key = (...tuple) => {
172
+ const a = extractArgs(tuple);
173
+ const params = a?.params;
174
+ const query = a?.query;
175
+ const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
176
+ return buildCacheKey({ leaf, params, query: qForKey });
177
+ };
178
+ const invalidateExact = async (...tuple) => {
179
+ const queryKey = key(...tuple);
180
+ await queryClient.invalidateQueries({ queryKey, exact: true });
181
+ emit({ type: "invalidate", key: queryKey, exact: true });
182
+ };
183
+ const setData = (...args) => {
184
+ const [updater, ...rest] = args;
185
+ const k = key(...rest);
186
+ if (isGet && isFeed) {
187
+ queryClient.setQueryData(
188
+ k,
189
+ (prev) => typeof updater === "function" ? updater(prev) : updater
190
+ );
191
+ } else {
192
+ queryClient.setQueryData(
193
+ k,
194
+ (prev) => typeof updater === "function" ? updater(prev) : updater
195
+ );
196
+ }
197
+ emit({ type: "setData", key: k });
198
+ };
199
+ if (isGet && isFeed) {
200
+ const useEndpoint2 = (...tuple) => {
201
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "infiniteGet" });
202
+ const a = extractArgs(tuple);
203
+ const params = a?.params;
204
+ const query = a?.query;
205
+ const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
206
+ return useInfiniteQuery({
207
+ ...rqOpts,
208
+ queryKey: key(...tuple),
209
+ initialPageParam: void 0,
210
+ getNextPageParam: (lastPage) => getNextCursor(lastPage),
211
+ placeholderData: keepPreviousData,
212
+ queryFn: async ({ pageParam }) => {
213
+ const pageQuery = {
214
+ ...normalizedQuery,
215
+ ...pageParam ? { [cursorParam]: pageParam } : {}
216
+ };
217
+ const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
218
+ const startedAt = Date.now();
219
+ const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : void 0;
220
+ emit(
221
+ decorateDebugEvent(
222
+ { type: "fetch", stage: "start", method, url, leaf: leafLabel },
223
+ detail
224
+ )
225
+ );
226
+ try {
227
+ const out = await fetcher({ url, method });
228
+ const parsed = zParse(out, leaf.cfg.outputSchema);
229
+ emit(
230
+ decorateDebugEvent(
231
+ {
232
+ type: "fetch",
233
+ stage: "success",
234
+ method,
235
+ url,
236
+ leaf: leafLabel,
237
+ durationMs: Date.now() - startedAt
238
+ },
239
+ isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : void 0
240
+ )
241
+ );
242
+ return parsed;
243
+ } catch (error) {
244
+ emit(
245
+ decorateDebugEvent(
246
+ {
247
+ type: "fetch",
248
+ stage: "error",
249
+ method,
250
+ url,
251
+ leaf: leafLabel,
252
+ durationMs: Date.now() - startedAt,
253
+ error
254
+ },
255
+ detail
256
+ )
257
+ );
258
+ throw error;
259
+ }
260
+ }
261
+ // NOTE: TData is InfiniteData<T>, so we don't need a select here.
262
+ }, queryClient);
263
+ };
264
+ return {
265
+ key,
266
+ invalidate: invalidateExact,
267
+ setData,
268
+ useEndpoint: useEndpoint2
269
+ };
270
+ }
271
+ if (isGet) {
272
+ const useEndpoint2 = (...tuple) => {
273
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "get" });
274
+ const a = extractArgs(tuple);
275
+ const params = a?.params;
276
+ const query = a?.query;
277
+ const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
278
+ return useQuery({
279
+ ...rqOpts,
280
+ queryKey: key(...tuple),
281
+ placeholderData: keepPreviousData,
282
+ queryFn: async () => {
283
+ const startedAt = Date.now();
284
+ const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
285
+ emit(
286
+ decorateDebugEvent(
287
+ { type: "fetch", stage: "start", method, url, leaf: leafLabel },
288
+ detail
289
+ )
290
+ );
291
+ try {
292
+ const out = await fetcher({ url, method });
293
+ const parsed = zParse(out, leaf.cfg.outputSchema);
294
+ emit(
295
+ decorateDebugEvent(
296
+ {
297
+ type: "fetch",
298
+ stage: "success",
299
+ method,
300
+ url,
301
+ leaf: leafLabel,
302
+ durationMs: Date.now() - startedAt
303
+ },
304
+ isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
305
+ )
306
+ );
307
+ return parsed;
308
+ } catch (error) {
309
+ emit(
310
+ decorateDebugEvent(
311
+ {
312
+ type: "fetch",
313
+ stage: "error",
314
+ method,
315
+ url,
316
+ leaf: leafLabel,
317
+ durationMs: Date.now() - startedAt,
318
+ error
319
+ },
320
+ detail
321
+ )
322
+ );
323
+ throw error;
324
+ }
325
+ }
326
+ }, queryClient);
327
+ };
328
+ return {
329
+ key,
330
+ invalidate: invalidateExact,
331
+ setData,
332
+ useEndpoint: useEndpoint2
333
+ };
334
+ }
335
+ const fetchEndpoint = async (...tupleWithBody) => {
336
+ if (tupleWithBody.length === 0) {
337
+ throw new Error("Body is required when invoking a mutation fetch.");
338
+ }
339
+ const bodyIndex = tupleWithBody.length - 1;
340
+ const tuple = tupleWithBody.slice(0, bodyIndex);
341
+ const body = tupleWithBody[bodyIndex];
342
+ const args = extractArgs(tuple);
343
+ const params = args?.params;
344
+ const query = args?.query;
345
+ const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
346
+ const normalizedBody = zParse(body, leaf.cfg.bodySchema);
347
+ const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
348
+ const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
349
+ const startedAt = Date.now();
350
+ const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
351
+ emit(
352
+ decorateDebugEvent(
353
+ {
354
+ type: "fetch",
355
+ stage: "start",
356
+ method,
357
+ url,
358
+ leaf: leafLabel,
359
+ body: payload
360
+ },
361
+ detail
362
+ )
363
+ );
364
+ try {
365
+ const out = await fetcher({ url, method, body: payload });
366
+ const parsed = zParse(out, leaf.cfg.outputSchema);
367
+ emit(
368
+ decorateDebugEvent(
369
+ {
370
+ type: "fetch",
371
+ stage: "success",
372
+ method,
373
+ url,
374
+ leaf: leafLabel,
375
+ durationMs: Date.now() - startedAt
376
+ },
377
+ isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
378
+ )
379
+ );
380
+ return parsed;
381
+ } catch (error) {
382
+ emit(
383
+ decorateDebugEvent(
384
+ {
385
+ type: "fetch",
386
+ stage: "error",
387
+ method,
388
+ url,
389
+ leaf: leafLabel,
390
+ durationMs: Date.now() - startedAt,
391
+ body: payload,
392
+ error
393
+ },
394
+ detail
395
+ )
396
+ );
397
+ throw error;
398
+ }
399
+ };
400
+ const useEndpoint = (...tuple) => {
401
+ emit({ type: "useEndpoint", leaf: leafLabel, variant: "mutation" });
402
+ return useMutation({
403
+ ...rqOpts,
404
+ mutationKey: key(...tuple),
405
+ mutationFn: (body) => fetchEndpoint(...[...tuple, body])
406
+ }, queryClient);
407
+ };
408
+ return {
409
+ key,
410
+ invalidate: invalidateExact,
411
+ setData,
412
+ useEndpoint,
413
+ fetch: fetchEndpoint
414
+ };
415
+ }
416
+ return {
417
+ queryClient,
418
+ invalidate,
419
+ build: buildInternal
420
+ };
421
+ }
422
+ function toFormData(body) {
423
+ const fd = new FormData();
424
+ for (const [k, v] of Object.entries(body ?? {})) {
425
+ if (v == null) continue;
426
+ if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item));
427
+ else fd.append(k, v);
428
+ }
429
+ return fd;
430
+ }
431
+ export {
432
+ createRouteClient,
433
+ defaultFetcher
434
+ };
435
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuildMeta,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n RouteClientDebugEvent,\n RouteClientDebugLogger,\n RouteClientDebugMode,\n RouteClientDebugOptions,\n RouteClientDebugToggleOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Debug logging --------------------------------------------------------------\nconst noopDebugLogger: RouteClientDebugLogger = () => {};\n\nconst defaultDebugLogger: RouteClientDebugLogger = (event: RouteClientDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-client]', event);\n};\n\nconst debugEventTypes: RouteClientDebugEvent['type'][] = [\n 'fetch',\n 'invalidate',\n 'setData',\n 'build',\n 'useEndpoint',\n];\n\ntype DebugEmitter<Names extends string> = {\n emit: (event: RouteClientDebugEvent, name?: Names) => void;\n mode: RouteClientDebugMode;\n};\n\nconst noopEmit = () => {};\n\nfunction createDebugEmitter<Names extends string>(\n option?: RouteClientDebugOptions<Names>,\n environment?: string,\n): DebugEmitter<Names> {\n const disabled: DebugEmitter<Names> = { emit: noopEmit, mode: 'minimal' };\n\n if (environment && environment.toLowerCase() === 'production') {\n return disabled;\n }\n\n if (!option) {\n return disabled;\n }\n if (option === true || option === 'minimal') {\n return {\n emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),\n mode: 'minimal',\n };\n }\n if (option === 'complete') {\n return {\n emit: (event, name) => defaultDebugLogger(name ? { ...event, name } : event),\n mode: 'complete',\n };\n }\n if (typeof option === 'function') {\n return {\n emit: (event, name) => option(name ? { ...event, name } : event),\n mode: 'minimal',\n };\n }\n if (typeof option === 'object') {\n const toggles = option as RouteClientDebugToggleOptions<Names>;\n const verbose = Boolean(toggles.verbose);\n const enabledTypes = debugEventTypes.filter((type) => toggles[type]);\n if (enabledTypes.length === 0) {\n return { emit: noopEmit, mode: verbose ? 'complete' : 'minimal' };\n }\n const whitelist = new Set<RouteClientDebugEvent['type']>(enabledTypes);\n const onlySet =\n toggles.only && toggles.only.length > 0 ? new Set<Names>(toggles.only) : undefined;\n const logger = toggles.logger ?? defaultDebugLogger;\n const emit: DebugEmitter<Names>['emit'] = (event, name) => {\n if (!whitelist.has(event.type)) return;\n if (onlySet) {\n if (!name || !onlySet.has(name)) return;\n }\n logger(name ? { ...event, name } : event);\n };\n return { emit, mode: verbose ? 'complete' : 'minimal' };\n }\n\n return disabled;\n}\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL plus normalized params/query payloads.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery, normalizedParams };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient<Names extends string = string>(\n opts: RouteClientOptions<Names>,\n): RouteClient<Names> {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n const environment =\n opts.environment ??\n (typeof process !== 'undefined' && process.env ? process.env.NODE_ENV : undefined);\n const { emit: emitDebug, mode: debugMode } = createDebugEmitter<Names>(opts.debug, environment);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteClientDebugEvent>(\n event: T,\n details?: Partial<RouteClientDebugEvent>,\n ): RouteClientDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteClientDebugEvent;\n };\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n const queryKey = prefix as unknown as QueryKey;\n await queryClient.invalidateQueries({ queryKey, exact });\n emitDebug({ type: 'invalidate', key: queryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n meta?: BuildMeta<Names>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n const method = toUpper(leaf.method);\n const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;\n const debugName = meta?.name;\n const emit = (event: RouteClientDebugEvent) => emitDebug(event, debugName);\n emit({ type: 'build', leaf: leafLabel });\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = async (...tuple: ArgsTuple<L>) => {\n const queryKey = key(...tuple);\n await queryClient.invalidateQueries({ queryKey, exact: true });\n emit({ type: 'invalidate', key: queryKey, exact: true });\n };\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n emit({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'infiniteGet' });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const startedAt = Date.now();\n const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : undefined;\n emit(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'get' });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emit(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const fetchEndpoint: BuiltMutation<L>['fetch'] = async (\n ...tupleWithBody: [...ArgsTuple<L>, InferBody<L>]\n ) => {\n if (tupleWithBody.length === 0) {\n throw new Error('Body is required when invoking a mutation fetch.');\n }\n const bodyIndex = tupleWithBody.length - 1;\n const tuple = tupleWithBody.slice(0, bodyIndex) as ArgsTuple<L>;\n const body = tupleWithBody[bodyIndex] as InferBody<L>;\n const args = extractArgs<L>(tuple);\n const params = (args as any)?.params as InferParams<L> | undefined;\n const query = (args as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emit(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n body: payload,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n };\n\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n emit({ type: 'useEndpoint', leaf: leafLabel, variant: 'mutation' });\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: (body: InferBody<L>) =>\n fetchEndpoint(...([...tuple, body] as [...ArgsTuple<L>, InferBody<L>])),\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n fetch: fetchEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient<Names>['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
@@ -0,0 +1,7 @@
1
+ import { Fetcher } from './routesV3.client.types';
2
+ /**
3
+ * Default fetch implementation used by the route client helper.
4
+ * @param req Normalized request information (URL, method, body, headers).
5
+ * @returns Parsed JSON (or text fallback) from the server response.
6
+ */
7
+ export declare const defaultFetcher: Fetcher;
@@ -0,0 +1,7 @@
1
+ import type { RouteClient, RouteClientOptions } from './routesV3.client.types';
2
+ /**
3
+ * Construct typed React Query helpers backed by a routes-v3 registry leaf.
4
+ * @param opts Route client configuration (query client, fetcher overrides, etc).
5
+ * @returns Object that can build endpoint hooks/mutations from leaves.
6
+ */
7
+ export declare function createRouteClient<Names extends string = string>(opts: RouteClientOptions<Names>): RouteClient<Names>;
@@ -0,0 +1,218 @@
1
+ import type { InfiniteData, QueryClient, QueryKey, UseInfiniteQueryOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ import type { AnyLeaf, HttpMethod, InferBody, InferOutput, InferParams, InferQuery } from '@emeryld/rrroutes-contract';
3
+ /** Helper type used when setting React Query cache data. */
4
+ export type Updater<T> = T | ((prev: T | undefined) => T);
5
+ /** Cursor string extracted from paginated endpoints. */
6
+ export type Cursor = string | undefined;
7
+ /** Runtime request details consumed by a `Fetcher` implementation. */
8
+ export type FetchInput = {
9
+ /** Fully qualified URL that will be requested. */
10
+ url: string;
11
+ /** HTTP method (always uppercase). */
12
+ method: Uppercase<HttpMethod>;
13
+ /** Optional request body (already normalized by the client). */
14
+ body?: unknown;
15
+ /** Additional headers merged into the request. */
16
+ headers?: Record<string, string>;
17
+ };
18
+ /** Default signature for HTTP fetch implementations. */
19
+ export type Fetcher = <T>(req: FetchInput) => Promise<T>;
20
+ /** Debug verbosity levels supported by the client. */
21
+ export type RouteClientDebugMode = 'minimal' | 'complete';
22
+ type RouteClientDebugEventBase = {
23
+ /** Optional logical name supplied when building this endpoint. */
24
+ name?: string;
25
+ };
26
+ /** Discrete debug events emitted by the client internals. */
27
+ export type RouteClientDebugEvent = (RouteClientDebugEventBase & {
28
+ type: 'fetch';
29
+ stage: 'start' | 'success' | 'error';
30
+ method: Uppercase<HttpMethod>;
31
+ url: string;
32
+ leaf: string;
33
+ durationMs?: number;
34
+ body?: unknown;
35
+ params?: unknown;
36
+ query?: unknown;
37
+ output?: unknown;
38
+ error?: unknown;
39
+ }) | (RouteClientDebugEventBase & {
40
+ type: 'invalidate';
41
+ key: QueryKey;
42
+ exact: boolean;
43
+ }) | (RouteClientDebugEventBase & {
44
+ type: 'setData';
45
+ key: QueryKey;
46
+ }) | (RouteClientDebugEventBase & {
47
+ type: 'build';
48
+ leaf: string;
49
+ }) | (RouteClientDebugEventBase & {
50
+ type: 'useEndpoint';
51
+ leaf: string;
52
+ variant: 'get' | 'infiniteGet' | 'mutation';
53
+ });
54
+ /** Logger signature invoked when debug logging is enabled. */
55
+ export type RouteClientDebugLogger = (event: RouteClientDebugEvent) => void;
56
+ /**
57
+ * Toggle-specific configuration controlling which debug events log, whether verbose details
58
+ * are emitted, and which named endpoints are allowed to log.
59
+ */
60
+ export type RouteClientDebugToggleOptions<Names extends string = string> = Partial<Record<RouteClientDebugEvent['type'], boolean>> & {
61
+ /** When true, include verbose metadata (same as `mode === 'complete'`). */
62
+ verbose?: boolean;
63
+ /** Optional logger override used instead of the default console logger. */
64
+ logger?: RouteClientDebugLogger;
65
+ /**
66
+ * Limit logs to endpoints whose debug name is contained in this allow-list.
67
+ * Provide the counterpart by passing `meta: { name: '<value>' }` as the third argument to `client.build`.
68
+ */
69
+ only?: Names[];
70
+ };
71
+ /**
72
+ * Toggle or customize debug logging produced by the client.
73
+ * - Pass `true`/`'minimal'`/`'complete'` for simple modes.
74
+ * - Provide a custom logger function to redirect events.
75
+ * - Supply an object to enable individual event types, flip verbose mode, and/or limit logs via `only`.
76
+ */
77
+ export type RouteClientDebugOptions<Names extends string = string> = boolean | RouteClientDebugLogger | RouteClientDebugMode | RouteClientDebugToggleOptions<Names>;
78
+ /** Optional runtime environment; when set to 'production' logging is disabled. */
79
+ export type RouteClientEnvironment = 'development' | 'production' | (string & {});
80
+ /** Configuration passed to `createRouteClient`. */
81
+ export type RouteClientOptions<Names extends string = string> = {
82
+ /** Base URL prepended to every route (optional). */
83
+ baseUrl: string;
84
+ /** Custom fetch implementation; defaults to `defaultFetcher`. */
85
+ fetcher?: Fetcher;
86
+ /** Shared React Query client used for caching/invalidation. */
87
+ queryClient: QueryClient;
88
+ /** Query string key used for pagination cursors (default: "cursor"). */
89
+ cursorParam?: string;
90
+ /** Function that extracts the next cursor from an infinite query page. */
91
+ getNextCursor?: (page: unknown) => Cursor;
92
+ /** Optional debug logger for verbose insights (supports minimal/complete modes). */
93
+ debug?: RouteClientDebugOptions<Names>;
94
+ /**
95
+ * Optional environment hint used to silence logging in production.
96
+ * When set to `'production'`, all debug logging is disabled regardless of the `debug` option.
97
+ */
98
+ environment?: RouteClientEnvironment;
99
+ };
100
+ type OutputOf<L extends AnyLeaf> = InferOutput<L>;
101
+ type ParamsOf<L extends AnyLeaf> = InferParams<L>;
102
+ type QueryOf<L extends AnyLeaf> = InferQuery<L>;
103
+ type BodyOf<L extends AnyLeaf> = InferBody<L>;
104
+ /** Variadic args consumed by the direct fetch helper: optional args + body. */
105
+ export type MutationFetchArgs<L extends AnyLeaf> = [...ArgsTuple<L>, BodyOf<L>];
106
+ /** Signature for the fetch helper returned by mutation builds. */
107
+ export type MutationFetcher<L extends AnyLeaf> = (...args: MutationFetchArgs<L>) => Promise<OutputOf<L>>;
108
+ /** Optional metadata provided when building a helper (used for debug filtering). */
109
+ export type BuildMeta<Names extends string = string> = {
110
+ /**
111
+ * Logical name assigned to the built endpoint (e.g., the screen or feature name).
112
+ * Combine with `debug.only` to limit which endpoints emit logs.
113
+ */
114
+ name?: Names;
115
+ };
116
+ /** Object shape the user passes to hooks and helpers (params/query are optional). */
117
+ export type ArgsFor<L extends AnyLeaf> = (ParamsOf<L> extends never ? {} : {
118
+ params: ParamsOf<L>;
119
+ }) & (QueryOf<L> extends never ? {} : {
120
+ query: QueryOf<L>;
121
+ });
122
+ /** Variadic tuple representation that omits the argument entirely when not needed. */
123
+ export type ArgsTuple<L extends AnyLeaf> = keyof ArgsFor<L> extends never ? [] : [args: ArgsFor<L>];
124
+ /** Cache data shape for setData(...) */
125
+ export type DataShape<L extends AnyLeaf> = L['method'] extends 'get' ? L['cfg']['feed'] extends true ? InfiniteData<OutputOf<L>> : OutputOf<L> : OutputOf<L>;
126
+ /** React Query build options specialized for a plain GET leaf. */
127
+ export type QueryBuildOptionsFor<L extends AnyLeaf> = Omit<UseQueryOptions<OutputOf<L>, unknown, OutputOf<L>, QueryKey>, 'queryKey' | 'queryFn'>;
128
+ /** Build options for feed-style GET leaves (`cfg.feed === true`). */
129
+ export type InfiniteBuildOptionsFor<L extends AnyLeaf> = Omit<UseInfiniteQueryOptions<OutputOf<L>, unknown, InfiniteData<OutputOf<L>>, QueryKey, Cursor>, 'queryKey' | 'queryFn' | 'initialPageParam' | 'getNextPageParam'>;
130
+ /** Build options for mutation leaves (non-GET). */
131
+ export type MutationBuildOptionsFor<L extends AnyLeaf> = Omit<UseMutationOptions<OutputOf<L>, unknown, BodyOf<L>, unknown>, 'mutationFn' | 'mutationKey'>;
132
+ /** Shared capabilities exposed by every built endpoint helper. */
133
+ export type BuiltCommon<L extends AnyLeaf> = {
134
+ /**
135
+ * Deterministic key (infinite keys omit the cursor by design).
136
+ * @param args Optional params/query tuple for the leaf.
137
+ * @returns Query key tuple.
138
+ */
139
+ key: (...args: ArgsTuple<L>) => QueryKey;
140
+ /**
141
+ * Invalidate exactly this endpoint instance.
142
+ * @param args Optional params/query tuple for the leaf.
143
+ */
144
+ invalidate: (...args: ArgsTuple<L>) => Promise<void>;
145
+ /**
146
+ * Directly update the cache for this endpoint. Handles infinite vs regular.
147
+ * @param updater New value or function applied to existing cache data.
148
+ * @param rest Optional params/query tuple for the leaf.
149
+ */
150
+ setData: (...args: [updater: Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => void;
151
+ };
152
+ /** Hook+helpers for a standard GET endpoint. */
153
+ export type BuiltQuery<L extends AnyLeaf> = BuiltCommon<L> & {
154
+ /**
155
+ * React hook bound to the GET leaf.
156
+ * @param args Optional params/query tuple for the leaf.
157
+ */
158
+ useEndpoint: (...args: ArgsTuple<L>) => UseQueryResult<OutputOf<L>, unknown>;
159
+ };
160
+ /** Hook+helpers for a cursor-paginated GET endpoint. */
161
+ export type BuiltInfinite<L extends AnyLeaf> = BuiltCommon<L> & {
162
+ /**
163
+ * React hook bound to an infinite GET leaf.
164
+ * @param args Optional params/query tuple for the leaf.
165
+ */
166
+ useEndpoint: (...args: ArgsTuple<L>) => UseInfiniteQueryResult<InfiniteData<OutputOf<L>>, unknown>;
167
+ };
168
+ /** Hook+helpers for non-GET endpoints (mutations). */
169
+ export type BuiltMutation<L extends AnyLeaf> = BuiltCommon<L> & {
170
+ /**
171
+ * React hook bound to a mutation leaf.
172
+ * @param args Optional params/query tuple for the leaf.
173
+ */
174
+ useEndpoint: (...args: ArgsTuple<L>) => UseMutationResult<OutputOf<L>, unknown, BodyOf<L>, unknown>;
175
+ /**
176
+ * Direct fetch helper that bypasses React Query, useful for server actions or scripts.
177
+ * Pass params/query first (if required), followed by the body payload.
178
+ */
179
+ fetch: MutationFetcher<L>;
180
+ };
181
+ /** Type-safe union that resolves to the correct built helper for a leaf. */
182
+ export type BuiltForLeaf<L extends AnyLeaf> = L['method'] extends 'get' ? L['cfg']['feed'] extends true ? BuiltInfinite<L> : BuiltQuery<L> : BuiltMutation<L>;
183
+ /** Public surface returned by `createRouteClient`. */
184
+ export type RouteClient<Names extends string = string> = {
185
+ /**
186
+ * Invalidate by a key prefix (e.g., ['get','api','users']).
187
+ * @param prefix Key parts shared by matching endpoints.
188
+ * @param exact When true, invalidate only exact key matches.
189
+ */
190
+ invalidate: (prefix: string[], exact?: boolean) => Promise<void>;
191
+ /** Build a typed endpoint from a leaf. */
192
+ build: {
193
+ <L extends AnyLeaf & {
194
+ method: 'get';
195
+ cfg: {
196
+ feed: true;
197
+ };
198
+ }>(leaf: L, opts?: InfiniteBuildOptionsFor<L>,
199
+ /** Optional metadata (third arg) to assign a debug name for filtering via `debug.only`. */
200
+ meta?: BuildMeta<Names>): BuiltInfinite<L>;
201
+ <L extends AnyLeaf & {
202
+ method: 'get';
203
+ cfg: {
204
+ feed?: false | undefined;
205
+ };
206
+ }>(leaf: L, opts?: QueryBuildOptionsFor<L>,
207
+ /** Optional metadata (third arg) to assign a debug name for filtering via `debug.only`. */
208
+ meta?: BuildMeta<Names>): BuiltQuery<L>;
209
+ <L extends AnyLeaf & {
210
+ method: Exclude<HttpMethod, 'get'>;
211
+ }>(leaf: L, opts?: MutationBuildOptionsFor<L>,
212
+ /** Optional metadata (third arg) to assign a debug name for filtering via `debug.only`. */
213
+ meta?: BuildMeta<Names>): BuiltMutation<L>;
214
+ };
215
+ /** Underlying React Query client (exposed for advanced scenarios). */
216
+ queryClient: QueryClient;
217
+ };
218
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-client",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -19,8 +19,8 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@emeryld/rrroutes-contract": "^1.3.0",
23
- "zod": "^4.1.8"
22
+ "@emeryld/rrroutes-contract": "^1.3.5",
23
+ "zod": "^4.1.12"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "@tanstack/react-query": "^5.87.4"