Skip to content

Commit

Permalink
feat(core): Provide OperationResultSource from Client methods (#3060)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Mar 16, 2023
1 parent 2fc9842 commit c2c1328
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-bees-rhyme.md
Original file line number Diff line number Diff line change
@@ -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`.
105 changes: 51 additions & 54 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import {
OperationInstance,
OperationContext,
OperationResult,
OperationResultSource,
OperationType,
RequestPolicy,
PromisifiedSource,
DebugEvent,
} from './types';

Expand Down Expand Up @@ -313,21 +313,21 @@ export interface Client {
Variables extends AnyVariables = AnyVariables
>(
operation: Operation<Data, Variables>
): Source<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** 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,
Expand Down Expand Up @@ -361,7 +361,7 @@ export interface Client {
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context?: Partial<OperationContext>
): PromisifiedSource<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** Returns the first synchronous result a `Client` provides for a given operation.
*
Expand Down Expand Up @@ -405,7 +405,7 @@ export interface Client {
executeQuery<Data = any, Variables extends AnyVariables = AnyVariables>(
query: GraphQLRequest<Data, Variables>,
opts?: Partial<OperationContext> | undefined
): Source<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** Creates a `Source` that executes the GraphQL subscription operation created from the passed parameters.
*
Expand Down Expand Up @@ -453,7 +453,7 @@ export interface Client {
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context?: Partial<OperationContext>
): Source<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** Creates a `Source` that executes the GraphQL subscription operation for the passed `GraphQLRequest`.
*
Expand All @@ -474,7 +474,7 @@ export interface Client {
>(
query: GraphQLRequest<Data, Variables>,
opts?: Partial<OperationContext> | undefined
): Source<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** Creates a `Source` that executes the GraphQL mutation operation created from the passed parameters.
*
Expand Down Expand Up @@ -522,7 +522,7 @@ export interface Client {
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context?: Partial<OperationContext>
): PromisifiedSource<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;

/** Creates a `Source` that executes the GraphQL mutation operation for the passed `GraphQLRequest`.
*
Expand All @@ -540,7 +540,7 @@ export interface Client {
executeMutation<Data = any, Variables extends AnyVariables = AnyVariables>(
query: GraphQLRequest<Data, Variables>,
opts?: Partial<OperationContext> | undefined
): Source<OperationResult<Data, Variables>>;
): OperationResultSource<OperationResult<Data, Variables>>;
}

export const Client: new (opts: ClientOptions) => Client = function Client(
Expand Down Expand Up @@ -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<OperationResult>(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<OperationResult>(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) {
Expand All @@ -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) {
Expand All @@ -812,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);

Expand Down
30 changes: 22 additions & 8 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -127,16 +127,30 @@ 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<T = any> = Source<T> & {
toPromise: () => Promise<T>;
};
export type OperationResultSource<T extends OperationResult> = Source<T> &
PromiseLike<T> & {
/** 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<T>;
/** 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.
*
Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/utils/streamUtils.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
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.
* @returns The passed `source$` with a patched `toPromise` method as a {@link PromisifiedSource}.
* @internal
*/
export function withPromise<T extends OperationResult>(
source$: Source<T>
): PromisifiedSource<T> {
(source$ as PromisifiedSource<T>).toPromise = () =>
_source$: Source<T>
): OperationResultSource<T> {
const source$ = ((sink: Sink<T>) =>
_source$(sink)) as OperationResultSource<T>;
source$.toPromise = () =>
pipe(
source$,
filter(result => !result.stale && !result.hasNext),
take(1),
toPromise
);

return source$ as PromisifiedSource<T>;
source$.then = (onResolve, onReject) =>
source$.toPromise().then(onResolve, onReject);
source$.subscribe = onResult => subscribe(onResult)(source$);
return source$;
}
5 changes: 4 additions & 1 deletion packages/vue-urql/src/useMutation.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OperationResult, OperationResultSource } from '@urql/core';
import { reactive } from 'vue';
import { vi, expect, it, beforeEach, describe } from 'vitest';

Expand Down Expand Up @@ -25,7 +26,9 @@ describe('useMutation', () => {
const subject = makeSubject<any>();
const clientMutation = vi
.spyOn(client, 'executeMutation')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const mutation = reactive(
useMutation(
Expand Down
9 changes: 7 additions & 2 deletions packages/vue-urql/src/useQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OperationResult, OperationResultSource } from '@urql/core';
import { nextTick, reactive, ref } from 'vue';
import { vi, expect, it, describe } from 'vitest';

Expand All @@ -18,7 +19,9 @@ describe('useQuery', () => {
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeQuery')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const _query = useQuery({
query: `{ test }`,
Expand Down Expand Up @@ -109,7 +112,9 @@ describe('useQuery', () => {
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeQuery')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const _query = useQuery({
query: `{ test }`,
Expand Down
13 changes: 10 additions & 3 deletions packages/vue-urql/src/useSubscription.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OperationResult, OperationResultSource } from '@urql/core';
import { nextTick, reactive, ref } from 'vue';
import { vi, expect, it, describe } from 'vitest';

Expand All @@ -18,7 +19,9 @@ describe('useSubscription', () => {
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeSubscription')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const sub = reactive(
useSubscription({
Expand Down Expand Up @@ -60,7 +63,9 @@ describe('useSubscription', () => {
const subject = makeSubject<any>();
const executeSubscription = vi
.spyOn(client, 'executeSubscription')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const variables = ref({});
const sub = reactive(
Expand Down Expand Up @@ -101,7 +106,9 @@ describe('useSubscription', () => {
const subject = makeSubject<any>();
const executeSubscription = vi
.spyOn(client, 'executeSubscription')
.mockImplementation(() => subject.source);
.mockImplementation(
() => subject.source as OperationResultSource<OperationResult>
);

const scanHandler = (currentState: any, nextState: any) => ({
counter: (currentState ? currentState.counter : 0) + nextState.counter,
Expand Down

0 comments on commit c2c1328

Please sign in to comment.