fix(optimistic): apply optimistic UI patch client-side (closes #16)#17
Merged
Conversation
The optimistic-UI protocol was implemented server-side and the client
defined applyOptimistic(), but dispatch() never called it — the patch
shipped in the same response as the full render, so the UI only ever
reflected the authoritative server state after the round-trip.
applyOptimistic was dead code and the docstrings promised instant
feedback the runtime never delivered.
Implements Option A from the issue (declarative client-side prediction):
- dispatch() accepts an options.optimistic patch and applies it
synchronously before the fetch, reconciling on success and rolling
back on error.
- Triggers declare predictions via data-optimistic='{...}' or the
data-optimistic-toggle="field" shorthand; the bind handler computes the
patch and passes it to dispatch().
- applyOptimistic() now also exposes data-optimistic and
data-optimistic-<field> attributes as CSS styling hooks.
- Ship component-framework.css with [data-loading] / [data-optimistic]
default styles (reduced-motion aware).
- Correct the docstrings on dispatch(), applyOptimistic(),
get_optimistic_patch(), and the .d.ts to describe actual behavior.
- Add node:test coverage (tests/js/optimistic.test.mjs) proving the
prediction is applied before the response and reconciled/rolled back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
…markers Review of #17 surfaced three gaps: - JS tests were orphaned: `just test` runs pytest only and CI had no Node step, so the suite never ran automatically. Add a `test-js` justfile recipe (+ `test-all`) and a `test-js` CI job (Node 22). - The documented run command was a bare directory, which Node 25 treats as a module path and fails; use a node-expanded glob instead, and fix the in-file run instructions (avoiding `*/` inside the block comment). - `update()` left `data-optimistic*` markers stuck on the element when the server returned empty HTML (element not replaced). Add `_clearOptimistic()` and call it on that early-return path; covered by a new test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
These files (spec-kit scaffolding, docs, an issue template) carried trailing-whitespace / missing-final-newline that the hand-rolled CI lint never checked. Running the pre-commit hooks via prek in CI surfaces them, so normalize once. Mechanical only — no content changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
CI's lint job hand-rolled `ruff`/`ty` calls, which could (and did) diverge from local pre-commit. ty is pre-1.0 and a version after 0.0.25 changed its exit code to fail on warnings, turning the intentionally warn-level Django mixin/ORM diagnostics into a red build. - Pin `ty==0.0.25` in the dev deps so CI matches the local/working version that exits 0 on warn-level diagnostics; bump deliberately. - Replace the lint job's manual ruff/ty steps with `prek run --all-files` so CI runs the exact pinned hooks from .pre-commit-config.yaml (single source of truth with local pre-commit). Add `prek` to dev deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
The CI jobs hand-rolled `pip install -e .[dev]`, but the project standardizes on uv with a committed uv.lock. Switch all Python jobs to `astral-sh/setup-uv` + `uv sync --extra dev --locked` + `uv run`, so CI installs the exact locked dependency set and matches local workflow. Refresh uv.lock for the pyproject changes (adds prek, pins ty==0.0.25). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
uv.lock is gitignored, so `uv sync --locked` has no lockfile to resolve against in a clean CI checkout. Use `uv pip install --system -e .[dev]` instead — still uv (project standard), resolves fresh like a library, and needs no committed lock. Commands run directly on the runner's Python. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #16. The optimistic-UI protocol was implemented server-side (
get_optimistic_patch→response.optimistic) and the client definedapplyOptimistic(), butdispatch()never called it. Because the patch shipped in the same HTTP response as the full render, instant feedback was structurally impossible —applyOptimisticwas dead code and the docstrings promised behavior the runtime never delivered.This implements Option A from the issue (declarative client-side prediction).
Changes
component-client.jsdispatch(componentId, event, payload, options)now acceptsoptions.optimisticand applies the predicted patch synchronously beforefetch, reconciling viaupdate()on success androllback()on error (snapshot/rollback already existed).data-optimistic='{...}'(explicit) ordata-optimistic-toggle="field"(boolean toggle shorthand) and passes it through.applyOptimistic()now also exposesdata-optimisticanddata-optimistic-<field>attributes as CSS styling hooks (scalars only).css/component-framework.css(new) — default[data-loading]and[data-optimistic]styles,prefers-reduced-motionaware.component-client.d.ts— newDispatchOptions, updateddispatch/applyOptimistic/ComponentResponse.optimisticdocs.core/component.py—get_optimistic_patchdocstring corrected: the server patch ships with the render and does not itself produce instant feedback; points users to the client-side declarative mechanism.Acceptance criteria (from #16)
data-optimistic*+[data-loading]/[data-optimistic]CSS).applyOptimisticis wired into a real code path.get_optimistic_patchanddispatch()match actual behavior.Testing
tests/js/optimistic.test.mjs(node:test, no jsdom) — 7 tests proving the prediction is applied before the response, reconciled on success, and rolled back on error. Run:node --test tests/js/optimistic.test.mjs.ruff check/ruff format --checkclean.Notes
No TS→JS or SCSS build step exists in this repo;
.js/.d.ts/.cssare hand-authored source. Assets follow that convention.🤖 Generated with Claude Code