1import type { AnyVariables, Client, DocumentInput } from '@urql/core';
2import type { WatchStopHandle } from 'vue';
3import { getCurrentInstance, onMounted, onBeforeUnmount } from 'vue';
4
5import { useClient } from './useClient';
6
7import type { UseQueryArgs, UseQueryResponse } from './useQuery';
8import { callUseQuery } from './useQuery';
9
10import type { UseMutationResponse } from './useMutation';
11import { callUseMutation } from './useMutation';
12
13import type {
14 UseSubscriptionArgs,
15 SubscriptionHandlerArg,
16 UseSubscriptionResponse,
17} from './useSubscription';
18import { callUseSubscription } from './useSubscription';
19
20/** Handle to create GraphQL operations outside of Vue’s `setup` functions.
21 *
22 * @remarks
23 * The `ClientHandle` object is created inside a Vue `setup` function but
24 * allows its methods to be called outside of `setup` functions, delaying
25 * the creation of GraphQL operations, as an alternative to pausing queries
26 * or subscriptions.
27 *
28 * This is also important when chaining multiple functions inside an
29 * `async setup()` function.
30 *
31 * Hint: If you only need a single, non-updating result and want to execute
32 * queries programmatically, it may be easier to call the {@link Client.query}
33 * method.
34 */
35export interface ClientHandle {
36 /** The {@link Client} that’ll be used to execute GraphQL operations. */
37 client: Client;
38
39 /** Calls {@link useQuery} outside of a synchronous Vue `setup` function.
40 *
41 * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options.
42 * @returns a {@link UseQueryResponse} object.
43 *
44 * @remarks
45 * Creates a {@link UseQueryResponse} outside of a synchronous Vue `setup`
46 * function or when chained in an `async setup()` function.
47 */
48 useQuery<T = any, V extends AnyVariables = AnyVariables>(
49 args: UseQueryArgs<T, V>
50 ): UseQueryResponse<T, V>;
51
52 /** Calls {@link useSubscription} outside of a synchronous Vue `setup` function.
53 *
54 * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options.
55 * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results.
56 * @returns a {@link UseSubscriptionResponse} object.
57 *
58 * @remarks
59 * Creates a {@link UseSubscriptionResponse} outside of a synchronous Vue `setup`
60 * function or when chained in an `async setup()` function.
61 */
62 useSubscription<T = any, R = T, V extends AnyVariables = AnyVariables>(
63 args: UseSubscriptionArgs<T, V>,
64 handler?: SubscriptionHandlerArg<T, R>
65 ): UseSubscriptionResponse<T, R, V>;
66
67 /** Calls {@link useMutation} outside of a synchronous Vue `setup` function.
68 *
69 * @param query - a GraphQL mutation document which `useMutation` will execute.
70 * @returns a {@link UseMutationResponse} object.
71 *
72 * @remarks
73 * Creates a {@link UseMutationResponse} outside of a synchronous Vue `setup`
74 * function or when chained in an `async setup()` function.
75 */
76 useMutation<T = any, V extends AnyVariables = AnyVariables>(
77 query: DocumentInput<T, V>
78 ): UseMutationResponse<T, V>;
79}
80
81/** Creates a {@link ClientHandle} inside a Vue `setup` function.
82 *
83 * @remarks
84 * `useClientHandle` creates and returns a {@link ClientHandle}
85 * when called in a Vue `setup` function, which allows queries,
86 * mutations, and subscriptions to be created _outside_ of
87 * `setup` functions.
88 *
89 * This is also important when chaining multiple functions inside an
90 * `async setup()` function.
91 *
92 * {@link useQuery} and other GraphQL functions must usually
93 * be created in Vue `setup` functions so they can stop GraphQL
94 * operations when your component unmounts. However, while they
95 * queries and subscriptions can be paused, sometimes it’s easier
96 * to delay the creation of their response objects.
97 *
98 *
99 * @example
100 * ```ts
101 * import { ref, computed } from 'vue';
102 * import { gql, useClientHandle } from '@urql/vue';
103 *
104 * export default {
105 * async setup() {
106 * const handle = useClientHandle();
107 *
108 * const pokemons = await handle.useQuery({
109 * query: gql`{ pokemons(limit: 10) { id, name } }`,
110 * });
111 *
112 * const index = ref(0);
113 *
114 * // The `handle` allows another `useQuery` call to now be setup again
115 * const pokemon = await handle.useQuery({
116 * query: gql`
117 * query ($id: ID!) {
118 * pokemon(id: $id) { id, name }
119 * }
120 * `,
121 * variables: computed(() => ({
122 * id: pokemons.data.value.pokemons[index.value].id,
123 * }),
124 * });
125 * }
126 * };
127 * ```
128 */
129export function useClientHandle(): ClientHandle {
130 const client = useClient();
131 const stops: WatchStopHandle[] = [];
132
133 onBeforeUnmount(() => {
134 let stop: WatchStopHandle | void;
135 while ((stop = stops.shift())) stop();
136 });
137
138 const handle: ClientHandle = {
139 client: client.value,
140
141 useQuery<T = any, V extends AnyVariables = AnyVariables>(
142 args: UseQueryArgs<T, V>
143 ): UseQueryResponse<T, V> {
144 return callUseQuery(args, client, stops);
145 },
146
147 useSubscription<T = any, R = T, V extends AnyVariables = AnyVariables>(
148 args: UseSubscriptionArgs<T, V>,
149 handler?: SubscriptionHandlerArg<T, R>
150 ): UseSubscriptionResponse<T, R, V> {
151 return callUseSubscription(args, handler, client, stops);
152 },
153
154 useMutation<T = any, V extends AnyVariables = AnyVariables>(
155 query: DocumentInput<T, V>
156 ): UseMutationResponse<T, V> {
157 return callUseMutation(query, client);
158 },
159 };
160
161 if (process.env.NODE_ENV !== 'production') {
162 onMounted(() => {
163 Object.assign(handle, {
164 useQuery<T = any, V extends AnyVariables = AnyVariables>(
165 args: UseQueryArgs<T, V>
166 ): UseQueryResponse<T, V> {
167 if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) {
168 throw new Error(
169 '`handle.useQuery()` should only be called in the `setup()` or a lifecycle hook.'
170 );
171 }
172
173 return callUseQuery(args, client, stops);
174 },
175
176 useSubscription<T = any, R = T, V extends AnyVariables = AnyVariables>(
177 args: UseSubscriptionArgs<T, V>,
178 handler?: SubscriptionHandlerArg<T, R>
179 ): UseSubscriptionResponse<T, R, V> {
180 if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) {
181 throw new Error(
182 '`handle.useSubscription()` should only be called in the `setup()` or a lifecycle hook.'
183 );
184 }
185
186 return callUseSubscription(args, handler, client, stops);
187 },
188 });
189 });
190 }
191
192 return handle;
193}