omerman-apollo-link-debounce
Version:
An Apollo Link to debounce requests
159 lines (144 loc) • 4.91 kB
text/typescript
import {
ApolloLink,
Operation,
Observable,
NextLink,
} from '@apollo/client';
import {
ExecutionResult,
} from 'graphql';
import {
ExecutionResultDataDefault,
} from 'graphql/execution/execute';
export interface ObservableValue {
value?: ExecutionResult | Error;
delay?: number;
type: 'next' | 'error' | 'complete';
}
export interface Unsubscribable {
unsubscribe: () => void;
}
export interface NextEvent {
type: 'next';
delay?: number;
value: ExecutionResult;
}
export interface ErrorEvent {
type: 'error';
delay?: number;
value: Error;
}
export interface CompleteEvent {
type: 'complete';
delay?: number;
}
export type ObservableEvent = NextEvent | ErrorEvent | CompleteEvent;
export class TestLink extends ApolloLink {
public operations: Operation[];
constructor() {
super();
this.operations = [];
}
public request (operation: Operation) {
this.operations.push(operation);
// TODO(helfer): Throw an error if neither testError nor testResponse is defined
return new Observable(observer => {
if (operation.getContext().testError) {
setTimeout(() => observer.error(operation.getContext().testError), 0);
return;
}
setTimeout(() => observer.next(operation.getContext().testResponse), 0);
setTimeout(() => observer.complete(), 0);
});
}
}
export class TestSequenceLink extends ApolloLink {
public operations: Operation[];
constructor() {
super();
this.operations = [];
}
public request (operation: Operation, forward: NextLink) {
if (!operation.getContext().testSequence) {
return forward(operation);
}
this.operations.push(operation);
// TODO(helfer): Throw an error if neither testError nor testResponse is defined
return new Observable(observer => {
operation.getContext().testSequence.forEach((event: ObservableEvent) => {
if (event.type === 'error') {
setTimeout(() => observer.error(event.value), event.delay || 0);
return;
}
if (event.type === 'next') {
setTimeout(() => observer.next(event.value), event.delay || 0);
}
if (event.type === 'complete') {
setTimeout(() => observer.complete(), event.delay || 0);
}
});
});
}
}
export function mergeObservables(...observables: Observable<ExecutionResult>[]) {
return new Observable(observer => {
const numObservables = observables.length;
let completedObservables = 0;
observables.forEach(o => {
o.subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: () => {
completedObservables++;
if (completedObservables === numObservables) {
observer.complete();
}
},
});
});
// TODO(helfer): unsubscribe
});
}
export function toResultValue(e: ObservableEvent): ObservableEvent {
const obj = { ...e };
delete obj.delay;
return obj;
}
export const assertObservableSequence = (
observable: Observable<ExecutionResult>,
sequence: ObservableValue[],
initializer: (sub: Unsubscribable) => void = () => undefined,
): Promise<boolean | Error> => {
let index = 0;
if (sequence.length === 0) {
throw new Error('Observable sequence must have at least one element');
}
return new Promise((resolve, reject) => {
const sub = observable.subscribe({
next: (value) => {
expect({ type: 'next', value }).toEqual(sequence[index]);
index++;
if (index === sequence.length) {
resolve(true);
}
},
error: (value) => {
expect({ type: 'error', value }).toEqual(sequence[index]);
index++;
// This check makes sure that there is no next element in
// the sequence. If there is, it will print a somewhat useful error
expect(undefined).toEqual(sequence[index]);
resolve(true);
},
complete: () => {
expect({ type: 'complete' }).toEqual(sequence[index]);
index++;
// This check makes sure that there is no next element in
// the sequence. If there is, it will print a somewhat useful error
expect(undefined).toEqual(sequence[index]);
resolve(true);
},
});
initializer(sub);
});
};