Skip to content

query: Normalize cache key using schema to prevent stale reads from extra input properties #16029

Description

@Stadly

Problem

When calling a query function with an object that has extra properties beyond what the schema defines, the cache key includes those extra properties. This causes cache misses between .set() calls (which use only schema-relevant fields) and reads from pages that pass a superset object (like route params).

Reproduction

// versions.remote.ts
const getVersionSchema = z.object({
  orgAppId: z.string(),
  versionId: z.string(),
});

export const getVersion = query(getVersionSchema, async ({ orgAppId, versionId }) => {
  return await db.version.findUnique({ where: { id: versionId } });
});
<!-- /org/[organizationId]/versions/[versionId]/+page.svelte -->
<script lang="ts">
  // params = { organizationId, orgAppId, versionId }
  const version = $derived(await resolve(getVersion(params)));
</script>
// After mutation:
void getVersion({ orgAppId: data.orgAppId, versionId: data.id }).set(updated);

The .set() produces a cache key from { orgAppId, versionId }, but the page computes a key from { organizationId, orgAppId, versionId }. These keys differ, so the page shows stale data until a full reload.

Expected behavior

The cache key should only consider properties defined in the schema, since extra properties are stripped by Zod on the server anyway. Passing { orgAppId, versionId, organizationId } and { orgAppId, versionId } to the same query should produce the same cache key.

Suggested solution

On the client side, before computing the cache key, strip the input object to only include keys that the server schema defines. Some options:

  1. Ship schema key names to the client — during compilation, extract the top-level keys from the Zod schema and embed them in the query metadata. The client uses this key list to filter the input before serialization.

  2. Use schema.parse() on the server and return the normalized key — the server already validates the input; include the canonical cache key in the response so the client can reconcile.

  3. Document the footgun — at minimum, warn in the docs that passing extra properties produces a different cache key than .set() with only schema fields.

Workaround

Explicitly destructure only the needed fields at each call site:

const version = $derived(
  await resolve(getVersion({ orgAppId: params.orgAppId, versionId: params.versionId }))
);

This is error-prone since TypeScript's structural typing doesn't flag extra properties on existing variables.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions