Releases: AIRCentre/HyperSignal.jl
Releases · AIRCentre/HyperSignal.jl
v0.4.0
0.4.0 — 2026-05-30
Added
ds_computed(name, expr)— declare a read-only derived signal
(data-computed:<name>);ds_style(prop, expr)— bind an inline CSS
style property reactively (data-style:<prop>), the natural partner to
ds_class/ds_attr; andds_json_signals()(+ a filter overload) —
the baredata-json-signalsin-page signal-store debugger. All three are
FREE-tier Datastar v1.0 attributes that previously had no first-class
helper. (exported)
Fixed
- Thread-safe validator caches.
_VALID_TAG_NAMES/_VALID_ATTR_NAMES
were plainSet{Symbol}mutated lock-free on every tag/attribute render.
Under multithreadedHTTP.serve, concurrentpush!into a cold cache
races arehash!that swaps the backing arrays non-atomically — which can
corrupt the cache (poisoning later validations) or segfault the
process, not merely "duplicate work" as the old comment claimed. Both
caches are now guarded by aReentrantLock(a_NameCacheholding the Set- its lock); every
in/push!runs under the lock, and a name is validated
outside it. The lock is uncontended after warm-up — the cache only grows
during the first traffic burst, then every call is a read hit.
- its lock); every
preset_buttonnow escapes'in a preset value. The whole
querySelector('…')selector is a single-quoted JS string, so a value like
it'sclosed it early and made the generatedonclickaSyntaxError
(the preset silently did nothing)._escape_preset_valueescaped only"
and\; it now escapes'too.preset_buttonalso rejects a non-CSS-
identifier name (digit-leading, e.g.name=123) at build time rather than
emitting aninput[name=123]selector that throws in the browser.patch_svgid-namespacing no longer rewritesdata-id/xml:id/
aria-id. The_ID_REmatcher used a bare\bword boundary, so under
id_prefixit mutated the values of any*-id/xml:idattribute, not
just the SVGid. Anchored to a name-char boundary ((?<![\w:-])).ds_signalnow emits the keyeddata-signals:<name>form (colon,
plural). It previously rendered the singulardata-signal-<name>, which
is not a Datastar v1.0 attribute — Datastar matched no plugin and ignored
it, so the signal was never created (a silent client-side no-op, the
exact failure class this library exists to prevent). Datastar's
kebab→camel mapping still applies (ds_signal("my-signal", …)→$mySignal).- JS line terminators are now escaped in the single-quoted JS string
built byaction_js(the URL and every stringextrasvalue) and by
redirect_via_fragment(which shares_js_str_escape). A raw LF, CR,
U+2028, or U+2029 — reachable via a reflected query param or a multi-line
search box — is an ECMAScriptSyntaxError, so the whole Datastar action
(or inline-<script>redirect) silently failed to compile. The escapes
round-trip to the same character after JS parsing, so the fetched
URL / navigated location is unchanged. Mirrors the SSE path's existing
CR/LF defenses. DSActionextras with a structured value (headers=Dict(...),
filterSignals=(include=...), array-valued options) now serialize as a
JSON object/array literal — valid JS — instead of Julia'srepr, which
is not (Dict("a"=>"b")had rendered asDict{String,…}(...))._js_valuerenders non-finite floats as JS globalsInfinity/
-Infinity/NaNinstead of Julia'sInf/-Inf(a bareInfis a
JSReferenceError). Relevant to numeric action options such as
retryMaxCount=Inf._validate_preset_namenow anchors with\z, not$. PCRE$also
matches just before a trailing\n, so a preset name like"foo\n"slipped
past the CSS-identifier check and landed a raw newline in the
querySelectorselector (silently breaking the handler in the browser).
Changed
- An
Attributethat reachesrenderas a child now raises an actionable
ArgumentErrornaming the fix (splat the collection) instead of an opaque
internalMethodError._make_elementonly lifts anAttributeinto attrs
when it is a top-level positional arg; nesting one inside a
Vector/Tuple/Generator(e.g. collecting attrs into a vector as
signal_dialogdoes, then forgetting to splat) previously failed deep in
the renderer. - A caller-supplied header no longer double-emits. Every response helper
(html_response,fragment_response,signals_response,
script_response,redirect_*,sse_response,sse_stream) prepended
its library-ownedContent-Type(and the SSE trio) then appended the
caller'sheaders, so a caller passing their ownContent-Type(custom
charset,application/problem+json, …) put twoContent-Typelines on
the wire — a malformed message whose interpretation diverges across
consumers. A new_with_defaultskips the library default when the caller
already supplied that field (matched case-insensitively); the caller wins,
with exactly one header. The default path is byte-identical. parse_signalsnow throwsArgumentError(not the bareErrorException
fromerror()) for a top-level non-object JSON body, matching the
malformed-JSON path — both bad-request-body cases now raise one
consistent type.cls(badPairvalue / unhandled type) and
preset_button(invalid input name) likewise now throwArgumentError
rather than a bareerror(), so every caller-input mistake in the library
raises one consistent exception type. The SSEselectorCR/LF rejection
message is now prefixed (patch_elements:) and echoes the offending value,
matching the rest of the library's error messages.patch_elementsnow validates theselectorfor CR/LF at build time
(alongside the existingmodecheck) rather than only at encode time, so
the mistake surfaces at the call site;_encode_eventkeeps the check as
defense-in-depth for a directly-constructed event.redirect_via_fragmentnow rejects aselectorthat isn't a single#id
(a class/compound/whitespace selector, or a missing#). The helper renders
the morph target withid= selector minus#, so only#idever worked —
a non-matching selector previously silently no-op'd the redirect, and a CR/LF
would inject into thedatastar-selectorheader. Now anArgumentError.- Internal, no behavior change: the render hot path streams
DSAction
attributes straight into the responseIO(dropping a throwaway
intermediateStringthatescape_htmlthen re-walked);escape_html
is split into concreteString/SubString{String}methods so the
per-child dispatch lands directly (no runtimeisaladder);is_voidis
hoisted to one lookup per element; and the tag/attribute name validators
share one_is_invalid_name_bytepredicate.
Docs
security.mdnow lists the fullDSActionJS-string escape set — the four
JS line terminators (LF,CR,U+2028,U+2029) alongside the original
'/\/</— and drops the now-incorrect "triple-escape" wording.- Documentation overhaul (README + Documenter site). Fixed accuracy bugs:
the README quickstart's undefinedformat_number, adata-signalsexample
showing single-quoted output the renderer never emits, the counter example's
misattributeddatastar-selectorheader, av0.1.0benchmark tag, and a
docs-securitylink pointing at the docs root. The Datastar guide gained a
full attribute/action/parse_signalsreference (incl.ds_computed/
ds_style/ds_json_signals) plus thehtml_responseand redirect
helpers;security.mddocuments theredirect_via_fragment#id
requirement and the build-time SSE-selector check;performance.mdgained a
concurrent-serving note;index.md/cairomakie.md/api.mdaccuracy and
completeness fixes; and CONTRIBUTING + the.githubtemplates now cover the
doctest build and Pluto smoke job. README/docs examples were verified to run
against the current API.
Fixed
- MapLibre
click_post/bbox_postpayloads now actually reach the
server. The click and shift-drag-bbox handlers set$_payloadbefore
@posting it, but Datastar's default request filter excludes any
signal matching/(^|\.)_/from request bodies (underscore signals are
client-local), so the payload was set locally but never sent — the
handler saw no payload and silently fell back. The payload signal is
now a plain$payload, so both posts carry their{lat, lon, properties}/{w, s, e, n}data. - MapLibre shift-drag box-select no longer pans the map or collapses to
a zero-area bbox. Disabling MapLibre's built-inboxZoom(to stop it
double-firing) also removed thedragPansuppressionboxZoom
performs during a shift-drag, so the gesture panned the map and
start/end unprojected to the same coordinate. The handler now disables
dragPanitself on shift-mousedown and re-enables it on mouseup
(before any early return), and draws a live selection rectangle to
restore the visual feedbackboxZoomused to provide.
Docs
- The MapLibre example notebook (
docs/src/notebooks/example.jl) stacks
the map over a full-width time-series chart (single column), makes the
date sliders recolor the map and shade the selected year window on
the chart, and bumps its Pluto format header to v1.0.1. The MapLibre
guide's payload table is updated to the non-underscore$payload
signal.
v0.3.1
Documentation-only patch release; no code changes since 0.3.0.
Docs
- New MapLibre guide covering the 0.3.0 extension end to end (
map_view/marker, sources, layers, paint-expression DSL, GeoInterface bridge, server-returned JS helpers). api.mdindexessse_streamandDATASTAR_SUPPORTED_VERSION;security.mddocuments thepatch_svgadd_class/aria_labelroot-<svg>escaping.- Fixed stale home-page copy (CairoMakie-only intro, pre-0.3.0 demo blurb) and the Datastar "SSE is future" note.
- Install snippet is build-context-aware: tagged docs show
] add HyperSignal, dev docs keep the Git URL.
v0.3.0
Highlights
New: MapLibre extension (HyperSignalMapLibreExt) — paint DSL, sources, layers, JS helpers, map_view, GeoInterface bridge (Point/Line/Polygon + Multi* + GeometryCollection, null-geometry support).
New: SSE / Datastar responses — sse_stream (chunked streaming), sse_response, patch_elements/patch_signals, signals_response, script_response; fragment_response gains mode + view_transition.
Adversarial hardening (#39):
- SVG root-patch byte-index fix for multi-byte UTF-8
- SSE encoder honors CR / CRLF line terminators per spec
action_jsURL now JS-string escapedadd_classattribute-injection (XSS) fix inpatch_svg- Void elements with children now error loudly
- Duplicate attributes deduped per HTML5 (first-wins position, last value)
Docs: CONVENTIONS.md restructured; Datastar pinned to v1.0.1.
v0.2.0
HyperSignal v0.2.0
Breaking changes
div,select,summary,mark, andtimeare no longer in HyperSignal'sexportlist.using HyperSignalno longer brings them into scope, because those names shadowBase(and Makie). Opt in explicitly withHyperSignal.@using_tags(recommended) orusing HyperSignal: div, select, summary, mark, time.
See CHANGELOG.md for the full v0.2.0 entry.
Closed issues:
- Three-way MIME round-trip test (text/html ↔ text/plain ↔ HTTP.Response) (#4)
- Type-piracy + @generated/hasmethod audit and CI guard (#6)
- Benchmark page in docs + nightly bench-history branch (#7)
- Land ≥1 downstream user on the registered 0.x release (#9)
- Document Raw as the only escape-hatch (with adversarial doctest) (#10)
v0.1.0
HyperSignal v0.1.0
Initial public release.
Features
- Streaming HTML AST with auto-escape (
Element,Frag,Raw,DOCTYPE) - Typed Datastar actions:
ds_get/ds_post/ds_put/ds_delete, bound viaon/on_click/on_submit/on_change_debounced/on_interval - Full Datastar attribute helpers:
ds_indicator,ds_bind,ds_signal,ds_signals,ds_show,ds_text,ds_ref,ds_attr,ds_class,ds_effect,ds_init,ds_ignore_morph - Signal decoding via
parse_signals - Response wrappers:
html_response,fragment_response,redirect_via_fragment,redirect_to - Form-component helpers:
cls,radio_field,checkbox_field,text_field,form_legend,form_section,help_tooltip,preset_button,signal_dialog - CairoMakie front-row support via
HyperSignalMakieExtpackage extension:patch_svg/inline_svgstrip XML prologs, namespace IDs (so two figures share a page safely), drop hard-coded px sizes, and add ARIA labels - 190 tests