This is a pre-release. djust 0.9.2 has shipped since: read the djust 0.9.2 release notes.
First release candidate for 0.9.2. Bundles three drain buckets shipped after 0.9.1 (2026-04-30): 0.9.2-1 (SSE transport DRY refactor — 5 issues, headlined by #1237), 0.9.2-2 (pipeline-template canon batch — 3 issues), 0.9.2-3 (VDOM correctness hardening Phase 1 — 5 issues). Plus the v0.9.2-3 audit doc (docs/vdom/AUDIT-2026-04-30.md). 13 issues closed across 5 PRs (#1238, #1239, #1241, #1242, #1246, #1247, #1257, #1258); 1 known issue surfaced during RC pre-flight (#1260 — fuzz-test mixed-keyed/unkeyed diff round-trip; deferred to v0.9.2-4 before stable).
Fixed
VDOM: stale
cached_htmlfordj-update="ignore"subtrees (#1252).splice_ignore_subtrees(crates/djust_vdom/src/lib.rs) used to copy the old node'scached_htmlinto the new node, which meant a conditional re-render that wraps an ignored subtree would keep serving the OLD cached HTML on subsequent diffs. The cache is now cleared (= None) during splice;cache_ignore_subtree_htmlrecomputes lazily on the next render. 4 regression tests incrates/djust_vdom/tests/test_ignore_subtree_invalidation_1252.rs.VDOM:
dj-idtemplate-injection defense-in-depth (#1253). The Rust parser now validates user-supplieddj-idattribute values against base62 (^[0-9a-zA-Z]+$) before the server-side ID generator overwrites them. Malformed values (whitespace, special chars, Unicode tricks) are dropped with a debug-levelparser_trace!warning. The server-generated ID always wins; this fix tightens the pre-overwrite read path so any error/log surface that touches the prior value sees a sanitized form. 4 regression tests incrates/djust_vdom/tests/test_dj_id_validation_1253.rs.VDOM: duplicate
dj-keyand mixed-keyed-unkeyed warnings now fire attracing::warn!(#1254). Both warnings incrates/djust_vdom/src/diff.rspreviously usedvdom_trace!()(gated behindDJUST_VDOM_TRACE=1), so developers in production had no visibility into silent VDOM correctness issues. The mixed-keyed warning now fires with stable error codeDJE-050; the duplicate-key warning withDJE-051. The previously-citedhttps://djust.org/errors/DJE-050URL — which didn't exist — has been removed. Structured logging (key passed via{}placeholder) ensures the warnings are not log-injection vulnerable. 4 regression tests incrates/djust_vdom/tests/test_diff_warnings_1254.rs.VDOM JS: Web Components and custom elements no longer silently replaced with
<span>(#1255). The patcher's element-creation whitelist inpython/djust/static/djust/src/12-vdom-patch.jswas hardcoded toALLOWED_HTML_TAGS+SVG_TAGS, rejecting Web Components (<my-component>,<sl-button>,<model-viewer>, etc.) and replacing them with a fallback<span>. The patcher now accepts any tag matching the HTML spec's custom-element rule (tag.includes('-')) and exposes awindow.djustAllowedTagsruntime-configurable hook for same-origin allowlist extensions.<script>and<iframe>remain blocked unchanged (<script>is not in the allowlist and lacks a hyphen;<iframe>is unaffected by this change since it's already in the existingALLOWED_HTML_TAGSwhitelist for legitimate use). 7 regression tests intests/js/vdom_web_components_1255.test.js.VDOM: extended SVG attribute camelCase normalization (#1256). The Rust parser's
normalize_svg_attribute()table incrates/djust_vdom/src/parser.rswas missing modern SVG attributes (filter primitives, animation timing, gradient transforms, font-face metrics). Browsers'setAttributeNSis case-sensitive; without normalization, the unknown camelCase attrs were silently ignored, producing visually-broken SVG. 11 new attrs added; 11 regression tests incrates/djust_vdom/tests/test_svg_attr_normalization_1256.rs- 10 new cases on the existing in-module test.
Added
- Transport-agnostic
ViewRuntimeshared between WebSocket and SSE (#1237). Newpython/djust/runtime.pymodule factors out view-lifecycle dispatch (dispatch_mount,dispatch_event,dispatch_url_change) so both transports share one code path for these message types. WebSocket'shandle_url_changeis now a thin shim overViewRuntime.dispatch_url_change; SSE's newPOST /djust/sse/<session_id>/message/endpoint dispatches identically. First slice of a multi-PR migration that will progressively move the remaining WS handlers (handle_event,handle_mount,handle_mount_batch) onto the shared runtime. Architecture decision documented in ADR-016. LiveViewSSE.sendMessage(data)— parity withLiveViewWebSocket(#1237). ExistingliveViewWS.sendMessage(...)call sites in18-navigation.js,02-response-handler.js,13-lazy-hydration.js, and15-uploads.jsnow work transparently when the SSE transport is active — no callsite-by-callsite branching. The existingsendEventAPI is preserved (delegates tosendMessage). The legacyPOST /djust/sse/<sid>/event/endpoint stays as a back-compat alias.
Fixed
- SSE: URL kwargs resolved from the mount-frame URL, not the SSE endpoint path (#1237). Previously a view like
path("items/<int:pk>/", ItemView.as_view())mounted with emptykwargsover SSE because_sse_mount_viewresolved againstrequest.path(the SSE endpoint URL/djust/sse/<uuid>/, not the page). The client now sends a WebSocket-shaped mount frame containingurl: window.location.pathname, and the server resolves kwargs against that URL — matching the WebSocket transport exactly. The HTTP Referer header is deliberately not used for this; seedocs/sse-transport.md#why-not-the-referer-headerfor why. - SSE:
LiveView.handle_params()is now invoked after mount and onurl_change(#1237). Phoenix-parity contract:handle_params(params, uri)fires once aftermount()and on every subsequent URL change. Previously SSE never called it, causing views that read URL state inhandle_params(active tab, sort, page) to keep mount-time defaults regardless of query string. - SSE:
liveViewWS.sendMessage({type: 'url_change', ...})no longer TypeErrors (#1237)._executePatch()in18-navigation.jscallssendMessagefordj-patchURL updates; under SSE this previously crashed becauseLiveViewSSEhad nosendMessagemethod. Now both transports expose the same outbound API. Eight other JS call sites (popstate, lazy-hydration, response-handler, uploads, navigation) are also unblocked. - Service Worker reconnection bridge no longer needlessly buffers SSE payloads (#1237).
33-sw-registration.jspatchessendMessageto buffer payloads when the WebSocket is closed. With SSE now also exposingsendMessage, the patch was unconditionally applying — and becausews.wsis undefined onLiveViewSSE, every SSE payload was treated as "socket closed" and forwarded to the SW. The patch now short-circuits onLiveViewSSEinstances since SSE usesfetch()directly and doesn't need the WS reconnection buffer. ViewRuntime.dispatch_mountrejectsuse_actors=Trueviews with a structured error envelope over SSE (#1240). Closes plan-fidelity gap from #1237 — actor-based state management requires the channel-layer code inwebsocket.pywhich the runtime path doesn't traverse. Previously ause_actors=Truemount over SSE would partially succeed and fail downstream with an opaqueAttributeError. Nowdispatch_mountshort-circuits with a clear "use_actors is not supported over SSE; mount over WebSocket instead" envelope. ADR-016 §Implementation notes promised this guard; PR #1239 deferred it to this follow-up.
Developer Experience
- Pipeline-bypass CI check — daily retro-gate audit (#1234). New scheduled GHA
.github/workflows/retro-gate-audit.ymlrunsscripts/audit-pipeline-bypass.pydaily at 13:00 UTC against the most recent 50 merged PRs and surfaces any PR missing retro markers as workflow annotations. Part 2 of #1212 (part 1 was the audit script shipped in PR #1229). Manualworkflow_dispatchtrigger included for ad-hoc audits. - Isolated cargo-test target for
filter_registry::tests(#1235). The hot-path short-circuit tests for theANY_CUSTOM_FILTERS_REGISTEREDAtomicBool now live atcrates/djust_templates/tests/test_filter_registry_isolated.rs(an integration-test binary). Cargo runs each integration-test file in its own process, so the process-global flag starts clean for every run — the previousOnceLockworkaround that gated the in-module test on whether a prior test had already registered a filter is no longer needed. Carryover from #1180 item 4. - VDOM engine audit and v0.9.2-3 milestone (
docs/vdom/AUDIT-2026-04-30.md). Synthesizes architecture map, bug archaeology (14 historical bugs across 7 themes), 10 ranked current-code weaknesses (3 🔴 / 7 🟡), test gaps, and a 4-phase improvement roadmap. Phase 1 (5 quick wins, #1252-#1256) opens as the v0.9.2-3 drain bucket; Phase 2 (correctness hardening) and Phase 3 (architectural — text-node djust_ids, unified focus state- machine) are deferred to later milestones. - Pipeline-template canon — Stage 4 + Stage 7 additions (#1243 + #1244). Two mandatory checklist items added symmetrically to
.pipeline-templates/{feature,bugfix}-state.json:- Stage 4 VERIFY LITERAL API CONTRACTS — for every literal API call in the plan (function names, kwargs, return shapes), grep for the existing convention before locking. Pattern from #1240/#1242 where the plan said
type="mount_error"but convention waserror_type=. - Stage 7 WORKFLOW-HEADER CROSS-REF — when changed files include
.github/workflows/*.ymlor any file with a runtime-behavior docstring, list every behavioural claim and verify each against actual step semantics. Pattern from #1241 where the workflow's header said "annotations not red runs" butpipefailmade every flagged run red.
- Stage 4 VERIFY LITERAL API CONTRACTS — for every literal API call in the plan (function names, kwargs, return shapes), grep for the existing convention before locking. Pattern from #1240/#1242 where the plan said
- Pipeline-run Stage 14 retro-post — Write tool +
gh --body-file(#1245). Updates.pipeline-templates/{feature,bugfix}-state.jsonStage 14 subagent_prompt to use Claude'sWritetool to createpr/feedback/retro-<N>.mdandgh pr comment <N> --body-file <path>to post — replacing the previouscat > file <<EOF+--body "$(cat file)"pattern that silently failed under zshset -o noclobber(a common .zshrc safety guard). All 3 v0.9.2-1 implementation PRs (#1239, #1241, #1242) hit this and had their retros backfilled during the milestone retro audit; the new pattern is structural (sidesteps any shell-init quirk, not just noclobber) rather than a per-quirk patch. - Release-workflow dep-bump label gate (#1236). New GHA
.github/workflows/check-release-workflow-deps.ymlruns on PRs modifying release-critical workflow files (release.yml,publish.yml,release-drafter.yml,pre-release-security-audit.yml) and fails unless the PR carries therelease-workflow-reviewedlabel, forcing explicit human risk-review before merge. Triggered by PR #1233 (action-gh-release v2 → v3) landing in the same window as the v0.9.1 cut. Therelease-workflow-reviewedlabel was added to the repo alongside this workflow.