From ecf39b4b836ae62cbf32a6472cb4acaecbcc32de Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 15:29:20 +0100 Subject: [PATCH 1/8] Upgrade to wonka@6.3.0 --- package.json | 2 +- pnpm-lock.yaml | 66 +++++++++++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) 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/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: From d31d47678524fb177a1a3917de8e491ac7d48406 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 15:29:43 +0100 Subject: [PATCH 2/8] Replace take(1) with takeWhile(x => !!x.hasNext, true) --- packages/core/src/client.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 02dfb102e1..a7d9ed5b3a 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -622,24 +622,14 @@ export const Client: new (opts: ClientOptions) => Client = function Client( if (operation.kind !== 'query') { result$ = pipe( result$, + takeWhile(result => !!result.hasNext, true), + // TODO: Remove manual onStart here onStart(() => { - nextOperation(operation); + dispatchOperation(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 From 32328f9f13710ca18dc0f09038ad4527d66f7b7d Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 15:39:42 +0100 Subject: [PATCH 3/8] Clean up result source assembly in makeResultSource() --- packages/core/src/client.ts | 122 ++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index a7d9ed5b3a..180adfbdf8 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -602,24 +602,25 @@ 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) { - result$ = pipe( - result$, - map(res => ({ ...res, data: maskTypename(res.data, true) })) - ); - } - if (operation.kind !== 'query') { + // Interrupt subscriptions and mutations when they have no more results result$ = pipe( result$, takeWhile(result => !!result.hasNext, true), @@ -628,55 +629,66 @@ export const Client: new (opts: ClientOptions) => Client = function Client( dispatchOperation(operation); }) ); + } 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 })) + ), + ]); + }) + ); } - 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 - ); + 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); + // 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) + ); + }) + ); + } + + // 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 = From 38aff7a3b3f37f9b1b0a7ac61a0f124e7368b554 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 15:50:46 +0100 Subject: [PATCH 4/8] Move around dispatching logic for subscriptions and mutations --- packages/core/src/client.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 180adfbdf8..799b3040ba 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -623,11 +623,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( // Interrupt subscriptions and mutations when they have no more results result$ = pipe( result$, - takeWhile(result => !!result.hasNext, true), - // TODO: Remove manual onStart here - onStart(() => { - dispatchOperation(operation); - }) + takeWhile(result => !!result.hasNext, true) ); } else { result$ = pipe( @@ -669,6 +665,8 @@ export const Client: new (opts: ClientOptions) => Client = function Client( 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); @@ -678,6 +676,14 @@ export const Client: new (opts: ClientOptions) => Client = function Client( ); }) ); + } else { + result$ = pipe( + result$, + // Send mutation operation on start + onStart(() => { + nextOperation(operation); + }) + ); } // Mask typename properties if the option for it is turned on @@ -752,7 +758,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( operation.context.requestPolicy === 'cache-and-network' || operation.context.requestPolicy === 'network-only'; if (operation.kind !== 'query') { - return; + return dispatchOperation(operation); } else if (isNetworkOperation) { dispatchOperation(operation); if (prevReplay && !prevReplay.hasNext) prevReplay.stale = true; @@ -768,7 +774,6 @@ export const Client: new (opts: ClientOptions) => Client = function Client( } }), onEnd(() => { - isOperationBatchActive = false; observer.complete(); }), subscribe(observer.next) From 872daf9845550e40e4273f34a43850e00d2bb1a9 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 16:20:46 +0100 Subject: [PATCH 5/8] =?UTF-8?q?Refactor=20client=E2=80=99s=20execution=20s?= =?UTF-8?q?ource=20from=20make=20to=20lazy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/client.ts | 67 +++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 799b3040ba..6b267eb99b 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, @@ -744,40 +744,49 @@ 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 dispatchOperation(operation); - } else if (isNetworkOperation) { + const isNetworkOperation = + operation.context.requestPolicy === 'cache-and-network' || + operation.context.requestPolicy === 'network-only'; + const replay = + operation.kind === 'query' ? replays.get(operation.key) : undefined; + if (replay) { + return merge([ + pipe( + source, + onStart(() => { + if (isNetworkOperation) { + dispatchOperation(operation); + } + }) + ), + 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 pipe( + source, + onStart(() => { 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(() => { - observer.complete(); - }), - subscribe(observer.next) - ).unsubscribe; + }) + ); + } }) ); }, From e58ba40317b64c7a31eb70bc77b4fa9418c02abb Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 16:22:58 +0100 Subject: [PATCH 6/8] Refactor onStart for queries --- packages/core/src/client.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6b267eb99b..22d57df052 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -755,16 +755,18 @@ export const Client: new (opts: ClientOptions) => Client = function Client( operation.context.requestPolicy === 'network-only'; const replay = operation.kind === 'query' ? replays.get(operation.key) : undefined; + if (operation.kind !== 'query' || !replay || isNetworkOperation) { + source = pipe( + source, + onStart(() => { + dispatchOperation(operation); + }) + ); + } + if (replay) { return merge([ - pipe( - source, - onStart(() => { - if (isNetworkOperation) { - dispatchOperation(operation); - } - }) - ), + source, pipe( fromValue(replay), filter(replay => { @@ -780,12 +782,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( ), ]); } else { - return pipe( - source, - onStart(() => { - dispatchOperation(operation); - }) - ); + return source; } }) ); From 2f466a077f44bbedc61fe5229972566a2c6541d2 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 16:26:16 +0100 Subject: [PATCH 7/8] Clean up conditional replay --- packages/core/src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 22d57df052..9790e5fc3b 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -753,8 +753,8 @@ export const Client: new (opts: ClientOptions) => Client = function Client( const isNetworkOperation = operation.context.requestPolicy === 'cache-and-network' || operation.context.requestPolicy === 'network-only'; - const replay = - operation.kind === 'query' ? replays.get(operation.key) : undefined; + const replay = replays.get(operation.key); + if (operation.kind !== 'query' || !replay || isNetworkOperation) { source = pipe( source, @@ -764,7 +764,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client( ); } - if (replay) { + if (operation.kind === 'query' && replay) { return merge([ source, pipe( From 3169c0977afb7644a90174d30d87d9b4198f1d36 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 27 Mar 2023 16:26:44 +0100 Subject: [PATCH 8/8] Add changeset --- .changeset/soft-glasses-guess.md | 7 +++++++ 1 file changed, 7 insertions(+) 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.