Skip to content

fix(optimistic): apply optimistic UI patch client-side (closes #16)#17

Merged
fsecada01 merged 6 commits into
masterfrom
fix/16-optimistic-ui-client-prediction
Jun 24, 2026
Merged

fix(optimistic): apply optimistic UI patch client-side (closes #16)#17
fsecada01 merged 6 commits into
masterfrom
fix/16-optimistic-ui-client-prediction

Conversation

@fsecada01

Copy link
Copy Markdown
Owner

Summary

Fixes #16. The optimistic-UI protocol was implemented server-side (get_optimistic_patchresponse.optimistic) and the client defined applyOptimistic(), but dispatch() never called it. Because the patch shipped in the same HTTP response as the full render, instant feedback was structurally impossible — applyOptimistic was 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.js
    • dispatch(componentId, event, payload, options) now accepts options.optimistic and applies the predicted patch synchronously before fetch, reconciling via update() on success and rollback() on error (snapshot/rollback already existed).
    • The declarative bind handler computes the patch from data-optimistic='{...}' (explicit) or data-optimistic-toggle="field" (boolean toggle shorthand) and passes it through.
    • applyOptimistic() now also exposes data-optimistic and data-optimistic-<field> attributes as CSS styling hooks (scalars only).
  • css/component-framework.css (new) — default [data-loading] and [data-optimistic] styles, prefers-reduced-motion aware.
  • component-client.d.ts — new DispatchOptions, updated dispatch/applyOptimistic/ComponentResponse.optimistic docs.
  • core/component.pyget_optimistic_patch docstring 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)

  • A click produces an immediate visual change before the server responds (predicted state via data-optimistic* + [data-loading]/[data-optimistic] CSS).
  • applyOptimistic is wired into a real code path.
  • Docstrings on get_optimistic_patch and dispatch() match actual behavior.

Testing

  • New 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.
  • Full Python suite green: 408 passed.
  • ruff check / ruff format --check clean.

Notes

No TS→JS or SCSS build step exists in this repo; .js/.d.ts/.css are hand-authored source. Assets follow that convention.

🤖 Generated with Claude Code

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
@fsecada01 fsecada01 added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request priority:3-medium Medium priority labels Jun 24, 2026
fsecada01 and others added 5 commits June 24, 2026 15:43
…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
@fsecada01 fsecada01 merged commit 52c341b into master Jun 24, 2026
7 checks passed
@fsecada01 fsecada01 mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request priority:3-medium Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimistic UI patch is never applied client-side — applyOptimistic is dead code

1 participant