From 3baedd36a22fa751a60e77996e0b1c5406c2d286 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 16 Mar 2023 14:12:07 +0000 Subject: [PATCH 1/4] feat(core): Provide OperationResultSource from Client methods --- .changeset/curly-bees-rhyme.md | 5 ++ packages/core/src/client.ts | 101 ++++++++++++------------- packages/core/src/types.ts | 22 ++++-- packages/core/src/utils/streamUtils.ts | 18 +++-- 4 files changed, 80 insertions(+), 66 deletions(-) create mode 100644 .changeset/curly-bees-rhyme.md diff --git a/.changeset/curly-bees-rhyme.md b/.changeset/curly-bees-rhyme.md new file mode 100644 index 0000000000..c17d804464 --- /dev/null +++ b/.changeset/curly-bees-rhyme.md @@ -0,0 +1,5 @@ +--- +'@urql/core': minor +--- + +Return a new `OperationResultSource` from all `Client` methods (which replaces `PromisifiedSource` on shortcut methods). This allows not only `toPromise()` to be called, but it can also be used as an awaitable `PromiseLike` and has a `.subscribe(onResult)` method aliasing the subscribe utility from `wonka`. diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index bb83a5dba8..0dc27434bf 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -37,9 +37,9 @@ import { OperationInstance, OperationContext, OperationResult, + OperationResultSource, OperationType, RequestPolicy, - PromisifiedSource, DebugEvent, } from './types'; @@ -313,21 +313,21 @@ export interface Client { Variables extends AnyVariables = AnyVariables >( operation: Operation - ): Source>; + ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL query operation created from the passed parameters. * * @param query - a GraphQL document containing the query operation that will be executed. * @param variables - the variables used to execute the operation. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. - * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. + * @returns A {@link OperationResultSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.query` method is useful to programmatically create and issue a GraphQL query operation. * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and * {@link client.executeRequestOperation} for you, and is a convenience method. * - * Since it returns a {@link PromisifiedSource} it may be chained with a `toPromise()` call to only + * Since it returns a {@link OperationResultSource} it may be chained with a `toPromise()` call to only * await a single result in an async function. * * Hint: This is the recommended way to create queries programmatically when not using the bindings, @@ -361,7 +361,7 @@ export interface Client { query: DocumentNode | TypedDocumentNode | string, variables: Variables, context?: Partial - ): PromisifiedSource>; + ): OperationResultSource>; /** Returns the first synchronous result a `Client` provides for a given operation. * @@ -405,7 +405,7 @@ export interface Client { executeQuery( query: GraphQLRequest, opts?: Partial | undefined - ): Source>; + ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL subscription operation created from the passed parameters. * @@ -453,7 +453,7 @@ export interface Client { query: DocumentNode | TypedDocumentNode | string, variables: Variables, context?: Partial - ): Source>; + ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL subscription operation for the passed `GraphQLRequest`. * @@ -474,7 +474,7 @@ export interface Client { >( query: GraphQLRequest, opts?: Partial | undefined - ): Source>; + ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL mutation operation created from the passed parameters. * @@ -522,7 +522,7 @@ export interface Client { query: DocumentNode | TypedDocumentNode | string, variables: Variables, context?: Partial - ): PromisifiedSource>; + ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL mutation operation for the passed `GraphQLRequest`. * @@ -540,7 +540,7 @@ export interface Client { executeMutation( query: GraphQLRequest, opts?: Partial | undefined - ): Source>; + ): OperationResultSource>; } export const Client: new (opts: ClientOptions) => Client = function Client( @@ -721,45 +721,47 @@ export const Client: new (opts: ClientOptions) => Client = function Client( executeRequestOperation(operation) { if (operation.kind === 'mutation') { - return makeResultSource(operation); + return withPromise(makeResultSource(operation)); } - return make(observer => { - let source = active.get(operation.key); - if (!source) { - active.set(operation.key, (source = makeResultSource(operation))); - } - - return pipe( - source, - onStart(() => { - const prevReplay = replays.get(operation.key); - const isNetworkOperation = - operation.context.requestPolicy === 'cache-and-network' || - operation.context.requestPolicy === 'network-only'; - if (operation.kind !== 'query') { - return; - } else if (isNetworkOperation) { - dispatchOperation(operation); - if (prevReplay && !prevReplay.hasNext) prevReplay.stale = true; - } - - if ( - prevReplay != null && - prevReplay === replays.get(operation.key) - ) { - observer.next(prevReplay); - } else if (!isNetworkOperation) { - dispatchOperation(operation); - } - }), - onEnd(() => { - isOperationBatchActive = false; - observer.complete(); - }), - subscribe(observer.next) - ).unsubscribe; - }); + return withPromise( + make(observer => { + let source = active.get(operation.key); + if (!source) { + active.set(operation.key, (source = makeResultSource(operation))); + } + + return pipe( + source, + onStart(() => { + const prevReplay = replays.get(operation.key); + const isNetworkOperation = + operation.context.requestPolicy === 'cache-and-network' || + operation.context.requestPolicy === 'network-only'; + if (operation.kind !== 'query') { + return; + } else if (isNetworkOperation) { + dispatchOperation(operation); + if (prevReplay && !prevReplay.hasNext) prevReplay.stale = true; + } + + if ( + prevReplay != null && + prevReplay === replays.get(operation.key) + ) { + observer.next(prevReplay); + } else if (!isNetworkOperation) { + dispatchOperation(operation); + } + }), + onEnd(() => { + isOperationBatchActive = false; + observer.complete(); + }), + subscribe(observer.next) + ).unsubscribe; + }) + ); }, executeQuery(query, opts) { @@ -785,10 +787,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( if (!context || typeof context.suspense !== 'boolean') { context = { ...context, suspense: false }; } - - return withPromise( - client.executeQuery(createRequest(query, variables), context) - ); + return client.executeQuery(createRequest(query, variables), context); }, readQuery(query, variables, context) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a1e6bcf464..5eaa7604be 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,5 @@ import type { GraphQLError, DocumentNode } from 'graphql'; -import { Source } from 'wonka'; +import { Subscription, Source } from 'wonka'; import { Client } from './client'; import { CombinedError } from './utils/error'; @@ -127,16 +127,22 @@ export interface ExecutionResult { hasNext?: boolean; } -/** A `Source` with a `PromisifiedSource.toPromise` helper method, to promisify a single result. +/** A source of {@link OperationResult | OperationResults}, convertable to a promise, subscribable, or Wonka Source. * * @remarks - * The {@link Client} will often return a `PromisifiedSource` to provide the `toPromise` method. When called, this returns - * a promise of the source that resolves on the first {@link OperationResult} of the `Source` that doesn't have `stale: true` - * nor `hasNext: true` set, meaning, it'll resolve to the first result that is stable and complete. + * The {@link Client} will often return a `OperationResultSource` to provide a more flexible Wonka {@link Source}. + * + * While a {@link Source} may require you to import helpers to convert it to a `Promise` for a single result, or + * to subscribe to it, the `OperationResultSource` is a `PromiseLike` and has methods to convert it to a promise, + * or to subscribe to it with a single method call. */ -export type PromisifiedSource = Source & { - toPromise: () => Promise; -}; +export type OperationResultSource = Source & + PromiseLike & { + /** Returns the first non-stale, settled results of the source. */ + toPromise(): Promise; + /** Alias for Wonka's `subscribe` and calls `onResult` when subscribed to for each new `OperationResult`. */ + subscribe(onResult: (value: T) => void): Subscription; + }; /** A type of Operation, either a GraphQL `query`, `mutation`, or `subscription`; or a `teardown` signal. * diff --git a/packages/core/src/utils/streamUtils.ts b/packages/core/src/utils/streamUtils.ts index fb8476d2d7..9a71cd8e04 100644 --- a/packages/core/src/utils/streamUtils.ts +++ b/packages/core/src/utils/streamUtils.ts @@ -1,5 +1,5 @@ -import { Source, take, filter, toPromise, pipe } from 'wonka'; -import { OperationResult, PromisifiedSource } from '../types'; +import { Sink, Source, subscribe, take, filter, toPromise, pipe } from 'wonka'; +import { OperationResult, OperationResultSource } from '../types'; /** Patches a `toPromise` method onto the `Source` passed to it. * @param source$ - the Wonka {@link Source} to patch. @@ -7,15 +7,19 @@ import { OperationResult, PromisifiedSource } from '../types'; * @internal */ export function withPromise( - source$: Source -): PromisifiedSource { - (source$ as PromisifiedSource).toPromise = () => + _source$: Source +): OperationResultSource { + const source$ = ((sink: Sink) => + _source$(sink)) as OperationResultSource; + source$.toPromise = () => pipe( source$, filter(result => !result.stale && !result.hasNext), take(1), toPromise ); - - return source$ as PromisifiedSource; + source$.then = (onResolve, onReject) => + source$.toPromise().then(onResolve, onReject); + source$.subscribe = onResult => subscribe(onResult)(source$); + return source$; } From cfdac552f6edcbc572db4208314bea877f0b0218 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 16 Mar 2023 01:48:13 +0000 Subject: [PATCH 2/4] Add TSDoc clarification that toPromise gives no updates --- packages/core/src/types.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5eaa7604be..7a4b407aef 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -138,7 +138,15 @@ export interface ExecutionResult { */ export type OperationResultSource = Source & PromiseLike & { - /** Returns the first non-stale, settled results of the source. */ + /** Returns the first non-stale, settled results of the source. + * @remarks + * The `toPromise` method gives you the first result of an `OperationResultSource` + * that has `hasNext: false` and `stale: false` set as a `Promise`. + * + * Hint: If you're trying to get updates for your results, this won't work. + * This gives you only a single, promisified result, so it won't receive + * cache or other updates. + */ toPromise(): Promise; /** Alias for Wonka's `subscribe` and calls `onResult` when subscribed to for each new `OperationResult`. */ subscribe(onResult: (value: T) => void): Subscription; From 5314a374b5fb4d8456d889a388702d46f761939e Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 16 Mar 2023 01:51:17 +0000 Subject: [PATCH 3/4] Update tests --- packages/vue-urql/src/useMutation.test.ts | 5 ++++- packages/vue-urql/src/useQuery.test.ts | 9 +++++++-- packages/vue-urql/src/useSubscription.test.ts | 13 ++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/vue-urql/src/useMutation.test.ts b/packages/vue-urql/src/useMutation.test.ts index f7d29b2f52..991bf7fdec 100644 --- a/packages/vue-urql/src/useMutation.test.ts +++ b/packages/vue-urql/src/useMutation.test.ts @@ -1,3 +1,4 @@ +import { OperationResult, OperationResultSource } from '@urql/core'; import { reactive } from 'vue'; import { vi, expect, it, beforeEach, describe } from 'vitest'; @@ -25,7 +26,9 @@ describe('useMutation', () => { const subject = makeSubject(); const clientMutation = vi .spyOn(client, 'executeMutation') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const mutation = reactive( useMutation( diff --git a/packages/vue-urql/src/useQuery.test.ts b/packages/vue-urql/src/useQuery.test.ts index f3a878ebf9..6ac721cccf 100644 --- a/packages/vue-urql/src/useQuery.test.ts +++ b/packages/vue-urql/src/useQuery.test.ts @@ -1,3 +1,4 @@ +import { OperationResult, OperationResultSource } from '@urql/core'; import { nextTick, reactive, ref } from 'vue'; import { vi, expect, it, describe } from 'vitest'; @@ -18,7 +19,9 @@ describe('useQuery', () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeQuery') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const _query = useQuery({ query: `{ test }`, @@ -109,7 +112,9 @@ describe('useQuery', () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeQuery') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const _query = useQuery({ query: `{ test }`, diff --git a/packages/vue-urql/src/useSubscription.test.ts b/packages/vue-urql/src/useSubscription.test.ts index 3c8e3b31f6..3ab0e57cb5 100644 --- a/packages/vue-urql/src/useSubscription.test.ts +++ b/packages/vue-urql/src/useSubscription.test.ts @@ -1,3 +1,4 @@ +import { OperationResult, OperationResultSource } from '@urql/core'; import { nextTick, reactive, ref } from 'vue'; import { vi, expect, it, describe } from 'vitest'; @@ -18,7 +19,9 @@ describe('useSubscription', () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeSubscription') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const sub = reactive( useSubscription({ @@ -60,7 +63,9 @@ describe('useSubscription', () => { const subject = makeSubject(); const executeSubscription = vi .spyOn(client, 'executeSubscription') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const variables = ref({}); const sub = reactive( @@ -101,7 +106,9 @@ describe('useSubscription', () => { const subject = makeSubject(); const executeSubscription = vi .spyOn(client, 'executeSubscription') - .mockImplementation(() => subject.source); + .mockImplementation( + () => subject.source as OperationResultSource + ); const scanHandler = (currentState: any, nextState: any) => ({ counter: (currentState ? currentState.counter : 0) + nextState.counter, From 7c62e931f28a1c4d8db8d56469e18aafa81f3806 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 16 Mar 2023 04:22:36 +0000 Subject: [PATCH 4/4] Remove redundant withPromise from client.mutation --- packages/core/src/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0dc27434bf..86b317304f 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -811,9 +811,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( }, mutation(query, variables, context) { - return withPromise( - client.executeMutation(createRequest(query, variables), context) - ); + return client.executeMutation(createRequest(query, variables), context); }, } as Client);