1/* eslint-disable @typescript-eslint/no-use-before-define */
2
3import type { Source, Subscription } from 'wonka';
4import {
5 lazy,
6 filter,
7 makeSubject,
8 onEnd,
9 onPush,
10 onStart,
11 pipe,
12 share,
13 take,
14 takeUntil,
15 takeWhile,
16 publish,
17 subscribe,
18 switchMap,
19 fromValue,
20 merge,
21 map,
22} from 'wonka';
23
24import { composeExchanges } from './exchanges';
25import { fallbackExchange } from './exchanges/fallback';
26
27import type {
28 DocumentInput,
29 AnyVariables,
30 Exchange,
31 ExchangeInput,
32 GraphQLRequest,
33 Operation,
34 OperationInstance,
35 OperationContext,
36 OperationResult,
37 OperationResultSource,
38 OperationType,
39 RequestPolicy,
40 DebugEvent,
41} from './types';
42
43import {
44 createRequest,
45 withPromise,
46 noop,
47 makeOperation,
48 getOperationType,
49} from './utils';
50
51/** Configuration options passed when creating a new {@link Client}.
52 *
53 * @remarks
54 * The `ClientOptions` are passed when creating a new {@link Client}, and
55 * are used to instantiate the pipeline of {@link Exchange | Exchanges}, configure
56 * options used to initialize {@link OperationContext | OperationContexts}, or to
57 * change the general behaviour of the {@link Client}.
58 */
59export interface ClientOptions {
60 /** Target URL used by fetch exchanges to make GraphQL API requests to.
61 *
62 * @remarks
63 * This is the URL that fetch exchanges will call to make GraphQL API requests.
64 * This value is copied to {@link OperationContext.url}.
65 */
66 url: string;
67 /** Additional options used by fetch exchanges that'll be passed to the `fetch` call on API requests.
68 *
69 * @remarks
70 * The options in this object or an object returned by a callback function will be merged into the
71 * {@link RequestInit} options passed to the `fetch` call.
72 *
73 * Hint: If you're trying to implement more complex changes per {@link Operation}, it's worth considering
74 * to use the {@link mapExchange} instead, which allows you to change `Operation`s and `OperationResult`s.
75 *
76 * Hint: If you're trying to use this as a function for authentication, consider checking out
77 * `@urql/exchange-auth` instead, which allows you to handle refresh auth flows, and more
78 * complex auth flows.
79 *
80 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} for a description of this object.
81 */
82 fetchOptions?: RequestInit | (() => RequestInit);
83 /** A `fetch` function polyfill used by fetch exchanges to make API calls.
84 *
85 * @remarks
86 * This is the fetch polyfill used by any fetch exchange to make an API request. By default, when this
87 * option isn't set, any fetch exchange will attempt to use the globally available `fetch` function
88 * to make a request instead.
89 *
90 * It's recommended to only pass a polyfill, if any of the environments you're running the {@link Client}
91 * in don't support the Fetch API natively.
92 *
93 * Hint: If you're using the "Incremental Delivery" multipart spec, for instance with `@defer` directives,
94 * you're better off using the native `fetch` function, or must ensure that your polyfill supports streamed
95 * results. However, a "Streaming requests unsupported" error will be thrown, to let you know that your `fetch`
96 * API doesn't support incrementally streamed responses, if this mode is used.
97 *
98 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec.
99 */
100 fetch?: typeof fetch;
101 /** Allows a subscription to be executed using a `fetch` API request.
102 *
103 * @remarks
104 * If your API supports the `text/event-stream` and/or `multipart/mixed` response protocol, and you use
105 * this protocol to handle subscriptions, then you may switch this flag to `true`.
106 *
107 * This means you won’t have to create a {@link subscriptionExchange} to handle subscriptions with an
108 * external transport, and will instead be able to use GraphQL over HTTP transports.
109 */
110 fetchSubscriptions?: boolean;
111 /** A list of `Exchange`s that will be used to create the `Client`'s execution pipeline.
112 *
113 * @remarks
114 * The {@link Client} accepts and composes a list of {@link Exchange | Exchanges} into an “exchange pipeline”
115 * which receive a stream of {@link Operation | Operations} the `Client` wishes to execute, and return a stream
116 * of {@link OperationResult | OperationResults}.
117 *
118 * This is the basis for how `urql` handles GraphQL operations, and exchanges handle the creation, execution,
119 * and control flow of exchanges for the `Client`.
120 *
121 * To easily get started you should consider using the {@link cacheExchange} and {@link fetchExchange}
122 * these are all exported from the core package.
123 *
124 * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information
125 * on what `Exchange`s are and how they work.
126 */
127 exchanges: Exchange[];
128 /** A configuration flag indicating whether support for "Suspense" is activated.
129 *
130 * @remarks
131 * This configuration flag is only relevant for using `urql` with the React or Preact bindings.
132 * When activated it allows `useQuery` to "suspend" instead of returning a loading state, which
133 * will stop updates in a querying component and instead cascade
134 * to a higher suspense boundary for a loading state.
135 *
136 * Hint: While, when this option is enabled, by default all `useQuery` hooks will suspense, you can
137 * disable Suspense selectively for each hook.
138 *
139 * @see {@link https://beta.reactjs.org/blog/2022/03/29/react-v18#new-suspense-features} for more information on React Suspense.
140 */
141 suspense?: boolean;
142 /** The request and caching strategy that all `Operation`s on this `Client` will use by default.
143 *
144 * @remarks
145 * The {@link RequestPolicy} instructs cache exchanges how to use and treat their cached results.
146 * By default `cache-first` is set and used, which will use cache results, and only make an API request
147 * on a cache miss.
148 *
149 * The `requestPolicy` can be overriden per operation, since it's added to the {@link OperationContext},
150 * which allows you to change the policy per `Operation`, rather than changing it by default here.
151 *
152 * Hint: We don’t recommend changing this from the default `cache-first` option, unless you know what
153 * you‘re doing. Setting this to `cache-and-network` is not recommend and may not lead to the behaviour
154 * you expect. If you’re looking to always update your cache frequently, use `@urql/exchange-request-policy`
155 * instead.
156 */
157 requestPolicy?: RequestPolicy;
158 /** Instructs fetch exchanges to use a GET request.
159 *
160 * @remarks
161 * This changes the {@link OperationContext.preferGetMethod} option, which tells fetch exchanges
162 * to use GET requests for queries instead of POST requests.
163 *
164 * When set to `true` or `'within-url-limit'`, built-in fetch exchanges will always attempt to send query
165 * operations as GET requests, unless the resulting URL exceeds a length of 2,048 characters.
166 * If you want to bypass this restriction, set this option to `'force'` instead, to always send GET.
167 * requests for queries.
168 */
169 preferGetMethod?: boolean | 'force' | 'within-url-limit';
170}
171
172/** The `Client` is the central hub for your GraphQL operations and holds `urql`'s state.
173 *
174 * @remarks
175 * The `Client` manages your active GraphQL operations and their state, and contains the
176 * {@link Exchange} pipeline to execute your GraphQL operations.
177 *
178 * It contains methods that allow you to execute GraphQL operations manually, but the `Client`
179 * is also interacted with by bindings (for React, Preact, Vue, Svelte, etc) to execute GraphQL
180 * operations.
181 *
182 * While {@link Exchange | Exchanges} are ultimately responsible for the control flow of operations,
183 * sending API requests, and caching, the `Client` still has the important responsibility for
184 * creating operations, managing consumers of active operations, sharing results for operations,
185 * and more tasks as a “central hub”.
186 *
187 * @see {@link https://urql.dev/goto/docs/architecture/#requests-and-operations-on-the-client} for more information
188 * on what the `Client` is and does.
189 */
190export interface Client {
191 new (options: ClientOptions): Client;
192
193 /** Exposes the stream of `Operation`s that is passed to the `Exchange` pipeline.
194 *
195 * @remarks
196 * This is a Wonka {@link Source} that issues the {@link Operation | Operations} going into
197 * the exchange pipeline.
198 * @internal
199 */
200 operations$: Source<Operation>;
201
202 /** Flag indicating whether support for “Suspense” is activated.
203 *
204 * @remarks
205 * This flag indicates whether support for “Suspense” has been activated via the
206 * {@link ClientOptions.suspense} flag.
207 *
208 * When this is enabled, the {@link Client} itself doesn’t function any differently, and the flag
209 * only serves as an instructions for the React/Preact bindings to change their behaviour.
210 *
211 * @see {@link ClientOptions.suspense} for more information.
212 * @internal
213 */
214 suspense: boolean;
215
216 /** Dispatches an `Operation` to the `Exchange` pipeline, if this `Operation` is active.
217 *
218 * @remarks
219 * This method is frequently used in {@link Exchange | Exchanges}, for instance caches, to reexecute
220 * an operation. It’s often either called because an `Operation` will need to be queried against the
221 * cache again, if a cache result has changed or been invalidated, or it’s called with an {@link Operation}'s
222 * {@link RequestPolicy} set to `network-only` to issue a network request.
223 *
224 * This method will only dispatch an {@link Operation} if it has active consumers, meaning,
225 * active subscribers to the sources of {@link OperationResult}. For instance, if no bindings
226 * (e.g. `useQuery`) is subscribed to the `Operation`, then `reexecuteOperation` will do nothing.
227 *
228 * All operations are put onto a queue and executed after a micro-tick. The queue of operations is
229 * emptied eagerly and synchronously, similar to a trampoline scheduler.
230 */
231 reexecuteOperation(operation: Operation): void;
232
233 /** Subscribe method to add an event listener to debug events.
234 *
235 * @param onEvent - A callback called with new debug events, each time an `Exchange` issues them.
236 * @returns A Wonka {@link Subscription} which is used to optionally terminate the event listener.
237 *
238 * @remarks
239 * This is a method that's only available in development, and allows the `urql-devtools` to receive
240 * to debug events that are issued by exchanges, giving the devtools more information about the flow
241 * and execution of {@link Operation | Operations}.
242 *
243 * @see {@link DebugEventTypes} for a description of all debug events.
244 * @internal
245 */
246 subscribeToDebugTarget?(onEvent: (event: DebugEvent) => void): Subscription;
247
248 /** Creates an `Operation` from a `GraphQLRequest` and optionally, overriding `OperationContext` options.
249 *
250 * @param kind - The {@link OperationType} of GraphQL operation, i.e. `query`, `mutation`, or `subscription`.
251 * @param request - A {@link GraphQLRequest} created prior to calling this method.
252 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
253 * @returns An {@link Operation} created from the parameters.
254 *
255 * @remarks
256 * This method is expected to be called with a `kind` set to the `OperationType` of the GraphQL operation.
257 * In development, this is enforced by checking that the GraphQL document's operation matches this `kind`.
258 *
259 * Hint: While bindings will use this method combined with {@link Client.executeRequestOperation}, if
260 * you’re executing operations manually, you can use one of the other convenience methods instead.
261 *
262 * @see {@link Client.executeRequestOperation} for the method used to execute operations.
263 * @see {@link createRequest} which creates a `GraphQLRequest` from a `DocumentNode` and variables.
264 */
265 createRequestOperation<
266 Data = any,
267 Variables extends AnyVariables = AnyVariables,
268 >(
269 kind: OperationType,
270 request: GraphQLRequest<Data, Variables>,
271 opts?: Partial<OperationContext> | undefined
272 ): Operation<Data, Variables>;
273
274 /** Creates a `Source` that executes the `Operation` and issues `OperationResult`s for this `Operation`.
275 *
276 * @param operation - {@link Operation} that will be executed.
277 * @returns A Wonka {@link Source} of {@link OperationResult | OperationResults} for the passed `Operation`.
278 *
279 * @remarks
280 * The {@link Operation} will be dispatched to the pipeline of {@link Exchange | Exchanges} when
281 * subscribing to the returned {@link Source}, which issues {@link OperationResult | OperationResults}
282 * belonging to this `Operation`.
283 *
284 * Internally, {@link OperationResult | OperationResults} are filtered and deliverd to this source by
285 * comparing the {@link Operation.key} on the operation and the {@link OperationResult.operation}.
286 * For mutations, the {@link OperationContext._instance | `OperationContext._instance`} will additionally be compared, since two mutations
287 * with, even given the same variables, will have two distinct results and will be executed separately.
288 *
289 * The {@link Client} dispatches the {@link Operation} when we subscribe to the returned {@link Source}
290 * and will from then on consider the `Operation` as “active” until we unsubscribe. When all consumers unsubscribe
291 * from an `Operation` and it becomes “inactive” a `teardown` signal will be dispatched to the
292 * {@link Exchange | Exchanges}.
293 *
294 * Hint: While bindings will use this method, if you’re executing operations manually, you can use one
295 * of the other convenience methods instead, like {@link Client.executeQuery} et al.
296 */
297 executeRequestOperation<
298 Data = any,
299 Variables extends AnyVariables = AnyVariables,
300 >(
301 operation: Operation<Data, Variables>
302 ): OperationResultSource<OperationResult<Data, Variables>>;
303
304 /** Creates a `Source` that executes the GraphQL query operation created from the passed parameters.
305 *
306 * @param query - a GraphQL document containing the query operation that will be executed.
307 * @param variables - the variables used to execute the operation.
308 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
309 * @returns A {@link OperationResultSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
310 *
311 * @remarks
312 * The `Client.query` method is useful to programmatically create and issue a GraphQL query operation.
313 * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and
314 * {@link client.executeRequestOperation} for you, and is a convenience method.
315 *
316 * Since it returns a {@link OperationResultSource} it may be chained with a `toPromise()` call to only
317 * await a single result in an async function.
318 *
319 * Hint: This is the recommended way to create queries programmatically when not using the bindings,
320 * or when you’re trying to get a single, promisified result.
321 *
322 * @example
323 * ```ts
324 * const getBookQuery = gql`
325 * query GetBook($id: ID!) {
326 * book(id: $id) {
327 * id
328 * name
329 * author {
330 * name
331 * }
332 * }
333 * }
334 * `;
335 *
336 * async function getBook(id) {
337 * const result = await client.query(getBookQuery, { id }).toPromise();
338 * if (result.error) {
339 * throw result.error;
340 * }
341 *
342 * return result.data.book;
343 * }
344 * ```
345 */
346 query<Data = any, Variables extends AnyVariables = AnyVariables>(
347 query: DocumentInput<Data, Variables>,
348 variables: Variables,
349 context?: Partial<OperationContext>
350 ): OperationResultSource<OperationResult<Data, Variables>>;
351
352 /** Returns the first synchronous result a `Client` provides for a given operation.
353 *
354 * @param query - a GraphQL document containing the query operation that will be executed.
355 * @param variables - the variables used to execute the operation.
356 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
357 * @returns An {@link OperationResult} if one became available synchronously or `null`.
358 *
359 * @remarks
360 * The `Client.readQuery` method returns a result synchronously or defaults to `null`. This is useful
361 * as it limits the result for a query operation to whatever the cache {@link Exchange} of a {@link Client}
362 * had stored and available at that moment.
363 *
364 * In `urql`, it's expected that cache exchanges return their results synchronously. The bindings
365 * and this method exploit this by using synchronous results, like these, to check what data is already
366 * in the cache.
367 *
368 * This method is similar to what all bindings do to synchronously provide the initial state for queries,
369 * regardless of whether effects afterwards that subscribe to the query operation update this state synchronously
370 * or asynchronously.
371 */
372 readQuery<Data = any, Variables extends AnyVariables = AnyVariables>(
373 query: DocumentInput<Data, Variables>,
374 variables: Variables,
375 context?: Partial<OperationContext>
376 ): OperationResult<Data, Variables> | null;
377
378 /** Creates a `Source` that executes the GraphQL query operation for the passed `GraphQLRequest`.
379 *
380 * @param query - a {@link GraphQLRequest}
381 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
382 * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
383 *
384 * @remarks
385 * The `Client.executeQuery` method is used to programmatically issue a GraphQL query operation.
386 * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you,
387 * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first.
388 *
389 * @see {@link Client.query} for a method that doesn't require calling {@link createRequest} yourself.
390 */
391 executeQuery<Data = any, Variables extends AnyVariables = AnyVariables>(
392 query: GraphQLRequest<Data, Variables>,
393 opts?: Partial<OperationContext> | undefined
394 ): OperationResultSource<OperationResult<Data, Variables>>;
395
396 /** Creates a `Source` that executes the GraphQL subscription operation created from the passed parameters.
397 *
398 * @param query - a GraphQL document containing the subscription operation that will be executed.
399 * @param variables - the variables used to execute the operation.
400 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
401 * @returns A Wonka {@link Source} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
402 *
403 * @remarks
404 * The `Client.subscription` method is useful to programmatically create and issue a GraphQL subscription operation.
405 * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and
406 * {@link client.executeRequestOperation} for you, and is a convenience method.
407 *
408 * Hint: This is the recommended way to create subscriptions programmatically when not using the bindings.
409 *
410 * @example
411 * ```ts
412 * import { pipe, subscribe } from 'wonka';
413 *
414 * const getNewsSubscription = gql`
415 * subscription GetNews {
416 * breakingNews {
417 * id
418 * text
419 * createdAt
420 * }
421 * }
422 * `;
423 *
424 * function subscribeToBreakingNews() {
425 * const subscription = pipe(
426 * client.subscription(getNewsSubscription, {}),
427 * subscribe(result => {
428 * if (result.data) {
429 * console.log(result.data.breakingNews.text);
430 * }
431 * })
432 * );
433 *
434 * return subscription.unsubscribe;
435 * }
436 * ```
437 */
438 subscription<Data = any, Variables extends AnyVariables = AnyVariables>(
439 query: DocumentInput<Data, Variables>,
440 variables: Variables,
441 context?: Partial<OperationContext>
442 ): OperationResultSource<OperationResult<Data, Variables>>;
443
444 /** Creates a `Source` that executes the GraphQL subscription operation for the passed `GraphQLRequest`.
445 *
446 * @param query - a {@link GraphQLRequest}
447 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
448 * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
449 *
450 * @remarks
451 * The `Client.executeSubscription` method is used to programmatically issue a GraphQL subscription operation.
452 * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you,
453 * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first.
454 *
455 * @see {@link Client.subscription} for a method that doesn't require calling {@link createRequest} yourself.
456 */
457 executeSubscription<
458 Data = any,
459 Variables extends AnyVariables = AnyVariables,
460 >(
461 query: GraphQLRequest<Data, Variables>,
462 opts?: Partial<OperationContext> | undefined
463 ): OperationResultSource<OperationResult<Data, Variables>>;
464
465 /** Creates a `Source` that executes the GraphQL mutation operation created from the passed parameters.
466 *
467 * @param query - a GraphQL document containing the mutation operation that will be executed.
468 * @param variables - the variables used to execute the operation.
469 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
470 * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
471 *
472 * @remarks
473 * The `Client.mutation` method is useful to programmatically create and issue a GraphQL mutation operation.
474 * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and
475 * {@link client.executeRequestOperation} for you, and is a convenience method.
476 *
477 * Since it returns a {@link PromisifiedSource} it may be chained with a `toPromise()` call to only
478 * await a single result in an async function. Since mutations will only typically issue one result,
479 * using this method is recommended.
480 *
481 * Hint: This is the recommended way to create mutations programmatically when not using the bindings,
482 * or when you’re trying to get a single, promisified result.
483 *
484 * @example
485 * ```ts
486 * const createPostMutation = gql`
487 * mutation CreatePost($text: String!) {
488 * createPost(text: $text) {
489 * id
490 * text
491 * }
492 * }
493 * `;
494 *
495 * async function createPost(text) {
496 * const result = await client.mutation(createPostMutation, {
497 * text,
498 * }).toPromise();
499 * if (result.error) {
500 * throw result.error;
501 * }
502 *
503 * return result.data.createPost;
504 * }
505 * ```
506 */
507 mutation<Data = any, Variables extends AnyVariables = AnyVariables>(
508 query: DocumentInput<Data, Variables>,
509 variables: Variables,
510 context?: Partial<OperationContext>
511 ): OperationResultSource<OperationResult<Data, Variables>>;
512
513 /** Creates a `Source` that executes the GraphQL mutation operation for the passed `GraphQLRequest`.
514 *
515 * @param query - a {@link GraphQLRequest}
516 * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}.
517 * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation.
518 *
519 * @remarks
520 * The `Client.executeMutation` method is used to programmatically issue a GraphQL mutation operation.
521 * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you,
522 * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first.
523 *
524 * @see {@link Client.mutation} for a method that doesn't require calling {@link createRequest} yourself.
525 */
526 executeMutation<Data = any, Variables extends AnyVariables = AnyVariables>(
527 query: GraphQLRequest<Data, Variables>,
528 opts?: Partial<OperationContext> | undefined
529 ): OperationResultSource<OperationResult<Data, Variables>>;
530}
531
532export const Client: new (opts: ClientOptions) => Client = function Client(
533 this: Client | {},
534 opts: ClientOptions
535) {
536 if (process.env.NODE_ENV !== 'production' && !opts.url) {
537 throw new Error('You are creating an urql-client without a url.');
538 }
539
540 let ids = 0;
541
542 const replays = new Map<number, OperationResult>();
543 const active: Map<number, Source<OperationResult>> = new Map();
544 const dispatched = new Set<number>();
545 const queue: Operation[] = [];
546
547 const baseOpts = {
548 url: opts.url,
549 fetchSubscriptions: opts.fetchSubscriptions,
550 fetchOptions: opts.fetchOptions,
551 fetch: opts.fetch,
552 preferGetMethod:
553 opts.preferGetMethod != null ? opts.preferGetMethod : 'within-url-limit',
554 requestPolicy: opts.requestPolicy || 'cache-first',
555 };
556
557 // This subject forms the input of operations; executeOperation may be
558 // called to dispatch a new operation on the subject
559 const operations = makeSubject<Operation>();
560
561 function nextOperation(operation: Operation) {
562 if (
563 operation.kind === 'mutation' ||
564 operation.kind === 'teardown' ||
565 !dispatched.has(operation.key)
566 ) {
567 if (operation.kind === 'teardown') {
568 dispatched.delete(operation.key);
569 } else if (operation.kind !== 'mutation') {
570 dispatched.add(operation.key);
571 }
572 operations.next(operation);
573 }
574 }
575
576 // We define a queued dispatcher on the subject, which empties the queue when it's
577 // activated to allow `reexecuteOperation` to be trampoline-scheduled
578 let isOperationBatchActive = false;
579 function dispatchOperation(operation?: Operation | void) {
580 if (operation) nextOperation(operation);
581
582 if (!isOperationBatchActive) {
583 isOperationBatchActive = true;
584 while (isOperationBatchActive && (operation = queue.shift()))
585 nextOperation(operation);
586 isOperationBatchActive = false;
587 }
588 }
589
590 /** Defines how result streams are created */
591 const makeResultSource = (operation: Operation) => {
592 let result$ = pipe(
593 results$,
594 // Filter by matching key (or _instance if it’s set)
595 filter(
596 (res: OperationResult) =>
597 res.operation.kind === operation.kind &&
598 res.operation.key === operation.key &&
599 (!res.operation.context._instance ||
600 res.operation.context._instance === operation.context._instance)
601 ),
602 // End the results stream when an active teardown event is sent
603 takeUntil(
604 pipe(
605 operations.source,
606 filter(op => op.kind === 'teardown' && op.key === operation.key)
607 )
608 )
609 );
610
611 if (operation.kind !== 'query') {
612 // Interrupt subscriptions and mutations when they have no more results
613 result$ = pipe(
614 result$,
615 takeWhile(result => !!result.hasNext, true)
616 );
617 } else {
618 result$ = pipe(
619 result$,
620 // Add `stale: true` flag when a new operation is sent for queries
621 switchMap(result => {
622 const value$ = fromValue(result);
623 return result.stale || result.hasNext
624 ? value$
625 : merge([
626 value$,
627 pipe(
628 operations.source,
629 filter(op => op.key === operation.key),
630 take(1),
631 map(() => {
632 result.stale = true;
633 return result;
634 })
635 ),
636 ]);
637 })
638 );
639 }
640
641 if (operation.kind !== 'mutation') {
642 result$ = pipe(
643 result$,
644 // Store replay result
645 onPush(result => {
646 if (result.stale) {
647 if (!result.hasNext) {
648 // we are dealing with an optimistic mutation or a partial result
649 dispatched.delete(operation.key);
650 } else {
651 // If the current result has queued up an operation of the same
652 // key, then `stale` refers to it
653 for (let i = 0; i < queue.length; i++) {
654 const operation = queue[i];
655 if (operation.key === result.operation.key) {
656 dispatched.delete(operation.key);
657 break;
658 }
659 }
660 }
661 } else if (!result.hasNext) {
662 dispatched.delete(operation.key);
663 }
664 replays.set(operation.key, result);
665 }),
666 // Cleanup active states on end of source
667 onEnd(() => {
668 // Delete the active operation handle
669 dispatched.delete(operation.key);
670 replays.delete(operation.key);
671 active.delete(operation.key);
672 // Interrupt active queue
673 isOperationBatchActive = false;
674 // Delete all queued up operations of the same key on end
675 for (let i = queue.length - 1; i >= 0; i--)
676 if (queue[i].key === operation.key) queue.splice(i, 1);
677 // Dispatch a teardown signal for the stopped operation
678 nextOperation(
679 makeOperation('teardown', operation, operation.context)
680 );
681 })
682 );
683 } else {
684 result$ = pipe(
685 result$,
686 // Send mutation operation on start
687 onStart(() => {
688 nextOperation(operation);
689 })
690 );
691 }
692
693 return share(result$);
694 };
695
696 const instance: Client =
697 this instanceof Client ? this : Object.create(Client.prototype);
698 const client: Client = Object.assign(instance, {
699 suspense: !!opts.suspense,
700 operations$: operations.source,
701
702 reexecuteOperation(operation: Operation) {
703 // Reexecute operation only if any subscribers are still subscribed to the
704 // operation's exchange results
705 if (operation.kind === 'teardown') {
706 dispatchOperation(operation);
707 } else if (operation.kind === 'mutation') {
708 queue.push(operation);
709 Promise.resolve().then(dispatchOperation);
710 } else if (active.has(operation.key)) {
711 let queued = false;
712 for (let i = 0; i < queue.length; i++) {
713 if (queue[i].key === operation.key) {
714 queue[i] = operation;
715 queued = true;
716 }
717 }
718
719 if (
720 !queued &&
721 (!dispatched.has(operation.key) ||
722 operation.context.requestPolicy === 'network-only')
723 ) {
724 queue.push(operation);
725 Promise.resolve().then(dispatchOperation);
726 } else {
727 dispatched.delete(operation.key);
728 Promise.resolve().then(dispatchOperation);
729 }
730 }
731 },
732
733 createRequestOperation(kind, request, opts) {
734 if (!opts) opts = {};
735
736 let requestOperationType: string | undefined;
737 if (
738 process.env.NODE_ENV !== 'production' &&
739 kind !== 'teardown' &&
740 (requestOperationType = getOperationType(request.query)) !== kind
741 ) {
742 throw new Error(
743 `Expected operation of type "${kind}" but found "${requestOperationType}"`
744 );
745 }
746
747 return makeOperation(kind, request, {
748 _instance:
749 kind === 'mutation'
750 ? ((ids = (ids + 1) | 0) as OperationInstance)
751 : undefined,
752 ...baseOpts,
753 ...opts,
754 requestPolicy: opts.requestPolicy || baseOpts.requestPolicy,
755 suspense: opts.suspense || (opts.suspense !== false && client.suspense),
756 });
757 },
758
759 executeRequestOperation(operation) {
760 if (operation.kind === 'mutation') {
761 return withPromise(makeResultSource(operation));
762 }
763
764 return withPromise(
765 lazy<OperationResult>(() => {
766 let source = active.get(operation.key);
767 if (!source) {
768 active.set(operation.key, (source = makeResultSource(operation)));
769 }
770
771 source = pipe(
772 source,
773 onStart(() => {
774 dispatchOperation(operation);
775 })
776 );
777
778 const replay = replays.get(operation.key);
779 if (
780 operation.kind === 'query' &&
781 replay &&
782 (replay.stale || replay.hasNext)
783 ) {
784 return pipe(
785 merge([
786 source,
787 pipe(
788 fromValue(replay),
789 filter(replay => replay === replays.get(operation.key))
790 ),
791 ]),
792 switchMap(fromValue)
793 );
794 } else {
795 return source;
796 }
797 })
798 );
799 },
800
801 executeQuery(query, opts) {
802 const operation = client.createRequestOperation('query', query, opts);
803 return client.executeRequestOperation(operation);
804 },
805
806 executeSubscription(query, opts) {
807 const operation = client.createRequestOperation(
808 'subscription',
809 query,
810 opts
811 );
812 return client.executeRequestOperation(operation);
813 },
814
815 executeMutation(query, opts) {
816 const operation = client.createRequestOperation('mutation', query, opts);
817 return client.executeRequestOperation(operation);
818 },
819
820 readQuery(query, variables, context) {
821 let result: OperationResult | null = null;
822
823 pipe(
824 client.query(query, variables, context),
825 subscribe(res => {
826 result = res;
827 })
828 ).unsubscribe();
829
830 return result;
831 },
832
833 query(query, variables, context) {
834 return client.executeQuery(createRequest(query, variables), context);
835 },
836
837 subscription(query, variables, context) {
838 return client.executeSubscription(
839 createRequest(query, variables),
840 context
841 );
842 },
843
844 mutation(query, variables, context) {
845 return client.executeMutation(createRequest(query, variables), context);
846 },
847 } as Client);
848
849 let dispatchDebug: ExchangeInput['dispatchDebug'] = noop;
850 if (process.env.NODE_ENV !== 'production') {
851 const { next, source } = makeSubject<DebugEvent>();
852 client.subscribeToDebugTarget = (onEvent: (e: DebugEvent) => void) =>
853 pipe(source, subscribe(onEvent));
854 dispatchDebug = next as ExchangeInput['dispatchDebug'];
855 }
856
857 // All exchange are composed into a single one and are called using the constructed client
858 // and the fallback exchange stream
859 const composedExchange = composeExchanges(opts.exchanges);
860
861 // All exchanges receive inputs using which they can forward operations to the next exchange
862 // and receive a stream of results in return, access the client, or dispatch debugging events
863 // All operations then run through the Exchange IOs in a pipeline-like fashion
864 const results$ = share(
865 composedExchange({
866 client,
867 dispatchDebug,
868 forward: fallbackExchange({ dispatchDebug }),
869 })(operations.source)
870 );
871
872 // Prevent the `results$` exchange pipeline from being closed by active
873 // cancellations cascading up from components
874 pipe(results$, publish);
875
876 return client;
877} as any;
878
879/** Accepts `ClientOptions` and creates a `Client`.
880 * @param opts - A {@link ClientOptions} objects with options for the `Client`.
881 * @returns A {@link Client} instantiated with `opts`.
882 */
883export const createClient = Client as any as (opts: ClientOptions) => Client;