Skip to content

Commit

Permalink
feat(core): Deprecate the dedupExchange and absorb hasNext checks int…
Browse files Browse the repository at this point in the history
…o Client (#3058)
  • Loading branch information
kitten authored Mar 16, 2023
1 parent 40911d2 commit 7222ee1
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 180 deletions.
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 @@ -909,7 +909,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 @@ -824,7 +838,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$);

0 comments on commit 7222ee1

Please sign in to comment.