Skip to content

v0.1.47

Choose a tag to compare

@github-actions github-actions released this 23 Sep 16:34
· 11 commits to main since this release

Give each render its own id sequence, not the process one

renderPage reset the counter and then awaited — resolving the page module,
gathering shared props, calling the component. Two requests overlapping across
any of those awaits shared one counter: the second reset it under the first,
and that response shipped trigger-2 while its own browser, hydrating from
zero, looked for trigger-1. Every id-based lookup then answered null, which
is silent — a tooltip that never opens on a page where everything else works.

Found by an external audit. The sequential tests could not see it, and now
there is one that does: two renders interleaved inside resolve(), both
expected to start at 1. It fails against the old counter.

The counter moved into the per-request AsyncLocalStorage renderPage already
runs — the same seam the cookie store and the route manifest use. id.ts
imports nothing from node:: it reads its cell through a reader the server
installs, and falls back to a module-level one in the browser, where a page is
rendered once and there is nothing to race.

release: aurora 0.1.47

Keep both lifecycles, and reset the id sequence per pass

Two bugs, one symptom: a component whose whole job happens on mount doing
nothing at all, silently, on a page where every binding works. A tooltip that
never opens while its trigger's aria-describedby toggles correctly.

FIRST — the lifecycle rides on the returned TemplateResult, under a symbol. A
component is free to return another one's result rather than a template of its
own:

const TextField = component((props) => Field({ ... }))
const Passthrough = component((props) => props.children)

and the outer wrap assigned straight over the inner one. The inner component's
setup ran, its bindings worked, its onMount was dropped. Merged now, outer
first — the order the renderer drains the queue in everywhere else.

SECOND — ids. Templates have no ref directive, so a component that must anchor
or measure a node finds it by id, and ARIA needs one regardless. The counter is
monotonic, which makes the sequence survive hydration ONLY if both passes start
from the same place. A server process is long-lived: with no reset its counter
climbs across requests, so the second page it serves ships trigger-14 while the
browser, starting fresh, looks up trigger-1. Every lookup then answers null —
and answers it silently.

uid / resetIds / byId now live here, and renderPage and hydrate each
reset before they build. nebula had its own counter, which saw none of those
resets; it re-exports these instead.


Changes since v0.1.46.