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