Added
WebSocket per-message compression toggle —
DJUST_WS_COMPRESSION(v0.6.0) — VDOM patches compress extremely well (repetitive HTML fragments + JSON structure → 60-80 % wire-size reduction via zlib). Uvicorn and Daphne both negotiatepermessage-deflatewith browsers out of the box, so the wire-level compression is already free in most deployments — this change adds the declarative config toggle + documentation so operators can verify it's active, reason about the ~64 KB per-connection zlib context cost, and disable it cleanly on extreme-connection-density deployments or when running behind a compressing CDN. Newwebsocket_compressionconfig key (defaultTrue) exposed viadjust.config.config, bridged from a top-levelsettings.DJUST_WS_COMPRESSIONfor discoverability, and surfaced to the injected client bootstrap aswindow.DJUST_WS_COMPRESSION(application code can branch on it to skip manualJSON.stringifyoptimizations that only help without wire-level compression). 6 tests intests/unit/test_ws_compression_config.pycover default, override to True/False, truthy/falsy coercion, and client-script emission. Deployment guide (docs/website/guides/deployment.md) gains a new "WebSocket per-message compression" section covering the memory tradeoff, CDN double-compression footgun, and Uvicorn/Daphne flags. (python/djust/config.py,python/djust/mixins/post_processing.py)Declarative UX attributes —
dj-mutation,dj-sticky-scroll,dj-track-static(v0.6.0) — Three small client-side declarative attributes that replace boilerplatedj-hooks every production app tends to write.dj-mutation(newstatic/djust/src/37-dj-mutation.js, ~100 LOC) fires adj-mutation-fireCustomEvent when the marked element's attributes or children change via MutationObserver, withdj-mutation-attr="class,style"for targeted attribute filters anddj-mutation-debounce="N"for burst coalescing (default 150 ms).dj-sticky-scroll(new38-dj-sticky-scroll.js, ~90 LOC) keeps a scrollable container pinned to the bottom when children are appended but backs off when the user scrolls up to read history and resumes when they return to the bottom — the canonical chat / log viewer UX with a 1 px sub-pixel tolerance.dj-track-static(new39-dj-track-static.js, ~90 LOC; Phoenixphx-track-staticparity) snapshots tracked<script src>/<link href>values on page load and, on every subsequentdjust:ws-reconnectedevent, diffs against the snapshot — dispatchesdj:stale-assetsCustomEvent on changed URLs, or callswindow.location.reload()when the changed element carrieddj-track-static="reload". Without this last one, clients on long-lived WebSocket connections silently run stale JS after a deploy — zero-downtime on the server but broken behavior on connected clients. Supporting change in03-websocket.js:onopennow dispatchesdocument.dispatchEvent(new CustomEvent('djust:ws-reconnected'))on every reconnect so application code (not justdj-track-static) can hook reconnects without touching internal WS state. Convenience Django template tag{% djust_track_static %}inlive_tags.pyemits the bare attribute for discoverability. All three attributes live-register via a document-level MutationObserver root (same pattern asdj-dialog) so VDOM morphs that inject or remove the marker re-wire observers automatically. 15 JSDOM test cases acrosstests/js/dj_mutation.test.js,tests/js/dj_sticky_scroll.test.js,tests/js/dj_track_static.test.js; 4 Python test cases intests/unit/test_djust_track_static_tag.py. (python/djust/static/djust/src/37-dj-mutation.js,38-dj-sticky-scroll.js,39-dj-track-static.js,03-websocket.js,python/djust/templatetags/live_tags.py)See
docs/website/guides/declarative-ux-attrs.md.djust.db.untrack(model)— disconnect signal receivers wired by@notify_on_save(#809) — Previously the only way to detach thepost_save/post_deletereceivers from a@notify_on_save-decorated model was to clear the entiresignals.receiverslist, which scorched unrelated test fixtures.untrack()now disconnects exactly the two receivers stashed onmodel._djust_notify_receiversand wipes the introspection attributes (_djust_notify_channel,_djust_notify_receivers) so a re-decoration goes through cleanly with a fresh channel. ReturnsTrueon success,Falseon a never-decorated model — idempotent, safe to call twice. Primarily for pytest teardowns in projects that decorate models at class-definition time. 5 tests intests/unit/test_db_notifications.py::TestUntrack. Exported fromdjust.dband documented in thedjust.dbmodule docstring. (python/djust/db/decorators.py,python/djust/db/__init__.py)See
docs/website/guides/database-notifications.md.Pre-minified
client.jsdistribution (v0.6.0 P1) — Production now servesclient.min.js(terser-minified) instead of the 35-module readable concat, with.gzand.brpre-compressed siblings built alongside it for whitenoise / nginx static serving. Measured impact:client.js410 KB →client.min.js146 KB raw → 39 KB gzip → 33 KB brotli (~92% reduction wire-size over the raw file).DEBUG=Truecontinues to serve the readableclient.jsso stack traces point at meaningful line numbers and contributors can poke at source directly. An explicitDJUST_CLIENT_JS_MINIFIEDsetting (bool) overrides theDEBUGheuristic in either direction so operators can validate the minified file locally or keep the readable build in production if they want to debug in-situ.scripts/build-client.shgained aminify_and_compresshelper that runs terser (fromnode_modules/.bin/terseror PATH), then gzip-9and brotli-q 11; the step is skipped gracefully when terser isn't installed so contributors can still iterate on raw sources withoutnpm install. Source-maps (.min.js.map) are emitted for production-side debugging.djust.C012system check now recognizes bothclient.jsandclient.min.jsin manual-loading detection. 6 tests intests/unit/test_client_minified.pycover build-artifact presence + size reduction, DEBUG-vs-production script selection, and the explicit override in both directions. (scripts/build-client.sh,python/djust/mixins/post_processing.py,python/djust/checks.py,package.json)
Changed
- Documented block-handler nesting + loader-access constraints (#803, #804) — Two low-priority gaps deferred from PR #802 are now surfaced in both the Rust-side
register_block_tag_handlerdocstring (crates/djust_templates/src/registry.rs) and the Python-side.pyistub (python/djust/_rust.pyi). The "no parent-tag propagation" constraint (#804) means a nested block handler is not informed it sits inside a parent handler — pass a hint throughcontextinstead. The "no loader access from handlers" constraint (#803) means block handlers cannot call{% render_template %}-style loads — pre-render child templates in the view. Both constraints were silently-true before this change; surfacing them prevents surprise when handler authors reach for features the current dispatcher doesn't yet support. No runtime behavior change. (crates/djust_templates/src/registry.rs,python/djust/_rust.pyi)
Fixed
assign_asyncconcurrent same-name cancellation semantics (#793) — Two rapidassign_async("metrics", loader)calls used to race: the first loader's worker thread could still be in-flight when the second call scheduled a new task, and when the slow loader finally completed, itssetattr(self, "metrics", AsyncResult.succeeded(stale))clobbered the freshAsyncResult.pending()that the second call had just written.assign_async()now maintains a per-attribute generation counter (self._assign_async_gens[name]) bumped on every call; each loader's runner closure captures the generation at creation time and short-circuits on both the success and error paths when a newer call has superseded it. The in-flight stale runner still completes (no mid-flight cancellation), but its result is discarded via a DEBUG log — the fresh pending state survives. 4 regression cases intests/unit/test_assign_async.py: sync success-path, sync error-path, async-loader success-path, and a generation-counter sanity check. (python/djust/mixins/async_work.py)Template dep-tracking: filter-arg bare identifiers (#787) —
{{ value|default:fallback }}now tracksfallbackas a template dependency alongsidevalue. Previously the dep-extractor walked filter chains but dropped all filter arguments, so a pattern like{% if show %}{{ value|default:dynamic }}{% endif %}would fail to re-render when onlydynamicchanged — the render cache classified the node as dep-clean and the partial-render pipeline skipped it. Literal filter args (default:"none",default:'none',default:0,default:-1) are correctly excluded from the dep set; only bare identifiers and dotted paths are tracked. Landed via a two-step:parse_filter_specsnow preserves surrounding quotes on literal args so the extractor can distinguish literals from identifiers, and render-time filter application strips quotes via the newstrip_filter_arg_quoteshelper. No change to filter runtime semantics. 15 regression cases intests/unit/test_template_dep_tracking_787_806.py. (crates/djust_templates/src/parser.rs,crates/djust_templates/src/renderer.rs)Template for-iterables resolve through getattr walk (#806) —
{% for x in foo.bar %}now usesContext::resolve(which walks getattr through the raw-PyObject sidecar) with a fallback toContext::get, instead of only consulting the value-stack. Previously dotted iterables silently rendered as empty when the attribute was not a top-level dict key — affecting Django QuerySet relations (user.orders), dataclass attributes, and nested Python objects. Covered by two direct-access tests (nested attributes + relation stub) + existing top-level + empty-block + missing-attr regression tests. (crates/djust_templates/src/renderer.rs)send_pg_notifypayload size guard (#810) — PostgreSQL capsNOTIFYpayloads at 8000 bytes.send_pg_notify()now warns at 4KB (soft limit) and drops + error-logs at 7500 bytes (hard limit). (python/djust/db/decorators.py)PostgresNotifyListener.areset_for_tests()awaits task cancellation (#811) — The existingreset_for_tests()fire-and-forget cancel is now documented as such; new async variant awaits the cancelled task so async test teardowns don't race. (python/djust/db/notifications.py)db_notifyrender-lock timeout documented (#813) — 100ms timeout is best-effort under contention; dropped notifications do not queue. (python/djust/websocket.py)Regression test: consumer handles views without
NotificationMixin(#812) — Locks in thatgetattr(view, '_listen_channels', None)+ truthy gate handles both absent-attr and empty-set paths. (tests/unit/test_db_notifications.py)stream()withlimit=Npre-trims emitted inserts (#799) — Server trimsitems_listto at-mostlimitbefore emitting inserts. (python/djust/mixins/streams.py)teardownVirtualListrestores original children (#798) — Teardown now restores pre-virtualization children and removes the shell/spacer. (python/djust/static/djust/src/29-virtual-list.js)stream_prune.childrenfilter redundancy removed (#801) — Cosmetic cleanup. (python/djust/static/djust/src/17-streaming.js)LiveViewTestClient.render_async()invokeshandle_async_result(#843) — Test-client drain now mirrors the production WS consumer. (python/djust/testing.py)LiveViewTestClient.follow_redirect()refuses to pick silently when multiple redirects queued (#844) — RaisesAssertionErrorwith all queued paths. (python/djust/testing.py)UploadWriter
close()return validated as JSON-serializable (#825) — Non-JSON returns caught at finalize time and abort the upload cleanly. (python/djust/uploads.py)BufferedUploadWriter
write_chunk()afterclose()raises (#823) —_finalizedflag now actively enforced; repeatedclose()is idempotent. (python/djust/uploads.py)Upload-manager drops trailing chunks silently after abort (#824, partial) — Fast-path at DEBUG log;
writer.abort()called once. (python/djust/uploads.py)Morph-path honors
dj-ignore-attrs(#815) — The VDOM morph loop atpython/djust/static/djust/src/12-vdom-patch.js:746-758previously stripped and overwrote attributes without consultingdjust.isIgnoredAttr. Attributes listed indj-ignore-attrswould survive individualSetAttrpatches (the guard added in PR #814) but could still get wiped during a full-element morph. The morph-path remove-loop and set-loop both now skip ignored attribute names. Two regression tests intests/js/ignore_attrs.test.jscover remove-loop and set-loop preservation. (python/djust/static/djust/src/12-vdom-patch.js)
Changed
dj-ignore-attrsCSV empty-token hardening (#816) —isIgnoredAttrnow skips empty tokens produced by double-comma ("open,,close") or trailing-comma ("open,") CSV values, and rejects empty attribute-name queries. Previously those edge cases could accidentally match an empty attribute name. Four regression tests intests/js/ignore_attrs.test.jscover empty string, whitespace-only, double comma, and trailing comma. (python/djust/static/djust/src/31-ignore-attrs.js)
Added
djust_typecheck—{% firstof %}/{% cycle %}/{% blocktrans with %}tag support (#850) — The extractor now captures positional context-variable references in{% firstof a b c %}and{% cycle a b c %}(string literals andas <name>suffixes are correctly ignored), and thewith x=expr(andcount x=expr) clauses of{% blocktrans %}/{% blocktranslate %}produce both the template-local binding (x) and the reference (expr). Eliminates a class of false positives (blocktrans locals) and false negatives (firstof/cycle args). (python/djust/management/commands/djust_typecheck.py)See
docs/website/guides/typecheck.md.
Changed
djust_typecheck— walk MRO for parent-classself.foo = ...assigns (#851) —_extract_context_keys_from_astnow iteratescls.__mro__(skippingdjust.*,djust_*,django.*,rest_framework.*, andbuiltins), so a child view that relies on attributes set in a parentmount()no longer produces spurious "unresolved" reports. The filter drops Django'sView/ namespace-framework attrs (request,head,kwargs,args) that would otherwise surface from the base class. (python/djust/management/commands/djust_typecheck.py)Shared class-introspection helpers (#852) —
_walk_subclasses,_is_user_class, and_app_label_for_classare now a single source of truth in the newdjust.management._introspectmodule;djust_auditanddjust_typecheckboth import from it. No behavior change; purely a refactor to prevent drift as the set of management commands grows._introspect.walk_subclassesalso gained cycle-safety (diamond-inheritance deduplication) which the old recursive implementation lacked. (python/djust/management/_introspect.py,python/djust/management/commands/djust_audit.py,python/djust/management/commands/djust_typecheck.py)Service worker + main-only middleware follow-ups to PR #826 (closes #827/#828/#829/#830) —
- #828 —
DjustMainOnlyMiddlewarenow early-returns on responses withstatus_code >= 400. Error pages render full-page layouts (status message, "go back" link, etc.); trimming them to<main>would strip that context from shell-navigation clients. Regression tests cover 4xx and 5xx. - #830 — HTML response detection widened to include
application/xhtml+xmlin addition totext/html. Charset and boundary suffixes (text/html; charset=utf-8; boundary=xyz) are stripped before matching. Defensive test confirmsapplication/rss+xmlis still treated as non-HTML. - #829 —
djust.registerServiceWorker()is now idempotent. A second call returns the cached registration promise without re-runninginitInstantShell/initReconnectionBridge, so drain listeners and the WSsendMessagepatch are applied at most once. Previous behavior caused buffered replays to double on repeat init. - #827 — Documented the
<script>-inside-<main>limitation of the instant-shellinnerHTMLswap at the top of33-sw-registration.js. The doc block was also corrected:dj-click/dj-submit/etc. work through document-level event delegation (not MutationObserver), anddj-hooknow explicitly re-runs via adjust.reinitAfterDOMUpdate(placeholder)call after the swap — dj-hook content inside<main>actually works post-swap as a result (previous implementation silently skipped hook re-binding).
Tests: 9 → 13 Python cases in
tests/unit/test_main_only_middleware.py, +2 JS cases intests/js/service_worker.test.js(12 total). (python/djust/middleware.py,python/djust/static/djust/src/33-sw-registration.js)- #828 —