@reliantlabs/forge-web-runtime 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { type DescMessage, type MessageInitShape } from "@bufbuild/protobuf";
2
- import { type InfiniteData, type UseInfiniteQueryOptions, type UseInfiniteQueryResult, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryResult } from "@tanstack/react-query";
2
+ import { type DataTag, type InfiniteData, type UseInfiniteQueryOptions, type UseInfiniteQueryResult, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryResult } from "@tanstack/react-query";
3
3
  import type { ConnectClientError } from "./errors.js";
4
4
  /**
5
5
  * A generated query key. The tuple is [service, entity?, method,
@@ -20,6 +20,31 @@ export type ServiceQueryKey = readonly unknown[];
20
20
  * cache entry has to hash its request the identical way.
21
21
  */
22
22
  export declare function requestKey<Schema extends DescMessage>(schema: Schema, req: MessageInitShape<Schema>): unknown;
23
+ /**
24
+ * One read's key entry in a generated factory: `orderServiceKeys.getOrder`.
25
+ *
26
+ * Called, it is that request's key, [service, entity?, method,
27
+ * protojson(req)]. Its `.all` is the rpc's scope, [service, entity?,
28
+ * method]: a prefix of every key the rpc caches under — every request, the
29
+ * plain query and the `<method>Infinite` read alike — and of no other rpc's.
30
+ *
31
+ * queryClient.invalidateQueries({ queryKey: orderServiceKeys.listOrders.all })
32
+ * queryClient.setQueriesData({ queryKey: orderServiceKeys.getOrder.all }, patch)
33
+ *
34
+ * It exists so code never names a key's tuple positions. A predicate like
35
+ * `query.queryKey[1] === "listOrders"` breaks silently when a
36
+ * `// forge:query-scope` marker inserts the entity at index 1; `.all` is
37
+ * built by the same function that builds the keys, so it moves with them.
38
+ *
39
+ * `.all` is not data-tagged. It names a SET of entries, and for a paged
40
+ * read that set holds two shapes (the response, and the infinite read's
41
+ * `{ pages, pageParams }`), so a `setQueriesData` updater over it must
42
+ * handle both.
43
+ */
44
+ export type ServiceMethodKey<Schema extends DescMessage> = ((req: MessageInitShape<Schema>) => ServiceQueryKey) & {
45
+ /** Every cached request of this rpc: [service, entity?, method]. */
46
+ readonly all: ServiceQueryKey;
47
+ };
23
48
  /** The three key-scope builders a generated key factory is assembled from. */
24
49
  export interface ServiceKeyBuilders {
25
50
  /** Every query on this service: [service]. */
@@ -29,9 +54,10 @@ export interface ServiceKeyBuilders {
29
54
  /**
30
55
  * One query: [service, entity?, method, protojson(req)]. Returns the
31
56
  * per-request key FUNCTION, which is what both the query hook and any
32
- * call-site prefetch/setQueryData use.
57
+ * call-site prefetch/setQueryData use, with the rpc's scope
58
+ * [service, entity?, method] on it as `.all` (ServiceMethodKey).
33
59
  */
34
- query<Schema extends DescMessage>(method: string, schema: Schema, entityScope?: string): (req: MessageInitShape<Schema>) => ServiceQueryKey;
60
+ query<Schema extends DescMessage>(method: string, schema: Schema, entityScope?: string): ServiceMethodKey<Schema>;
35
61
  /**
36
62
  * One paged read: [service, entity?, method, protojson(req), "infinite"],
37
63
  * where req is the request WITHOUT its page token — the hook owns the
@@ -82,26 +108,76 @@ export declare function serviceKeys(service: string): ServiceKeyBuilders;
82
108
  */
83
109
  export type ServiceCall<Schema extends DescMessage, Response> = (req: MessageInitShape<Schema>, signal?: AbortSignal) => Promise<Response>;
84
110
  /**
85
- * A generated query hook: `useGetOrder(req, options?)`.
111
+ * The options a generated read takes — its hook (`useGetOrder(req,
112
+ * options?)`) and its query-options builder
113
+ * (`orderServiceQueries.getOrder(req, options?)`) alike: React Query's own,
114
+ * minus the two the read owns, the key and the fetch. Everything else
115
+ * (`select`, `enabled`, `staleTime`, `placeholderData`, …) passes through.
86
116
  *
87
- * TData is what the hook's `data` is, exactly as in React Query's own
88
- * useQuery: the response by default, or whatever `select` returns. A call
89
- * site mapping the proto response to a view model passes
90
- * `select: (r) => toView(r)` and gets `data: View` — no useMemo over `data`,
91
- * and React Query re-runs the mapping only when the response changes (or
92
- * when `select` itself does, so hoist it or wrap it in useCallback when it
93
- * is expensive). TData is inferred from `select` and is a parameter of the
94
- * HOOK, not of the factory, so every call site picks its own.
117
+ * TData is what `data` is, exactly as in React Query's own useQuery: the
118
+ * response by default, or whatever `select` returns. A call site mapping the
119
+ * proto response to a view model passes `select: (r) => toView(r)` and gets
120
+ * `data: View` — no useMemo over `data`, and React Query re-runs the mapping
121
+ * only when the response changes (or when `select` itself does, so hoist it
122
+ * or wrap it in useCallback when it is expensive).
123
+ */
124
+ export type ServiceQueryInput<Response, TData = Response> = Partial<Omit<UseQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey>, "queryKey" | "queryFn">>;
125
+ /**
126
+ * What a query-options builder returns: TanStack's `queryOptions(...)`
127
+ * result, so it goes anywhere React Query takes one —
128
+ * `queryClient.fetchQuery`, `ensureQueryData`, `prefetchQuery`,
129
+ * `useQueries({ queries })`, `useQuery`, `useSuspenseQuery`. Its `queryKey`
130
+ * is data-tagged, so `queryClient.getQueryData(opts.queryKey)` and
131
+ * `setQueryData` are typed as the response with no annotation.
132
+ *
133
+ * fetchQuery / ensureQueryData resolve to the RESPONSE, not to `select`'s
134
+ * TData: they return what the cache holds, and `select` is an observer
135
+ * option that only a hook or useQueries applies.
136
+ */
137
+ export type ServiceQueryOptions<Response, TData = Response> = UseQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey> & {
138
+ queryKey: DataTag<ServiceQueryKey, Response, ConnectClientError>;
139
+ };
140
+ /**
141
+ * A generated query-options builder: `orderServiceQueries.getOrder(req,
142
+ * options?)`. It is the read's single definition — the generated hook
143
+ * `useGetOrder` is `useQuery` over exactly this — so code outside React (an
144
+ * imperative action, a stream handler, a router loader, a prefetch) shares
145
+ * the hook's cache entry and fetch by construction rather than by copying
146
+ * its key and its client call.
147
+ */
148
+ export type ServiceQueryOptionsBuilder<Schema extends DescMessage, Response> = <TData = Response>(req: MessageInitShape<Schema>, options?: ServiceQueryInput<Response, TData>) => ServiceQueryOptions<Response, TData>;
149
+ /**
150
+ * A generated query hook: `useGetOrder(req, options?)` — `useQuery` over the
151
+ * service's query-options builder for the same rpc. TData is inferred from
152
+ * `select` and is a parameter of the HOOK, not of the factory, so every call
153
+ * site picks its own.
95
154
  */
96
- export type ServiceQueryHook<Schema extends DescMessage, Response> = <TData = Response>(req: MessageInitShape<Schema>, options?: Partial<UseQueryOptions<Response, ConnectClientError, TData>>) => UseQueryResult<TData, ConnectClientError>;
155
+ export type ServiceQueryHook<Schema extends DescMessage, Response> = <TData = Response>(req: MessageInitShape<Schema>, options?: ServiceQueryInput<Response, TData>) => UseQueryResult<TData, ConnectClientError>;
97
156
  /**
98
- * The options a generated infinite hook takes: React Query's own, minus the
99
- * four the hook owns — the key, the fetch, and the page-token plumbing.
157
+ * The options a generated infinite read takes: React Query's own, minus the
158
+ * three the read owns — the key, the fetch, and the first page param.
100
159
  * `getNextPageParam` defaults to the response's `nextPageToken` (empty means
101
160
  * last page) and may be overridden; everything else (`select`,
102
161
  * `placeholderData`, `staleTime`, `enabled`, `maxPages`, …) passes through.
103
162
  */
104
- export type ServiceInfiniteQueryOptions<Response, TData> = Partial<Omit<UseInfiniteQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey, string>, "queryKey" | "queryFn" | "initialPageParam">>;
163
+ export type ServiceInfiniteQueryInput<Response, TData> = Partial<Omit<UseInfiniteQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey, string>, "queryKey" | "queryFn" | "initialPageParam">>;
164
+ /**
165
+ * What an infinite query-options builder returns: TanStack's
166
+ * `infiniteQueryOptions(...)` result, for `queryClient.fetchInfiniteQuery`,
167
+ * `prefetchInfiniteQuery`, `ensureInfiniteQueryData` and
168
+ * `useInfiniteQuery`. Its key is data-tagged as `InfiniteData<Response,
169
+ * string>`, the `{ pages, pageParams }` the cache holds.
170
+ */
171
+ export type ServiceInfiniteQueryOptions<Response, TData = InfiniteData<Response, string>> = UseInfiniteQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey, string> & {
172
+ queryKey: DataTag<ServiceQueryKey, InfiniteData<Response, string>, ConnectClientError>;
173
+ };
174
+ /**
175
+ * A generated infinite query-options builder:
176
+ * `orderServiceQueries.listOrdersInfinite(req, options?)`, for a read that
177
+ * follows forge's list-pagination shape. `useListOrdersInfinite` is
178
+ * `useInfiniteQuery` over exactly this.
179
+ */
180
+ export type ServiceInfiniteQueryOptionsBuilder<Schema extends DescMessage, Response extends PagedResponse> = <TData = InfiniteData<Response, string>>(req: PageRequest<Schema>, options?: ServiceInfiniteQueryInput<Response, TData>) => ServiceInfiniteQueryOptions<Response, TData>;
105
181
  /**
106
182
  * A generated infinite-query hook: `useListOrdersInfinite(req, options?)`,
107
183
  * for a read that follows forge's list-pagination shape (`string page_token`
@@ -112,7 +188,7 @@ export type ServiceInfiniteQueryOptions<Response, TData> = Partial<Omit<UseInfin
112
188
  * flattening the pages into one list is
113
189
  * `select: (d) => d.pages.flatMap((p) => p.orders)`.
114
190
  */
115
- export type ServiceInfiniteQueryHook<Schema extends DescMessage, Response extends PagedResponse> = <TData = InfiniteData<Response, string>>(req: PageRequest<Schema>, options?: ServiceInfiniteQueryOptions<Response, TData>) => UseInfiniteQueryResult<TData, ConnectClientError>;
191
+ export type ServiceInfiniteQueryHook<Schema extends DescMessage, Response extends PagedResponse> = <TData = InfiniteData<Response, string>>(req: PageRequest<Schema>, options?: ServiceInfiniteQueryInput<Response, TData>) => UseInfiniteQueryResult<TData, ConnectClientError>;
116
192
  /**
117
193
  * A generated mutation hook: `useCreateOrder(options?)`.
118
194
  *
@@ -127,34 +203,51 @@ export type ServiceInfiniteQueryHook<Schema extends DescMessage, Response extend
127
203
  */
128
204
  export type ServiceMutationHook<Schema extends DescMessage, Response> = <TContext = unknown>(options?: Partial<UseMutationOptions<Response, ConnectClientError, MessageInitShape<Schema>, TContext>>) => UseMutationResult<Response, ConnectClientError, MessageInitShape<Schema>, TContext>;
129
205
  /**
130
- * createQueryHook builds one useQuery wrapper.
206
+ * createQueryOptions builds one read's query-options builder — the single
207
+ * definition of its cache key and its fetch that the generated hook, and
208
+ * every caller outside React, share.
209
+ *
210
+ * The error is typed as ConnectClientError rather than React Query's default
211
+ * `Error`, on the options themselves, because React Query takes a result's
212
+ * error type from the TError it reads there: `query.error` is a
213
+ * ConnectClientError at every call site with nothing to pass. Typing it as
214
+ * `Error` hid `.reason`, `.codeName`, `.status` and `.retryable`, and left
215
+ * `err.message.includes(...)` as the only expression that compiled — the one
216
+ * thing the error contract forbids.
131
217
  *
132
- * The signature it returns is the one call sites already use — `(req,
133
- * options?)`, with the error typed as ConnectClientError rather than React
134
- * Query's default `Error`. That type is declared on the OPTIONS parameter
135
- * because React Query takes the result's error type from the same TError it
136
- * reads there, so `query.error` is a ConnectClientError at every call site
137
- * with nothing to pass. Typing it as `Error` hid `.reason`, `.codeName`, `.status`
138
- * and `.retryable`, and left `err.message.includes(...)` as the only
139
- * expression that compiled — the one thing the error contract forbids.
218
+ * The caller's options are spread BEFORE the key and the fetch: a caller may
219
+ * set anything else, never the two that make this read this read.
140
220
  */
141
- export declare function createQueryHook<Schema extends DescMessage, Response>(keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey, call: ServiceCall<Schema, Response>): ServiceQueryHook<Schema, Response>;
221
+ export declare function createQueryOptions<Schema extends DescMessage, Response>(keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey, call: ServiceCall<Schema, Response>): ServiceQueryOptionsBuilder<Schema, Response>;
142
222
  /**
143
- * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged
144
- * read.
223
+ * createQueryHook builds one useQuery wrapper over a read's query-options
224
+ * builder. It adds nothing to the options: a hook and a `fetchQuery` of the
225
+ * same request therefore read and write one cache entry with one fetch.
226
+ */
227
+ export declare function createQueryHook<Schema extends DescMessage, Response>(optionsFor: ServiceQueryOptionsBuilder<Schema, Response>): ServiceQueryHook<Schema, Response>;
228
+ /**
229
+ * createInfiniteQueryOptions builds one paged read's infinite query-options
230
+ * builder.
145
231
  *
146
- * The page token is the hook's: the first page is fetched with the caller's
147
- * request as given (no token, so an `optional string page_token` stays
148
- * unset), each later one with the previous response's `nextPageToken`, and
149
- * an empty token ends the listing. The key is `keys.infinite(...)`, which
150
- * strips the token and sits under the service and entity scopes, so the
151
- * mutations that invalidate a plain list refetch this one too.
232
+ * The page token is the builder's: the first page is fetched with the
233
+ * caller's request as given (no token, so an `optional string page_token`
234
+ * stays unset), each later one with the previous response's
235
+ * `nextPageToken`, and an empty token ends the listing. The key is
236
+ * `keys.infinite(...)`, which strips the token and sits under the service and
237
+ * entity scopes, so the mutations that invalidate a plain list refetch this
238
+ * one too.
152
239
  *
153
240
  * The caller's options are spread BETWEEN the default getNextPageParam and
154
- * the four fields the hook owns: a caller may replace the default, never the
155
- * key or the fetch.
241
+ * the three fields the read owns: a caller may replace the default, never the
242
+ * key, the fetch or the first page.
243
+ */
244
+ export declare function createInfiniteQueryOptions<Schema extends DescMessage, Response extends PagedResponse>(keyFor: (req: PageRequest<Schema>) => ServiceQueryKey, call: ServiceCall<Schema, Response>): ServiceInfiniteQueryOptionsBuilder<Schema, Response>;
245
+ /**
246
+ * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged
247
+ * read's infinite query-options builder, adding nothing to it — so a hook
248
+ * and a `fetchInfiniteQuery` of the same request share one cache entry.
156
249
  */
157
- export declare function createInfiniteQueryHook<Schema extends DescMessage, Response extends PagedResponse>(keyFor: (req: PageRequest<Schema>) => ServiceQueryKey, call: ServiceCall<Schema, Response>): ServiceInfiniteQueryHook<Schema, Response>;
250
+ export declare function createInfiniteQueryHook<Schema extends DescMessage, Response extends PagedResponse>(optionsFor: ServiceInfiniteQueryOptionsBuilder<Schema, Response>): ServiceInfiniteQueryHook<Schema, Response>;
158
251
  /**
159
252
  * createMutationHook builds one useMutation wrapper with scoped invalidation.
160
253
  *
@@ -36,7 +36,7 @@
36
36
  // either: "CreateOrder succeeded" leaks RPC names into the UI, so the toast /
37
37
  // redirect / inline confirmation stays the call site's via options.onSuccess.
38
38
  import { create, toJson, } from "@bufbuild/protobuf";
39
- import { useInfiniteQuery, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
39
+ import { infiniteQueryOptions, queryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
40
40
  /**
41
41
  * requestKey projects a request init into protojson for use inside a query
42
42
  * key. Two reasons this is not the raw request object:
@@ -78,64 +78,78 @@ function pageRequestKey(schema, req) {
78
78
  * what forge previously inlined into every generated hook file.
79
79
  */
80
80
  export function serviceKeys(service) {
81
+ // The rpc scope every key below starts with. Both builders derive their
82
+ // keys from it, so `.all` is a prefix of them by construction, not by a
83
+ // second copy of the tuple layout.
84
+ const rpcScope = (method, entityScope) => entityScope === undefined
85
+ ? [service, method]
86
+ : [service, entityScope, method];
81
87
  return {
82
88
  all: [service],
83
89
  entity: (name) => [service, name],
84
- query: (method, schema, entityScope) => (req) => entityScope === undefined
85
- ? [service, method, requestKey(schema, req)]
86
- : [service, entityScope, method, requestKey(schema, req)],
87
- infinite: (method, schema, entityScope) => (req) => entityScope === undefined
88
- ? [
89
- service,
90
- method,
91
- pageRequestKey(schema, req),
92
- "infinite",
93
- ]
94
- : [
95
- service,
96
- entityScope,
97
- method,
98
- pageRequestKey(schema, req),
99
- "infinite",
100
- ],
90
+ query: (method, schema, entityScope) => {
91
+ const all = rpcScope(method, entityScope);
92
+ return Object.assign((req) => [
93
+ ...all,
94
+ requestKey(schema, req),
95
+ ], { all });
96
+ },
97
+ infinite: (method, schema, entityScope) => (req) => [
98
+ ...rpcScope(method, entityScope),
99
+ pageRequestKey(schema, req),
100
+ "infinite",
101
+ ],
101
102
  };
102
103
  }
103
104
  /**
104
- * createQueryHook builds one useQuery wrapper.
105
+ * createQueryOptions builds one read's query-options builder — the single
106
+ * definition of its cache key and its fetch that the generated hook, and
107
+ * every caller outside React, share.
105
108
  *
106
- * The signature it returns is the one call sites already use — `(req,
107
- * options?)`, with the error typed as ConnectClientError rather than React
108
- * Query's default `Error`. That type is declared on the OPTIONS parameter
109
- * because React Query takes the result's error type from the same TError it
110
- * reads there, so `query.error` is a ConnectClientError at every call site
111
- * with nothing to pass. Typing it as `Error` hid `.reason`, `.codeName`, `.status`
112
- * and `.retryable`, and left `err.message.includes(...)` as the only
113
- * expression that compiled — the one thing the error contract forbids.
109
+ * The error is typed as ConnectClientError rather than React Query's default
110
+ * `Error`, on the options themselves, because React Query takes a result's
111
+ * error type from the TError it reads there: `query.error` is a
112
+ * ConnectClientError at every call site with nothing to pass. Typing it as
113
+ * `Error` hid `.reason`, `.codeName`, `.status` and `.retryable`, and left
114
+ * `err.message.includes(...)` as the only expression that compiled — the one
115
+ * thing the error contract forbids.
116
+ *
117
+ * The caller's options are spread BEFORE the key and the fetch: a caller may
118
+ * set anything else, never the two that make this read this read.
114
119
  */
115
- export function createQueryHook(keyFor, call) {
116
- return (req, options) => useQuery({
120
+ export function createQueryOptions(keyFor, call) {
121
+ return (req, options) => queryOptions({
122
+ ...options,
117
123
  queryKey: keyFor(req),
118
124
  queryFn: ({ signal }) => call(req, signal),
119
- ...options,
120
125
  });
121
126
  }
122
127
  /**
123
- * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged
124
- * read.
128
+ * createQueryHook builds one useQuery wrapper over a read's query-options
129
+ * builder. It adds nothing to the options: a hook and a `fetchQuery` of the
130
+ * same request therefore read and write one cache entry with one fetch.
131
+ */
132
+ export function createQueryHook(optionsFor) {
133
+ return (req, options) => useQuery(optionsFor(req, options));
134
+ }
135
+ /**
136
+ * createInfiniteQueryOptions builds one paged read's infinite query-options
137
+ * builder.
125
138
  *
126
- * The page token is the hook's: the first page is fetched with the caller's
127
- * request as given (no token, so an `optional string page_token` stays
128
- * unset), each later one with the previous response's `nextPageToken`, and
129
- * an empty token ends the listing. The key is `keys.infinite(...)`, which
130
- * strips the token and sits under the service and entity scopes, so the
131
- * mutations that invalidate a plain list refetch this one too.
139
+ * The page token is the builder's: the first page is fetched with the
140
+ * caller's request as given (no token, so an `optional string page_token`
141
+ * stays unset), each later one with the previous response's
142
+ * `nextPageToken`, and an empty token ends the listing. The key is
143
+ * `keys.infinite(...)`, which strips the token and sits under the service and
144
+ * entity scopes, so the mutations that invalidate a plain list refetch this
145
+ * one too.
132
146
  *
133
147
  * The caller's options are spread BETWEEN the default getNextPageParam and
134
- * the four fields the hook owns: a caller may replace the default, never the
135
- * key or the fetch.
148
+ * the three fields the read owns: a caller may replace the default, never the
149
+ * key, the fetch or the first page.
136
150
  */
137
- export function createInfiniteQueryHook(keyFor, call) {
138
- return (req, options) => useInfiniteQuery({
151
+ export function createInfiniteQueryOptions(keyFor, call) {
152
+ return (req, options) => infiniteQueryOptions({
139
153
  getNextPageParam: (last) => last.nextPageToken || undefined,
140
154
  ...options,
141
155
  queryKey: keyFor(req),
@@ -148,6 +162,14 @@ export function createInfiniteQueryHook(keyFor, call) {
148
162
  initialPageParam: "",
149
163
  });
150
164
  }
165
+ /**
166
+ * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged
167
+ * read's infinite query-options builder, adding nothing to it — so a hook
168
+ * and a `fetchInfiniteQuery` of the same request share one cache entry.
169
+ */
170
+ export function createInfiniteQueryHook(optionsFor) {
171
+ return (req, options) => useInfiniteQuery(optionsFor(req, options));
172
+ }
151
173
  /**
152
174
  * createMutationHook builds one useMutation wrapper with scoped invalidation.
153
175
  *
@@ -1 +1 @@
1
- {"version":3,"file":"service-hooks.js","sourceRoot":"","sources":["../src/service-hooks.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Every export below is a React hook or builds one, so this module belongs in\n// the CLIENT graph. Without the directive the Next.js App Router compiles it\n// into the SERVER graph, where it binds a SECOND @tanstack/react-query\n// instance and every generated hook throws \"No QueryClient set\" at runtime —\n// invisible to tsc, eslint AND `next build`. published-surface.test.ts asserts\n// the directive survives into dist/. It is NOT applied package-wide: the\n// ./interceptors subpath exists to be importable without React.\n//\n// The React Query wrapper machinery behind forge's generated per-service hook\n// files (`src/hooks/<svc>-service-hooks_gen.ts`).\n//\n// WHAT WAS HERE BEFORE. Each generated file re-emitted the same three things\n// per RPC: a query-key entry, a useQuery or useMutation wrapper, and — for\n// mutations — the entity-scoped invalidateQueries plus the onSuccess\n// composition, with the reasoning for each re-inlined as comments. A\n// five-service project rendered five copies of machinery that differed only in\n// RPC names and message types. This module IS that machinery, written once.\n// The generated file keeps exactly what varies per RPC: the name, the request\n// schema, the response type, query-vs-mutation, and the entity scope.\n//\n// WHY A SUBPATH AND NOT THE BARREL. This module imports @tanstack/react-query.\n// Every browser frontend forge scaffolds depends on it — but the barrel is\n// also what a consumer imports for interceptors and errors alone, and\n// anchoring React Query there would make a non-React consumer resolve a\n// package it never installed. Same reasoning as ./mock-transport and ./otel:\n// an engine with its own dependency footprint gets its own entry point, and\n// React Query is an OPTIONAL peer.\n//\n// WHAT IT DELIBERATELY DOES NOT DO. It does not emit error toasts — the\n// app-wide MutationCache in src/lib/query-client.ts is the single error-toast\n// chokepoint, and pages that render errors inline pass\n// `meta: { silenceErrorToast: true }`. It never defaults success-side UX\n// either: \"CreateOrder succeeded\" leaks RPC names into the UI, so the toast /\n// redirect / inline confirmation stays the call site's via options.onSuccess.\nimport {\n create,\n toJson,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n type InfiniteData,\n type UseInfiniteQueryOptions,\n type UseInfiniteQueryResult,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\n\nimport type { ConnectClientError } from \"./errors.js\";\n\n/**\n * A generated query key. The tuple is [service, entity?, method,\n * protojson(req)] for a method key and [service] / [service, entity] for the\n * broader scopes.\n */\nexport type ServiceQueryKey = readonly unknown[];\n\n/**\n * requestKey projects a request init into protojson for use inside a query\n * key. Two reasons this is not the raw request object:\n *\n * - React Query's default hasher is JSON.stringify, which THROWS on bigint\n * fields (any proto int64) — the raw message crashes the cache.\n * - protojson normalizes representations, so { id: 1 } and { id: \"1\" } land\n * in ONE cache entry instead of silently splitting the cache.\n *\n * Exported because a hand-written hook that wants to share a generated hook's\n * cache entry has to hash its request the identical way.\n */\nexport function requestKey<Schema extends DescMessage>(\n schema: Schema,\n req: MessageInitShape<Schema>,\n): unknown {\n return toJson(schema, create(schema, req));\n}\n\n/** The three key-scope builders a generated key factory is assembled from. */\nexport interface ServiceKeyBuilders {\n /** Every query on this service: [service]. */\n readonly all: readonly [string];\n /** Every query for one CRUD entity: [service, entity]. Mutations invalidate this. */\n entity(name: string): readonly [string, string];\n /**\n * One query: [service, entity?, method, protojson(req)]. Returns the\n * per-request key FUNCTION, which is what both the query hook and any\n * call-site prefetch/setQueryData use.\n */\n query<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): (req: MessageInitShape<Schema>) => ServiceQueryKey;\n /**\n * One paged read: [service, entity?, method, protojson(req), \"infinite\"],\n * where req is the request WITHOUT its page token — the hook owns the\n * token, so every page of one listing shares one cache entry.\n *\n * It nests under the same scopes as `query`, so an entity or service-wide\n * invalidation refetches it. The \"infinite\" suffix keeps it OFF the plain\n * query's entry for the same request: an infinite query caches\n * `{ pages, pageParams }`, not one response, and a plain hook reading that\n * entry would get the wrong shape.\n */\n infinite<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): (req: PageRequest<Schema>) => ServiceQueryKey;\n}\n\n/**\n * Omit distributed over a union. MessageInitShape is `Message | init\n * object`, and a plain Omit over a union keeps only the keys both members\n * share — which would silently drop every field from the init side.\n */\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown\n ? Omit<T, K>\n : never;\n\n/**\n * The request a generated infinite hook takes: the rpc's request without\n * `pageToken`, which the hook supplies page by page.\n */\nexport type PageRequest<Schema extends DescMessage> = DistributiveOmit<\n MessageInitShape<Schema>,\n \"pageToken\"\n>;\n\n/**\n * The response shape forge's list-pagination convention (AIP-158) gives a\n * paged read: `string next_page_token`, empty on the last page. Optional so\n * an `optional string next_page_token` response satisfies it too.\n */\nexport interface PagedResponse {\n nextPageToken?: string;\n}\n\n/**\n * pageRequestKey is requestKey with the page token dropped, so every page of\n * one listing hashes to one key even when a caller (JS, or a cast) passes a\n * token anyway. It is dropped from the protojson rather than from the init\n * object: a spread Message keeps its $typeName, and create() would hand that\n * token-less copy back as if it were a complete message.\n */\nfunction pageRequestKey<Schema extends DescMessage>(\n schema: Schema,\n req: PageRequest<Schema>,\n): unknown {\n const json = requestKey(schema, req as unknown as MessageInitShape<Schema>);\n if (json === null || typeof json !== \"object\" || Array.isArray(json)) {\n return json;\n }\n const { pageToken: _pageToken, ...rest } = json as Record<string, unknown>;\n return rest;\n}\n\n/**\n * serviceKeys returns the builders for one service's query-key factory.\n *\n * Cache-key semantics are a compatibility contract, not an implementation\n * detail: an existing app's\n * `invalidateQueries({ queryKey: orderServiceKeys.order })` has to keep\n * matching the keys its queries were registered under across a version bump.\n * So the tuple order and contents produced here are fixed, and identical to\n * what forge previously inlined into every generated hook file.\n */\nexport function serviceKeys(service: string): ServiceKeyBuilders {\n return {\n all: [service] as const,\n entity: (name: string) => [service, name] as const,\n query:\n <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ) =>\n (req: MessageInitShape<Schema>): ServiceQueryKey =>\n entityScope === undefined\n ? ([service, method, requestKey(schema, req)] as const)\n : ([service, entityScope, method, requestKey(schema, req)] as const),\n infinite:\n <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ) =>\n (req: PageRequest<Schema>): ServiceQueryKey =>\n entityScope === undefined\n ? ([\n service,\n method,\n pageRequestKey(schema, req),\n \"infinite\",\n ] as const)\n : ([\n service,\n entityScope,\n method,\n pageRequestKey(schema, req),\n \"infinite\",\n ] as const),\n };\n}\n\n/**\n * How a generated file calls one rpc. `signal` is React Query's: a query\n * that is unmounted or superseded (a new key while typing) aborts its\n * request instead of letting it land on a cache entry nobody reads.\n */\nexport type ServiceCall<Schema extends DescMessage, Response> = (\n req: MessageInitShape<Schema>,\n signal?: AbortSignal,\n) => Promise<Response>;\n\n/**\n * A generated query hook: `useGetOrder(req, options?)`.\n *\n * TData is what the hook's `data` is, exactly as in React Query's own\n * useQuery: the response by default, or whatever `select` returns. A call\n * site mapping the proto response to a view model passes\n * `select: (r) => toView(r)` and gets `data: View` — no useMemo over `data`,\n * and React Query re-runs the mapping only when the response changes (or\n * when `select` itself does, so hoist it or wrap it in useCallback when it\n * is expensive). TData is inferred from `select` and is a parameter of the\n * HOOK, not of the factory, so every call site picks its own.\n */\nexport type ServiceQueryHook<Schema extends DescMessage, Response> = <\n TData = Response,\n>(\n req: MessageInitShape<Schema>,\n options?: Partial<UseQueryOptions<Response, ConnectClientError, TData>>,\n) => UseQueryResult<TData, ConnectClientError>;\n\n/**\n * The options a generated infinite hook takes: React Query's own, minus the\n * four the hook owns — the key, the fetch, and the page-token plumbing.\n * `getNextPageParam` defaults to the response's `nextPageToken` (empty means\n * last page) and may be overridden; everything else (`select`,\n * `placeholderData`, `staleTime`, `enabled`, `maxPages`, …) passes through.\n */\nexport type ServiceInfiniteQueryOptions<Response, TData> = Partial<\n Omit<\n UseInfiniteQueryOptions<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey,\n string\n >,\n \"queryKey\" | \"queryFn\" | \"initialPageParam\"\n >\n>;\n\n/**\n * A generated infinite-query hook: `useListOrdersInfinite(req, options?)`,\n * for a read that follows forge's list-pagination shape (`string page_token`\n * on the request, `string next_page_token` on the response).\n *\n * `req` is the first page's request without its token. `data` is React\n * Query's `{ pages, pageParams }` by default; `select` reshapes it, so\n * flattening the pages into one list is\n * `select: (d) => d.pages.flatMap((p) => p.orders)`.\n */\nexport type ServiceInfiniteQueryHook<\n Schema extends DescMessage,\n Response extends PagedResponse,\n> = <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryOptions<Response, TData>,\n) => UseInfiniteQueryResult<TData, ConnectClientError>;\n\n/**\n * A generated mutation hook: `useCreateOrder(options?)`.\n *\n * TContext is React Query's fourth type parameter — what `onMutate` returns\n * and `onError` / `onSettled` receive back. It is a parameter of the HOOK,\n * not of the factory, so each call site gets its own: TypeScript infers it\n * from the `onMutate` the caller passes, and an optimistic update reads its\n * rollback off `context` with no cast. Fixing it at the factory (or leaving\n * it out, which pinned it to `unknown`) forced every such call site to\n * narrow `unknown` by hand. It defaults to `unknown`, so a call without\n * `onMutate` types exactly as before.\n */\nexport type ServiceMutationHook<Schema extends DescMessage, Response> = <\n TContext = unknown,\n>(\n options?: Partial<\n UseMutationOptions<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >\n >,\n) => UseMutationResult<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n>;\n\n/**\n * createQueryHook builds one useQuery wrapper.\n *\n * The signature it returns is the one call sites already use — `(req,\n * options?)`, with the error typed as ConnectClientError rather than React\n * Query's default `Error`. That type is declared on the OPTIONS parameter\n * because React Query takes the result's error type from the same TError it\n * reads there, so `query.error` is a ConnectClientError at every call site\n * with nothing to pass. Typing it as `Error` hid `.reason`, `.codeName`, `.status`\n * and `.retryable`, and left `err.message.includes(...)` as the only\n * expression that compiled — the one thing the error contract forbids.\n */\nexport function createQueryHook<Schema extends DescMessage, Response>(\n keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey,\n call: ServiceCall<Schema, Response>,\n): ServiceQueryHook<Schema, Response> {\n return <TData = Response>(\n req: MessageInitShape<Schema>,\n options?: Partial<UseQueryOptions<Response, ConnectClientError, TData>>,\n ) =>\n useQuery<Response, ConnectClientError, TData>({\n queryKey: keyFor(req),\n queryFn: ({ signal }) => call(req, signal),\n ...options,\n });\n}\n\n/**\n * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged\n * read.\n *\n * The page token is the hook's: the first page is fetched with the caller's\n * request as given (no token, so an `optional string page_token` stays\n * unset), each later one with the previous response's `nextPageToken`, and\n * an empty token ends the listing. The key is `keys.infinite(...)`, which\n * strips the token and sits under the service and entity scopes, so the\n * mutations that invalidate a plain list refetch this one too.\n *\n * The caller's options are spread BETWEEN the default getNextPageParam and\n * the four fields the hook owns: a caller may replace the default, never the\n * key or the fetch.\n */\nexport function createInfiniteQueryHook<\n Schema extends DescMessage,\n Response extends PagedResponse,\n>(\n keyFor: (req: PageRequest<Schema>) => ServiceQueryKey,\n call: ServiceCall<Schema, Response>,\n): ServiceInfiniteQueryHook<Schema, Response> {\n return <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryOptions<Response, TData>,\n ) =>\n useInfiniteQuery<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey,\n string\n >({\n getNextPageParam: (last) => last.nextPageToken || undefined,\n ...options,\n queryKey: keyFor(req),\n queryFn: ({ pageParam, signal }) =>\n call(\n (pageParam === \"\"\n ? req\n : {\n ...req,\n pageToken: pageParam,\n }) as unknown as MessageInitShape<Schema>,\n signal,\n ),\n initialPageParam: \"\",\n });\n}\n\n/**\n * createMutationHook builds one useMutation wrapper with scoped invalidation.\n *\n * invalidateKey is the entity scope when the RPC maps to a CRUD entity, and\n * the whole-service scope otherwise — a bespoke mutation may touch anything,\n * so over-invalidating is the safe default there.\n *\n * The invalidation is composed BEFORE the caller's onSuccess and is never\n * replaced by it: onSuccess is destructured out and re-invoked after the\n * invalidate, while the rest of the options object is spread separately. A\n * caller passing onSuccess therefore ADDS behaviour, rather than silently\n * disabling the cache refresh — which is exactly what a plain `...options`\n * spread over a default onSuccess would do.\n */\nexport function createMutationHook<Schema extends DescMessage, Response>(\n invalidateKey: ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceMutationHook<Schema, Response> {\n return <TContext = unknown>(\n options?: Partial<\n UseMutationOptions<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >\n >,\n ) => {\n const queryClient = useQueryClient();\n const { onSuccess, ...rest } = options ?? {};\n return useMutation<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >({\n mutationFn: (req: MessageInitShape<Schema>) => call(req),\n onSuccess: (...args) => {\n queryClient.invalidateQueries({ queryKey: invalidateKey });\n onSuccess?.(...args);\n },\n ...rest,\n });\n };\n}\n"]}
1
+ {"version":3,"file":"service-hooks.js","sourceRoot":"","sources":["../src/service-hooks.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Every export below is a React hook or builds one, so this module belongs in\n// the CLIENT graph. Without the directive the Next.js App Router compiles it\n// into the SERVER graph, where it binds a SECOND @tanstack/react-query\n// instance and every generated hook throws \"No QueryClient set\" at runtime —\n// invisible to tsc, eslint AND `next build`. published-surface.test.ts asserts\n// the directive survives into dist/. It is NOT applied package-wide: the\n// ./interceptors subpath exists to be importable without React.\n//\n// The React Query wrapper machinery behind forge's generated per-service hook\n// files (`src/hooks/<svc>-service-hooks_gen.ts`).\n//\n// WHAT WAS HERE BEFORE. Each generated file re-emitted the same three things\n// per RPC: a query-key entry, a useQuery or useMutation wrapper, and — for\n// mutations — the entity-scoped invalidateQueries plus the onSuccess\n// composition, with the reasoning for each re-inlined as comments. A\n// five-service project rendered five copies of machinery that differed only in\n// RPC names and message types. This module IS that machinery, written once.\n// The generated file keeps exactly what varies per RPC: the name, the request\n// schema, the response type, query-vs-mutation, and the entity scope.\n//\n// WHY A SUBPATH AND NOT THE BARREL. This module imports @tanstack/react-query.\n// Every browser frontend forge scaffolds depends on it — but the barrel is\n// also what a consumer imports for interceptors and errors alone, and\n// anchoring React Query there would make a non-React consumer resolve a\n// package it never installed. Same reasoning as ./mock-transport and ./otel:\n// an engine with its own dependency footprint gets its own entry point, and\n// React Query is an OPTIONAL peer.\n//\n// WHAT IT DELIBERATELY DOES NOT DO. It does not emit error toasts — the\n// app-wide MutationCache in src/lib/query-client.ts is the single error-toast\n// chokepoint, and pages that render errors inline pass\n// `meta: { silenceErrorToast: true }`. It never defaults success-side UX\n// either: \"CreateOrder succeeded\" leaks RPC names into the UI, so the toast /\n// redirect / inline confirmation stays the call site's via options.onSuccess.\nimport {\n create,\n toJson,\n type DescMessage,\n type MessageInitShape,\n} from \"@bufbuild/protobuf\";\nimport {\n infiniteQueryOptions,\n queryOptions,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n type DataTag,\n type InfiniteData,\n type UseInfiniteQueryOptions,\n type UseInfiniteQueryResult,\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\n\nimport type { ConnectClientError } from \"./errors.js\";\n\n/**\n * A generated query key. The tuple is [service, entity?, method,\n * protojson(req)] for a method key and [service] / [service, entity] for the\n * broader scopes.\n */\nexport type ServiceQueryKey = readonly unknown[];\n\n/**\n * requestKey projects a request init into protojson for use inside a query\n * key. Two reasons this is not the raw request object:\n *\n * - React Query's default hasher is JSON.stringify, which THROWS on bigint\n * fields (any proto int64) — the raw message crashes the cache.\n * - protojson normalizes representations, so { id: 1 } and { id: \"1\" } land\n * in ONE cache entry instead of silently splitting the cache.\n *\n * Exported because a hand-written hook that wants to share a generated hook's\n * cache entry has to hash its request the identical way.\n */\nexport function requestKey<Schema extends DescMessage>(\n schema: Schema,\n req: MessageInitShape<Schema>,\n): unknown {\n return toJson(schema, create(schema, req));\n}\n\n/**\n * One read's key entry in a generated factory: `orderServiceKeys.getOrder`.\n *\n * Called, it is that request's key, [service, entity?, method,\n * protojson(req)]. Its `.all` is the rpc's scope, [service, entity?,\n * method]: a prefix of every key the rpc caches under — every request, the\n * plain query and the `<method>Infinite` read alike — and of no other rpc's.\n *\n * queryClient.invalidateQueries({ queryKey: orderServiceKeys.listOrders.all })\n * queryClient.setQueriesData({ queryKey: orderServiceKeys.getOrder.all }, patch)\n *\n * It exists so code never names a key's tuple positions. A predicate like\n * `query.queryKey[1] === \"listOrders\"` breaks silently when a\n * `// forge:query-scope` marker inserts the entity at index 1; `.all` is\n * built by the same function that builds the keys, so it moves with them.\n *\n * `.all` is not data-tagged. It names a SET of entries, and for a paged\n * read that set holds two shapes (the response, and the infinite read's\n * `{ pages, pageParams }`), so a `setQueriesData` updater over it must\n * handle both.\n */\nexport type ServiceMethodKey<Schema extends DescMessage> = ((\n req: MessageInitShape<Schema>,\n) => ServiceQueryKey) & {\n /** Every cached request of this rpc: [service, entity?, method]. */\n readonly all: ServiceQueryKey;\n};\n\n/** The three key-scope builders a generated key factory is assembled from. */\nexport interface ServiceKeyBuilders {\n /** Every query on this service: [service]. */\n readonly all: readonly [string];\n /** Every query for one CRUD entity: [service, entity]. Mutations invalidate this. */\n entity(name: string): readonly [string, string];\n /**\n * One query: [service, entity?, method, protojson(req)]. Returns the\n * per-request key FUNCTION, which is what both the query hook and any\n * call-site prefetch/setQueryData use, with the rpc's scope\n * [service, entity?, method] on it as `.all` (ServiceMethodKey).\n */\n query<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): ServiceMethodKey<Schema>;\n /**\n * One paged read: [service, entity?, method, protojson(req), \"infinite\"],\n * where req is the request WITHOUT its page token — the hook owns the\n * token, so every page of one listing shares one cache entry.\n *\n * It nests under the same scopes as `query`, so an entity or service-wide\n * invalidation refetches it. The \"infinite\" suffix keeps it OFF the plain\n * query's entry for the same request: an infinite query caches\n * `{ pages, pageParams }`, not one response, and a plain hook reading that\n * entry would get the wrong shape.\n */\n infinite<Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): (req: PageRequest<Schema>) => ServiceQueryKey;\n}\n\n/**\n * Omit distributed over a union. MessageInitShape is `Message | init\n * object`, and a plain Omit over a union keeps only the keys both members\n * share — which would silently drop every field from the init side.\n */\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown\n ? Omit<T, K>\n : never;\n\n/**\n * The request a generated infinite hook takes: the rpc's request without\n * `pageToken`, which the hook supplies page by page.\n */\nexport type PageRequest<Schema extends DescMessage> = DistributiveOmit<\n MessageInitShape<Schema>,\n \"pageToken\"\n>;\n\n/**\n * The response shape forge's list-pagination convention (AIP-158) gives a\n * paged read: `string next_page_token`, empty on the last page. Optional so\n * an `optional string next_page_token` response satisfies it too.\n */\nexport interface PagedResponse {\n nextPageToken?: string;\n}\n\n/**\n * pageRequestKey is requestKey with the page token dropped, so every page of\n * one listing hashes to one key even when a caller (JS, or a cast) passes a\n * token anyway. It is dropped from the protojson rather than from the init\n * object: a spread Message keeps its $typeName, and create() would hand that\n * token-less copy back as if it were a complete message.\n */\nfunction pageRequestKey<Schema extends DescMessage>(\n schema: Schema,\n req: PageRequest<Schema>,\n): unknown {\n const json = requestKey(schema, req as unknown as MessageInitShape<Schema>);\n if (json === null || typeof json !== \"object\" || Array.isArray(json)) {\n return json;\n }\n const { pageToken: _pageToken, ...rest } = json as Record<string, unknown>;\n return rest;\n}\n\n/**\n * serviceKeys returns the builders for one service's query-key factory.\n *\n * Cache-key semantics are a compatibility contract, not an implementation\n * detail: an existing app's\n * `invalidateQueries({ queryKey: orderServiceKeys.order })` has to keep\n * matching the keys its queries were registered under across a version bump.\n * So the tuple order and contents produced here are fixed, and identical to\n * what forge previously inlined into every generated hook file.\n */\nexport function serviceKeys(service: string): ServiceKeyBuilders {\n // The rpc scope every key below starts with. Both builders derive their\n // keys from it, so `.all` is a prefix of them by construction, not by a\n // second copy of the tuple layout.\n const rpcScope = (method: string, entityScope?: string): ServiceQueryKey =>\n entityScope === undefined\n ? ([service, method] as const)\n : ([service, entityScope, method] as const);\n return {\n all: [service] as const,\n entity: (name: string) => [service, name] as const,\n query: <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ): ServiceMethodKey<Schema> => {\n const all = rpcScope(method, entityScope);\n return Object.assign(\n (req: MessageInitShape<Schema>): ServiceQueryKey => [\n ...all,\n requestKey(schema, req),\n ],\n { all },\n );\n },\n infinite:\n <Schema extends DescMessage>(\n method: string,\n schema: Schema,\n entityScope?: string,\n ) =>\n (req: PageRequest<Schema>): ServiceQueryKey => [\n ...rpcScope(method, entityScope),\n pageRequestKey(schema, req),\n \"infinite\",\n ],\n };\n}\n\n/**\n * How a generated file calls one rpc. `signal` is React Query's: a query\n * that is unmounted or superseded (a new key while typing) aborts its\n * request instead of letting it land on a cache entry nobody reads.\n */\nexport type ServiceCall<Schema extends DescMessage, Response> = (\n req: MessageInitShape<Schema>,\n signal?: AbortSignal,\n) => Promise<Response>;\n\n/**\n * The options a generated read takes — its hook (`useGetOrder(req,\n * options?)`) and its query-options builder\n * (`orderServiceQueries.getOrder(req, options?)`) alike: React Query's own,\n * minus the two the read owns, the key and the fetch. Everything else\n * (`select`, `enabled`, `staleTime`, `placeholderData`, …) passes through.\n *\n * TData is what `data` is, exactly as in React Query's own useQuery: the\n * response by default, or whatever `select` returns. A call site mapping the\n * proto response to a view model passes `select: (r) => toView(r)` and gets\n * `data: View` — no useMemo over `data`, and React Query re-runs the mapping\n * only when the response changes (or when `select` itself does, so hoist it\n * or wrap it in useCallback when it is expensive).\n */\nexport type ServiceQueryInput<Response, TData = Response> = Partial<\n Omit<\n UseQueryOptions<Response, ConnectClientError, TData, ServiceQueryKey>,\n \"queryKey\" | \"queryFn\"\n >\n>;\n\n/**\n * What a query-options builder returns: TanStack's `queryOptions(...)`\n * result, so it goes anywhere React Query takes one —\n * `queryClient.fetchQuery`, `ensureQueryData`, `prefetchQuery`,\n * `useQueries({ queries })`, `useQuery`, `useSuspenseQuery`. Its `queryKey`\n * is data-tagged, so `queryClient.getQueryData(opts.queryKey)` and\n * `setQueryData` are typed as the response with no annotation.\n *\n * fetchQuery / ensureQueryData resolve to the RESPONSE, not to `select`'s\n * TData: they return what the cache holds, and `select` is an observer\n * option that only a hook or useQueries applies.\n */\nexport type ServiceQueryOptions<Response, TData = Response> = UseQueryOptions<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey\n> & {\n queryKey: DataTag<ServiceQueryKey, Response, ConnectClientError>;\n};\n\n/**\n * A generated query-options builder: `orderServiceQueries.getOrder(req,\n * options?)`. It is the read's single definition — the generated hook\n * `useGetOrder` is `useQuery` over exactly this — so code outside React (an\n * imperative action, a stream handler, a router loader, a prefetch) shares\n * the hook's cache entry and fetch by construction rather than by copying\n * its key and its client call.\n */\nexport type ServiceQueryOptionsBuilder<Schema extends DescMessage, Response> = <\n TData = Response,\n>(\n req: MessageInitShape<Schema>,\n options?: ServiceQueryInput<Response, TData>,\n) => ServiceQueryOptions<Response, TData>;\n\n/**\n * A generated query hook: `useGetOrder(req, options?)` — `useQuery` over the\n * service's query-options builder for the same rpc. TData is inferred from\n * `select` and is a parameter of the HOOK, not of the factory, so every call\n * site picks its own.\n */\nexport type ServiceQueryHook<Schema extends DescMessage, Response> = <\n TData = Response,\n>(\n req: MessageInitShape<Schema>,\n options?: ServiceQueryInput<Response, TData>,\n) => UseQueryResult<TData, ConnectClientError>;\n\n/**\n * The options a generated infinite read takes: React Query's own, minus the\n * three the read owns — the key, the fetch, and the first page param.\n * `getNextPageParam` defaults to the response's `nextPageToken` (empty means\n * last page) and may be overridden; everything else (`select`,\n * `placeholderData`, `staleTime`, `enabled`, `maxPages`, …) passes through.\n */\nexport type ServiceInfiniteQueryInput<Response, TData> = Partial<\n Omit<\n UseInfiniteQueryOptions<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey,\n string\n >,\n \"queryKey\" | \"queryFn\" | \"initialPageParam\"\n >\n>;\n\n/**\n * What an infinite query-options builder returns: TanStack's\n * `infiniteQueryOptions(...)` result, for `queryClient.fetchInfiniteQuery`,\n * `prefetchInfiniteQuery`, `ensureInfiniteQueryData` and\n * `useInfiniteQuery`. Its key is data-tagged as `InfiniteData<Response,\n * string>`, the `{ pages, pageParams }` the cache holds.\n */\nexport type ServiceInfiniteQueryOptions<\n Response,\n TData = InfiniteData<Response, string>,\n> = UseInfiniteQueryOptions<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey,\n string\n> & {\n queryKey: DataTag<\n ServiceQueryKey,\n InfiniteData<Response, string>,\n ConnectClientError\n >;\n};\n\n/**\n * A generated infinite query-options builder:\n * `orderServiceQueries.listOrdersInfinite(req, options?)`, for a read that\n * follows forge's list-pagination shape. `useListOrdersInfinite` is\n * `useInfiniteQuery` over exactly this.\n */\nexport type ServiceInfiniteQueryOptionsBuilder<\n Schema extends DescMessage,\n Response extends PagedResponse,\n> = <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryInput<Response, TData>,\n) => ServiceInfiniteQueryOptions<Response, TData>;\n\n/**\n * A generated infinite-query hook: `useListOrdersInfinite(req, options?)`,\n * for a read that follows forge's list-pagination shape (`string page_token`\n * on the request, `string next_page_token` on the response).\n *\n * `req` is the first page's request without its token. `data` is React\n * Query's `{ pages, pageParams }` by default; `select` reshapes it, so\n * flattening the pages into one list is\n * `select: (d) => d.pages.flatMap((p) => p.orders)`.\n */\nexport type ServiceInfiniteQueryHook<\n Schema extends DescMessage,\n Response extends PagedResponse,\n> = <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryInput<Response, TData>,\n) => UseInfiniteQueryResult<TData, ConnectClientError>;\n\n/**\n * A generated mutation hook: `useCreateOrder(options?)`.\n *\n * TContext is React Query's fourth type parameter — what `onMutate` returns\n * and `onError` / `onSettled` receive back. It is a parameter of the HOOK,\n * not of the factory, so each call site gets its own: TypeScript infers it\n * from the `onMutate` the caller passes, and an optimistic update reads its\n * rollback off `context` with no cast. Fixing it at the factory (or leaving\n * it out, which pinned it to `unknown`) forced every such call site to\n * narrow `unknown` by hand. It defaults to `unknown`, so a call without\n * `onMutate` types exactly as before.\n */\nexport type ServiceMutationHook<Schema extends DescMessage, Response> = <\n TContext = unknown,\n>(\n options?: Partial<\n UseMutationOptions<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >\n >,\n) => UseMutationResult<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n>;\n\n/**\n * createQueryOptions builds one read's query-options builder — the single\n * definition of its cache key and its fetch that the generated hook, and\n * every caller outside React, share.\n *\n * The error is typed as ConnectClientError rather than React Query's default\n * `Error`, on the options themselves, because React Query takes a result's\n * error type from the TError it reads there: `query.error` is a\n * ConnectClientError at every call site with nothing to pass. Typing it as\n * `Error` hid `.reason`, `.codeName`, `.status` and `.retryable`, and left\n * `err.message.includes(...)` as the only expression that compiled — the one\n * thing the error contract forbids.\n *\n * The caller's options are spread BEFORE the key and the fetch: a caller may\n * set anything else, never the two that make this read this read.\n */\nexport function createQueryOptions<Schema extends DescMessage, Response>(\n keyFor: (req: MessageInitShape<Schema>) => ServiceQueryKey,\n call: ServiceCall<Schema, Response>,\n): ServiceQueryOptionsBuilder<Schema, Response> {\n return <TData = Response>(\n req: MessageInitShape<Schema>,\n options?: ServiceQueryInput<Response, TData>,\n ): ServiceQueryOptions<Response, TData> =>\n queryOptions<Response, ConnectClientError, TData, ServiceQueryKey>({\n ...options,\n queryKey: keyFor(req),\n queryFn: ({ signal }) => call(req, signal),\n }) as ServiceQueryOptions<Response, TData>;\n}\n\n/**\n * createQueryHook builds one useQuery wrapper over a read's query-options\n * builder. It adds nothing to the options: a hook and a `fetchQuery` of the\n * same request therefore read and write one cache entry with one fetch.\n */\nexport function createQueryHook<Schema extends DescMessage, Response>(\n optionsFor: ServiceQueryOptionsBuilder<Schema, Response>,\n): ServiceQueryHook<Schema, Response> {\n return <TData = Response>(\n req: MessageInitShape<Schema>,\n options?: ServiceQueryInput<Response, TData>,\n ) => useQuery(optionsFor(req, options));\n}\n\n/**\n * createInfiniteQueryOptions builds one paged read's infinite query-options\n * builder.\n *\n * The page token is the builder's: the first page is fetched with the\n * caller's request as given (no token, so an `optional string page_token`\n * stays unset), each later one with the previous response's\n * `nextPageToken`, and an empty token ends the listing. The key is\n * `keys.infinite(...)`, which strips the token and sits under the service and\n * entity scopes, so the mutations that invalidate a plain list refetch this\n * one too.\n *\n * The caller's options are spread BETWEEN the default getNextPageParam and\n * the three fields the read owns: a caller may replace the default, never the\n * key, the fetch or the first page.\n */\nexport function createInfiniteQueryOptions<\n Schema extends DescMessage,\n Response extends PagedResponse,\n>(\n keyFor: (req: PageRequest<Schema>) => ServiceQueryKey,\n call: ServiceCall<Schema, Response>,\n): ServiceInfiniteQueryOptionsBuilder<Schema, Response> {\n return <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryInput<Response, TData>,\n ): ServiceInfiniteQueryOptions<Response, TData> =>\n infiniteQueryOptions<\n Response,\n ConnectClientError,\n TData,\n ServiceQueryKey,\n string\n >({\n getNextPageParam: (last) => last.nextPageToken || undefined,\n ...options,\n queryKey: keyFor(req),\n queryFn: ({ pageParam, signal }) =>\n call(\n (pageParam === \"\"\n ? req\n : {\n ...req,\n pageToken: pageParam,\n }) as unknown as MessageInitShape<Schema>,\n signal,\n ),\n initialPageParam: \"\",\n }) as ServiceInfiniteQueryOptions<Response, TData>;\n}\n\n/**\n * createInfiniteQueryHook builds one useInfiniteQuery wrapper over a paged\n * read's infinite query-options builder, adding nothing to it — so a hook\n * and a `fetchInfiniteQuery` of the same request share one cache entry.\n */\nexport function createInfiniteQueryHook<\n Schema extends DescMessage,\n Response extends PagedResponse,\n>(\n optionsFor: ServiceInfiniteQueryOptionsBuilder<Schema, Response>,\n): ServiceInfiniteQueryHook<Schema, Response> {\n return <TData = InfiniteData<Response, string>>(\n req: PageRequest<Schema>,\n options?: ServiceInfiniteQueryInput<Response, TData>,\n ) => useInfiniteQuery(optionsFor(req, options));\n}\n\n/**\n * createMutationHook builds one useMutation wrapper with scoped invalidation.\n *\n * invalidateKey is the entity scope when the RPC maps to a CRUD entity, and\n * the whole-service scope otherwise — a bespoke mutation may touch anything,\n * so over-invalidating is the safe default there.\n *\n * The invalidation is composed BEFORE the caller's onSuccess and is never\n * replaced by it: onSuccess is destructured out and re-invoked after the\n * invalidate, while the rest of the options object is spread separately. A\n * caller passing onSuccess therefore ADDS behaviour, rather than silently\n * disabling the cache refresh — which is exactly what a plain `...options`\n * spread over a default onSuccess would do.\n */\nexport function createMutationHook<Schema extends DescMessage, Response>(\n invalidateKey: ServiceQueryKey,\n call: (req: MessageInitShape<Schema>) => Promise<Response>,\n): ServiceMutationHook<Schema, Response> {\n return <TContext = unknown>(\n options?: Partial<\n UseMutationOptions<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >\n >,\n ) => {\n const queryClient = useQueryClient();\n const { onSuccess, ...rest } = options ?? {};\n return useMutation<\n Response,\n ConnectClientError,\n MessageInitShape<Schema>,\n TContext\n >({\n mutationFn: (req: MessageInitShape<Schema>) => call(req),\n onSuccess: (...args) => {\n queryClient.invalidateQueries({ queryKey: invalidateKey });\n onSuccess?.(...args);\n },\n ...rest,\n });\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reliantlabs/forge-web-runtime",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "The forge frontend runtime: Connect interceptor stack, session, error boundary, toast queue, <Resource> container, W3C trace propagation and the HyperDX browser SDK wiring (errors, console, network, traces; replay off by default). The web twin of forge/pkg.",
5
5
  "license": "Apache-2.0",
6
6
  "//repository note": "REQUIRED for `npm publish --provenance`, not decoration. npm cross-checks this against the repository in the signed provenance statement and rejects the publish with a 422 if they disagree — an absent field reads as \"\" and fails that comparison. `directory` names the subdirectory this package lives in, so the provenance points at the real source. Omitting this is what made the first 0.3.1 publish fail AFTER the tarball had been signed and written to the sigstore transparency log.",
@@ -78,7 +78,7 @@
78
78
  "@connectrpc/connect": "^2.0.0",
79
79
  "@hyperdx/browser": "0.26.0",
80
80
  "@opentelemetry/api": "^1.9.0",
81
- "@tanstack/react-query": "^5.59.0",
81
+ "@tanstack/react-query": "^5.62.0",
82
82
  "react": "^18.3.0 || ^19.0.0"
83
83
  },
84
84
  "peerDependenciesMeta": {
@@ -103,7 +103,7 @@
103
103
  "@connectrpc/connect": "^2.0.0",
104
104
  "@hyperdx/browser": "0.26.0",
105
105
  "@opentelemetry/api": "^1.9.0",
106
- "@tanstack/react-query": "^5.59.0",
106
+ "@tanstack/react-query": "^5.62.0",
107
107
  "@types/node": "^22.14.0",
108
108
  "@types/react": "^19.1.0",
109
109
  "@types/react-18": "npm:@types/react@~18.3.0",