Skip to content

Cued-up query .refresh()es in commands not deferred until operation is complete #16202

Description

@Conduitry

Describe the bug

If you run a some_query().refresh() during a remote function command, it should cue up the refresh but only kick it off once the command finishes. Instead it appears that it starts the refresh immediately. If it takes a while before the state change happens that would have changed the response of that query, the server-driven refresh can return an outdated value.

Reproduction

<script>
	import { get_state, increment_state } from '$lib/state.remote.js';
</script>

State: {await get_state()}
<button type='button' onclick={() => increment_state()}>increment</button>

state.remote.js:

import { command, query } from '$app/server';

let state = 0;

export const get_state = query(() => state);

export const increment_state = command(async () => {
	get_state().refresh();
	await new Promise(res => setTimeout(res, 1000));
	state++;
});

Pressing the 'increment' button leaves the 'state' counter one click behind what it actually is. Refreshing the page again changes it once more to be the correct value.

Logs

System Info

System:
    OS: Linux 6.18 Ubuntu 26.04 LTS 26.04 (Resolute Raccoon)
    CPU: (8) x64 Intel(R) Core(TM) Ultra 7 258V
    Memory: 10.33 GB / 15.39 GB
    Container: Yes
    Shell: 5.3.9 - /bin/bash
  Binaries:
    Node: 26.4.0
    Yarn: 1.22.22
    npm: 11.17.0
    pnpm: 11.9.0
  npmPackages:
    @sveltejs/adapter-auto: ^7.0.1 => 7.0.1 
    @sveltejs/kit: ^2.63.0 => 2.68.0 
    @sveltejs/vite-plugin-svelte: ^7.1.2 => 7.1.2 
    svelte: ^5.56.1 => 5.56.4 
    vite: ^8.0.16 => 8.1.0

Severity

annoyance

Additional Information

No response

Activity

  1. theetrain commented on Jun 30, 2026

    @theetrain
    Contributor

    Although I welcome the idea of refreshed queries being queued and deferred to the end of a command (or other query's) function scope, this feels a bit magic or less deterministic than refreshing queries after a mutation ends.

    For example:

    export const increment_state = command(async () => {
    - get_state().refresh();
      await new Promise(res => setTimeout(res, 1000));
      state++;
    + get_state().refresh();
    });

    Here, the mutation has completed successfully and we know it's safe to refresh the query because we're definitely sure the data will be fresh. This is particularly readable when performing error handling for an async mutation. Maybe if there were a .deferredRefresh() method name, it would more intuitively describe its behaviour.

  2. Conduitry commented on Jun 30, 2026

    @Conduitry
    MemberAuthor

    I had run this by Rich first in Discord, and he said that what I described here was what he believed he intended the behavior to be.

  3. elliott-with-the-longest-name-on-github commented on Jun 30, 2026

    @elliott-with-the-longest-name-on-github
    Contributor

    Yeah this is indeed how it's already supposed to work

  4. phi-bre commented on Jun 30, 2026

    @phi-bre
    Contributor

    I've ran into issues with refreshes in commands before and opened #14877 a while ago because of a similar issue.

    If this behaviour were actually implemented as in this bug report you'd end up with stale values if you depend on the query data itself during the command, so having refresh not actually do the refresh feels wrong to me.

    export const addTodo = command(async (todo) => {
      const todos = await getTodos();
    
      // save todos to a JSON field in a db that requires all old todos, not just a simple insert
    
      await getTodos().refresh(); // refresh both server cache AND single flight mutation
    
      return (await getTodos()).length
    });

    The fact that the promise of refresh is not awaiting the actual request is a regression already IMO. I am aware that it was a deliberate choice so that multiple awaited refreshes don't cause a waterfall, but that's what Promise.all is for, I definitely agree with @theetrain that sveltekit should stay predictable with the mental model of Javascript itself.

  5. elliott-with-the-longest-name-on-github commented on Jul 1, 2026

    @elliott-with-the-longest-name-on-github
    Contributor

    Actually I think the implementation would work just fine. refresh will immediately invalidate the request cache but won't actually kick off the refresh until after the command finishes. So the next call to getTodos will return the new data and populate the cache.

    refresh basically means "invalidate, mark this as a single flight mutation, and if we don't have a cache value at the end of this command, populate the cache".

  6. theetrain commented on Jul 1, 2026

    @theetrain
    Contributor

    It certainly won't hurt having this behaviour; we could call .refresh() at the top or bottom of a function and there wouldn't be a race condition concern, so people may choose whichever placement they prefer.

    It's definitely worth documenting these behaviours, especially if .refresh() is different when called server-side or browser-side.

  7. linked a pull request that will close this issuefix: defer `refresh` until after command #16225on Jul 3, 2026
  8. added theissue type on Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions