Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 9.9 kB view raw
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};