From 8829fc6a8c33b2c47abe4b7d9f797eb7888e52c5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 9 Feb 2026 13:39:43 +0100 Subject: [PATCH 1/2] fix: don't fetch query when overridden but unused Fixes #14299 --- .changeset/smart-badgers-yell.md | 5 +++ .../client/remote-functions/query.svelte.js | 40 ++++++++++++++++++- .../remote/optimistic-new-query/+page.svelte | 12 ++++++ .../optimistic-new-query/data.remote.js | 9 +++++ .../kit/test/apps/async/test/client.test.js | 15 +++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 .changeset/smart-badgers-yell.md create mode 100644 packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/data.remote.js diff --git a/.changeset/smart-badgers-yell.md b/.changeset/smart-badgers-yell.md new file mode 100644 index 000000000000..51e9d7d8f3b3 --- /dev/null +++ b/.changeset/smart-badgers-yell.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't fetch query when overridden but unused diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 196a959d25a7..e052e4191208 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -132,6 +132,8 @@ export class Query { _key; #init = false; + /** @type {undefined | true | (() => void)} */ + #started; /** @type {() => Promise} */ #fn; #loading = $state(true); @@ -189,6 +191,14 @@ export class Query { this.#promise = $state.raw(this.#run()); } + #resolve_started() { + if (typeof this.#started === 'function') { + this.#started(); + } else { + this.#started = true; + } + } + #run() { // Prevent state_unsafe_mutation error on first run when the resource is created within the template if (this.#init) { @@ -213,7 +223,20 @@ export class Query { resolve ); - Promise.resolve(this.#fn()) + Promise.resolve() + .then(() => { + // Avoid running the query if it is not used yet but withOverride was + // called on it already. + if (this.#overrides.length && !this.#started) { + /** @type {Promise} */ + const promise = new Promise((res) => { + resolve = res; + }); + this.#started = resolve; + return promise; + } + }) + .then(() => this.#fn()) .then((value) => { // Skip the response if resource was refreshed with a later promise while we were waiting for this one to resolve const idx = this.#latest.indexOf(resolve); @@ -240,11 +263,21 @@ export class Query { return promise; } + // Hack because create_remote_function accesses .then right away, + // and we don't want to track that as "accessed now start" yet. + #first_then_invocation = false; + get then() { + if (this.#first_then_invocation) { + this.#resolve_started(); + } else { + this.#first_then_invocation = true; + } return this.#then; } get catch() { + this.#resolve_started(); this.#then; return (/** @type {any} */ reject) => { return this.#then(undefined, reject); @@ -252,6 +285,7 @@ export class Query { } get finally() { + this.#resolve_started(); this.#then; return (/** @type {any} */ fn) => { return this.#then( @@ -268,10 +302,12 @@ export class Query { } get current() { + this.#resolve_started(); return this.#current; } get error() { + this.#resolve_started(); return this.#error; } @@ -279,6 +315,7 @@ export class Query { * Returns true if the resource is loading or reloading. */ get loading() { + this.#resolve_started(); return this.#loading; } @@ -286,6 +323,7 @@ export class Query { * Returns true once the resource has been loaded for the first time. */ get ready() { + this.#resolve_started(); return this.#ready; } diff --git a/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/+page.svelte new file mode 100644 index 000000000000..80aecdb0374a --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/+page.svelte @@ -0,0 +1,12 @@ + + + + + +{#if show} +

{await foo()}

+{/if} diff --git a/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/data.remote.js b/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/data.remote.js new file mode 100644 index 000000000000..ee4c16621c76 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/optimistic-new-query/data.remote.js @@ -0,0 +1,9 @@ +import { command, query } from '$app/server'; + +export const foo = query(() => { + return 'foo'; +}); + +export const mutate = command(() => { + foo().set('baz'); +}); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index d6d4a4da9717..c06f3dc1a6ef 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -254,6 +254,21 @@ test.describe('remote function mutations', () => { await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); }); + test('optimistic overrides avoid unused query requests', async ({ page }) => { + await page.goto('/remote/optimistic-new-query'); + + let request_count = 0; + page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0)); + + await page.getByText('override').click(); + await page.waitForTimeout(100); // allow any requests to happen + expect(request_count).toBe(1); // just the command + + await page.getByText('show').click(); + await expect(page.locator('p')).toHaveText('baz'); + expect(request_count).toBe(2); // now the query, too + }); + // TODO once we have async SSR adjust the test and move this into test.js test('query.batch works', async ({ page }) => { await page.goto('/remote/batch'); From 12ed0370240dd956204877f717c641e93a9c4a2b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 9 Feb 2026 13:59:03 +0100 Subject: [PATCH 2/2] fix --- .../client/remote-functions/query.svelte.js | 18 +++++++++++++++--- .../kit/test/apps/async/test/client.test.js | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index e052e4191208..ed8e500ce3bd 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -194,9 +194,8 @@ export class Query { #resolve_started() { if (typeof this.#started === 'function') { this.#started(); - } else { - this.#started = true; } + this.#started = true; } #run() { @@ -236,7 +235,12 @@ export class Query { return promise; } }) - .then(() => this.#fn()) + .then(() => { + // Skip fetching if we waited on override and a set() or refresh() happened in the meantime + const idx = this.#latest.indexOf(resolve); + if (idx === -1) return; + return this.#fn(); + }) .then((value) => { // Skip the response if resource was refreshed with a later promise while we were waiting for this one to resolve const idx = this.#latest.indexOf(resolve); @@ -344,6 +348,14 @@ export class Query { this.#error = undefined; this.#raw = value; this.#promise = Promise.resolve(); + // any in-flight-fetches are now outdated + for (const latest of this.#latest) { + latest(); + } + this.#latest = []; + // resolve potential pending promise to prevent memory leaks (.then() in create_remote_function would never resolve). + // It's fine to resolve the promise because it's a noop due to if condition in #fn + this.#resolve_started(); } /** diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index c06f3dc1a6ef..f7eea97aa93d 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -265,7 +265,7 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // just the command await page.getByText('show').click(); - await expect(page.locator('p')).toHaveText('baz'); + await expect(page.locator('p')).toHaveText('foo'); // set() from command mutation is obsolete as query was removed from cache expect(request_count).toBe(2); // now the query, too });