1/* eslint-disable react-hooks/rules-of-hooks */
2
3import { pipe, subscribe, onEnd } from 'wonka';
4
5import type { Ref, WatchStopHandle } from 'vue';
6import { shallowRef, isRef, watchEffect } from 'vue';
7
8import type {
9 Client,
10 GraphQLRequestParams,
11 AnyVariables,
12 CombinedError,
13 OperationContext,
14 Operation,
15} from '@urql/core';
16
17import { useClient } from './useClient';
18
19import type { MaybeRefOrGetter, MaybeRefOrGetterObj } from './utils';
20import { useRequestState, useClientState } from './utils';
21
22/** Input arguments for the {@link useSubscription} function.
23 *
24 * @param query - The GraphQL subscription document that `useSubscription` executes.
25 * @param variables - The variables for the GraphQL subscription that `useSubscription` executes.
26 */
27export type UseSubscriptionArgs<
28 Data = any,
29 Variables extends AnyVariables = AnyVariables,
30> = {
31 /** Prevents {@link useSubscription} from automatically executing GraphQL subscription operations.
32 *
33 * @remarks
34 * `pause` may be set to `true` to stop {@link useSubscription} from starting
35 * its subscription automatically. This will pause the subscription until
36 * {@link UseSubscriptionResponse.resume} is called, or, if `pause` is a reactive
37 * ref of a boolean, until this ref changes to `true`.
38 */
39 pause?: MaybeRefOrGetter<boolean>;
40 /** Updates the {@link OperationContext} for the executed GraphQL subscription operation.
41 *
42 * @remarks
43 * `context` may be passed to {@link useSubscription}, to update the {@link OperationContext}
44 * of a subscription operation. This may be used to update the `context` that exchanges
45 * will receive for a single hook.
46 *
47 * @example
48 * ```ts
49 * const result = useQuery({
50 * query,
51 * context: {
52 * additionalTypenames: ['Item'],
53 * },
54 * });
55 * ```
56 */
57 context?: MaybeRefOrGetter<Partial<OperationContext>>;
58} & MaybeRefOrGetterObj<GraphQLRequestParams<Data, Variables>>;
59
60/** Combines previous data with an incoming subscription result’s data.
61 *
62 * @remarks
63 * A `SubscriptionHandler` may be passed to {@link useSubscription} to
64 * aggregate subscription results into a combined {@link UseSubscriptionResponse.data}
65 * value.
66 *
67 * This is useful when a subscription event delivers a single item, while
68 * you’d like to display a list of events.
69 *
70 * @example
71 * ```ts
72 * const NotificationsSubscription = gql`
73 * subscription { newNotification { id, text } }
74 * `;
75 *
76 * const combineNotifications = (notifications = [], data) => {
77 * return [...notifications, data.newNotification];
78 * };
79 *
80 * const result = useSubscription(
81 * { query: NotificationsSubscription },
82 * combineNotifications,
83 * );
84 * ```
85 */
86export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
87
88/** A {@link SubscriptionHandler} or a reactive ref of one. */
89export type SubscriptionHandlerArg<T, R> =
90 | Ref<SubscriptionHandler<T, R>>
91 | SubscriptionHandler<T, R>;
92
93/** State of the current query, your {@link useSubscription} function is executing.
94 *
95 * @remarks
96 * `UseSubscriptionResponse` is returned by {@link useSubscription} and
97 * gives you the updating {@link OperationResult} of GraphQL subscriptions.
98 *
99 * Each value that is part of the result is wrapped in a reactive ref
100 * and updates as results come in.
101 *
102 * Hint: Even when the query and variables update, the prior state of
103 * the last result is preserved.
104 */
105export interface UseSubscriptionResponse<
106 T = any,
107 R = T,
108 V extends AnyVariables = AnyVariables,
109> {
110 /** Indicates whether `useSubscription`’s subscription is active.
111 *
112 * @remarks
113 * When `useSubscription` starts a subscription, the `fetching` flag
114 * is set to `true` and will remain `true` until the subscription
115 * completes on the API, or `useSubscription` is paused.
116 */
117 fetching: Ref<boolean>;
118 /** Indicates that the subscription result is not fresh.
119 *
120 * @remarks
121 * This is mostly unused for subscriptions and will rarely affect you, and
122 * is more relevant for queries.
123 *
124 * @see {@link OperationResult.stale} for the source of this value.
125 */
126 stale: Ref<boolean>;
127 /** Reactive {@link OperationResult.data} for the executed subscription, or data returned by the handler.
128 *
129 * @remarks
130 * `data` will be set to the last {@link OperationResult.data} value
131 * received for the subscription.
132 *
133 * It will instead be set to the values that {@link SubscriptionHandler}
134 * returned, if a handler has been passed to {@link useSubscription}.
135 */
136 data: Ref<R | undefined>;
137 /** Reactive {@link OperationResult.error} for the executed subscription. */
138 error: Ref<CombinedError | undefined>;
139 /** Reactive {@link OperationResult.extensions} for the executed mutation. */
140 extensions: Ref<Record<string, any> | undefined>;
141 /** Reactive {@link Operation} that the current state is for.
142 *
143 * @remarks
144 * This is the subscription {@link Operation} that is currently active.
145 * When {@link UseSubscriptionResponse.fetching} is `true`, this is the
146 * last `Operation` that the current state was for.
147 */
148 operation: Ref<Operation<T, V> | undefined>;
149 /** Indicates whether {@link useSubscription} is currently paused.
150 *
151 * @remarks
152 * When `useSubscription` has been paused, it will stop receiving updates
153 * from the {@link Client} and won’t execute the subscription, until
154 * {@link UseSubscriptionArgs.pause} becomes true or
155 * {@link UseSubscriptionResponse.resume} is called.
156 */
157 isPaused: Ref<boolean>;
158 /** Resumes {@link useSubscription} if it’s currently paused.
159 *
160 * @remarks
161 * Resumes or starts {@link useSubscription}’s subscription, if it’s currently paused.
162 */
163 resume(): void;
164 /** Pauses {@link useSubscription} to stop the subscription.
165 *
166 * @remarks
167 * Pauses {@link useSubscription}’s subscription, which stops it
168 * from receiving updates from the {@link Client} and to stop executing
169 * the subscription operation.
170 */
171 pause(): void;
172 /** Triggers {@link useQuery} to reexecute a GraphQL subscription operation.
173 *
174 * @param opts - optionally, context options that will be merged with
175 * {@link UseQueryArgs.context} and the `Client`’s options.
176 *
177 * @remarks
178 * When called, {@link useSubscription} will re-execute the GraphQL subscription
179 * operation it currently holds, unless it’s currently paused.
180 */
181 executeSubscription(opts?: Partial<OperationContext>): void;
182}
183
184/** Function to run a GraphQL subscription and get reactive GraphQL results.
185 *
186 * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options.
187 * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results.
188 * @returns a {@link UseSubscriptionResponse} object.
189 *
190 * @remarks
191 * `useSubscription` allows GraphQL subscriptions to be defined and executed inside
192 * Vue `setup` functions.
193 * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the
194 * provided {@link Client}.
195 *
196 * The returned result updates when the `Client` has new results
197 * for the subscription, and `data` is updated with the result’s data
198 * or with the `data` that a `handler` returns.
199 *
200 * @example
201 * ```ts
202 * import { gql, useSubscription } from '@urql/vue';
203 *
204 * const NotificationsSubscription = gql`
205 * subscription { newNotification { id, text } }
206 * `;
207 *
208 * export default {
209 * setup() {
210 * const result = useSubscription(
211 * { query: NotificationsSubscription },
212 * function combineNotifications(notifications = [], data) {
213 * return [...notifications, data.newNotification];
214 * },
215 * );
216 * // ...
217 * },
218 * };
219 * ```
220 */
221export function useSubscription<
222 T = any,
223 R = T,
224 V extends AnyVariables = AnyVariables,
225>(
226 args: UseSubscriptionArgs<T, V>,
227 handler?: SubscriptionHandlerArg<T, R>
228): UseSubscriptionResponse<T, R, V> {
229 return callUseSubscription(args, handler);
230}
231
232export function callUseSubscription<
233 T = any,
234 R = T,
235 V extends AnyVariables = AnyVariables,
236>(
237 args: UseSubscriptionArgs<T, V>,
238 handler?: SubscriptionHandlerArg<T, R>,
239 client: Ref<Client> = useClient(),
240 stops?: WatchStopHandle[]
241): UseSubscriptionResponse<T, R, V> {
242 const data: Ref<R | undefined> = shallowRef();
243
244 const { fetching, operation, extensions, stale, error } = useRequestState<
245 T,
246 V
247 >();
248
249 const { isPaused, source, pause, resume, execute, teardown } = useClientState<
250 T,
251 V
252 >(args, client, 'executeSubscription');
253
254 const teardownSubscription = watchEffect(onInvalidate => {
255 if (source.value) {
256 fetching.value = true;
257
258 onInvalidate(
259 pipe(
260 source.value,
261 onEnd(() => {
262 fetching.value = false;
263 }),
264 subscribe(result => {
265 fetching.value = true;
266 error.value = result.error;
267 extensions.value = result.extensions;
268 stale.value = !!result.stale;
269 operation.value = result.operation;
270
271 if (result.data != null && handler) {
272 const cb = isRef(handler) ? handler.value : handler;
273 if (typeof cb === 'function') {
274 data.value = cb(data.value, result.data);
275 return;
276 }
277 }
278 data.value = result.data as R;
279 })
280 ).unsubscribe
281 );
282 } else {
283 fetching.value = false;
284 }
285 });
286
287 stops && stops.push(teardown, teardownSubscription);
288
289 const state: UseSubscriptionResponse<T, R, V> = {
290 data,
291 stale,
292 error,
293 operation,
294 extensions,
295 fetching,
296 isPaused,
297 pause,
298 resume,
299 executeSubscription(
300 opts?: Partial<OperationContext>
301 ): UseSubscriptionResponse<T, R, V> {
302 execute(opts);
303 return state;
304 },
305 };
306
307 return state;
308}