@kibinrpc/tanstack-query 0.3.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ixexel661
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # @kibinrpc/tanstack-query
2
+
3
+ TanStack Query (v5) adapter for [kibinrpc](../../README.md) — type-safe `queryOptions`, `mutationOptions`, and query key factories, fully inferred from your server router.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @kibinrpc/tanstack-query @kibinrpc/client @tanstack/react-query
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ // src/kibin.ts
15
+ import { createKibinClient } from '@kibinrpc/client'
16
+ import { createKibinQuery } from '@kibinrpc/tanstack-query'
17
+ import { QueryClient } from '@tanstack/react-query'
18
+ import type { AppRouter } from './server/router'
19
+
20
+ export const queryClient = new QueryClient()
21
+ export const client = createKibinClient<AppRouter>({ baseUrl: '/api/rpc' })
22
+ export const query = createKibinQuery(client)
23
+ ```
24
+
25
+ ```tsx
26
+ // In a component
27
+ import { useQuery, useMutation } from '@tanstack/react-query'
28
+ import { query, queryClient } from './kibin'
29
+
30
+ function UserList() {
31
+ const users = useQuery(query.user.listUsers.queryOptions([]))
32
+
33
+ const createUser = useMutation(
34
+ query.user.createUser.mutationOptions({
35
+ onSuccess: () =>
36
+ queryClient.invalidateQueries({ queryKey: query.user.queryKey() }),
37
+ }),
38
+ )
39
+
40
+ return (
41
+ <>
42
+ <ul>{users.data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
43
+ <button onClick={() => createUser.mutate([{ name: 'Alice', email: 'alice@example.com' }])}>
44
+ Add
45
+ </button>
46
+ </>
47
+ )
48
+ }
49
+ ```
50
+
51
+ ## Query keys
52
+
53
+ Every namespace and method exposes a stable `queryKey()` factory. TanStack Query's prefix-matching means invalidating a broader key covers all narrower ones beneath it.
54
+
55
+ ```ts
56
+ query.user.queryKey() // ['@kibinrpc', 'user']
57
+ query.user.getUser.queryKey() // ['@kibinrpc', 'user', 'getUser']
58
+ query.user.getUser.queryKey(['user-1']) // ['@kibinrpc', 'user', 'getUser', { args: ['user-1'] }]
59
+ ```
60
+
61
+ ```ts
62
+ // Invalidate all user queries
63
+ queryClient.invalidateQueries({ queryKey: query.user.queryKey() })
64
+
65
+ // Invalidate exactly one call
66
+ queryClient.invalidateQueries({ queryKey: query.user.getUser.queryKey(['user-1']) })
67
+ ```
68
+
69
+ ## Queries
70
+
71
+ `queryOptions` returns a fully-typed options object compatible with `useQuery`, `useSuspenseQuery`, `prefetchQuery`, `fetchQuery`, and `useQueries`.
72
+
73
+ ```ts
74
+ // Basic
75
+ useQuery(query.post.listPosts.queryOptions([]))
76
+
77
+ // With overrides
78
+ useSuspenseQuery(query.user.getUser.queryOptions([id], { staleTime: 60_000 }))
79
+
80
+ // Parallel — auto-batched by kibinrpc into one HTTP request
81
+ useQueries({
82
+ queries: [
83
+ query.user.getUser.queryOptions(['1']),
84
+ query.user.getUser.queryOptions(['2']),
85
+ ],
86
+ })
87
+
88
+ // Prefetch in a route loader or server component
89
+ await queryClient.prefetchQuery(query.post.listPosts.queryOptions([]))
90
+ ```
91
+
92
+ ## Mutations
93
+
94
+ `mutationOptions` returns a fully-typed options object for `useMutation`. Arguments are passed as a tuple matching the server method's parameter list.
95
+
96
+ ```ts
97
+ const createPost = useMutation(
98
+ query.post.createPost.mutationOptions({
99
+ onSuccess: () =>
100
+ queryClient.invalidateQueries({ queryKey: query.post.queryKey() }),
101
+ }),
102
+ )
103
+
104
+ // args tuple — same order as the server method signature
105
+ createPost.mutate([{ title: 'Hello', body: 'World', authorId: '1' }])
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ ```ts
111
+ // Change the query key prefix (default: '@kibinrpc')
112
+ const query = createKibinQuery(client, { queryKeyPrefix: 'myapp' })
113
+
114
+ query.user.queryKey() // ['myapp', 'user']
115
+ ```
116
+
117
+ ## Error handling
118
+
119
+ All query and mutation errors are typed as `KibinError`. Use `isKibinError` to narrow the type:
120
+
121
+ ```ts
122
+ import { isKibinError } from '@kibinrpc/client'
123
+
124
+ if (isKibinError(error)) {
125
+ console.log(error.code) // e.g. 'NOT_FOUND', 'TIMEOUT'
126
+ console.log(error.message)
127
+ }
128
+ ```
129
+
130
+ ## API
131
+
132
+ ### `createKibinQuery(client, config?)`
133
+
134
+ | Parameter | Type | Description |
135
+ |---|---|---|
136
+ | `client` | `KibinClient<Router>` | The kibinrpc client to wrap |
137
+ | `config.queryKeyPrefix` | `string` | Prefix for all generated keys. Default: `'@kibinrpc'` |
138
+
139
+ ### `KibinQueryProxy<Router>`
140
+
141
+ For every namespace `NS` and method `M`:
142
+
143
+ | Property | Description |
144
+ |---|---|
145
+ | `query[NS][M].queryOptions(args, options?)` | Options for `useQuery`, `useSuspenseQuery`, `prefetchQuery`, … |
146
+ | `query[NS][M].queryKey(args?)` | Call-level key (with args) or method-level key (without) |
147
+ | `query[NS][M].mutationOptions(options?)` | Options for `useMutation` |
148
+ | `query[NS][M].mutationKey()` | Stable key for devtools filtering |
149
+ | `query[NS].queryKey()` | Namespace-level key for broad invalidation |
150
+
151
+ ### Exported types
152
+
153
+ ```ts
154
+ import type {
155
+ KibinQueryProxy,
156
+ KibinQueryConfig,
157
+ MethodUtils,
158
+ } from '@kibinrpc/tanstack-query'
159
+ ```
160
+
161
+ ## Known limitations
162
+
163
+ **TanStack Query's `signal` is not forwarded.** When a component unmounts, TQ passes an `AbortSignal` to `queryFn` to cancel the request — kibinrpc does not support per-call signals yet, so the underlying HTTP request will complete even if TQ discards the result. As a workaround, override `queryFn` manually:
164
+
165
+ ```ts
166
+ query.user.listUsers.queryOptions([], {
167
+ queryFn: ({ signal }) =>
168
+ createKibinClient<AppRouter>({ baseUrl: '/api/rpc', signal }).user.listUsers(),
169
+ })
170
+ ```
171
+
172
+ ## Related packages
173
+
174
+ | Package | Description |
175
+ |---|---|
176
+ | [`@kibinrpc/server`](../server/README.md) | Server-side router and action decorators |
177
+ | [`@kibinrpc/client`](../client/README.md) | Type-safe fetch client with automatic batching and retry |
@@ -0,0 +1,48 @@
1
+ import { MutationObserverOptions, QueryObserverOptions } from "@tanstack/react-query";
2
+ import { KibinClient, KibinError } from "@kibinrpc/client";
3
+
4
+ //#region src/types.d.ts
5
+ type AnyAsyncFn = (...args: never[]) => Promise<unknown>;
6
+ type MethodUtils<F extends AnyAsyncFn> = {
7
+ /**
8
+ * Returns query options compatible with useQuery, useSuspenseQuery,
9
+ * prefetchQuery, queryClient.fetchQuery, and more.
10
+ *
11
+ * @param args Method arguments as a tuple
12
+ * @param options Additional QueryObserverOptions to merge
13
+ */
14
+ queryOptions(args: Parameters<F>, options?: Omit<QueryObserverOptions<Awaited<ReturnType<F>>, KibinError>, 'queryKey' | 'queryFn'>): QueryObserverOptions<Awaited<ReturnType<F>>, KibinError>;
15
+ /**
16
+ * Returns a stable query key for this method.
17
+ * Omit `args` to get a broader key that matches all calls to this method.
18
+ *
19
+ * @example
20
+ * query.user.getUser.queryKey(['id-1']) // exact call
21
+ * query.user.getUser.queryKey() // all getUser calls
22
+ */
23
+ queryKey(args?: Parameters<F>): readonly unknown[];
24
+ /**
25
+ * Returns mutation options for use with useMutation.
26
+ * Variables passed to `mutate()` must be the argument tuple.
27
+ *
28
+ * @example
29
+ * const { mutate } = useMutation(query.user.createUser.mutationOptions())
30
+ * mutate(['Alice', 'alice@example.com'])
31
+ */
32
+ mutationOptions(options?: Omit<MutationObserverOptions<Awaited<ReturnType<F>>, KibinError, Parameters<F>>, 'mutationFn' | 'mutationKey'>): MutationObserverOptions<Awaited<ReturnType<F>>, KibinError, Parameters<F>>; /** Stable mutation key for devtools filtering and tracking */
33
+ mutationKey(): readonly unknown[];
34
+ };
35
+ type NamespaceUtils<NS extends Record<string, AnyAsyncFn>> = { [M in keyof NS]: MethodUtils<NS[M]> } & {
36
+ /** Broad query key covering all methods in this namespace — useful for invalidation */queryKey(): readonly unknown[];
37
+ };
38
+ type KibinQueryProxy<Router> = { [NS in Exclude<keyof KibinClient<Router>, '$unbatched'>]: KibinClient<Router>[NS] extends Record<string, AnyAsyncFn> ? NamespaceUtils<KibinClient<Router>[NS]> : never };
39
+ interface KibinQueryConfig {
40
+ /** Prefix for all generated query keys. Default: '@kibinrpc' */
41
+ queryKeyPrefix?: string;
42
+ }
43
+ //#endregion
44
+ //#region src/query.d.ts
45
+ declare function createKibinQuery<Router>(client: KibinClient<Router>, config?: KibinQueryConfig): KibinQueryProxy<Router>;
46
+ //#endregion
47
+ export { type KibinQueryConfig, type KibinQueryProxy, type MethodUtils, createKibinQuery };
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/query.ts"],"mappings":";;;;KAGK,UAAA,OAAiB,IAAA,cAAkB,OAAO;AAAA,KAEnC,WAAA,WAAsB,UAAA;EAF7B;;;;AAA0C;AAE/C;;EAQC,YAAA,CACC,IAAA,EAAM,UAAA,CAAW,CAAA,GACjB,OAAA,GAAU,IAAA,CACT,oBAAA,CAAqB,OAAA,CAAQ,UAAA,CAAW,CAAA,IAAK,UAAA,6BAG5C,oBAAA,CAAqB,OAAA,CAAQ,UAAA,CAAW,CAAA,IAAK,UAAA;EAdf;;;;;;;;EAwBjC,QAAA,CAAS,IAAA,GAAO,UAAA,CAAW,CAAA;EAVgB;;;;;;;;EAoB3C,eAAA,CACC,OAAA,GAAU,IAAA,CACT,uBAAA,CAAwB,OAAA,CAAQ,UAAA,CAAW,CAAA,IAAK,UAAA,EAAY,UAAA,CAAW,CAAA,oCAGtE,uBAAA,CAAwB,OAAA,CAAQ,UAAA,CAAW,CAAA,IAAK,UAAA,EAAY,UAAA,CAAW,CAAA,IAHhD;EAM1B,WAAA;AAAA;AAAA,KAGI,cAAA,YAA0B,MAAA,SAAe,UAAA,mBACjC,EAAA,GAAK,WAAA,CAAY,EAAA,CAAG,CAAA;EAPc,uFAU9C,QAAA;AAAA;AAAA,KAGW,eAAA,oBACJ,OAAA,OAAc,WAAA,CAAY,MAAA,mBAAyB,WAAA,CAAY,MAAA,EAAQ,EAAA,UAAY,MAAA,SAEzF,UAAA,IAEE,cAAA,CAAe,WAAA,CAAY,MAAA,EAAQ,EAAA;AAAA,UAItB,gBAAA;EAtBU;EAwB1B,cAAc;AAAA;;;iBC3DC,gBAAA,QAAA,CACf,MAAA,EAAQ,WAAA,CAAY,MAAA,GACpB,MAAA,GAAQ,gBAAA,GACN,eAAA,CAAgB,MAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import { mutationOptions, queryOptions } from "@tanstack/react-query";
2
+ //#region src/query.ts
3
+ const DEFAULT_PREFIX = "@kibinrpc";
4
+ function createKibinQuery(client, config = {}) {
5
+ const prefix = config.queryKeyPrefix ?? DEFAULT_PREFIX;
6
+ function nsKey(namespace) {
7
+ return [prefix, namespace];
8
+ }
9
+ function methodKey(namespace, method, args) {
10
+ if (args !== void 0) return [
11
+ prefix,
12
+ namespace,
13
+ method,
14
+ { args }
15
+ ];
16
+ return [
17
+ prefix,
18
+ namespace,
19
+ method
20
+ ];
21
+ }
22
+ function makeMethodUtils(namespace, method) {
23
+ const fn = client[namespace][method];
24
+ return {
25
+ queryOptions(args, options = {}) {
26
+ return queryOptions({
27
+ queryKey: methodKey(namespace, method, args),
28
+ queryFn: () => fn(...args),
29
+ ...options
30
+ });
31
+ },
32
+ queryKey(args) {
33
+ return methodKey(namespace, method, args);
34
+ },
35
+ mutationOptions(options = {}) {
36
+ return mutationOptions({
37
+ mutationKey: methodKey(namespace, method),
38
+ mutationFn: (args) => fn(...args),
39
+ ...options
40
+ });
41
+ },
42
+ mutationKey() {
43
+ return methodKey(namespace, method);
44
+ }
45
+ };
46
+ }
47
+ function makeNamespaceProxy(namespace) {
48
+ return new Proxy({}, { get(_, prop) {
49
+ if (prop === "queryKey") return () => nsKey(namespace);
50
+ if (typeof prop !== "string") return void 0;
51
+ return makeMethodUtils(namespace, prop);
52
+ } });
53
+ }
54
+ return new Proxy({}, { get(_, namespace) {
55
+ if (typeof namespace !== "string") return void 0;
56
+ return makeNamespaceProxy(namespace);
57
+ } });
58
+ }
59
+ //#endregion
60
+ export { createKibinQuery };
61
+
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/query.ts"],"sourcesContent":["import type { KibinClient } from '@kibinrpc/client';\nimport { mutationOptions, queryOptions } from '@tanstack/react-query';\nimport type { KibinQueryConfig, KibinQueryProxy } from './types.js';\n\nconst DEFAULT_PREFIX = '@kibinrpc';\n\ntype AnyFn = (...args: unknown[]) => Promise<unknown>;\ntype AnyRecord = Record<string, AnyFn>;\n\nexport function createKibinQuery<Router>(\n\tclient: KibinClient<Router>,\n\tconfig: KibinQueryConfig = {},\n): KibinQueryProxy<Router> {\n\tconst prefix = config.queryKeyPrefix ?? DEFAULT_PREFIX;\n\n\tfunction nsKey(namespace: string): readonly unknown[] {\n\t\treturn [prefix, namespace];\n\t}\n\n\tfunction methodKey(namespace: string, method: string, args?: unknown[]): readonly unknown[] {\n\t\tif (args !== undefined) return [prefix, namespace, method, { args }];\n\t\treturn [prefix, namespace, method];\n\t}\n\n\tfunction makeMethodUtils(namespace: string, method: string) {\n\t\tconst fn = (client as Record<string, AnyRecord>)[namespace][method];\n\n\t\treturn {\n\t\t\tqueryOptions(args: unknown[], options = {}) {\n\t\t\t\treturn queryOptions({\n\t\t\t\t\tqueryKey: methodKey(namespace, method, args),\n\t\t\t\t\tqueryFn: () => fn(...args),\n\t\t\t\t\t...options,\n\t\t\t\t});\n\t\t\t},\n\n\t\t\tqueryKey(args?: unknown[]) {\n\t\t\t\treturn methodKey(namespace, method, args);\n\t\t\t},\n\n\t\t\tmutationOptions(options = {}) {\n\t\t\t\treturn mutationOptions({\n\t\t\t\t\tmutationKey: methodKey(namespace, method),\n\t\t\t\t\tmutationFn: (args: unknown[]) => fn(...args),\n\t\t\t\t\t...options,\n\t\t\t\t});\n\t\t\t},\n\n\t\t\tmutationKey() {\n\t\t\t\treturn methodKey(namespace, method);\n\t\t\t},\n\t\t};\n\t}\n\n\tfunction makeNamespaceProxy(namespace: string) {\n\t\treturn new Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget(_, prop: string | symbol) {\n\t\t\t\t\tif (prop === 'queryKey') return () => nsKey(namespace);\n\t\t\t\t\tif (typeof prop !== 'string') return undefined;\n\t\t\t\t\treturn makeMethodUtils(namespace, prop);\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\treturn new Proxy(\n\t\t{},\n\t\t{\n\t\t\tget(_, namespace: string | symbol) {\n\t\t\t\tif (typeof namespace !== 'string') return undefined;\n\t\t\t\treturn makeNamespaceProxy(namespace);\n\t\t\t},\n\t\t},\n\t) as unknown as KibinQueryProxy<Router>;\n}\n"],"mappings":";;AAIA,MAAM,iBAAiB;AAKvB,SAAgB,iBACf,QACA,SAA2B,CAAC,GACF;CAC1B,MAAM,SAAS,OAAO,kBAAkB;CAExC,SAAS,MAAM,WAAuC;EACrD,OAAO,CAAC,QAAQ,SAAS;CAC1B;CAEA,SAAS,UAAU,WAAmB,QAAgB,MAAsC;EAC3F,IAAI,SAAS,KAAA,GAAW,OAAO;GAAC;GAAQ;GAAW;GAAQ,EAAE,KAAK;EAAC;EACnE,OAAO;GAAC;GAAQ;GAAW;EAAM;CAClC;CAEA,SAAS,gBAAgB,WAAmB,QAAgB;EAC3D,MAAM,KAAM,OAAqC,WAAW;EAE5D,OAAO;GACN,aAAa,MAAiB,UAAU,CAAC,GAAG;IAC3C,OAAO,aAAa;KACnB,UAAU,UAAU,WAAW,QAAQ,IAAI;KAC3C,eAAe,GAAG,GAAG,IAAI;KACzB,GAAG;IACJ,CAAC;GACF;GAEA,SAAS,MAAkB;IAC1B,OAAO,UAAU,WAAW,QAAQ,IAAI;GACzC;GAEA,gBAAgB,UAAU,CAAC,GAAG;IAC7B,OAAO,gBAAgB;KACtB,aAAa,UAAU,WAAW,MAAM;KACxC,aAAa,SAAoB,GAAG,GAAG,IAAI;KAC3C,GAAG;IACJ,CAAC;GACF;GAEA,cAAc;IACb,OAAO,UAAU,WAAW,MAAM;GACnC;EACD;CACD;CAEA,SAAS,mBAAmB,WAAmB;EAC9C,OAAO,IAAI,MACV,CAAC,GACD,EACC,IAAI,GAAG,MAAuB;GAC7B,IAAI,SAAS,YAAY,aAAa,MAAM,SAAS;GACrD,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;GACrC,OAAO,gBAAgB,WAAW,IAAI;EACvC,EACD,CACD;CACD;CAEA,OAAO,IAAI,MACV,CAAC,GACD,EACC,IAAI,GAAG,WAA4B;EAClC,IAAI,OAAO,cAAc,UAAU,OAAO,KAAA;EAC1C,OAAO,mBAAmB,SAAS;CACpC,EACD,CACD;AACD"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@kibinrpc/tanstack-query",
3
+ "version": "0.3.0",
4
+ "description": "TanStack Query adapter for @kibinrpc/client",
5
+ "license": "MIT",
6
+ "author": "ixexel661",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ixexel661/kibin",
10
+ "directory": "packages/tanstack-query"
11
+ },
12
+ "keywords": [
13
+ "rpc",
14
+ "tanstack-query",
15
+ "react-query",
16
+ "typescript"
17
+ ],
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "peerDependencies": {
32
+ "@tanstack/react-query": "^5.0.0",
33
+ "@kibinrpc/client": "0.3.0"
34
+ },
35
+ "devDependencies": {
36
+ "@tanstack/react-query": "^5.0.0",
37
+ "tsdown": "^0.22.0",
38
+ "vitest": "^4.1.7",
39
+ "@kibinrpc/client": "0.3.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsdown",
46
+ "dev": "tsdown --watch",
47
+ "test": "vitest run"
48
+ }
49
+ }