Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Deprecate the dedupExchange and absorb hasNext checks into Client #3058

Merged
merged 10 commits into from
Mar 16, 2023
10 changes: 10 additions & 0 deletions .changeset/wise-cherries-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@urql/core': minor
---

Deprecate the `dedupExchange`. The functionality of deduplicating queries and subscriptions has now been moved into and absorbed by the `Client`.

Previously, the `Client` already started doing some work to share results between
queries, and to avoid dispatching operations as needed. It now only dispatches operations
strictly when the `dedupExchange` would allow so as well, moving its logic into the
`Client`.
2 changes: 1 addition & 1 deletion packages/core/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ describe('shared sources behavior', () => {
return merge([
pipe(
ops$,
map(op => ({ data: 1, operation: op })),
map(op => ({ hasNext: true, data: 1, operation: op })),
take(1)
),
never,
Expand Down
58 changes: 36 additions & 22 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Source,
take,
takeUntil,
takeWhile,
publish,
subscribe,
switchMap,
Expand Down Expand Up @@ -566,13 +567,20 @@ export const Client: new (opts: ClientOptions) => Client = function Client(

// This subject forms the input of operations; executeOperation may be
// called to dispatch a new operation on the subject
const { source: operations$, next: nextOperation } = makeSubject<Operation>();
const operations = makeSubject<Operation>();

function nextOperation(operation: Operation) {
const prevReplay = replays.get(operation.key);
if (operation.kind === 'mutation' || !prevReplay || !prevReplay.hasNext)
operations.next(operation);
}

// We define a queued dispatcher on the subject, which empties the queue when it's
// activated to allow `reexecuteOperation` to be trampoline-scheduled
let isOperationBatchActive = false;
function dispatchOperation(operation?: Operation | void) {
if (operation) nextOperation(operation);

if (!isOperationBatchActive) {
isOperationBatchActive = true;
while (isOperationBatchActive && (operation = queue.shift()))
Expand Down Expand Up @@ -602,21 +610,33 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
);
}

if (operation.kind !== 'query') {
result$ = pipe(
result$,
onStart(() => {
nextOperation(operation);
})
);
}

// A mutation is always limited to just a single result and is never shared
if (operation.kind === 'mutation') {
return pipe(
return pipe(result$, take(1));
}

if (operation.kind === 'subscription') {
result$ = pipe(
result$,
onStart(() => nextOperation(operation)),
take(1)
takeWhile(result => !!result.hasNext)
);
}

const source = pipe(
return pipe(
result$,
// End the results stream when an active teardown event is sent
takeUntil(
pipe(
operations$,
operations.source,
filter(op => op.kind === 'teardown' && op.key === operation.key)
)
),
Expand All @@ -629,7 +649,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
fromValue(result),
// Mark a result as stale when a new operation is sent for it
pipe(
operations$,
operations.source,
filter(
op =>
op.kind === 'query' &&
Expand All @@ -656,15 +676,13 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
}),
share
);

return source;
};

const instance: Client =
this instanceof Client ? this : Object.create(Client.prototype);
const client: Client = Object.assign(instance, {
suspense: !!opts.suspense,
operations$,
operations$: operations.source,

reexecuteOperation(operation: Operation) {
// Reexecute operation only if any subscribers are still subscribed to the
Expand Down Expand Up @@ -708,33 +726,29 @@ export const Client: new (opts: ClientOptions) => Client = function Client(

return make<OperationResult>(observer => {
let source = active.get(operation.key);

if (!source) {
active.set(operation.key, (source = makeResultSource(operation)));
}

const isNetworkOperation =
operation.context.requestPolicy === 'cache-and-network' ||
operation.context.requestPolicy === 'network-only';

return pipe(
source,
onStart(() => {
const prevReplay = replays.get(operation.key);

if (operation.kind === 'subscription') {
return dispatchOperation(operation);
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(
isNetworkOperation ? { ...prevReplay, stale: true } : prevReplay
);
observer.next(prevReplay);
} else if (!isNetworkOperation) {
dispatchOperation(operation);
}
Expand Down Expand Up @@ -827,7 +841,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
client,
dispatchDebug,
forward: fallbackExchange({ dispatchDebug }),
})(operations$)
})(operations.source)
);

// Prevent the `results$` exchange pipeline from being closed by active
Expand Down
104 changes: 0 additions & 104 deletions packages/core/src/exchanges/dedup.test.ts

This file was deleted.

56 changes: 3 additions & 53 deletions packages/core/src/exchanges/dedup.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,7 @@
import { filter, pipe, tap } from 'wonka';
import { Exchange } from '../types';

/** Default deduplication exchange.
*
* @remarks
* The `dedupExchange` deduplicates queries and subscriptions that are
* started with identical documents and variables by deduplicating by
* their {@link Operation.key}.
* This can prevent duplicate requests from being sent to your GraphQL API.
*
* Because this is a very safe exchange to add to any GraphQL setup, it’s
* not only the default, but we also recommend you to always keep this
* exchange added and included in your setup.
*
* Hint: In React and Vue, some common usage patterns can trigger duplicate
* operations. For instance, in React a single render will actually
* trigger two phases that execute an {@link Operation}.
* @deprecated
* This exchange's functionality is now built into the {@link Client}.
*/
export const dedupExchange: Exchange = ({ forward, dispatchDebug }) => {
const inFlightKeys = new Set<number>();
return ops$ =>
pipe(
forward(
pipe(
ops$,
filter(operation => {
if (
operation.kind === 'teardown' ||
operation.kind === 'mutation'
) {
inFlightKeys.delete(operation.key);
return true;
}

const isInFlight = inFlightKeys.has(operation.key);
inFlightKeys.add(operation.key);

if (isInFlight) {
dispatchDebug({
type: 'dedup',
message: 'An operation has been deduped.',
operation,
});
}

return !isInFlight;
})
)
),
tap(result => {
if (!result.hasNext) {
inFlightKeys.delete(result.operation.key);
}
})
);
};
export const dedupExchange: Exchange = ({ forward }) => ops$ => forward(ops$);