Skip to content

Commit

Permalink
feat: added abort controller support to useQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Mar 7, 2021
1 parent 61a88af commit fb4ceed
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 8 deletions.
7 changes: 4 additions & 3 deletions packages/villus/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ export class Client {
private async execute<TData, TVars>(
operation: Operation<TData, TVars> | OperationWithCachePolicy<TData, TVars>,
type: OperationType,
queryContext?: QueryExecutionContext
queryContext?: Partial<QueryExecutionContext>
): Promise<OperationResult<TData>> {
let result: OperationResult<TData> | undefined;
const opContext: FetchOptions = {
url: this.url,
...DEFAULT_FETCH_OPTS,
headers: { ...DEFAULT_FETCH_OPTS.headers, ...(queryContext?.headers || {}) },
signal: queryContext?.signal,
};

let terminateSignal = false;
Expand Down Expand Up @@ -122,14 +123,14 @@ export class Client {

public async executeQuery<TData = any, TVars = QueryVariables>(
operation: OperationWithCachePolicy<TData, TVars>,
queryContext?: QueryExecutionContext
queryContext?: Partial<QueryExecutionContext>
): Promise<OperationResult> {
return this.execute<TData, TVars>(operation, 'query', queryContext);
}

public async executeMutation<TData = any, TVars = QueryVariables>(
operation: Operation<TData, TVars>,
queryContext?: QueryExecutionContext
queryContext?: Partial<QueryExecutionContext>
): Promise<OperationResult> {
return this.execute<TData, TVars>(operation, 'mutation', queryContext);
}
Expand Down
13 changes: 12 additions & 1 deletion packages/villus/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLError } from 'graphql';
import { ClientPlugin } from './types';
import { makeFetchOptions, resolveGlobalFetch, parseResponse } from '../../shared/src';
import { CombinedError } from './utils';
import { CombinedError, isAbortError } from './utils';

interface FetchPluginOpts {
fetch?: typeof window['fetch'];
Expand All @@ -21,6 +21,17 @@ export function fetch(opts?: FetchPluginOpts): ClientPlugin {
try {
response = await fetch(opContext.url as string, fetchOpts).then(parseResponse);
} catch (err) {
if (isAbortError(err)) {
return useResult(
{
data: null,
error: null,
aborted: true,
},
true
);
}

return useResult(
{
data: null,
Expand Down
2 changes: 2 additions & 0 deletions packages/villus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ExecutionResult } from 'graphql';
export interface OperationResult<TData = any> {
data: TData | null;
error: CombinedError | null;
aborted?: boolean;
}

export type CachePolicy = 'cache-and-network' | 'network-only' | 'cache-first' | 'cache-only';
Expand Down Expand Up @@ -49,6 +50,7 @@ export type ClientPluginOperation = OperationWithCachePolicy<unknown, QueryVaria

export interface QueryExecutionContext {
headers: Record<string, string>;
signal: AbortSignal;
}

export interface ClientPluginContext {
Expand Down
38 changes: 34 additions & 4 deletions packages/villus/src/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isReactive, isRef, onMounted, Ref, ref, unref, watch } from 'vue-demi';
import { isReactive, isRef, onMounted, Ref, ref, shallowRef, unref, watch } from 'vue-demi';
import stringify from 'fast-json-stable-stringify';
import { CachePolicy, MaybeReactive, OperationResult, QueryExecutionContext, QueryVariables } from './types';
import { hash, CombinedError, toWatchableSource, injectWithSelf } from './utils';
import { hash, CombinedError, toWatchableSource, injectWithSelf, createAbortController } from './utils';
import { VILLUS_CLIENT } from './symbols';
import { Operation } from '../../shared/src';

Expand Down Expand Up @@ -29,6 +29,7 @@ export interface BaseQueryApi<TData = any, TVars = QueryVariables> {
unwatchVariables(): void;
watchVariables(): void;
isWatchingVariables: Ref<boolean>;
abort(): void;
}

export interface QueryApi<TData, TVars> extends BaseQueryApi<TData, TVars> {
Expand All @@ -47,19 +48,32 @@ function useQuery<TData = any, TVars = QueryVariables>(
const isFetching = ref<boolean>(fetchOnMount ?? false);
const isDone = ref(false);
const error: Ref<CombinedError | null> = ref(null);
const abortController = shallowRef<AbortController | undefined>();
function abort() {
abortController.value?.abort();
}

// This is to prevent state mutation for racing requests, basically favoring the very last one
let lastPendingOperation: Promise<OperationResult<TData>> | undefined;

async function execute(overrideOpts?: Partial<QueryExecutionOpts<TVars>>) {
isFetching.value = true;
const vars = (isRef(variables) ? variables.value : variables) || {};
if (lastPendingOperation) {
abort();
}

abortController.value = createAbortController();
const pendingExecution = client.executeQuery<TData, TVars>(
{
query: isRef(query) ? query.value : query,
variables: overrideOpts?.variables || (vars as TVars), // FIXME: Try to avoid casting
cachePolicy: overrideOpts?.cachePolicy || cachePolicy,
},
unref(opts?.context)
{
signal: abortController.value?.signal,
...unref(opts?.context || {}),
}
);

lastPendingOperation = pendingExecution;
Expand All @@ -70,11 +84,17 @@ function useQuery<TData = any, TVars = QueryVariables>(
return { data: res.data as TData, error: res.error };
}

if (res.aborted) {
isFetching.value = false;
return res;
}

data.value = res.data as TData;
error.value = res.error;
isDone.value = true;
isFetching.value = false;
lastPendingOperation = undefined;
abortController.value = undefined;

return { data: data.value, error: error.value };
}
Expand Down Expand Up @@ -125,7 +145,17 @@ function useQuery<TData = any, TVars = QueryVariables>(

beginWatchingVars();

const api = { data, isFetching, isDone, error, execute, unwatchVariables, watchVariables, isWatchingVariables };
const api = {
data,
isFetching,
isDone,
error,
execute,
unwatchVariables,
watchVariables,
isWatchingVariables,
abort,
};

onMounted(() => {
if (fetchOnMount) {
Expand Down
15 changes: 15 additions & 0 deletions packages/villus/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ export function injectWithSelf<T>(symbol: InjectionKey<T>, onMissing: () => Erro

return injection;
}

/**
* Creates an abort controller while being aware of the execution environment
*/
export function createAbortController(): AbortController | undefined {
if (typeof window !== 'undefined' && 'AbortController' in window && window.AbortController) {
return new window.AbortController();
}

if (typeof global !== 'undefined' && 'AbortController' in global && global.AbortController) {
return new global.AbortController();
}

return undefined;
}
4 changes: 4 additions & 0 deletions packages/villus/src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ export class CombinedError extends Error {
return this.message;
}
}

export function isAbortError(err: Error) {
return err.name === 'AbortError';
}

0 comments on commit fb4ceed

Please sign in to comment.