Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

feat(core): Provide OperationResultSource from Client methods (#3060)

Changed files
+108 -73
.changeset
packages
+5
.changeset/curly-bees-rhyme.md
···
+
---
+
'@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`.
+49 -52
packages/core/src/client.ts
···
OperationInstance,
OperationContext,
OperationResult,
+
OperationResultSource,
OperationType,
RequestPolicy,
-
PromisifiedSource,
DebugEvent,
} from './types';
···
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,
···
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.
*
···
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.
*
···
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`.
*
···
>(
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.
*
···
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`.
*
···
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(
···
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 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;
-
}
+
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;
-
});
+
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) {
···
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) {
···
},
mutation(query, variables, context) {
-
return withPromise(
-
client.executeMutation(createRequest(query, variables), context)
-
);
+
return client.executeMutation(createRequest(query, variables), context);
},
} as Client);
+22 -8
packages/core/src/types.ts
···
import type { GraphQLError, DocumentNode } from 'graphql';
-
import { Source } from 'wonka';
+
import { Subscription, Source } from 'wonka';
import { Client } from './client';
import { CombinedError } from './utils/error';
···
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.
*
+11 -7
packages/core/src/utils/streamUtils.ts
···
-
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.
···
* @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$;
}
+4 -1
packages/vue-urql/src/useMutation.test.ts
···
+
import { OperationResult, OperationResultSource } from '@urql/core';
import { reactive } from 'vue';
import { vi, expect, it, beforeEach, describe } from 'vitest';
···
const subject = makeSubject<any>();
const clientMutation = vi
.spyOn(client, 'executeMutation')
-
.mockImplementation(() => subject.source);
+
.mockImplementation(
+
() => subject.source as OperationResultSource<OperationResult>
+
);
const mutation = reactive(
useMutation(
+7 -2
packages/vue-urql/src/useQuery.test.ts
···
+
import { OperationResult, OperationResultSource } from '@urql/core';
import { nextTick, reactive, ref } from 'vue';
import { vi, expect, it, describe } from 'vitest';
···
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeQuery')
-
.mockImplementation(() => subject.source);
+
.mockImplementation(
+
() => subject.source as OperationResultSource<OperationResult>
+
);
const _query = useQuery({
query: `{ test }`,
···
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeQuery')
-
.mockImplementation(() => subject.source);
+
.mockImplementation(
+
() => subject.source as OperationResultSource<OperationResult>
+
);
const _query = useQuery({
query: `{ test }`,
+10 -3
packages/vue-urql/src/useSubscription.test.ts
···
+
import { OperationResult, OperationResultSource } from '@urql/core';
import { nextTick, reactive, ref } from 'vue';
import { vi, expect, it, describe } from 'vitest';
···
const subject = makeSubject<any>();
const executeQuery = vi
.spyOn(client, 'executeSubscription')
-
.mockImplementation(() => subject.source);
+
.mockImplementation(
+
() => subject.source as OperationResultSource<OperationResult>
+
);
const sub = reactive(
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(
···
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,