From 849025fd29aa62f16ab9b61da88b83d7c07bf8de Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 16:50:56 +0100 Subject: [PATCH] refactor: Clean up Client result source logic and allow multiple mutation results (#3102) --- .changeset/soft-glasses-guess.md | 7 ++ package.json | 2 +- packages/core/src/client.ts | 189 +++++++++++++++++-------------- pnpm-lock.yaml | 66 +++++------ 4 files changed, 142 insertions(+), 122 deletions(-) create mode 100644 .changeset/soft-glasses-guess.md diff --git a/.changeset/soft-glasses-guess.md b/.changeset/soft-glasses-guess.md new file mode 100644 index 0000000000..3f256e4c5e --- /dev/null +++ b/.changeset/soft-glasses-guess.md @@ -0,0 +1,7 @@ +--- +'@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. diff --git a/package.json b/package.json index ac60a73da1..e7d7b0c7e2 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "react-is": "^17.0.2", "styled-components": "^5.2.3", "vite": "^3.2.4", - "wonka": "^6.2.6" + "wonka": "^6.3.0" } }, "devDependencies": { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 02dfb102e1..9790e5fc3b 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import { + lazy, filter, - make, makeSubject, onEnd, onPush, @@ -602,91 +602,99 @@ export const Client: new (opts: ClientOptions) => Client = function Client( 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) + ) ) ); - // Mask typename properties if the option for it is turned on - if (opts.maskTypename) { + if (operation.kind !== 'query') { + // Interrupt subscriptions and mutations when they have no more results result$ = pipe( result$, - map(res => ({ ...res, data: maskTypename(res.data, true) })) + 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 !== 'query') { + 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); }) ); } - // 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') { + // Mask typename properties if the option for it is turned on + if (opts.maskTypename) { result$ = pipe( result$, - takeWhile(result => !!result.hasNext) + map(res => ({ ...res, data: maskTypename(res.data, true) })) ); } - 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 - ); + return share(result$); }; const instance: Client = @@ -736,41 +744,46 @@ export const Client: new (opts: ClientOptions) => Client = function Client( } return withPromise( - make(observer => { + lazy(() => { 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) { + 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); - } - }), - onEnd(() => { - isOperationBatchActive = false; - observer.complete(); - }), - subscribe(observer.next) - ).unsubscribe; + }) + ); + } + + 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; + } }) ); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45208bcaa5..e1b2b05bc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ overrides: react-is: ^17.0.2 styled-components: ^5.2.3 vite: ^3.2.4 - wonka: ^6.2.6 + wonka: ^6.3.0 importers: @@ -129,10 +129,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -140,10 +140,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -151,10 +151,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -170,11 +170,11 @@ importers: react: ^17.0.2 react-dom: ^17.0.2 urql: workspace:* - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@0no-co/graphql.web': 1.0.0_graphql@16.6.0 '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: '@cypress/react': 7.0.2_kxqn2c7raunyx4zfzvxjupflne '@urql/exchange-execute': link:../execute @@ -190,11 +190,11 @@ importers: '@urql/core': '>=3.2.2' extract-files: ^11.0.0 graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core extract-files: 11.0.0 - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -202,10 +202,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -213,10 +213,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -225,10 +225,10 @@ importers: '@types/react': ^17.0.39 '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: '@types/react': 17.0.52 graphql: 16.6.0 @@ -237,10 +237,10 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 @@ -248,20 +248,20 @@ importers: specifiers: '@urql/core': '>=3.2.2' graphql: ^16.6.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../../packages/core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 packages/core: specifiers: '@0no-co/graphql.web': ^1.0.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@0no-co/graphql.web': 1.0.0 - wonka: 6.2.6 + wonka: 6.3.0 packages/introspection: specifiers: @@ -310,10 +310,10 @@ importers: '@urql/core': ^3.2.2 graphql: ^16.6.0 preact: ^10.13.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: '@testing-library/preact': 2.0.1_preact@10.13.1 graphql: 16.6.0 @@ -336,10 +336,10 @@ importers: react-ssr-prepass: ^1.1.2 react-test-renderer: ^17.0.1 vite: ^3.2.4 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: '@cypress/react': 7.0.2_omnm57pgrvq3mbg7qqmuk7p7le '@cypress/vite-dev-server': 5.0.4 @@ -446,10 +446,10 @@ importers: '@urql/core': ^3.2.2 graphql: ^16.6.0 svelte: ^3.20.0 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: graphql: 16.6.0 svelte: 3.37.0 @@ -460,10 +460,10 @@ importers: '@vue/test-utils': ^2.3.0 graphql: ^16.6.0 vue: ^3.2.47 - wonka: ^6.2.6 + wonka: ^6.3.0 dependencies: '@urql/core': link:../core - wonka: 6.2.6 + wonka: 6.3.0 devDependencies: '@vue/test-utils': 2.3.0_vue@3.2.47 graphql: 16.6.0 @@ -15636,8 +15636,8 @@ packages: execa: 1.0.0 dev: true - /wonka/6.2.6: - resolution: {integrity: sha512-ExUBenRwEyf8YswAVOFZDmAdiUMgpnuyDV28G9bF+73o2hnhAG9tLqnn7LmtWgB2KCFQdWywbUfvUW3UgxARew==} + /wonka/6.3.0: + resolution: {integrity: sha512-7np+Kj4OnDQeEN0kafYLkPFKj1Qo+k7mNgyMHSgOeg+9AEvJbL8ipTBgSCTQfGcgVo6TPNU4T5+AZ2rAOyVrAw==} dev: false /word-wrap/1.2.3: