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

refactor: Clean up Client result source logic and allow multiple mutation results (#3102)

Changed files
+141 -121
.changeset
packages
core
src
+7
.changeset/soft-glasses-guess.md
···
···
+
---
+
'@urql/core': patch
+
---
+
+
Refactor `Client` result source construction code and allow multiple mutation
+
results, if `result.hasNext` on a mutation result is set to `true`, indicating
+
deferred or streamed results.
+1 -1
package.json
···
"react-is": "^17.0.2",
"styled-components": "^5.2.3",
"vite": "^3.2.4",
-
"wonka": "^6.2.6"
}
},
"devDependencies": {
···
"react-is": "^17.0.2",
"styled-components": "^5.2.3",
"vite": "^3.2.4",
+
"wonka": "^6.3.0"
}
},
"devDependencies": {
+100 -87
packages/core/src/client.ts
···
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
filter,
-
make,
makeSubject,
onEnd,
onPush,
···
const makeResultSource = (operation: Operation) => {
let result$ = pipe(
results$,
filter(
(res: OperationResult) =>
res.operation.kind === operation.kind &&
res.operation.key === operation.key &&
(!res.operation.context._instance ||
res.operation.context._instance === operation.context._instance)
)
);
-
// Mask typename properties if the option for it is turned on
-
if (opts.maskTypename) {
result$ = pipe(
result$,
-
map(res => ({ ...res, data: maskTypename(res.data, true) }))
);
}
-
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(result$, take(1));
-
}
-
-
if (operation.kind === 'subscription') {
result$ = pipe(
result$,
-
takeWhile(result => !!result.hasNext)
);
}
-
return pipe(
-
result$,
-
// End the results stream when an active teardown event is sent
-
takeUntil(
-
pipe(
-
operations.source,
-
filter(op => op.kind === 'teardown' && op.key === operation.key)
-
)
-
),
-
switchMap(result => {
-
if (operation.kind !== 'query' || result.stale) {
-
return fromValue(result);
-
}
-
-
return merge([
-
fromValue(result),
-
// Mark a result as stale when a new operation is sent for it
-
pipe(
-
operations.source,
-
filter(
-
op =>
-
op.kind === 'query' &&
-
op.key === operation.key &&
-
op.context.requestPolicy !== 'cache-only'
-
),
-
take(1),
-
map(() => ({ ...result, stale: true }))
-
),
-
]);
-
}),
-
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
-
for (let i = queue.length - 1; i >= 0; i--)
-
if (queue[i].key === operation.key) queue.splice(i, 1);
-
// Dispatch a teardown signal for the stopped operation
-
nextOperation(makeOperation('teardown', operation, operation.context));
-
}),
-
share
-
);
};
const instance: Client =
···
}
return withPromise(
-
make<OperationResult>(observer => {
let source = active.get(operation.key);
if (!source) {
active.set(operation.key, (source = makeResultSource(operation)));
}
-
return pipe(
-
source,
-
onStart(() => {
-
const prevReplay = replays.get(operation.key);
-
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(prevReplay);
-
} else if (!isNetworkOperation) {
dispatchOperation(operation);
-
}
-
}),
-
onEnd(() => {
-
isOperationBatchActive = false;
-
observer.complete();
-
}),
-
subscribe(observer.next)
-
).unsubscribe;
})
);
},
···
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
+
lazy,
filter,
makeSubject,
onEnd,
onPush,
···
const makeResultSource = (operation: Operation) => {
let result$ = pipe(
results$,
+
// Filter by matching key (or _instance if it’s set)
filter(
(res: OperationResult) =>
res.operation.kind === operation.kind &&
res.operation.key === operation.key &&
(!res.operation.context._instance ||
res.operation.context._instance === operation.context._instance)
+
),
+
// End the results stream when an active teardown event is sent
+
takeUntil(
+
pipe(
+
operations.source,
+
filter(op => op.kind === 'teardown' && op.key === operation.key)
+
)
)
);
+
if (operation.kind !== 'query') {
+
// Interrupt subscriptions and mutations when they have no more results
result$ = pipe(
result$,
+
takeWhile(result => !!result.hasNext, true)
+
);
+
} else {
+
result$ = pipe(
+
result$,
+
// Add `stale: true` flag when a new operation is sent for queries
+
switchMap(result => {
+
const value$ = fromValue(result);
+
return result.stale
+
? value$
+
: merge([
+
value$,
+
pipe(
+
operations.source,
+
filter(
+
op =>
+
op.kind === 'query' &&
+
op.key === operation.key &&
+
op.context.requestPolicy !== 'cache-only'
+
),
+
take(1),
+
map(() => ({ ...result, stale: true }))
+
),
+
]);
+
})
);
}
+
if (operation.kind !== 'mutation') {
+
result$ = pipe(
+
result$,
+
// Store replay result
+
onPush(result => {
+
dispatched.delete(operation.key);
+
replays.set(operation.key, result);
+
}),
+
// Cleanup active states on end of source
+
onEnd(() => {
+
// Delete the active operation handle
+
dispatched.delete(operation.key);
+
replays.delete(operation.key);
+
active.delete(operation.key);
+
// Interrupt active queue
+
isOperationBatchActive = false;
+
// Delete all queued up operations of the same key on end
+
for (let i = queue.length - 1; i >= 0; i--)
+
if (queue[i].key === operation.key) queue.splice(i, 1);
+
// Dispatch a teardown signal for the stopped operation
+
nextOperation(
+
makeOperation('teardown', operation, operation.context)
+
);
+
})
+
);
+
} else {
result$ = pipe(
result$,
+
// Send mutation operation on start
onStart(() => {
nextOperation(operation);
})
);
}
+
// Mask typename properties if the option for it is turned on
+
if (opts.maskTypename) {
result$ = pipe(
result$,
+
map(res => ({ ...res, data: maskTypename(res.data, true) }))
);
}
+
return share(result$);
};
const instance: Client =
···
}
return withPromise(
+
lazy<OperationResult>(() => {
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';
+
const replay = replays.get(operation.key);
+
if (operation.kind !== 'query' || !replay || isNetworkOperation) {
+
source = pipe(
+
source,
+
onStart(() => {
dispatchOperation(operation);
+
})
+
);
+
}
+
+
if (operation.kind === 'query' && replay) {
+
return merge([
+
source,
+
pipe(
+
fromValue(replay),
+
filter(replay => {
+
if (replay === replays.get(operation.key)) {
+
if (isNetworkOperation && !replay.hasNext)
+
replay.stale = true;
+
return true;
+
} else {
+
if (!isNetworkOperation) dispatchOperation(operation);
+
return false;
+
}
+
})
+
),
+
]);
+
} else {
+
return source;
+
}
})
);
},
+33 -33
pnpm-lock.yaml
···
react-is: ^17.0.2
styled-components: ^5.2.3
vite: ^3.2.4
-
wonka: ^6.2.6
importers:
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
react: ^17.0.2
react-dom: ^17.0.2
urql: workspace:*
-
wonka: ^6.2.6
dependencies:
'@0no-co/graphql.web': 1.0.0_graphql@16.6.0
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
'@cypress/react': 7.0.2_kxqn2c7raunyx4zfzvxjupflne
'@urql/exchange-execute': link:../execute
···
'@urql/core': '>=3.2.2'
extract-files: ^11.0.0
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
extract-files: 11.0.0
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
'@types/react': ^17.0.39
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
'@types/react': 17.0.52
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../../packages/core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
packages/core:
specifiers:
'@0no-co/graphql.web': ^1.0.0
-
wonka: ^6.2.6
dependencies:
'@0no-co/graphql.web': 1.0.0
-
wonka: 6.2.6
packages/introspection:
specifiers:
···
'@urql/core': ^3.2.2
graphql: ^16.6.0
preact: ^10.13.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../core
-
wonka: 6.2.6
devDependencies:
'@testing-library/preact': 2.0.1_preact@10.13.1
graphql: 16.6.0
···
react-ssr-prepass: ^1.1.2
react-test-renderer: ^17.0.1
vite: ^3.2.4
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../core
-
wonka: 6.2.6
devDependencies:
'@cypress/react': 7.0.2_omnm57pgrvq3mbg7qqmuk7p7le
'@cypress/vite-dev-server': 5.0.4
···
'@urql/core': ^3.2.2
graphql: ^16.6.0
svelte: ^3.20.0
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../core
-
wonka: 6.2.6
devDependencies:
graphql: 16.6.0
svelte: 3.37.0
···
'@vue/test-utils': ^2.3.0
graphql: ^16.6.0
vue: ^3.2.47
-
wonka: ^6.2.6
dependencies:
'@urql/core': link:../core
-
wonka: 6.2.6
devDependencies:
'@vue/test-utils': 2.3.0_vue@3.2.47
graphql: 16.6.0
···
execa: 1.0.0
dev: true
-
/wonka/6.2.6:
-
resolution: {integrity: sha512-ExUBenRwEyf8YswAVOFZDmAdiUMgpnuyDV28G9bF+73o2hnhAG9tLqnn7LmtWgB2KCFQdWywbUfvUW3UgxARew==}
dev: false
/word-wrap/1.2.3:
···
react-is: ^17.0.2
styled-components: ^5.2.3
vite: ^3.2.4
+
wonka: ^6.3.0
importers:
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
react: ^17.0.2
react-dom: ^17.0.2
urql: workspace:*
+
wonka: ^6.3.0
dependencies:
'@0no-co/graphql.web': 1.0.0_graphql@16.6.0
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
'@cypress/react': 7.0.2_kxqn2c7raunyx4zfzvxjupflne
'@urql/exchange-execute': link:../execute
···
'@urql/core': '>=3.2.2'
extract-files: ^11.0.0
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
extract-files: 11.0.0
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
'@types/react': ^17.0.39
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
'@types/react': 17.0.52
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
···
specifiers:
'@urql/core': '>=3.2.2'
graphql: ^16.6.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../../packages/core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
packages/core:
specifiers:
'@0no-co/graphql.web': ^1.0.0
+
wonka: ^6.3.0
dependencies:
'@0no-co/graphql.web': 1.0.0
+
wonka: 6.3.0
packages/introspection:
specifiers:
···
'@urql/core': ^3.2.2
graphql: ^16.6.0
preact: ^10.13.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../core
+
wonka: 6.3.0
devDependencies:
'@testing-library/preact': 2.0.1_preact@10.13.1
graphql: 16.6.0
···
react-ssr-prepass: ^1.1.2
react-test-renderer: ^17.0.1
vite: ^3.2.4
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../core
+
wonka: 6.3.0
devDependencies:
'@cypress/react': 7.0.2_omnm57pgrvq3mbg7qqmuk7p7le
'@cypress/vite-dev-server': 5.0.4
···
'@urql/core': ^3.2.2
graphql: ^16.6.0
svelte: ^3.20.0
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../core
+
wonka: 6.3.0
devDependencies:
graphql: 16.6.0
svelte: 3.37.0
···
'@vue/test-utils': ^2.3.0
graphql: ^16.6.0
vue: ^3.2.47
+
wonka: ^6.3.0
dependencies:
'@urql/core': link:../core
+
wonka: 6.3.0
devDependencies:
'@vue/test-utils': 2.3.0_vue@3.2.47
graphql: 16.6.0
···
execa: 1.0.0
dev: true
+
/wonka/6.3.0:
+
resolution: {integrity: sha512-7np+Kj4OnDQeEN0kafYLkPFKj1Qo+k7mNgyMHSgOeg+9AEvJbL8ipTBgSCTQfGcgVo6TPNU4T5+AZ2rAOyVrAw==}
dev: false
/word-wrap/1.2.3: