sveltejs / sveltejs/kit

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

Open
#16,029 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

documentation
Dominant language
JavaScript
Stars
20.8k
Forks
2.3k
Avg merge
1d 16h
Merged PRs (30d)
156

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the mismatch using versions.remote.ts and /org/[organizationId]/versions/[versionId]/+page.svelte, then trace where query cache keys are generated on the client. Compare the key from the schema fields with the key from the superset params object. Done means both inputs share a cache key and the stale-read reproduction is covered by a test.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.