@emeryld/rrroutes-client 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,27 @@ function Users() {
33
33
  }
34
34
  ```
35
35
 
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).
37
+
38
+ ### Debug logging
39
+
40
+ You can opt into verbose logging by passing `debug` when constructing the client:
41
+
42
+ ```ts
43
+ const routeClient = createRouteClient({
44
+ 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
+ // },
52
+ });
53
+ ```
54
+
55
+ Events cover fetch lifecycles (`start`/`success`/`error`), cache updates, and invalidations so you can trace how RRRoutes interacts with React Query.
56
+
36
57
  ## Scripts
37
58
 
38
59
  ```sh
package/dist/index.cjs CHANGED
@@ -27,11 +27,6 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/routesV3.client.fetch.ts
30
- var logger = {
31
- debug: (...args) => {
32
- console.log("[routesV3.client.fetch]", ...args);
33
- }
34
- };
35
30
  var defaultFetcher = async (req) => {
36
31
  const headers = { ...req.headers ?? {} };
37
32
  const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
@@ -39,7 +34,6 @@ var defaultFetcher = async (req) => {
39
34
  headers["Content-Type"] || (headers["Content-Type"] = "application/json");
40
35
  headers["Accept"] || (headers["Accept"] = "application/json");
41
36
  }
42
- logger.debug("fetch", req.method, req.url, isFormData ? "FormData" : req.body);
43
37
  const res = await fetch(req.url, {
44
38
  method: req.method,
45
39
  headers,
@@ -95,6 +89,28 @@ function stripKey(obj, key) {
95
89
  return rest;
96
90
  }
97
91
  var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
92
+ var noopDebugLogger = () => {
93
+ };
94
+ var defaultDebugLogger = (event) => {
95
+ if (typeof console === "undefined") return;
96
+ const fn = console.debug ?? console.log;
97
+ fn?.call(console, "[rrroutes-client]", event);
98
+ };
99
+ function createDebugEmitter(option) {
100
+ if (!option) {
101
+ return noopDebugLogger;
102
+ }
103
+ if (option === true) {
104
+ return defaultDebugLogger;
105
+ }
106
+ if (typeof option === "function") {
107
+ return option;
108
+ }
109
+ if (option.enabled === false) {
110
+ return noopDebugLogger;
111
+ }
112
+ return option.logger ?? defaultDebugLogger;
113
+ }
98
114
  function extractArgs(args) {
99
115
  return args[0];
100
116
  }
@@ -111,12 +127,17 @@ function createRouteClient(opts) {
111
127
  const baseUrl = opts.baseUrl;
112
128
  const cursorParam = opts.cursorParam ?? "cursor";
113
129
  const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
130
+ const emitDebug = createDebugEmitter(opts.debug);
114
131
  async function invalidate(prefix, exact = false) {
115
- await queryClient.invalidateQueries({ queryKey: prefix, exact });
132
+ const queryKey = prefix;
133
+ await queryClient.invalidateQueries({ queryKey, exact });
134
+ emitDebug({ type: "invalidate", key: queryKey, exact });
116
135
  }
117
136
  function buildInternal(leaf, rqOpts) {
118
137
  const isGet = leaf.method === "get";
119
138
  const isFeed = !!leaf.cfg.feed;
139
+ const method = toUpper(leaf.method);
140
+ const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
120
141
  const key = (...tuple) => {
121
142
  const a = extractArgs(tuple);
122
143
  const params = a?.params;
@@ -124,7 +145,11 @@ function createRouteClient(opts) {
124
145
  const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
125
146
  return (0, import_rrroutes_contract.buildCacheKey)({ leaf, params, query: qForKey });
126
147
  };
127
- const invalidateExact = (...tuple) => queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });
148
+ const invalidateExact = async (...tuple) => {
149
+ const queryKey = key(...tuple);
150
+ await queryClient.invalidateQueries({ queryKey, exact: true });
151
+ emitDebug({ type: "invalidate", key: queryKey, exact: true });
152
+ };
128
153
  const setData = (...args) => {
129
154
  const [updater, ...rest] = args;
130
155
  const k = key(...rest);
@@ -139,6 +164,7 @@ function createRouteClient(opts) {
139
164
  (prev) => typeof updater === "function" ? updater(prev) : updater
140
165
  );
141
166
  }
167
+ emitDebug({ type: "setData", key: k });
142
168
  };
143
169
  if (isGet && isFeed) {
144
170
  const useEndpoint2 = (...tuple) => {
@@ -158,11 +184,34 @@ function createRouteClient(opts) {
158
184
  ...pageParam ? { [cursorParam]: pageParam } : {}
159
185
  };
160
186
  const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
161
- const out = await fetcher({ url, method: toUpper(leaf.method) });
162
- return zParse(out, leaf.cfg.outputSchema);
187
+ const startedAt = Date.now();
188
+ emitDebug({ type: "fetch", stage: "start", method, url, leaf: leafLabel });
189
+ try {
190
+ const out = await fetcher({ url, method });
191
+ emitDebug({
192
+ type: "fetch",
193
+ stage: "success",
194
+ method,
195
+ url,
196
+ leaf: leafLabel,
197
+ durationMs: Date.now() - startedAt
198
+ });
199
+ return zParse(out, leaf.cfg.outputSchema);
200
+ } catch (error) {
201
+ emitDebug({
202
+ type: "fetch",
203
+ stage: "error",
204
+ method,
205
+ url,
206
+ leaf: leafLabel,
207
+ durationMs: Date.now() - startedAt,
208
+ error
209
+ });
210
+ throw error;
211
+ }
163
212
  }
164
213
  // NOTE: TData is InfiniteData<T>, so we don't need a select here.
165
- });
214
+ }, queryClient);
166
215
  };
167
216
  return {
168
217
  key,
@@ -182,10 +231,33 @@ function createRouteClient(opts) {
182
231
  queryKey: key(...tuple),
183
232
  placeholderData: import_react_query.keepPreviousData,
184
233
  queryFn: async () => {
185
- const out = await fetcher({ url, method: toUpper(leaf.method) });
186
- return zParse(out, leaf.cfg.outputSchema);
234
+ const startedAt = Date.now();
235
+ emitDebug({ type: "fetch", stage: "start", method, url, leaf: leafLabel });
236
+ try {
237
+ const out = await fetcher({ url, method });
238
+ emitDebug({
239
+ type: "fetch",
240
+ stage: "success",
241
+ method,
242
+ url,
243
+ leaf: leafLabel,
244
+ durationMs: Date.now() - startedAt
245
+ });
246
+ return zParse(out, leaf.cfg.outputSchema);
247
+ } catch (error) {
248
+ emitDebug({
249
+ type: "fetch",
250
+ stage: "error",
251
+ method,
252
+ url,
253
+ leaf: leafLabel,
254
+ durationMs: Date.now() - startedAt,
255
+ error
256
+ });
257
+ throw error;
258
+ }
187
259
  }
188
- });
260
+ }, queryClient);
189
261
  };
190
262
  return {
191
263
  key,
@@ -206,10 +278,41 @@ function createRouteClient(opts) {
206
278
  const normalizedBody = zParse(body, leaf.cfg.bodySchema);
207
279
  const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
208
280
  const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
209
- const out = await fetcher({ url, method: toUpper(leaf.method), body: payload });
210
- return zParse(out, leaf.cfg.outputSchema);
281
+ const startedAt = Date.now();
282
+ emitDebug({
283
+ type: "fetch",
284
+ stage: "start",
285
+ method,
286
+ url,
287
+ leaf: leafLabel,
288
+ body: payload
289
+ });
290
+ try {
291
+ const out = await fetcher({ url, method, body: payload });
292
+ emitDebug({
293
+ type: "fetch",
294
+ stage: "success",
295
+ method,
296
+ url,
297
+ leaf: leafLabel,
298
+ durationMs: Date.now() - startedAt
299
+ });
300
+ return zParse(out, leaf.cfg.outputSchema);
301
+ } catch (error) {
302
+ emitDebug({
303
+ type: "fetch",
304
+ stage: "error",
305
+ method,
306
+ url,
307
+ leaf: leafLabel,
308
+ durationMs: Date.now() - startedAt,
309
+ body: payload,
310
+ error
311
+ });
312
+ throw error;
313
+ }
211
314
  }
212
- });
315
+ }, queryClient);
213
316
  };
214
317
  return {
215
318
  key,
@@ -1 +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\nconst logger = {\n debug: (...args: any[]) => {\n // Uncomment to enable debug logging\n console.log('[routesV3.client.fetch]', ...args);\n },\n};\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 logger.debug('fetch', req.method, req.url, isFormData ? 'FormData' : req.body);\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 BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\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// 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 and normalized query payload.\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 };\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(opts: RouteClientOptions): RouteClient {\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\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 await queryClient.invalidateQueries({ queryKey: prefix as unknown as 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 ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\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 = (...tuple: ArgsTuple<L>) =>\n queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });\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 };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\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 } = 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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\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 } = 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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\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 useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\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 } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method), body: payload });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['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"]}
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 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 RouteClientDebugOptions,\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\nfunction createDebugEmitter(option?: RouteClientDebugOptions): RouteClientDebugLogger {\n if (!option) {\n return noopDebugLogger;\n }\n if (option === true) {\n return defaultDebugLogger;\n }\n if (typeof option === 'function') {\n return option;\n }\n if (option.enabled === false) {\n return noopDebugLogger;\n }\n return option.logger ?? defaultDebugLogger;\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 and normalized query payload.\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 };\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(opts: RouteClientOptions): RouteClient {\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 emitDebug = createDebugEmitter(opts.debug);\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 ): 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\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 emitDebug({ 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 emitDebug({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\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 } = 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 emitDebug({ type: 'fetch', stage: 'start', method, url, leaf: leafLabel });\n try {\n const out = await fetcher<unknown>({ url, method });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\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\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\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 } = 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 emitDebug({ type: 'fetch', stage: 'start', method, url, leaf: leafLabel });\n try {\n const out = await fetcher<unknown>({ url, method });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\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 useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\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 } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\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 emitDebug({\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n });\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\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 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 return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['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"]}
package/dist/index.mjs CHANGED
@@ -1,11 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  // src/routesV3.client.fetch.ts
4
- var logger = {
5
- debug: (...args) => {
6
- console.log("[routesV3.client.fetch]", ...args);
7
- }
8
- };
9
4
  var defaultFetcher = async (req) => {
10
5
  const headers = { ...req.headers ?? {} };
11
6
  const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
@@ -13,7 +8,6 @@ var defaultFetcher = async (req) => {
13
8
  headers["Content-Type"] || (headers["Content-Type"] = "application/json");
14
9
  headers["Accept"] || (headers["Accept"] = "application/json");
15
10
  }
16
- logger.debug("fetch", req.method, req.url, isFormData ? "FormData" : req.body);
17
11
  const res = await fetch(req.url, {
18
12
  method: req.method,
19
13
  headers,
@@ -77,6 +71,28 @@ function stripKey(obj, key) {
77
71
  return rest;
78
72
  }
79
73
  var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
74
+ var noopDebugLogger = () => {
75
+ };
76
+ var defaultDebugLogger = (event) => {
77
+ if (typeof console === "undefined") return;
78
+ const fn = console.debug ?? console.log;
79
+ fn?.call(console, "[rrroutes-client]", event);
80
+ };
81
+ function createDebugEmitter(option) {
82
+ if (!option) {
83
+ return noopDebugLogger;
84
+ }
85
+ if (option === true) {
86
+ return defaultDebugLogger;
87
+ }
88
+ if (typeof option === "function") {
89
+ return option;
90
+ }
91
+ if (option.enabled === false) {
92
+ return noopDebugLogger;
93
+ }
94
+ return option.logger ?? defaultDebugLogger;
95
+ }
80
96
  function extractArgs(args) {
81
97
  return args[0];
82
98
  }
@@ -93,12 +109,17 @@ function createRouteClient(opts) {
93
109
  const baseUrl = opts.baseUrl;
94
110
  const cursorParam = opts.cursorParam ?? "cursor";
95
111
  const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
112
+ const emitDebug = createDebugEmitter(opts.debug);
96
113
  async function invalidate(prefix, exact = false) {
97
- await queryClient.invalidateQueries({ queryKey: prefix, exact });
114
+ const queryKey = prefix;
115
+ await queryClient.invalidateQueries({ queryKey, exact });
116
+ emitDebug({ type: "invalidate", key: queryKey, exact });
98
117
  }
99
118
  function buildInternal(leaf, rqOpts) {
100
119
  const isGet = leaf.method === "get";
101
120
  const isFeed = !!leaf.cfg.feed;
121
+ const method = toUpper(leaf.method);
122
+ const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
102
123
  const key = (...tuple) => {
103
124
  const a = extractArgs(tuple);
104
125
  const params = a?.params;
@@ -106,7 +127,11 @@ function createRouteClient(opts) {
106
127
  const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
107
128
  return buildCacheKey({ leaf, params, query: qForKey });
108
129
  };
109
- const invalidateExact = (...tuple) => queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });
130
+ const invalidateExact = async (...tuple) => {
131
+ const queryKey = key(...tuple);
132
+ await queryClient.invalidateQueries({ queryKey, exact: true });
133
+ emitDebug({ type: "invalidate", key: queryKey, exact: true });
134
+ };
110
135
  const setData = (...args) => {
111
136
  const [updater, ...rest] = args;
112
137
  const k = key(...rest);
@@ -121,6 +146,7 @@ function createRouteClient(opts) {
121
146
  (prev) => typeof updater === "function" ? updater(prev) : updater
122
147
  );
123
148
  }
149
+ emitDebug({ type: "setData", key: k });
124
150
  };
125
151
  if (isGet && isFeed) {
126
152
  const useEndpoint2 = (...tuple) => {
@@ -140,11 +166,34 @@ function createRouteClient(opts) {
140
166
  ...pageParam ? { [cursorParam]: pageParam } : {}
141
167
  };
142
168
  const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
143
- const out = await fetcher({ url, method: toUpper(leaf.method) });
144
- return zParse(out, leaf.cfg.outputSchema);
169
+ const startedAt = Date.now();
170
+ emitDebug({ type: "fetch", stage: "start", method, url, leaf: leafLabel });
171
+ try {
172
+ const out = await fetcher({ url, method });
173
+ emitDebug({
174
+ type: "fetch",
175
+ stage: "success",
176
+ method,
177
+ url,
178
+ leaf: leafLabel,
179
+ durationMs: Date.now() - startedAt
180
+ });
181
+ return zParse(out, leaf.cfg.outputSchema);
182
+ } catch (error) {
183
+ emitDebug({
184
+ type: "fetch",
185
+ stage: "error",
186
+ method,
187
+ url,
188
+ leaf: leafLabel,
189
+ durationMs: Date.now() - startedAt,
190
+ error
191
+ });
192
+ throw error;
193
+ }
145
194
  }
146
195
  // NOTE: TData is InfiniteData<T>, so we don't need a select here.
147
- });
196
+ }, queryClient);
148
197
  };
149
198
  return {
150
199
  key,
@@ -164,10 +213,33 @@ function createRouteClient(opts) {
164
213
  queryKey: key(...tuple),
165
214
  placeholderData: keepPreviousData,
166
215
  queryFn: async () => {
167
- const out = await fetcher({ url, method: toUpper(leaf.method) });
168
- return zParse(out, leaf.cfg.outputSchema);
216
+ const startedAt = Date.now();
217
+ emitDebug({ type: "fetch", stage: "start", method, url, leaf: leafLabel });
218
+ try {
219
+ const out = await fetcher({ url, method });
220
+ emitDebug({
221
+ type: "fetch",
222
+ stage: "success",
223
+ method,
224
+ url,
225
+ leaf: leafLabel,
226
+ durationMs: Date.now() - startedAt
227
+ });
228
+ return zParse(out, leaf.cfg.outputSchema);
229
+ } catch (error) {
230
+ emitDebug({
231
+ type: "fetch",
232
+ stage: "error",
233
+ method,
234
+ url,
235
+ leaf: leafLabel,
236
+ durationMs: Date.now() - startedAt,
237
+ error
238
+ });
239
+ throw error;
240
+ }
169
241
  }
170
- });
242
+ }, queryClient);
171
243
  };
172
244
  return {
173
245
  key,
@@ -188,10 +260,41 @@ function createRouteClient(opts) {
188
260
  const normalizedBody = zParse(body, leaf.cfg.bodySchema);
189
261
  const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
190
262
  const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
191
- const out = await fetcher({ url, method: toUpper(leaf.method), body: payload });
192
- return zParse(out, leaf.cfg.outputSchema);
263
+ const startedAt = Date.now();
264
+ emitDebug({
265
+ type: "fetch",
266
+ stage: "start",
267
+ method,
268
+ url,
269
+ leaf: leafLabel,
270
+ body: payload
271
+ });
272
+ try {
273
+ const out = await fetcher({ url, method, body: payload });
274
+ emitDebug({
275
+ type: "fetch",
276
+ stage: "success",
277
+ method,
278
+ url,
279
+ leaf: leafLabel,
280
+ durationMs: Date.now() - startedAt
281
+ });
282
+ return zParse(out, leaf.cfg.outputSchema);
283
+ } catch (error) {
284
+ emitDebug({
285
+ type: "fetch",
286
+ stage: "error",
287
+ method,
288
+ url,
289
+ leaf: leafLabel,
290
+ durationMs: Date.now() - startedAt,
291
+ body: payload,
292
+ error
293
+ });
294
+ throw error;
295
+ }
193
296
  }
194
- });
297
+ }, queryClient);
195
298
  };
196
299
  return {
197
300
  key,
@@ -1 +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\nconst logger = {\n debug: (...args: any[]) => {\n // Uncomment to enable debug logging\n console.log('[routesV3.client.fetch]', ...args);\n },\n};\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 logger.debug('fetch', req.method, req.url, isFormData ? 'FormData' : req.body);\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 BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\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// 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 and normalized query payload.\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 };\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(opts: RouteClientOptions): RouteClient {\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\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 await queryClient.invalidateQueries({ queryKey: prefix as unknown as 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 ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\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 = (...tuple: ArgsTuple<L>) =>\n queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });\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 };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\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 } = 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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\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 } = 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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\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 useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\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 } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\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 out = await fetcher<unknown>({ url, method: toUpper(leaf.method), body: payload });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['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"]}
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 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 RouteClientDebugOptions,\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\nfunction createDebugEmitter(option?: RouteClientDebugOptions): RouteClientDebugLogger {\n if (!option) {\n return noopDebugLogger;\n }\n if (option === true) {\n return defaultDebugLogger;\n }\n if (typeof option === 'function') {\n return option;\n }\n if (option.enabled === false) {\n return noopDebugLogger;\n }\n return option.logger ?? defaultDebugLogger;\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 and normalized query payload.\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 };\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(opts: RouteClientOptions): RouteClient {\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 emitDebug = createDebugEmitter(opts.debug);\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 ): 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\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 emitDebug({ 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 emitDebug({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\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 } = 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 emitDebug({ type: 'fetch', stage: 'start', method, url, leaf: leafLabel });\n try {\n const out = await fetcher<unknown>({ url, method });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\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\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\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 } = 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 emitDebug({ type: 'fetch', stage: 'start', method, url, leaf: leafLabel });\n try {\n const out = await fetcher<unknown>({ url, method });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\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 useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\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 } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\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 emitDebug({\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n });\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n emitDebug({\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n } catch (error) {\n emitDebug({\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 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 return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['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"]}
@@ -17,6 +17,31 @@ export type FetchInput = {
17
17
  };
18
18
  /** Default signature for HTTP fetch implementations. */
19
19
  export type Fetcher = <T>(req: FetchInput) => Promise<T>;
20
+ /** Discrete debug events emitted by the client internals. */
21
+ export type RouteClientDebugEvent = {
22
+ type: 'fetch';
23
+ stage: 'start' | 'success' | 'error';
24
+ method: Uppercase<HttpMethod>;
25
+ url: string;
26
+ leaf: string;
27
+ durationMs?: number;
28
+ body?: unknown;
29
+ error?: unknown;
30
+ } | {
31
+ type: 'invalidate';
32
+ key: QueryKey;
33
+ exact: boolean;
34
+ } | {
35
+ type: 'setData';
36
+ key: QueryKey;
37
+ };
38
+ /** Logger signature invoked when debug logging is enabled. */
39
+ export type RouteClientDebugLogger = (event: RouteClientDebugEvent) => void;
40
+ /** Toggle or customize debug logging produced by the client. */
41
+ export type RouteClientDebugOptions = boolean | RouteClientDebugLogger | {
42
+ enabled?: boolean;
43
+ logger?: RouteClientDebugLogger;
44
+ };
20
45
  /** Configuration passed to `createRouteClient`. */
21
46
  export type RouteClientOptions = {
22
47
  /** Base URL prepended to every route (optional). */
@@ -29,6 +54,8 @@ export type RouteClientOptions = {
29
54
  cursorParam?: string;
30
55
  /** Function that extracts the next cursor from an infinite query page. */
31
56
  getNextCursor?: (page: unknown) => Cursor;
57
+ /** Optional debug logger for verbose insights. */
58
+ debug?: RouteClientDebugOptions;
32
59
  };
33
60
  type OutputOf<L extends AnyLeaf> = InferOutput<L>;
34
61
  type ParamsOf<L extends AnyLeaf> = InferParams<L>;
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-client",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
8
8
  "types": "dist/index.d.ts",
9
+ "react-native": "dist/index.cjs",
9
10
  "exports": {
10
11
  ".": {
12
+ "react-native": "./dist/index.cjs",
11
13
  "types": "./dist/index.d.ts",
12
14
  "import": "./dist/index.mjs",
13
15
  "require": "./dist/index.cjs"