djust 1.1.0rc2

Pre-releaseReleased

This is a pre-release. djust 1.1.0 has shipped since: read the djust 1.1.0 release notes.

Fixed

  • live_redirect to a non-LiveView path now falls back to a full-page navigation instead of stranding the page (#1934). With auto_navigate defaulting ON in v1.1, a live_redirect whose target is a plain Django view (e.g. a TemplateView) left the URL bar on the new path while the previous LiveView stayed mounted — the URL led the DOM with no swap. Two coupled client bugs in handleLiveRedirect (python/djust/static/djust/src/18-navigation.js): (1) the pushState fired BEFORE the view resolution, so the URL changed for a target that never got a DOM swap; and (2) — the load-bearing root cause found by symptom-up tracing, NOT the issue's cited "resolveViewPath returns falsy" — the resolution used resolveViewPath(), which has a container fallback that returns the CURRENT [dj-view]'s class on a route-map miss. That fallback is documented "only works for live_patch, not cross-view navigation", so for a cross-view live_redirect to a non-LiveView it returned the SOURCE view (truthy) and the client SPA-mounted the OLD view under the NEW URL — the exact reported symptom (URL /onboarding/, but the jira view mounts). The server's #1647_resolve_view_path_from_url guard also returns None for a non-LiveView URL and keeps the stale client-supplied view, so the client must make the full-nav decision. Fix: a new STRICT resolveLiveViewPath() (route map ONLY, no container fallback) drives the cross-view decision; the pushState + URL-dependent side effects (updateAriaCurrent, scroll, before-navigate) are DEFERRED into the LiveView-resolved + WS-connected branch, so the URL never leads the DOM. A non-LiveView target (or a disconnected WS) does a full-page navigation validated through window.djust.safeNavigationTarget (mirroring the existing cross-origin branch, with the safeNavigationTarget open-redirect/javascript: guard). The popstate back-nav redirect (the #1646 twin) also switched to the strict resolver, so a back-nav to a non-LiveView reloads correctly instead of re-mounting the source view. The served minified bundle (client.min.js + .gz/.br/.map) was rebuilt to carry the fix. Reproduce-first + gate-off (#1468) verified: new cases in describe('issue #1934 …') (tests/js/navigation.test.js) — non-LiveView target: full-page nav, NO pushState, NO WS mount (strand-free), positive case: a LiveView target still SPA-mounts, and LiveView target but WS not connected: full-page nav — go RED when EITHER half of the fix is reverted (the strict-resolver call → the SPA branch fires safeNavigationTarget never called; the pushState-first order → the strand pushState assertion fails). Full JS suite green (1746 passed).

  • De-flaked TestMountAsyncAndPushDrain::test_mount_dispatches_async_work under parallel -n auto by OWNING THE COMPLETION SIGNAL instead of bounded-polling the scheduler (#1931; the async-dispatch sibling of #1930). The test mounts a view that schedules a background callback via start_async() in mount(), then asserts the callback ran (view.value == 42). The runtime dispatches that callback FIRE-AND-FORGET via asyncio.ensure_future(self._execute_async_task(...)) inside ViewRuntime._dispatch_async_work (python/djust/runtime.py:4444), and _execute_async_task ITSELF awaits a sync_to_async(callback) thread-pool round-trip before setting value=42. The original test waited for that to land via a BOUNDED poll — for _ in range(10): await asyncio.sleep(0) — a wall-clock-fragile race: under a CPU-saturated parallel loop the asyncio scheduler can fail to run the spawned task (which also competes for the thread pool) within 10 yields, so the assertion fired while value was still 0 (flaked 1/4 runs in the #1930 worktree; passed 3/3 in isolation). This is the CLAUDE.md bounded-poll-racing-a-real-scheduler class (#1830/#1815 family), NOT a code regression — the mount async-dispatch feature is correct and lands every time given enough scheduler turns. Fix (in python/djust/tests/test_transport_behavioral_parity.py, test-only — production unchanged): wrap the runtime's asyncio.ensure_future seam during dispatch_mount to capture the EXACT _execute_async_task task handle it spawns (the _flush_push_events fire-and-forget send, which uses the same primitive, is excluded by coroutine name so the gate-off stays sharp), then await asyncio.gather(*async_work_tasks) — a deterministic completion signal, no timing bound. Reproduce-first verified: shrinking the poll bound to 0–1 yields makes the OLD form fail 25/25 (proving the margin is razor-thin), while the new form passes 0/15 failures under 8-way CPU saturation. Gate-off (#1468) verified: disabling the _dispatch_async_work(None) call in dispatch_mount spawns no _execute_async_taskassert async_work_tasks fails (empty list), so the test is load-bearing on the actual async-dispatch path. 3-clean-runs gate (#1174): full suite -n auto × 3 all clean (8604 passed each). New behavior in test_mount_dispatches_async_work.

  • De-flaked the six rate-limit burst-exhaustion tests under -n auto by OWNING THE CLOCK — test_ping_flood_triggers_disconnect no longer flakes on a wall-clock token refill (#1930).TestRateLimiter + TestGlobalRateLimit (tests/unit/test_event_security.py) build test-local TokenBucket / ConnectionRateLimiter instances and assert burst exhaustion (e.g. rate=100, burst=2 → the 3rd check() must be False). TokenBucket read time.monotonic() directly, so under CPU-saturated parallel make test real wall-clock elapsed between consume() calls and the refill math tokens + elapsed * rate (rate=100 = 1 token / 10ms) added a token back — flipping a "burst exhausted → False" assertion non-deterministically to True. This is the CLAUDE.md flaky-timing class (never gate pass/fail on wall-clock; #1830/#1815 family), NOT the #1883 shared-global pollution class the issue hypothesized — every limiter here is test-local and reads no leaked global. Fix: a _monotonic = time.monotonic module-level seam in python/djust/rate_limit.py (production behavior identical; one indirection) routes TokenBucket.__init__ + consume() through a patchable name WITHOUT patching the global time module; a FakeClock + frozen_clock pytest fixture monkeypatches djust.rate_limit._monotonic so the five burst-exhaustion tests run on a FROZEN clock (elapsed == 0 → no refill → deterministic), and test_token_bucket_refills replaces time.sleep(0.05) with frozen_clock.advance(0.05) (deterministic, instant, still genuinely exercises the refill path). Reproduce-first verified: advancing the clock 15ms between burst checks flips the 3rd ping check False → True. Gate-off (#1468) verified: with an advancing clock the rate=100 tests go RED at a 15ms stall and the slower rate=10/rate=1 tests go RED at a 2s stall (frozen clock load-bearing for all six), and the refill advance() is load-bearing (without it the drained token stays unavailable). 3-clean-runs gate (#1174): full suite -n auto × 3 all clean (8603 passed each, the unrelated pre-existing async-timing flake #1931 deselected). Fixture applies to the burst tests in TestRateLimiter and TestGlobalRateLimit.

All releases · Atom feed