Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .claude/skills/kit-parity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
name: kit-parity
description: Check SvelteKit's remote-functions API for upstream changes and sync svelte-local-query with them, preserving the documented differences. Use when asked to check kit parity, sync with SvelteKit, or update the library to a new kit release.
---

# kit-parity — sync with upstream SvelteKit remote functions

This library is a port of SvelteKit's client-side remote-functions runtime with the
transport removed. **The parity rule (CLAUDE.md): behavior must match SvelteKit unless
the difference is documented in `DIFFERENCES.md`.** This skill keeps that promise over
time by diffing upstream and porting what changed.

Work on a fresh branch. Do everything below in order; do not skip the "ask the user"
gates.

## 1. Detect upstream changes

Run the helper script (from any scratch directory — it downloads into `./kit-upstream`):

```sh
.claude/skills/kit-parity/fetch-upstream.sh check
```

- It fetches every tracked upstream file from `sveltejs/kit@main` via
`raw.githubusercontent.com` (works in sandboxed sessions; `github.com` HTML and
`api.github.com` may be blocked) and compares content hashes against
`baseline.json` (which records the kit version and file hashes from the last sync).
- Exit 0 → no changes: report "in sync with kit <version>", update `checked_at` by
re-running `fetch-upstream.sh baseline`, commit that if anything changed, and stop.
- Exit 1 → it prints the `CHANGED`/`NEW`/`MISSING`/`REMOVED` files. A `MISSING` file
means it moved or was deleted upstream — locate its replacement in the tree (fetch
`https://raw.githubusercontent.com/sveltejs/kit/main/packages/kit/src/...` paths, or
use WebFetch on `https://github.com/sveltejs/kit/tree/...` if reachable) and update
the `FILES` list in `fetch-upstream.sh`.

Then gather context on _why_ things changed:

- Changelog: fetch
`https://raw.githubusercontent.com/sveltejs/kit/main/packages/kit/CHANGELOG.md` and
read the entries between `baseline.json`'s `kit_version` and the latest version,
looking for anything mentioning remote functions, `query`, `command`, `form`,
`prerender`, or validation.
- Docs: the tracked `documentation/docs/20-core-concepts/60-remote-functions.md` is
downloaded by the script — diff it against your local copy of the previous version if
behavior questions arise (WebFetch `https://svelte.dev/docs/kit/remote-functions` for
the rendered version).
- For each changed source file, read the upstream copy in `./kit-upstream/` next to our
port (mapping table below) and identify the actual behavioral delta. Changes to
`packages/kit/types/index.d.ts` are only relevant if they touch the `Remote*` types
or the `$app/server` `query`/`command`/`form` declarations — the file churns for
unrelated reasons.

## 2. Classify each change — and ask when in doubt

For every behavioral delta, decide which bucket it belongs to:

- **Transport/server-only** (fetch, devalue, SSR hydration, `requested()`, headers,
redirect-over-HTTP plumbing, prerender build machinery, SSE internals): no code
change needed. If it alters semantics we emulate (e.g. new invalidation defaults),
treat as behavioral.
- **Covered by a documented difference** (`DIFFERENCES.md`): no port needed, but check
whether the difference's description is still accurate and update it if kit's side of
the story changed.
- **Behavioral/API change that applies locally**: must be ported (step 4/5).
- **Unclear, or the local equivalent is not obvious** (e.g. a new feature that relies
on the server, a semantic change that interacts with one of our differences): **stop
and ask the user with AskUserQuestion** before implementing — present the upstream
change, the options for local behavior, and a recommendation. New divergences must
never be decided silently.

## 3. Diff the codebases

For each upstream file that changed, compare against our port with special attention to
the documented differences (the ported files deliberately drop transport code — don't
"fix" that):

| Upstream (`packages/kit/src/…`) | Local |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `runtime/client/remote-functions/query/instance.svelte.js` | `src/lib/query/instance.svelte.ts` |
| `runtime/client/remote-functions/query/proxy.js` | `src/lib/query/proxy.ts` |
| `runtime/client/remote-functions/query/index.js` | `src/lib/query/index.ts` (factory) |
| `runtime/client/remote-functions/query-batch.svelte.js` | `src/lib/query/index.ts` (`create_query_batch`) |
| `runtime/client/remote-functions/query-live/instance.svelte.js` | `src/lib/query-live/instance.svelte.ts` |
| `runtime/client/remote-functions/query-live/proxy.js` | `src/lib/query-live/proxy.ts` |
| `runtime/client/remote-functions/query-live/index.js` | `src/lib/query-live/index.ts` |
| `runtime/client/remote-functions/command.svelte.js` | `src/lib/command.svelte.ts` |
| `runtime/client/remote-functions/form.svelte.js` | `src/lib/form/index.svelte.ts` |
| `runtime/form-utils.js` | `src/lib/form/form-utils.ts` |
| `runtime/client/remote-functions/shared.svelte.js` | `src/lib/internal/shared.svelte.ts` |
| `runtime/client/remote-functions/cache.svelte.js` | `src/lib/internal/cache.svelte.ts` |
| `utils/shared-iterator.js` | `src/lib/internal/shared-iterator.ts` |
| `runtime/shared.js` (`stringify_remote_arg`, keys) | `src/lib/internal/stringify.ts` (stable JSON instead of devalue) |
| `types/index.d.ts` (`Remote*` types, `$app/server` overloads) | `src/lib/types.ts` (`Local*`), factory overloads |
| `runtime/client/remote-functions/query-live/iterator.js` | no direct port — local `AsyncIterable` consumption in `query-live/instance.svelte.ts` |
| `runtime/client/remote-functions/prerender.svelte.js` | intentionally not ported (DIFFERENCES.md) |

Local-only machinery with no upstream counterpart (upstream changes may still affect
it): `refresh_keys`/`refresh_all` + the mutation epoch in
`src/lib/internal/shared.svelte.ts` (our replacement for kit's single-flight server
round-trip and `invalidateAll()`), `src/lib/config.ts` (`init`/`redirect`/`onerror`),
`src/lib/validation.ts` (argument validation + the TS-inferred bare-handler
declaration, a documented local extension).

Invariants that any port must preserve (there are regression tests for each):

- resource factories passed to `cache.ensure_entry` must **never close over the proxy**
(`this`) — GC-based eviction breaks otherwise;
- no code path may create an unhandled promise rejection (`promise.catch(noop)` on
every stored rejected promise);
- rune-containing files keep the `.svelte.ts` suffix; public types stay structural
ports of kit's `Remote*` types renamed `Local*`.

## 4. Port relevant new tests

The upstream spec files are tracked and downloaded alongside the sources
(`*.spec.js` in `./kit-upstream/.../remote-functions/`). For every upstream test that
covers client-side behavior we emulate, add/adapt an equivalent in our suites:

| Upstream spec | Local suite |
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| `query/proxy.svelte.spec.js` | `src/lib/query/query.svelte.test.ts` |
| `cache.svelte.spec.js` | `src/lib/internal/cache.svelte.test.ts` |
| `instance.unhandled.svelte.spec.js` | unhandled-rejection tests in `query.svelte.test.ts` / `live.svelte.test.ts` |
| `shared.transport.spec.js` | `src/lib/internal/shared.svelte.test.ts` + `stringify.test.ts` (transport parts N/A) |
| `query-live/proxy.svelte.spec.js` | `src/lib/query-live/live.svelte.test.ts` |

Conventions: colocated `*.test.ts` / `*.svelte.test.ts`, GC-dependent tests gated on
`has_gc` from `src/tests/helpers.ts`, timing helpers `flush`/`wait_for`/`until` from the
same module. Skip tests that only exercise transport (fetch mocking, SSR payloads,
devalue) — note them as skipped in the PR description instead of porting.

## 5. Implement

Port the behavioral changes following the existing style (snake_case internals,
comments preserved from upstream where they explain subtle reactivity). For every
deliberate divergence introduced or touched, update `DIFFERENCES.md` in the same
commit. Update README examples if the public API surface changed.

## 6. Verify and finish

1. `npm run check` — 0 errors; `npm test` — all green; `npm run build` — publint clean;
`npm run lint` — formatted.
2. If runtime behavior changed, run the playground end-to-end (see the `verify` skill):
`npm run dev` + Playwright against Chromium.
3. Update the baseline so the next run starts from this sync:
`.claude/skills/kit-parity/fetch-upstream.sh baseline` (commit `baseline.json`).
4. Commit and open a PR that lists, per upstream change: what changed in kit, how it
was classified (ported / transport-only / covered by difference), and which tests
cover it.
32 changes: 32 additions & 0 deletions .claude/skills/kit-parity/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"kit_version": "2.69.3",
"checked_at": "2026-07-14",
"files": {
"packages/kit/src/runtime/client/remote-functions/index.js": "ce7f84a8ec047eedcea87724319d97c1ba4227b8597bbcdd53a3eedb4b01cab0",
"packages/kit/src/runtime/client/remote-functions/shared.svelte.js": "3592f502ac1792aa1828d2a044a98a6ffae7d8e746d48eadc1be213b7aaa3fa0",
"packages/kit/src/runtime/client/remote-functions/cache.svelte.js": "cbea68baf9a446e11db1b75d6a66c67a7200f35a1afdee6f54fab69da48ea850",
"packages/kit/src/runtime/client/remote-functions/command.svelte.js": "c28a2f9433ebe89b3748a35388d2bcee58f243c6d11d3e8828be09ff51abe6f5",
"packages/kit/src/runtime/client/remote-functions/form.svelte.js": "40ef548ea4da3e79a305ae25113506f1e019c4a7ef78d526c69dea23ee440401",
"packages/kit/src/runtime/client/remote-functions/query-batch.svelte.js": "73799c7da44d6c5e6cdde1251ac1c4a46af83b4258d22e50e30f1fd9b3f2a3f7",
"packages/kit/src/runtime/client/remote-functions/prerender.svelte.js": "3aea349775e772b255bb9621c072cc2479144e5fb2cbfcd3a51f56cdb8a0b880",
"packages/kit/src/runtime/client/remote-functions/query/index.js": "3631e5f61716c3c6f1c19016481fccf223e6dd50f268d701281b7db1f1c65b84",
"packages/kit/src/runtime/client/remote-functions/query/instance.svelte.js": "aaa1daa097663dd1c11a38d7056d241702651e0c800be384d5f8800c59643a05",
"packages/kit/src/runtime/client/remote-functions/query/proxy.js": "01f1a5fd30a4356acdb43d99c03f393ba425890a430dcad7111928c42a12e053",
"packages/kit/src/runtime/client/remote-functions/query/cache.js": "b550dd54a529fa9dfe3014b0ca763c2a8de4cc82f2dac05354e3582e3db20ffd",
"packages/kit/src/runtime/client/remote-functions/query/proxy.svelte.spec.js": "5c7cfb96a966a07f30af980aaf6cf48890df72065af9810457b92113db2e69c7",
"packages/kit/src/runtime/client/remote-functions/query-live/index.js": "fa25fec7b53b7e05457d8ad2a6d9b57b6e3e0cb92475e24bd960ff03bbb800d7",
"packages/kit/src/runtime/client/remote-functions/query-live/instance.svelte.js": "fc80362f8a14513d95b3d0fed099253ee5ec8a8b9ba5613ca8b6cac1bb99b2b7",
"packages/kit/src/runtime/client/remote-functions/query-live/proxy.js": "4e5e8ce21356d1073efb8e3450e91398156ae31b2c60418f9cce3b61134915ac",
"packages/kit/src/runtime/client/remote-functions/query-live/cache.js": "0dc6b11490a06c9c8d5f82bd65d7b90b1d358b327fbdeee14b05dd8f7384d207",
"packages/kit/src/runtime/client/remote-functions/query-live/iterator.js": "82db95d57fde0c3957bf183d142c4919a4bac08c4c0e51a130a5488ba6627f0f",
"packages/kit/src/runtime/client/remote-functions/query-live/proxy.svelte.spec.js": "ac3bdc06ab4bb712f367601e873e64079e85809ed342b02cf24984c7a7842c3e",
"packages/kit/src/runtime/client/remote-functions/cache.svelte.spec.js": "67a5e1ee8bfc17e0b880136b3c5c5797323eb4458c8ed6f0de71cea5183580e3",
"packages/kit/src/runtime/client/remote-functions/instance.unhandled.svelte.spec.js": "d46a25b0fbd50d184f5805aeef3ca56caabbdea0fc6e38ba15768180262cc0e5",
"packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js": "732b4b2cd697d25fc694ebdc940a4f94d4ddbb04706136aca5be25a1b921209f",
"packages/kit/src/runtime/form-utils.js": "98cd48645c4aa9c922e91258a8833f3e359d843eed4f179a857778359af3af01",
"packages/kit/src/runtime/shared.js": "f58d129f21b3414aee76545953e44c771878cf2ad75c2434f8df44d417f507da",
"packages/kit/src/utils/shared-iterator.js": "91031662df39dffc716bd9f464fdba24a448ac497624a2faedfb168b62ced74b",
"packages/kit/types/index.d.ts": "913c6d520a979bf3d4c4c6c324db5d385236f29d19ff171afaed3d53f8493655",
"documentation/docs/20-core-concepts/60-remote-functions.md": "ee1dd2b19712b6aa6ceac9cfbd4d6bd624658b2e1c94b62197bf812d6b7a9b2b"
}
}
147 changes: 147 additions & 0 deletions .claude/skills/kit-parity/fetch-upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env bash
# Fetch the SvelteKit files that svelte-local-query is ported from, hash them, and
# compare against baseline.json to detect upstream changes.
#
# Usage:
# fetch-upstream.sh check # download + report files changed since baseline
# fetch-upstream.sh baseline # download + rewrite baseline.json from current upstream
# fetch-upstream.sh download <dir> # just download everything into <dir>
#
# Files are downloaded into ./kit-upstream by default (or <dir> for `download`),
# preserving upstream relative paths. Exit code of `check` is 1 when changes exist.

set -euo pipefail

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASELINE="$SKILL_DIR/baseline.json"
BASE_URL="https://raw.githubusercontent.com/sveltejs/kit/main"

MODE="${1:-check}"
OUT_DIR="${2:-./kit-upstream}"

# Upstream files this library is ported from. Keep in sync with the mapping table in
# SKILL.md when adding coverage.
FILES=(
packages/kit/src/runtime/client/remote-functions/index.js
packages/kit/src/runtime/client/remote-functions/shared.svelte.js
packages/kit/src/runtime/client/remote-functions/cache.svelte.js
packages/kit/src/runtime/client/remote-functions/command.svelte.js
packages/kit/src/runtime/client/remote-functions/form.svelte.js
packages/kit/src/runtime/client/remote-functions/query-batch.svelte.js
packages/kit/src/runtime/client/remote-functions/prerender.svelte.js
packages/kit/src/runtime/client/remote-functions/query/index.js
packages/kit/src/runtime/client/remote-functions/query/instance.svelte.js
packages/kit/src/runtime/client/remote-functions/query/proxy.js
packages/kit/src/runtime/client/remote-functions/query/cache.js
packages/kit/src/runtime/client/remote-functions/query/proxy.svelte.spec.js
packages/kit/src/runtime/client/remote-functions/query-live/index.js
packages/kit/src/runtime/client/remote-functions/query-live/instance.svelte.js
packages/kit/src/runtime/client/remote-functions/query-live/proxy.js
packages/kit/src/runtime/client/remote-functions/query-live/cache.js
packages/kit/src/runtime/client/remote-functions/query-live/iterator.js
packages/kit/src/runtime/client/remote-functions/query-live/proxy.svelte.spec.js
packages/kit/src/runtime/client/remote-functions/cache.svelte.spec.js
packages/kit/src/runtime/client/remote-functions/instance.unhandled.svelte.spec.js
packages/kit/src/runtime/client/remote-functions/shared.transport.spec.js
packages/kit/src/runtime/form-utils.js
packages/kit/src/runtime/shared.js
packages/kit/src/utils/shared-iterator.js
packages/kit/types/index.d.ts
documentation/docs/20-core-concepts/60-remote-functions.md
)

download_all() {
local dir="$1"
for file in "${FILES[@]}"; do
mkdir -p "$dir/$(dirname "$file")"
if ! curl -sf "$BASE_URL/$file" -o "$dir/$file"; then
# a 404 usually means the file moved or was deleted upstream — that IS a change
echo "MISSING $file (moved or deleted upstream?)" >&2
rm -f "$dir/$file"
fi
done
}

hash_all() {
local dir="$1"
for file in "${FILES[@]}"; do
if [[ -f "$dir/$file" ]]; then
printf '%s %s\n' "$(sha256sum "$dir/$file" | cut -d' ' -f1)" "$file"
else
printf '%s %s\n' "missing" "$file"
fi
done
}

kit_version() {
curl -sf https://registry.npmjs.org/@sveltejs/kit/latest |
python3 -c 'import json,sys; print(json.load(sys.stdin)["version"])'
}

case "$MODE" in
download)
download_all "$OUT_DIR"
echo "downloaded to $OUT_DIR"
;;

baseline)
download_all "$OUT_DIR"
HASHES="$(hash_all "$OUT_DIR")"
VERSION="$(kit_version)"
printf '%s' "$HASHES" | python3 -c '
import datetime, json, sys
files = {}
for line in sys.stdin.read().strip().splitlines():
digest, path = line.split(" ", 1)
files[path] = digest
json.dump(
{
"kit_version": sys.argv[2],
"checked_at": datetime.date.today().isoformat(),
"files": files
},
open(sys.argv[1], "w"),
indent="\t"
)
print(f"baseline.json updated (kit {sys.argv[2]}, {len(files)} files)")
' "$BASELINE" "$VERSION"
;;

check)
download_all "$OUT_DIR"
HASHES="$(hash_all "$OUT_DIR")"
VERSION="$(kit_version)"
printf '%s' "$HASHES" | python3 -c '
import json, sys
baseline = json.load(open(sys.argv[1]))
baseline_version = baseline["kit_version"]
print(f"kit version: baseline={baseline_version} latest={sys.argv[2]}")
old_files = baseline["files"]
current = {}
for line in sys.stdin.read().strip().splitlines():
digest, path = line.split(" ", 1)
current[path] = digest
changed = []
for path, digest in current.items():
old = old_files.get(path)
if old is None:
changed.append(f"NEW {path}")
elif old != digest:
label = "MISSING " if digest == "missing" else "CHANGED "
changed.append(f"{label} {path}")
for path in old_files:
if path not in current:
changed.append(f"REMOVED {path}")
if changed:
print("\n".join(changed))
print(f"\n{len(changed)} tracked upstream file(s) differ from baseline")
sys.exit(1)
print("no upstream changes in tracked files")
' "$BASELINE" "$VERSION"
;;

*)
echo "unknown mode: $MODE (use check | baseline | download)" >&2
exit 2
;;
esac
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ without SvelteKit.
it or document the divergence in `DIFFERENCES.md`. Public types in `src/lib/types.ts`
are ports of kit's `Remote*` types renamed to `Local*` — keep them in sync structurally.

To check for and sync upstream changes, use the `kit-parity` skill
(`.claude/skills/kit-parity/`): it diffs tracked kit files against the recorded
baseline, classifies changes against DIFFERENCES.md, and walks through porting code and
tests.

## Architecture (3-layer split, ported from kit)

1. **Factory** (`query()`, `command()`, `form()` in `src/lib/query/index.ts`,
Expand Down
Loading