This is a pre-release. djust 1.0.0 has shipped since: read the djust 1.0.0 release notes.
Before you upgrade, read the upgrade guide.
Fixed
- VDOM
{% if %}/{% elif %}branch swap no longer produces a doubled or stale subtree (#1550, #1552; #1552 was a P0 regression from 0.9.6rc2). Both bugs trace to a single root cause: dj-id counter collisions whenlast_vdommigrates across worker threads OR is msgpack-roundtripped throughInMemoryStateBackend.get(). The thread-local djust_id counter generates monotonically-increasing ids duringparse_html_continue, but the new thread's counter is independent oflast_vdom's ids. The next parse generates ids1..kthat collide with surviving ids inlast_vdom. TheInsertSubtree.htmlpatch then carries dj-ids matching other elements in the parent's child list, and the client's id-first:scope > [dj-id=N]resolver picks the newer element instead of the older one to remove — wrong subtree removed, old content survives. Why v0.9.6rc2 worked: pre-#1538 (commit0a119962, serde-default fix), msgpack deserialize failed silently andstate_backends/memory.py:118returnedNoneon every cache lookup, full-remounting on every event without running the diff path. Post-#1538 deserialize succeeds, the diff path runs, and the collision shape became reachable. (The earlier diff-layer hypothesis pursued in #1553 —child_d: Nonepropagation post-#1538 — was empirically disconfirmed by VNode-level reproducers showing the differ correct at both 0.9.6rc2 AND 1.0.0rc4.) Fix: before eachparse_html_continueinRustLiveViewBackend::render_with_diffand the text-fast-path entry, walklast_vdom, computemax_djust_id_in(old_vdom), and callensure_id_counter_at_least(max + 1). The thread-local counter becomes effectively per-view, surviving thread handoff and msgpack roundtrip. Three new public helpers indjust_vdom:from_base62(s)(decode base62 string to u64),max_djust_id_in(node)(walk VNode tree for highest djust_id),ensure_id_counter_at_least(min)(monotonic counter advance). Covered by 12 Rust unit cases incrates/djust_vdom/tests/test_id_counter_monotonicity_1550_1552.rs(round-trip, invalid input rejection, max-walk on id-less / single / nested trees, monotonic semantics) plus 4 Python E2E cases inpython/tests/test_if_elif_swap_e2e_1550_1552.py(collision-free InsertSubtree.html, uniquely-resolvable RemoveChild post-insert, if/else branch swap, msgpack-roundtrip counter advance). - Multi-line
{# ... #}comments containing template-tag syntax no longer crash Django classical (#1551). djust ships two template renderers: the Rust engine (crates/djust_templates/src/lexer.rs:289-305) treats{# ... #}as opaque even across newlines, but Django's classical tokenizer (django.template.base.Lexer) uses a non-DOTALL regex — multi-line{# ... #}was NOT recognized as a single comment, so a{% if %}inside the comment body parsed as a real tag and raisedTemplateSyntaxError. The mismatch was silent: templates rendered fine via the LiveView WebSocket path (Rust) and crashed viaclient.get()in pytest, Django's debug error renderer, or any view usingrender()directly. Follow-up to #1423. Fix: newdjust.template.loaders.FilesystemLoaderanddjust.template.loaders.AppDirectoriesLoader— drop-in replacements for the standard Django loaders that preprocess{# ... #}blocks out of the template source before Django classical's tokenizer sees them. Single-line comments are stripped too (Django strips them anyway). Projects opt in by replacing the default loaders inTEMPLATES['OPTIONS']['loaders']. Performance: one regex pass per template load (<1ms); loaded templates are cached so this runs once per template, not per render. Covered by 10 regression cases inpython/djust/tests/test_multiline_comment_parity_1551.py, including a control that pins the bug shape in vanilla Django classical.