1import type {
2 AnyVariables,
3 GraphQLRequestParams,
4 Client,
5 OperationContext,
6} from '@urql/core';
7import { createRequest } from '@urql/core';
8
9import type { Source } from 'wonka';
10import {
11 pipe,
12 map,
13 fromValue,
14 switchMap,
15 subscribe,
16 concat,
17 scan,
18 never,
19} from 'wonka';
20
21import { derived, writable } from 'svelte/store';
22
23import type {
24 OperationResultState,
25 OperationResultStore,
26 Pausable,
27} from './common';
28import { initialResult, createPausable, fromStore } from './common';
29
30/** Combines previous data with an incoming subscription result’s data.
31 *
32 * @remarks
33 * A `SubscriptionHandler` may be passed to {@link subscriptionStore} to
34 * aggregate subscription results into a combined `data` value on the
35 * {@link OperationResultStore}.
36 *
37 * This is useful when a subscription event delivers a single item, while
38 * you’d like to display a list of events.
39 *
40 * @example
41 * ```ts
42 * const NotificationsSubscription = gql`
43 * subscription { newNotification { id, text } }
44 * `;
45 *
46 * subscriptionStore(
47 * { query: NotificationsSubscription },
48 * function combineNotifications(notifications = [], data) {
49 * return [...notifications, data.newNotification];
50 * },
51 * );
52 * ```
53 */
54export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
55
56/** Input arguments for the {@link subscriptionStore} function.
57 *
58 * @param query - The GraphQL subscription that the `subscriptionStore` executes.
59 * @param variables - The variables for the GraphQL subscription that `subscriptionStore` executes.
60 */
61export type SubscriptionArgs<
62 Data = any,
63 Variables extends AnyVariables = AnyVariables,
64> = {
65 /** The {@link Client} using which the subscription will be started.
66 *
67 * @remarks
68 * If you’ve previously provided a {@link Client} on Svelte’s context
69 * this can be set to {@link getContextClient}’s return value.
70 */
71 client: Client;
72 /** Updates the {@link OperationContext} for the GraphQL subscription operation.
73 *
74 * @remarks
75 * `context` may be passed to {@link subscriptionStore}, to update the
76 * {@link OperationContext} of a subscription operation. This may be used to update
77 * the `context` that exchanges will receive for a single hook.
78 *
79 * @example
80 * ```ts
81 * subscriptionStore({
82 * query,
83 * context: {
84 * additionalTypenames: ['Item'],
85 * },
86 * });
87 * ```
88 */
89 context?: Partial<OperationContext>;
90 /** Prevents the {@link subscriptionStore} from automatically starting the GraphQL subscription.
91 *
92 * @remarks
93 * `pause` may be set to `true` to stop the {@link subscriptionStore} from starting
94 * its subscription automatically. The store won't execute the subscription operation,
95 * until either it’s set to `false` or {@link Pausable.resume} is called.
96 */
97 pause?: boolean;
98} & GraphQLRequestParams<Data, Variables>;
99
100/** Function to create a `subscriptionStore` that starts a GraphQL subscription.
101 *
102 * @param args - a {@link QueryArgs} object, to pass a `query`, `variables`, and options.
103 * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results.
104 * @returns a {@link OperationResultStore} of subscription results, which implements {@link Pausable}.
105 *
106 * @remarks
107 * `subscriptionStore` allows GraphQL subscriptions to be defined as Svelte stores.
108 * Given {@link SubscriptionArgs.query}, it executes the GraphQL subsription on the
109 * {@link SubscriptionArgs.client}.
110 *
111 * The returned store updates with {@link OperationResult} values when
112 * the `Client` has new results for the subscription.
113 *
114 * @see {@link https://urql.dev/goto/docs/advanced/subscriptions#svelte} for
115 * `subscriptionStore` docs.
116 *
117 * @example
118 * ```ts
119 * import { subscriptionStore, gql, getContextClient } from '@urql/svelte';
120 *
121 * const todos = subscriptionStore({
122 * client: getContextClient(),
123 * query: gql`
124 * subscription {
125 * newNotification { id, text }
126 * }
127 * `,
128 * },
129 * function combineNotifications(notifications = [], data) {
130 * return [...notifications, data.newNotification];
131 * },
132 * );
133 * ```
134 */
135export function subscriptionStore<
136 Data,
137 Result = Data,
138 Variables extends AnyVariables = AnyVariables,
139>(
140 args: SubscriptionArgs<Data, Variables>,
141 handler?: SubscriptionHandler<Data, Result>
142): OperationResultStore<Result, Variables> & Pausable {
143 const request = createRequest(args.query, args.variables as Variables);
144
145 const operation = args.client.createRequestOperation(
146 'subscription',
147 request,
148 args.context
149 );
150 const initialState: OperationResultState<Result, Variables> = {
151 ...initialResult,
152 operation,
153 fetching: true,
154 };
155
156 const isPaused$ = writable(!!args.pause);
157
158 const result$ = writable(initialState, () => {
159 return pipe(
160 fromStore(isPaused$),
161 switchMap(
162 (isPaused): Source<Partial<OperationResultState<Data, Variables>>> => {
163 if (isPaused) {
164 return never as any;
165 }
166
167 return concat<Partial<OperationResultState<Data, Variables>>>([
168 fromValue({ fetching: true, stale: false }),
169 pipe(
170 args.client.executeRequestOperation(operation),
171 map(({ stale, data, error, extensions, operation }) => ({
172 fetching: true,
173 stale: !!stale,
174 data,
175 error,
176 operation,
177 extensions,
178 }))
179 ),
180 fromValue({ fetching: false }),
181 ]);
182 }
183 ),
184 scan((result: OperationResultState<Result, Variables>, partial) => {
185 const data =
186 partial.data != null
187 ? typeof handler === 'function'
188 ? handler(result.data, partial.data)
189 : partial.data
190 : result.data;
191 return {
192 ...result,
193 ...partial,
194 data,
195 } as OperationResultState<Result, Variables>;
196 }, initialState),
197 subscribe(result => {
198 result$.set(result);
199 })
200 ).unsubscribe;
201 });
202
203 return {
204 ...derived(result$, (result, set) => {
205 set(result);
206 }),
207 ...createPausable(isPaused$),
208 };
209}