diff --git a/.claude/skills/kit-parity/SKILL.md b/.claude/skills/kit-parity/SKILL.md new file mode 100644 index 0000000..e448013 --- /dev/null +++ b/.claude/skills/kit-parity/SKILL.md @@ -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 ", 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. diff --git a/.claude/skills/kit-parity/baseline.json b/.claude/skills/kit-parity/baseline.json new file mode 100644 index 0000000..3829432 --- /dev/null +++ b/.claude/skills/kit-parity/baseline.json @@ -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" + } +} diff --git a/.claude/skills/kit-parity/fetch-upstream.sh b/.claude/skills/kit-parity/fetch-upstream.sh new file mode 100755 index 0000000..e5f077b --- /dev/null +++ b/.claude/skills/kit-parity/fetch-upstream.sh @@ -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 # just download everything into +# +# Files are downloaded into ./kit-upstream by default (or 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 diff --git a/CLAUDE.md b/CLAUDE.md index 2e07f57..75dfe2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`,