omerman-apollo-link-debounce
Version:
An Apollo Link to debounce requests
415 lines (367 loc) • 14.2 kB
text/typescript
import DebounceLink from './DebounceLink';
import {
ObservableEvent,
TestSequenceLink,
toResultValue,
assertObservableSequence,
} from './TestUtils';
import {
gql,
execute,
GraphQLRequest,
ApolloLink,
} from '@apollo/client';
import {
ExecutionResult,
} from 'graphql';
describe('DebounceLink', () => {
let link: ApolloLink;
let testLink: TestSequenceLink;
let debounceLink: DebounceLink;
const DEBOUNCE_TIMEOUT = 500;
function makeSimpleResponse(value: string): ExecutionResult {
return {
data: {
hello: value,
},
};
}
const testResponse = makeSimpleResponse('world');
function makeSimpleSequence(response: ExecutionResult): ObservableEvent[] {
return [
{
type: 'next',
value: response,
},
{
type: 'complete',
},
];
}
function makeSimpleOp(sequence: ObservableEvent[], debounceKey: string, debounceTimeout: number): GraphQLRequest {
return {
query: gql`{ hello }`,
context: {
debounceKey,
debounceTimeout,
testSequence: sequence,
},
};
}
function getTestSubscriber(observedSequence: ObservableEvent[]) {
return {
next(value: ExecutionResult) {
observedSequence.push({
type: 'next',
value,
});
},
error(e: Error) {
observedSequence.push({
type: 'error',
value: e,
});
},
complete() {
observedSequence.push({ type: 'complete' });
},
};
}
const testSequence = makeSimpleSequence(testResponse);
const op = makeSimpleOp(
testSequence,
'key1',
);
const testError = new Error('Hello darkness my old friend');
const testErrorSequence = [{ type: 'error', value: testError }];
const opWithError: GraphQLRequest = {
query: gql`{ hello }`,
context: {
debounceKey: 'key1',
testSequence: testErrorSequence,
},
};
beforeEach(() => {
jest.useFakeTimers();
testLink = new TestSequenceLink();
debounceLink = new DebounceLink(DEBOUNCE_TIMEOUT);
link = ApolloLink.from([debounceLink, testLink]);
});
it('forwards the operation', () => {
return new Promise((resolve, reject) => {
execute(link, op).subscribe({
next: (data) => undefined,
error: (error) => reject(error),
complete: () => {
expect(testLink.operations.length).toBe(1);
expect(testLink.operations[0].query).toEqual(op.query);
resolve();
},
});
jest.runAllTimers();
});
});
it('forwards the operation if context.debounceKey is not defined', () => {
const opWithoutKey: GraphQLRequest = {
query: gql`{ hello }`,
context: {
testSequence: makeSimpleSequence(testResponse),
},
};
return new Promise((resolve, reject) => {
execute(link, opWithoutKey).subscribe({
next: (data) => undefined,
error: (error) => reject(error),
complete: () => {
expect(testLink.operations.length).toBe(1);
expect(testLink.operations[0].query).toEqual(op.query);
resolve();
},
});
jest.runAllTimers();
});
});
it('calls next and complete as expected', () => {
return Promise.resolve(assertObservableSequence(
execute(link, op),
[
{ type: 'next', value: testResponse },
{ type: 'complete' },
],
() => jest.runAllTimers(),
));
});
it('passes through errors', () => {
return Promise.resolve(assertObservableSequence(
execute(link, opWithError),
[
{ type: 'error', value: testError },
],
() => jest.runAllTimers(),
));
});
it('debounces multiple queries within the debounce interval', () => {
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, op).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT - 1);
// check that query did not execute.
expect(testLink.operations.length).toBe(0);
expect(observedSequence.length).toBe(0);
// make another query, different params.
const op2 = makeSimpleOp(
makeSimpleSequence(makeSimpleResponse('op2')),
'key1',
);
const s2 = execute(link, op2).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT - 1);
// check that query did not execute
expect(testLink.operations.length).toBe(0);
expect(observedSequence.length).toBe(0);
// make another query, different params
const op3sequence = makeSimpleSequence(makeSimpleResponse('op3'));
const op3 = makeSimpleOp(
op3sequence,
'key1',
);
op3.operationName = 'op3';
const s3 = execute(link, op3).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
// check that all queries returned the sequence of the last query.
const expectedSequence = [
toResultValue(op3sequence[0]),
toResultValue(op3sequence[0]),
toResultValue(op3sequence[0]),
toResultValue(op3sequence[1]),
toResultValue(op3sequence[1]),
toResultValue(op3sequence[1]),
];
expect(testLink.operations.length).toEqual(1);
expect(testLink.operations[0].operationName).toBe(op3.operationName);
expect(observedSequence.length).toEqual(6);
expect(observedSequence).toEqual(expectedSequence);
s1.unsubscribe();
s2.unsubscribe();
s3.unsubscribe();
});
it('debounces multiple queries within the custom debounce interval provided in context', () => {
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const customDebounceTimeout = DEBOUNCE_TIMEOUT / 4;
const op0 = makeSimpleOp(
testSequence,
'key1',
customDebounceTimeout,
);
const s1 = execute(link, op0).subscribe(subscriber);
jest.runTimersToTime(customDebounceTimeout - 1);
// check that query did not execute.
expect(testLink.operations.length).toBe(0);
expect(observedSequence.length).toBe(0);
// make another query, different params.
const op2 = makeSimpleOp(
makeSimpleSequence(makeSimpleResponse('op2')),
'key1',
customDebounceTimeout,
);
const s2 = execute(link, op2).subscribe(subscriber);
jest.runTimersToTime(customDebounceTimeout - 1);
// check that query did not execute
expect(testLink.operations.length).toBe(0);
expect(observedSequence.length).toBe(0);
// make another query, different params
const op3sequence = makeSimpleSequence(makeSimpleResponse('op3'));
const op3 = makeSimpleOp(
op3sequence,
'key1',
customDebounceTimeout,
);
op3.operationName = 'op3';
const s3 = execute(link, op3).subscribe(subscriber);
jest.runTimersToTime(customDebounceTimeout + 1);
// check that all queries returned the sequence of the last query.
const expectedSequence = [
toResultValue(op3sequence[0]),
toResultValue(op3sequence[0]),
toResultValue(op3sequence[0]),
toResultValue(op3sequence[1]),
toResultValue(op3sequence[1]),
toResultValue(op3sequence[1]),
];
expect(testLink.operations.length).toEqual(1);
expect(testLink.operations[0].operationName).toBe(op3.operationName);
expect(observedSequence.length).toEqual(6);
expect(observedSequence).toEqual(expectedSequence);
s1.unsubscribe();
s2.unsubscribe();
s3.unsubscribe();
});
it('does not debounce queries that are not within the interval', () => {
// make one query.
// run timer for debounce + 1
// check that query executed.
// make one query.
// run timer for debounce + 1
// check that query executed.
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, op).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
// check that query did not execute.
expect(testLink.operations.length).toBe(1);
expect(observedSequence.length).toBe(2);
// make another query, different params.
const op2sequence = makeSimpleSequence(testResponse);
const op2 = makeSimpleOp(
op2sequence,
'key1',
);
const s2 = execute(link, op2).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
// check that query executed
expect(testLink.operations.length).toBe(2);
expect(observedSequence.length).toBe(4);
const expectedSequence = [
toResultValue(testSequence[0]),
toResultValue(testSequence[1]),
toResultValue(op2sequence[0]),
toResultValue(op2sequence[1]),
];
expect(observedSequence).toEqual(expectedSequence);
s1.unsubscribe();
s2.unsubscribe();
});
it('does not debounce queries with different debounceKey (even within the interval)', () => {
// make query
// make another query with different debounceKey
// run timer for debounce +1.
// check that both queries ran and returned different values
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, op).subscribe(subscriber);
// make another query, different debounceKey.
const op2sequence = makeSimpleSequence(testResponse);
const op2 = makeSimpleOp(
op2sequence,
'key2',
);
const observedSequence2: ObservableEvent[] = [];
// Using a different subscriber, just for fun.
const subscriber2 = getTestSubscriber(observedSequence2);
const s2 = execute(link, op2).subscribe(subscriber2);
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
// check that both queries executed
expect(testLink.operations.length).toBe(2);
expect(observedSequence.length).toBe(2);
expect(observedSequence2.length).toBe(2);
const expectedSequence = [
toResultValue(testSequence[0]),
toResultValue(testSequence[1]),
];
const expectedSequence2 = [
toResultValue(op2sequence[0]),
toResultValue(op2sequence[1]),
];
expect(observedSequence).toEqual(expectedSequence);
expect(observedSequence2).toEqual(expectedSequence2);
s1.unsubscribe();
s2.unsubscribe();
});
it('does not make any query if you unsubscribe before interval is over', () => {
// make query
// run timer for debounce -1
// unsubscribe
// run timer for debounce +1.
// check that nothing ran
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, op).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT - 1);
s1.unsubscribe();
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
expect(testLink.operations.length).toBe(0);
expect(observedSequence.length).toBe(0);
});
it('correctly debounces a query that errors', () => {
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, opWithError).subscribe(subscriber);
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
expect(testLink.operations.length).toBe(1);
expect(observedSequence).toEqual(testErrorSequence);
s1.unsubscribe();
});
it('runs the second to last query if the last one was unsubscribed from', () => {
// make query
// make another query
// run timer for debounce -1
// unsubscribe from second query
// run timer for debounce +1.
// check that first query executed and returned value
const observedSequence: ObservableEvent[] = [];
const subscriber = getTestSubscriber(observedSequence);
const s1 = execute(link, op).subscribe(subscriber);
// make another query
const op2sequence = makeSimpleSequence(testResponse);
const op2 = makeSimpleOp(
op2sequence,
'key1',
);
const observedSequence2: ObservableEvent[] = [];
// Using a different subscriber, just for fun.
const subscriber2 = getTestSubscriber(observedSequence2);
const s2 = execute(link, op2).subscribe(subscriber2);
jest.runTimersToTime(DEBOUNCE_TIMEOUT - 1);
s2.unsubscribe();
jest.runTimersToTime(DEBOUNCE_TIMEOUT + 1);
expect(testLink.operations.length).toBe(1);
expect(observedSequence.length).toBe(2);
expect(observedSequence2.length).toBe(0);
const expectedSequence = [
toResultValue(testSequence[0]),
toResultValue(testSequence[1]),
];
expect(observedSequence).toEqual(expectedSequence);
s1.unsubscribe();
});
});