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

fix(core): Add missing dedupe operation logic (#3101)

Changed files
+27 -3
.changeset
packages
+5
.changeset/afraid-geckos-raise.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Deduplicate operations as the `dedupExchange` did; by filtering out duplicate operations until either the original operation has been cancelled (teardown) or a first result (without `hasNext: true`) has come in.
+8 -2
packages/core/src/client.test.ts
···
output.push(op);
if (
op.key === queryOperation.key &&
-
op.context.requestPolicy === 'cache-first'
+
op.context.requestPolicy !== 'network-only'
) {
client.reexecuteOperation({
...op,
···
});
it('does nothing when no operation result has been emitted yet', () => {
+
const dispatched = vi.fn();
+
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
-
map(op => ({ hasNext: false, stale: false, data: 1, operation: op })),
+
map(op => {
+
dispatched(op);
+
return { hasNext: false, stale: false, data: 1, operation: op };
+
}),
filter(() => false)
);
};
···
expect(resultOne).toHaveBeenCalledTimes(0);
expect(resultTwo).toHaveBeenCalledTimes(0);
+
expect(dispatched).toHaveBeenCalledTimes(1);
});
it('skips replaying results when a result is emitted immediately (network-only)', () => {
+14 -1
packages/core/src/client.ts
···
const replays = new Map<number, OperationResult>();
const active: Map<number, Source<OperationResult>> = new Map();
+
const dispatched = new Set<number>();
const queue: Operation[] = [];
const baseOpts = {
···
function nextOperation(operation: Operation) {
const prevReplay = replays.get(operation.key);
-
if (operation.kind === 'mutation' || !prevReplay || !prevReplay.hasNext)
+
if (
+
operation.kind === 'mutation' ||
+
operation.kind === 'teardown' ||
+
(prevReplay ? !prevReplay.hasNext : !dispatched.has(operation.key))
+
) {
+
if (operation.kind === 'teardown') {
+
dispatched.delete(operation.key);
+
} else if (operation.kind !== 'mutation') {
+
dispatched.add(operation.key);
+
}
operations.next(operation);
+
}
}
// We define a queued dispatcher on the subject, which empties the queue when it's
···
]);
}),
onPush(result => {
+
dispatched.delete(operation.key);
replays.set(operation.key, result);
}),
onEnd(() => {
// Delete the active operation handle
+
dispatched.delete(operation.key);
replays.delete(operation.key);
active.delete(operation.key);
// Delete all queued up operations of the same key on end