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