@askrjs/askr 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +874 -0
  2. package/README.md +61 -29
  3. package/capabilities.json +19 -9
  4. package/dist/actions/index.js +56 -12
  5. package/dist/benchmark.js +10 -15
  6. package/dist/boot/index.d.ts +3 -2
  7. package/dist/boot/index.js +1 -1
  8. package/dist/boot-DaoTYoCw.js +3989 -0
  9. package/dist/browser-activity-CB8e-V4f.js +220 -0
  10. package/dist/components/index.d.ts +8 -1
  11. package/dist/components/index.js +1 -43
  12. package/dist/{aria-hKjThHcp.js → compose-ref-DWzdbxsD.js} +41 -4
  13. package/dist/control/index.d.ts +2 -0
  14. package/dist/control/index.js +1 -1
  15. package/dist/control-nfpT2fbK.js +209 -0
  16. package/dist/core.d.ts +5 -9
  17. package/dist/csp-nonce-DwGw1A6Q.js +35 -0
  18. package/dist/data/index.js +3 -3
  19. package/dist/{data-2edW0jhF.js → data-BCwClJiF.js} +283 -94
  20. package/dist/data-runtime-C1bFAlba.js +242 -0
  21. package/dist/{deferred-CbgNhf67.js → deferred-bEEHHvic.js} +42 -20
  22. package/dist/domains/actions.d.ts +8 -3
  23. package/dist/domains/component.d.ts +1 -107
  24. package/dist/domains/context.d.ts +0 -15
  25. package/dist/domains/control.d.ts +36 -249
  26. package/dist/domains/data.d.ts +31 -13
  27. package/dist/domains/diagnostics.d.ts +1 -2
  28. package/dist/domains/lifecycle.d.ts +2 -5
  29. package/dist/domains/refs.d.ts +0 -2
  30. package/dist/domains/routing.d.ts +43 -8
  31. package/dist/domains/scheduler.d.ts +1 -76
  32. package/dist/domains/server.d.ts +5 -2
  33. package/dist/domains/state.d.ts +2 -5
  34. package/dist/domains/telemetry.d.ts +0 -2
  35. package/dist/elements.d.ts +214 -51
  36. package/dist/error-boundary-Bpwofwps.js +293 -0
  37. package/dist/foundations/icon/index.d.ts +0 -58
  38. package/dist/foundations/icon/index.js +2 -2
  39. package/dist/foundations/index.js +2 -1
  40. package/dist/foundations/interactions/index.js +1 -1
  41. package/dist/foundations/state/index.d.ts +3 -3
  42. package/dist/foundations/state/index.js +5 -3
  43. package/dist/foundations/structures/index.js +2 -1
  44. package/dist/foundations/utilities/index.d.ts +2 -0
  45. package/dist/foundations/utilities/index.js +1 -2
  46. package/dist/fx/index.d.ts +31 -58
  47. package/dist/fx/index.js +313 -195
  48. package/dist/hooks-BimKStr4.js +202 -0
  49. package/dist/index.d.ts +11 -74
  50. package/dist/index.js +8 -447
  51. package/dist/instance-BAnfmXOR.js +988 -0
  52. package/dist/{interactions-CTeACdT_.js → interactions-D2BoREb-.js} +3 -2
  53. package/dist/jsx-dev-runtime.d.ts +41 -18
  54. package/dist/jsx-dev-runtime.js +1 -6
  55. package/dist/{jsx-runtime-CPJXAOBF.js → jsx-runtime-C4idqHY0.js} +18 -7
  56. package/dist/jsx-runtime.js +1 -1
  57. package/dist/jsx.d.ts +61 -37
  58. package/dist/{manifest-D1P3UNWV.js → manifest-CW9gyNBY.js} +3 -3
  59. package/dist/{navigate-prC5V8XV.js → navigate-CP_0Zy5U.js} +454 -87
  60. package/dist/navigation.d.ts +12 -3
  61. package/dist/peer-types.d.ts +8 -0
  62. package/dist/portal-DuOKfnLa.js +349 -0
  63. package/dist/promise-Dz8iIHAm.js +16 -0
  64. package/dist/query-registry-WMCN0BJL.js +320 -0
  65. package/dist/readable-DFWOsTac.js +21 -0
  66. package/dist/{render-context-B7ohlh6t.js → render-context-DGHnEveC.js} +27 -11
  67. package/dist/render-sync-IC5lMFpq.js +1240 -0
  68. package/dist/resolution-Cx82-xjW.js +1244 -0
  69. package/dist/resource-CLjB7RBQ.js +377 -0
  70. package/dist/resources/index.d.ts +21 -11
  71. package/dist/resources/index.js +45 -244
  72. package/dist/router/index.d.ts +14 -5
  73. package/dist/router/index.js +22 -53
  74. package/dist/scope-CwpVSHVf.js +55 -0
  75. package/dist/{shared-kU8yvryp.js → shared-O2ZnmYAf.js} +1 -7
  76. package/dist/{snapshot-source-An4iKajG.js → snapshot-Cjx4N988.js} +6 -1
  77. package/dist/ssg/index.d.ts +1 -2
  78. package/dist/ssg/index.js +5 -13
  79. package/dist/ssr/index.d.ts +26 -28
  80. package/dist/ssr/index.js +4 -4
  81. package/dist/{ssr-CiNm6Vzl.js → ssr-BJ8ug7zH.js} +133 -122
  82. package/dist/ssr-DoV-vJ-5.js +229 -0
  83. package/dist/state-DWO1KzkX.js +232 -0
  84. package/dist/structures-D4dDMqHX.js +231 -0
  85. package/dist/testing/index.d.ts +8 -4
  86. package/dist/testing/index.js +29 -34
  87. package/dist/url-CnbfpX0K.js +185 -0
  88. package/dist/utilities.d.ts +48 -2
  89. package/dist/verify-hydration-DpVaDnqz.js +152 -0
  90. package/package.json +19 -9
  91. package/dist/access-CEA2pOry.js +0 -1046
  92. package/dist/activity-oy2YFTFF.js +0 -201
  93. package/dist/boot-BMBMWUGI.js +0 -1084
  94. package/dist/capabilities-Bbf0mlp9.js +0 -42
  95. package/dist/compose-ref-v0qGlHoN.js +0 -26
  96. package/dist/control-BR_mrX__.js +0 -173
  97. package/dist/control-C-ApnFs1.js +0 -20
  98. package/dist/csp-nonce-BfFWcd0w.js +0 -173
  99. package/dist/data-runtime-CxoY98lX.js +0 -149
  100. package/dist/default-portal-runtime-DvMvWJVp.js +0 -20
  101. package/dist/dom-renderer.d.ts +0 -77
  102. package/dist/domains/renderer.d.ts +0 -127
  103. package/dist/eager-control.d.ts +0 -8
  104. package/dist/effect-C3Zsc1k6.js +0 -292
  105. package/dist/for-state-BDykeOJL.js +0 -1753
  106. package/dist/instance-VAFYVJOa.js +0 -1027
  107. package/dist/notify-DxxuDPeY.js +0 -266
  108. package/dist/portal-w3oQ6oQU.js +0 -29
  109. package/dist/query-registry-LrYpQsDJ.js +0 -83
  110. package/dist/render-resolved-BC0W7_5b.js +0 -1123
  111. package/dist/renderer-CT0HeY58.js +0 -8623
  112. package/dist/resolution-3DskUkjG.js +0 -577
  113. package/dist/resource-18-sFRl9.js +0 -344
  114. package/dist/route-matching-CUzZ31gT.js +0 -551
  115. package/dist/router-internal-7nuNEHfG.js +0 -36
  116. package/dist/scope-Cq_iYbTR.js +0 -403
  117. package/dist/selector-BSPVkjKP.js +0 -311
  118. package/dist/ssr-EPjwJH5R.js +0 -29
  119. package/dist/state-BXJfudq1.js +0 -91
  120. package/dist/structures-CjEzfj4h.js +0 -654
  121. package/dist/timer-C7uqnJVt.js +0 -85
  122. package/dist/verify-hydration-ikaxotf7.js +0 -27
  123. package/dist/vnode-D24p6IW2.js +0 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,871 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.4.0 — 2026-09-28
6
+
7
+ - feat(router): expose `matchRoute(path, { registry })` for synchronous route
8
+ path checks without resolving policies or loading route data. The testing
9
+ helper uses the same implementation.
10
+
11
+ - fix(core): a component tree too deep for the call stack fails with a
12
+ `RenderDepthError` (exported from `@askrjs/askr`) that explains the limit,
13
+ instead of a bare `RangeError` (or Firefox `InternalError`). It is raised
14
+ where render errors are first caught, so error boundaries, updates, and SSR
15
+ see it. The rendering guide documents the supported depth (#624).
16
+
17
+ - perf(core): remove quadratic owner, scheduler, and context paths (#612).
18
+ Detaching an owner is O(1) (holes in the parent's child list, compacted when
19
+ they dominate), the render lane is a depth-ordered heap and the effect and
20
+ post lanes are O(1) queues, and each component caches its inherited context
21
+ revision. On the new `tier2-subsystem-core-scaling` bench: disposing 10,000
22
+ sibling owners 50.7 ms to 0.43 ms, 5,000 render jobs 13.4 ms to 0.87 ms,
23
+ 20,000 effect jobs 20.2 ms to 2.06 ms, patching a 2,000-deep chain 13.0 ms
24
+ to 0.72 ms (mean).
25
+
26
+ - fix(jsx): export `createElement` from `@askrjs/askr`. The automatic JSX
27
+ transform in TypeScript, esbuild, Babel, and oxc (Vite) compiles a `key`
28
+ written after a spread (`<Row {...props} key={id} />`) to `createElement`
29
+ from the import source, which previously failed to resolve. It removes the
30
+ development-only `__self`/`__source` props and, like `jsxs()`, marks several
31
+ child arguments as static children (#617).
32
+
33
+ - fix(foundations): `controllableState()` results destructure like `state()`
34
+ (`const [value, setValue] = controllableState(...)`). `ControllableState` is
35
+ now based on `StateTuple`, so the destructured getter and setter are typed.
36
+ The getter is readable like a `state()` getter, and an updater passed to its
37
+ setter runs once (#621).
38
+
39
+ - docs(foundations): document stacking dialogs and toasts through one writer
40
+ that owns the layer list and writes it through its own `definePortal()`
41
+ channel, since a portal shows the most recent writer's content (#495). The
42
+ default portal is part of the core client bundle, because every application
43
+ root provides and hosts it; the SSG hydration bundle check now asserts this
44
+ and the SSG guide no longer says otherwise (#611).
45
+
46
+ - **breaking** refactor(api): control, data, `getSignal` and `onRouteChange`
47
+ each have one import path (#487).
48
+ Migration:
49
+ - `For`, `Show`, `Case`, `Match` and their prop types: import from
50
+ `@askrjs/askr/control` instead of `@askrjs/askr`.
51
+ - `createQuery`, `createQueryCollection`, `defineQuery`, `serveQuery`,
52
+ `defineServerQueries`, `prefetchQuery`, `dehydrateDataRuntime`,
53
+ `hydrateDataRuntime` and the query types: import from `@askrjs/askr/data`
54
+ instead of `@askrjs/askr`.
55
+ - `getSignal`: import from `@askrjs/askr` instead of
56
+ `@askrjs/askr/resources`.
57
+ - `onRouteChange`, `RouteChangeCleanup`, `RouteChangeOptions`: import from
58
+ `@askrjs/askr/router` instead of `@askrjs/askr/resources`.
59
+
60
+ - **breaking** refactor(api): unused low-level exports are removed (#490).
61
+ `resolveRequest` and `renderResolvedToStringSync` are no longer exported from
62
+ `@askrjs/askr/ssr`. To resolve auth, redirects and status without rendering,
63
+ call `resolveRouteRequest(url, { registry, mode: 'ssr', auth, authContext,
64
+ request, signal })` from `@askrjs/askr/router`. Pass `mode: 'ssr'`
65
+ explicitly (the default is `'spa'` when a global `window` exists) and pass
66
+ `signal` explicitly (it is not taken from `request.signal`). The result may be a plain value or a
67
+ Promise, and errors can throw synchronously, so call it inside an `async`
68
+ function and `await` it to keep the old always-async behavior; to render, use
69
+ `renderToString`, `renderRouteRequest` or `renderRouteRequestToString`. `debounce` and `DebounceOptions` are no longer
70
+ exported from `@askrjs/askr/fx`; use `debounceEvent` for event handlers, or keep a local
71
+ debounce helper for code outside components.
72
+
73
+ - fix(types): intrinsic JSX typings accept MathML elements (`<math>`, `<mi>`,
74
+ `<mglyph>`, ...), `dangerouslySetInnerHTML` on every element, and the
75
+ `formAction`/`formMethod`/`formEncType`/`formNoValidate`/`formTarget`
76
+ attributes on `<button>` and `<input>`.
77
+
78
+ - fix(ssr): a `dangerouslySetInnerHTML` payload whose `__html` is `null` or
79
+ `undefined` renders no content, as on the client, instead of the text
80
+ `null`/`undefined`.
81
+
82
+ - fix(types): `resource<T>(load, deps)` with an explicit result type selects
83
+ the deps overload, `<ErrorBoundary>` accepts function and mixed children,
84
+ and `mergeProps()` returns `MergedProps` (base values win unless `undefined`;
85
+ a key the injected props always supply is required; index-signature props
86
+ keep the intersection) instead of an intersection that could collapse to
87
+ `never`. `MergedProps` is exported from `@askrjs/askr/foundations/utilities`.
88
+
89
+ - chore(tests): `tests/` is typechecked by `npm run typecheck` (#556).
90
+
91
+ - fix(types): JSX accepts a `string` or `number` `key` on function components
92
+ whose props do not declare it, as it does on intrinsic elements (#489).
93
+
94
+ - fix(types): `<For each={items}>` with a `state()` getter or `derive()`
95
+ result types each row as the list element instead of
96
+ `State<T[]> | StateSetter<T[]>`, including nullable getters, which render
97
+ nothing for `null`/`undefined`. A getter that does not return a list is now
98
+ a type error. `ForGetterProps` is exported.
99
+
100
+ - breaking(types): remove the `EagerControlPrimitive` type and the
101
+ `unknown`-returning `jsx()`/`jsxs()`/`jsxDEV()` overloads left from eager
102
+ control flow, and delete retired runtime declarations that no export
103
+ reached (#489, #610).
104
+
105
+ - fix(dom): a controlled `<select value>` keeps its value when a child
106
+ component, `For`, or function child adds, removes, or edits its options, or
107
+ an option's text or bound `value` changes, without the select itself
108
+ re-rendering (#608).
109
+
110
+ - fix(ssr): a value-less `<option>` (no `value`, or `null`/`false`) is
111
+ selected by its rendered text with whitespace stripped and collapsed, as the
112
+ browser's `option.value` reads it, including text from component children
113
+ and raw HTML entities, and excluding script text (#609).
114
+
115
+ - fix(state): `selector()` releases candidate entries once nothing reads
116
+ them, so querying many distinct values no longer grows it for the
117
+ component's lifetime (#613).
118
+
119
+ - fix(router): a navigation that changes the pathname remounts the route
120
+ leaf, so `state()`, `task()`, and resources start fresh for the new URL
121
+ (`/user/1` to `/user/2`). Layouts keep their instances and DOM.
122
+
123
+ - fix(router): when a destination's commit applies but a ref or binding
124
+ throws, navigation completes (history, location, metadata) and reports the
125
+ error. When a failed DOM write undoes the commit, navigation rolls back and
126
+ rethrows, keeping the previous page and location.
127
+
128
+ - fix(boot): mounting without hydration replaces the container's existing
129
+ content (a loading placeholder, unhydrated markup) instead of rendering
130
+ beside it.
131
+
132
+ - fix(foundations): an explicit `<DefaultPortal>` unmounted in the flush that
133
+ mounted it no longer hides `<Portal>` content from the automatic host.
134
+
135
+ - fix(core): renders that are discarded or rewound by an `ErrorBoundary`
136
+ release event delegation and `For` row property reads they created.
137
+
138
+ - fix(router): `<Resolve>` renders deferred route data after client
139
+ navigation. It claimed its resource hook only while the value was pending,
140
+ so the render after the promise settled failed the hook-order check and the
141
+ pending placeholder stayed on screen.
142
+
143
+ - fix(boot): `hydrate: { verifyMarkup: true }` no longer reports a mismatch
144
+ when a ref callback updates state after the hydration commit (for example,
145
+ adopting a persisted theme). Client markup passes if it matches the server
146
+ at the commit or after scheduled hydration work settles.
147
+
148
+ - breaking(api): remove `@askrjs/askr/experimental` and its runtime and
149
+ renderer-host construction APIs. Use the public boot, component, and state
150
+ APIs to mount and extend applications.
151
+
152
+ - breaking(types): drop the internal `_controlState` field from the published
153
+ `VNode`/`DOMElement` declaration. It exposed the retired runtime's control,
154
+ child-scope, and component-instance shapes, which the rebuilt core does not
155
+ have. No export is removed.
156
+
157
+ - fix(runtime): retain standalone reactive component updates in packed builds
158
+ by installing the DOM render host when a root is created.
159
+
160
+ - fix(renderer): forced bulk reuse now propagates key attribute write failures
161
+ and restores a partially written key before changing that row's content.
162
+
163
+ - fix(renderer): strip the forced positional bulk reuse diagnostic switch from
164
+ production bundles; it remains available in development tests.
165
+
166
+ - feat(fx): `scheduleRetry()` now returns a `result` promise with the final
167
+ success value, terminal error, or cancellation status. Terminal errors still
168
+ reach the host reporter.
169
+
170
+ - breaking(runtime): remove the scheduler's mutable `setInHandler` flag.
171
+ Use `runInHandlerScope()` to hold handler permissions for a lexical scope.
172
+
173
+ - feat(data): mutations support a synchronous `optimistic` callback that
174
+ returns a rollback for failure or abort. Overlapping executions no longer
175
+ abort earlier writes by default; explicit abort cancels all pending writes.
176
+
177
+ - fix(data): ownerless client queries now evict their cache lookup entry after
178
+ `gcTime` (five minutes by default, or immediately with `gcTime: 0`) while
179
+ the returned handle remains usable.
180
+ SSR request caches retain entries through dehydration.
181
+
182
+ - feat(data): `createQuery()` accepts `gcTime` to retain settled data for a
183
+ bounded interval after the last component reader unmounts. The default
184
+ remains immediate eviction.
185
+
186
+ - breaking(runtime): scheduler diagnostic state no longer includes
187
+ `taskCount`; use `queueLength` for pending work.
188
+
189
+ - breaking(runtime): scheduler diagnostic state no longer includes the
190
+ redundant `executionDepth` field; use `running` or `isExecuting()`.
191
+
192
+ - fix(runtime): report failed bulk-commit probes and reject work while commit
193
+ state is unknown instead of silently admitting it.
194
+
195
+ - fix(fx): `raf()` wrappers now expose `cancel()` and remain usable after a
196
+ callback throws.
197
+
198
+ - breaking(fx): remove `defer(fn)` from `@askrjs/askr/fx`; use the platform's
199
+ `queueMicrotask(fn)` for callback scheduling. `defer(promise)` remains in
200
+ `@askrjs/askr/router` for deferred route data.
201
+
202
+ - breaking(data): `DataRuntime` no longer exposes test override maps or accepts
203
+ them in `createDataRuntime()` options. Use the query and mutation test
204
+ registries to install fixtures. Both registries now accept an existing
205
+ runtime when a test needs to share one cache and fixture scope.
206
+
207
+ - fix(types): intrinsic event props now cover the DOM global handler map,
208
+ including animation, composition, drag, media, pointer-capture, and
209
+ transition events. Their `Capture` variants receive the same event type.
210
+
211
+ - fix(types): intrinsic JSX and `jsx()` refs now use each tag's element type.
212
+ An `<input>` ref can no longer be passed to `<button>`, and callback refs
213
+ receive the correct element type. This includes standard tags outside the
214
+ explicitly tailored intrinsic-prop set.
215
+
216
+ - breaking(types): Askr's JSX namespace now belongs only to the
217
+ `jsxImportSource` runtime modules. Import `type JSX` from
218
+ `@askrjs/askr/jsx-runtime` instead of using global `JSX`. Misspelled standard
219
+ tags now fail typechecking; hyphenated custom elements remain supported.
220
+ The unused class-component `ElementAttributesProperty` hook was removed.
221
+
222
+ - docs(scope): mark the published interaction and icon foundation subpaths as
223
+ platform internal contracts for sibling UI and icon packages. Their imports
224
+ and behavior remain available for those packages; application code should
225
+ use the composed UI and icon packages.
226
+
227
+ - breaking(api): runtime construction and renderer-host extension exports moved
228
+ from `@askrjs/askr` to `@askrjs/askr/experimental`. Change their import path;
229
+ their behavior and signatures remain the same. The root retains application
230
+ primitives. The experimental subpath is for runtime and renderer maintainers;
231
+ `createRuntime()` does not isolate mounted trees.
232
+
233
+ - fix(renderer): a chain of three or more components of the same type, each
234
+ returning the next directly, now keeps the state of every link when an
235
+ outer link re-renders. The update walk previously failed to find the deeper
236
+ links and recreated them, which also made updating a long chain quadratic in
237
+ its length. Updating a 10,000-component wrapper chain now takes linear time.
238
+ The runtime enforcement docs now state the nesting depths Askr guarantees
239
+ for each tree shape and rendering path: the 10,000-level guarantee covers
240
+ client wrapper chains only, while element nesting, server rendering, and
241
+ hydration recurse and are bounded by the engine's call stack.
242
+ - fix(renderer): a component that renders text, or a component whose result
243
+ spans several nodes, no longer gets a wrapper `<div>` on the client. This
244
+ applied when the result was the first render or followed an empty first
245
+ render, including `<Portal>{'x'}</Portal>`, Portal function children,
246
+ wrapper fragments rendering text, and content in the automatic default-portal
247
+ host. The client now places that content among its siblings inside
248
+ `askr-range` comment anchors, matching the server markup. A nested
249
+ component that renders nothing and later renders text keeps its instance
250
+ and state.
251
+
252
+ - fix(runtime): a failed render no longer leaves a structural function child
253
+ stale. A function child in a component's fragment or array result
254
+ (`<>{() => Array.from({ length: n() }, ...)}</>`) renders as a small
255
+ component; when its update joined a parent render that failed, the rollback
256
+ dropped that update, so the list kept the old item count until `n` changed
257
+ again, even after the parent recovered. The same applied to any child
258
+ component re-rendering on its own state. A rolled-back render now restores
259
+ the update the component was due (a queued run, or a scheduled render it
260
+ superseded), so the component renders again with the current state, as
261
+ fine-grained bindings do since #546.
262
+ - fix(runtime): cleanup failures are no longer swallowed in production. When
263
+ an update removes DOM or an app is cleaned up, a callback ref that throws on
264
+ `null`, a listener that cannot be removed, a throwing component cleanup
265
+ function (from a mount operation, task, or watch), or a failing root cleanup
266
+ callback was logged with a development-only warning. Every cleanup step still
267
+ runs, and the failures are now reported with `reportError()` in every build,
268
+ queued until the current task finishes: one report per removed subtree (a
269
+ single failure as-is, several as an `AggregateError`) and one per component.
270
+ Error handlers run after the update, can update state, and cannot roll it
271
+ back. `For` row disposal failures (previously a development-only
272
+ `console.error`), provisional component cleanup failures during a failed
273
+ render (previously dropped), and an async mount cleanup that resolves after
274
+ unmount (previously `console.error`) are reported the same way. A throwing
275
+ ref is no longer called with `null` twice. With
276
+ `cleanupStrict: true`, `cleanupApp()` now throws the failures of descendant
277
+ refs, listeners, and components, including components rendered inside `For`,
278
+ `Show`, and `Case`, instead of dropping them. In hosts without
279
+ `reportError()`, such as jsdom and Node under Vitest, the failures surface as
280
+ unhandled errors; stub `globalThis.reportError` in tests that throw from
281
+ cleanup on purpose.
282
+ - breaking(data): `dehydrateDataRuntime()` now throws a `TypeError` naming the
283
+ query key and property path when cached query data is not JSON
284
+ transport-safe (a `Date`, `Map`, `Set`, class instance, bigint, non-finite
285
+ number, `undefined`, accessor, or cyclic value). It was documented as
286
+ dropping non-serializable values, and in practice let `JSON.stringify()`
287
+ turn a `Date` into a string and a `Map` into `{}`. Query data now follows
288
+ the same transport rules as route hydration data. An SSR-mode
289
+ `prefetchQuery()` applies them as each value arrives and
290
+ `renderRouteRequest()` checks the data runtime before returning a streamed
291
+ result, so the render fails before a shell is sent. The shared validator
292
+ walks iteratively, so deeply nested data no longer overflows the stack, and
293
+ a fulfilled deferred that contains itself is reported as cyclic.
294
+ - breaking(actions): a failed `action().submit()` now always rejects with an
295
+ `Error` whose message carries the HTTP status (`Action failed (403): ...`),
296
+ including non-JSON bodies on any status. The server value (the envelope
297
+ `error`, or the body itself such as an RFC 7807 problem, whose `detail` or
298
+ `title` is used for the message) is kept as `cause`. It used to throw the
299
+ raw envelope `error` value, and a non-JSON body surfaced as a JSON
300
+ `SyntaxError` that hid the status. 422 validation replays keep their
301
+ `ActionValidationError` shape, and a bodiless success (204, 205, or an empty
302
+ 2xx body) resolves with `undefined` and still runs declared invalidations.
303
+ - fix(ssr): dehydrated query data now travels in a separate `queries` record
304
+ of the hydration payload instead of the `resources` record, so a query key
305
+ such as `r:0` no longer replaces resource slot data and resource slots are
306
+ no longer hydrated into the client data runtime. HTML rendered before this
307
+ change carries query data under `resources`; a new client bundle hydrating
308
+ such HTML fetches that query data again instead of reusing it.
309
+ - fix(data): concurrent `prefetchQuery()` calls for the same key and runtime
310
+ share one in-flight fetch. A joiner still rejects promptly with its own
311
+ `signal.reason`, and starts a replacement fetch as soon as the starting
312
+ context aborts. An `invalidate()` covering the key detaches the fetch: later
313
+ prefetches fetch again, and no caller of the detached fetch stores its
314
+ pre-invalidation result.
315
+ - perf(data): invalidation skips invalidation-listener dispatch entirely when
316
+ no listener is registered (listeners come only from
317
+ `createInvalidationRecorder()`).
318
+
319
+ - fix(runtime): cleanup failures are no longer swallowed in production. When
320
+ an update removes DOM or an app is cleaned up, a callback ref that throws on
321
+ `null`, a listener that cannot be removed, a throwing component cleanup
322
+ function (from a mount operation, task, or watch), or a failing root cleanup
323
+ callback was logged with a development-only warning. Every cleanup step still
324
+ runs, and the failures are now reported with `reportError()` in every build,
325
+ queued until the current task finishes: one report per removed DOM node, one
326
+ per component tree disposed together, and one per update for work that runs
327
+ after the update commits (a single failure as-is, several as an
328
+ `AggregateError`). Error handlers run after the update, can update state, and
329
+ cannot roll it back. `For` row disposal failures (previously a
330
+ development-only `console.error`), provisional component cleanup failures
331
+ during a failed render (previously dropped), an async mount cleanup that
332
+ resolves after unmount, cleanup of the previous route after navigation, and
333
+ mount or commit operations that throw after an update commits (all
334
+ previously `console.error`) are reported the same way. A throwing ref is no
335
+ longer called with `null` twice. With `cleanupStrict: true`, `cleanupApp()`
336
+ now throws the failures of descendant refs, listeners, and components,
337
+ including components rendered inside `For`, `Show`, and `Case` at any depth
338
+ and after re-renders, instead of dropping them; failures during ordinary
339
+ updates of a strict app are reported. A cleanup failure during server
340
+ rendering is thrown from the render call. In hosts without `reportError()`,
341
+ such as jsdom and Node under Vitest, reported failures surface as unhandled
342
+ errors; stub `globalThis.reportError` in tests that throw from cleanup on
343
+ purpose.
344
+ - fix(hydration): markup verification (`hydrate: { verifyMarkup }`, on by
345
+ default outside production) now also compares the server HTML with the DOM
346
+ the client renderer produces while hydrating it, so SSR/client renderer
347
+ divergences such as a function child the server rendered empty throw
348
+ `Hydration mismatch detected` instead of passing a server-against-server
349
+ comparison. Both comparisons normalize through the DOM and compare `style`
350
+ attributes by their parsed declarations, so the SSR `color:red;` and the
351
+ DOM's `color: red;` are equal. The client check is skipped for static pages
352
+ hydrated at a client-only query or hash and for pages with server-rendered
353
+ portal content. A mount failure under `hydrate: { deferUntilIdle: true }` now
354
+ rejects `hydrateSPA()` instead of leaving it pending.
355
+
356
+ - fix(runtime): an `ErrorBoundary` fallback now removes the portal content its
357
+ failed subtree wrote. A component that rendered no DOM of its own (such as
358
+ a writer that returns only `<Portal>`) shared the boundary's host node and
359
+ survived the fallback, so its `Portal` content stayed in the host, still
360
+ mounted, and its cleanups never ran. The fallback now disposes every
361
+ component inside the boundary. This applies to render, function-valued prop
362
+ and control-flow errors, and `reset()`/`resetKey` recovery writes the portal
363
+ again. An explicit `DefaultPortal` host replaced by a fallback (directly, or
364
+ inside a component or `Show`/`For`/`Case`) keeps the portal claimed, so its
365
+ content does not move to the automatic host. During SSR and SSG, portal
366
+ writes made by a subtree whose boundary renders its fallback are discarded
367
+ instead of being emitted.
368
+ - fix(events): delegated handlers on app nodes inside an open shadow root
369
+ attached within an app's tree now run, once and in native bubbling order,
370
+ with `stopPropagation()` respected and `event.target` set to the real target
371
+ inside the shadow tree (retargeted to the host outside it). Dispatch follows
372
+ `composedPath()` up to the app root, falling back to the target's ancestry
373
+ when a host's composed path skips ancestors. `change` and `submit`, which
374
+ are not composed and never leave a shadow root, are no longer delegated and
375
+ attach directly to their element, so `onChange` and `onSubmit` run inside
376
+ open and closed shadow roots; each element with one of these handlers now
377
+ carries its own native listener. Delegated handlers inside closed shadow roots,
378
+ and delegated event types dispatched with `composed: false` inside a shadow
379
+ root, still do not run; mount an app inside the shadow root instead.
380
+ - fix(state): a `derive()` or `selector()` owned by a component whose
381
+ ancestor is queued to re-render (including a portal writer) or whose `<For>`
382
+ is about to reconcile (including through a `derive()`/`selector()` chain
383
+ feeding `each`) is no longer evaluated in the derived lane with the
384
+ component's stale props. It waits for that render, so a list row being
385
+ removed no longer runs (and throws from) a derive that indexes by its old
386
+ prop; a surviving component still gets the updated value in the same flush.
387
+ - fix(router): navigation edge cases. `navigate()` and guard redirects to
388
+ another origin now load that URL with `location.assign()` (or `replace()`)
389
+ instead of rendering its path in-app. A hash that is not valid
390
+ percent-encoding (`#%E0`) no longer throws after the history entry was
391
+ written. `<Link target="_self">` is handled by the router. String targets for
392
+ `navigate()`, `<Link href>` and `redirect()` are always logical below a
393
+ registry `basePath`: `/app/settings` under `/app` now goes to
394
+ `/app/app/settings` instead of being treated as already mounted, and
395
+ development builds warn about such strings. `navigate()`, `redirect()`,
396
+ `loginPath` and `authenticatedRedirectTo` accept a typed destination from
397
+ `to()`, whose public href is used as-is; `RouteDestination` is branded so
398
+ only `to()` creates one. Only targets with an explicit `http:`/`https:`
399
+ scheme may leave the origin: path-like strings that resolve to another host
400
+ (`//evil.example`, `/\evil.example`) throw a `TypeError`, on the client,
401
+ in server redirect decisions, for `loginPath` and at `<Link href>` render.
402
+ A redirect to another origin during the first load is handed to the browser
403
+ instead of rendering its path locally, and an absolute `loginPath` on
404
+ another origin keeps its origin when `next` is appended. Same-origin paths whose
405
+ dot segments collapse to a leading `//` (`/.//evil.example`) are refused
406
+ too, and history writes and document loads receive absolute URLs.
407
+ `history.pushState()`/`replaceState()` calls from `navigate()`, `<Link>`,
408
+ redirects and `updateRouteQuery()` now pass an absolute same-origin URL
409
+ (`https://site.example/page`) instead of a root-relative one, so code that
410
+ wraps or spies on them sees the full URL. Enhanced action redirects are
411
+ checked the same way and assigned as absolute URLs.
412
+ - fix(resources): a synchronous `task()` registers its cleanup as it runs
413
+ instead of one microtask later, so removing its owner right after mount
414
+ (for example a function child remounting when its hooks change) runs the
415
+ cleanup before the replacement's task. A task that throws still reports
416
+ the error as before.
417
+ - fix(ssr): function children and props, and `state`/`derive` cells passed
418
+ as children or props, now render their current value on the
419
+ server instead of nothing (children) or the function's source text (props).
420
+ Each is called once, untracked, and escaped like a static value, so the
421
+ server markup matches the client and hydration adopts it in place. On both
422
+ server and client, a function child that returns a `state`/`derive` cell
423
+ renders that cell's value (and the client follows it); any other function
424
+ in a function child's result renders nothing. A component that returns a
425
+ function or a cell itself renders nothing, but function and cell items in
426
+ the fragment or array a component returns (for example a layout that
427
+ renders `<>{props.children}</>`), and a function child of `ErrorBoundary`,
428
+ now render reactively on the client as they do on the server; the client
429
+ dropped them. `ErrorBoundary` no longer wraps text or fragment content in a
430
+ `<div>` on the client. A function child may use hooks
431
+ (`state()`, `resource()`, `task()`, `watch()`), `Show`/`For`/`Case` and
432
+ `readScope()` in every position, on both sides. The server renders each
433
+ one as a `FunctionChild` component; on the server these threw. On the
434
+ client, an element's function child that only reads values stays a direct
435
+ DOM binding, and upgrades in place to a mounted `FunctionChild` component
436
+ the first time a run asks for a component (code before the first hook then
437
+ runs again); a function child whose hooks change between runs remounts
438
+ with fresh state rather than reporting a hook-order error. Before, hooks there rendered
439
+ nothing, resources never resolved, and `readScope()` could read the wrong
440
+ provider. A function child of `Portal` now renders on the client. Parent
441
+ re-renders no longer remove the text of an element's function child when
442
+ the element is a component's root (`<div data-n={n}>{() => o()}</div>`),
443
+ also with element siblings. Hydrating an element returned by a function child now sets up that
444
+ element's own function children instead of clearing them. A function
445
+ child that throws on the client now goes to the nearest `ErrorBoundary`,
446
+ or is thrown from the update without one, like a reactive prop; it was
447
+ logged and swallowed, leaving the element empty, while the server
448
+ rendered the boundary's fallback.
449
+ - fix(renderer): a child list that starts with an item that renders nothing
450
+ (a plain object, a function, `true`) no longer duplicates the following
451
+ text or elements when it updates existing nodes, such as server markup
452
+ being hydrated (#544).
453
+ - fix(ssr): `renderResolvedToStringSync()` no longer throws "no route found"
454
+ for a route without params when `params` is omitted.
455
+ - fix(renderer): a component returning a fragment or array now retains its
456
+ child components when it re-renders with new props. Matching children keep
457
+ their state and DOM identity, including after hydration; nested fragments
458
+ reconcile against the same flattened child list used at creation.
459
+ - fix(control): existing `For` rows now render with the latest row callback.
460
+ A value the parent computed during render and captured in the callback (for
461
+ example `const current = selected()`) kept its first value in rows that were
462
+ already mounted. When the parent rerenders with a new callback, retained rows
463
+ rerun with it and keep their DOM, key, and local state; a stable callback
464
+ still skips them. A row that reruns on its own, because it read a reactive
465
+ value, now keeps its key: a component in that row previously lost its local
466
+ state when the row rendered again in the same flush. The docs now also state
467
+ that a reactive read inside the callback subscribes the row that made it
468
+ (they previously said it did not subscribe). See docs/guides/control-flow.md.
469
+ - fix(renderer): a failed keyed reconciliation commit now propagates to the
470
+ component update, which rolls the DOM back and routes the error to the
471
+ nearest `ErrorBoundary` (or throws it from the flush). Previously any commit
472
+ error was swallowed and the parent was rebuilt with `replaceChildren()`,
473
+ which also tore down children that were being reused. Errors from grouped
474
+ blueprint bindings (the second and later instances of a component or `For`
475
+ row) and from reactive child functions (`{() => ...}`) now reach the nearest
476
+ `ErrorBoundary` like single reactive props, or are thrown from the update
477
+ when there is none, instead of a development-only warning. A reactive child
478
+ update that fails while changing the element's children is rolled back
479
+ instead of left half-applied.
480
+ - fix(fx): errors thrown by `scheduleTimeout`/`scheduleIdle` callbacks, by
481
+ handlers run later by `debounceEvent`/`throttleEvent`/`rafEvent`, and by
482
+ `scheduleRetry` (a synchronous throw, the last attempt's rejection, or a
483
+ throwing `backoff`) are reported with `reportError()` like
484
+ event handler errors, instead of only being logged.
485
+ - build(deps): `@askrjs/auth` and `@askrjs/schema` are now optional peer
486
+ dependencies instead of dependencies, so apps that do not use route auth or
487
+ schemas no longer install auth's SAML/XML stack. Askr uses them for types
488
+ only: route requirements are composed by Askr itself, and the published
489
+ declarations typecheck without either package installed. Apps that use
490
+ `@askrjs/auth` or `@askrjs/schema` must list them in their own
491
+ dependencies.
492
+ - fix(ssg): the `@askrjs/askr/ssg` declarations no longer contain a stray
493
+ `import 'node:fs/promises'`, so consumers without `@types/node` typecheck.
494
+ - test(test-utils): `npm run typecheck` (and so `npm run lint`) now also
495
+ typechecks `test-utils/**`, including the Playwright browser app, through
496
+ `test-utils/tsconfig.json`. The existing type errors are fixed: fixtures
497
+ import `state` from the root entry, the playwright app's Vite config uses
498
+ `oxc.jsx`, and two scenarios no longer rely on unsafe nullable state reads.
499
+ - test(benches): `npm run typecheck` also typechecks `benches/`. Bench fixtures
500
+ now use `htmlFor`, a numeric `tabIndex`, `RouteHandler` route handlers and a
501
+ complete auth context, matching the public types.
502
+ - perf(env): development/production checks and renderer debug-flag reads no
503
+ longer copy `process.env` on every call. `isProductionEnvironment()` runs on every component render, and
504
+ enumerating the environment is expensive on Windows, where it dominated
505
+ deep component trees (a 10,000-deep chain took ~60s in jsdom on Windows CI;
506
+ it now mounts in ~0.1s locally, down from ~2.9s).
507
+ - test: scheduler tests now exercise the behaviour their names describe
508
+ (render-time writes, nested handlers, `scheduleEventHandler` deferral,
509
+ mid-flush lane order, the render-time write guard error), the runtime and
510
+ native owner-view consumer contracts assert observable behaviour instead of
511
+ private fields, and the browser form tests wait on the
512
+ pending render instead of wall-clock timing.
513
+
514
+ - fix(ssr): `renderRouteRequest()` streams each deferred `Resolve` boundary as
515
+ soon as its value settles instead of awaiting boundaries one at a time in
516
+ declaration order, so a slow boundary no longer holds back faster ones.
517
+ Patches can arrive out of order; boundary ids stay deterministic. A `Resolve`
518
+ rendered inside a settled boundary's content now streams too (id `d:0.0`
519
+ under `d:0`) with the same request route state and auth, where its fallback
520
+ was previously never replaced.
521
+ - fix(renderer): a failed render no longer leaves fine-grained bindings
522
+ (function-valued props and children) stale. A binding whose function the
523
+ render replaced kept the new value after the DOM rolled back, so later
524
+ renders saw nothing to change, and an update the binding was due to run in
525
+ the same flush was dropped. Bindings now roll back with the render and then
526
+ catch up with their state; see "Fine-grained bindings and rollback" in
527
+ docs/core/rendering.md.
528
+ - fix(control): development and production now agree on invalid `For` keys and
529
+ `Case`/`Match` children. A null, undefined, or duplicate `For` key throws in
530
+ every build (production previously dropped rows and showed the last
531
+ duplicate's data), and a non-`Match` child of `Case` or a `Match` outside a
532
+ `Case` throws in every build (production previously dropped it). These errors
533
+ reach the nearest `ErrorBoundary`, including a duplicate key introduced by a
534
+ boundary-local list update, a list inside a `Show`/`Case` branch, and an
535
+ invalid `Case` child, which previously escaped the boundary. Component
536
+ errors rendered inside a `For`/`Show`/`Case` created above an
537
+ `ErrorBoundary` now reach that boundary too. `For` resolves each row key
538
+ once per update, and the development-only key-type-change check (which could
539
+ never fire) is removed.
540
+ - fix(renderer): props whose live state is not the attribute now set the DOM
541
+ property. `<video muted>` sets `video.muted` (and keeps the attribute),
542
+ `<input indeterminate>` sets `input.indeterminate` without an attribute, and
543
+ object/array values on custom elements are assigned as properties. New
544
+ `prop:name` and `attr:name` escape hatches force either path. SSR renders
545
+ only attribute-backed values; property-only values apply on hydration. Removed
546
+ properties reset to their default, property writes roll back with a failed
547
+ commit, and the escape hatches keep the URL and raw-HTML guards.
548
+ - fix(renderer): delegated event handlers now match native dispatch.
549
+ Delegated listeners attach at each app root instead of `document.body`, so
550
+ apps mounted in shadow roots or iframes receive events and nested apps each
551
+ dispatch their own handlers once. Non-bubbling events (`focus`, `blur`,
552
+ `scroll`) attach directly to their element, so an ancestor's `onScroll` or
553
+ `onFocus` no longer runs, ancestor-first, for a descendant. `onWheel`,
554
+ `onTouchStart` and `onTouchMove` attach directly with `{ passive: false }`,
555
+ so `preventDefault()` in them takes effect. Hydrated nodes use the same
556
+ delegated listeners as client-rendered ones, so a client-rendered child's
557
+ handler runs before (and can stop) a hydrated ancestor's handler.
558
+ - fix(renderer): `onFocus` and `onBlur` no longer bubble: they only run when
559
+ their own element gains or loses focus, as with native `focus`/`blur`.
560
+ Container components that tracked focus inside a subtree with `onFocus`/
561
+ `onBlur` should migrate to `onFocusIn`/`onFocusOut`.
562
+ - fix(resources): a `resource()` deps change seen by a render that is rolled
563
+ back (for example because a sibling component throws in the same render) no
564
+ longer leaves the resource stuck `pending`. The new deps, loader and generation are
565
+ committed with the render, so the next committed render still starts the
566
+ fetch, and one back on the committed deps keeps the committed value.
567
+ - breaking(data): raw-string `invalidate(prefix)`, `invalidateOnInterval()`
568
+ and mutation `affects` prefixes now match whole `:`-delimited key segments.
569
+ `invalidate('user:1')` still matches `user:1` and `user:1:permissions` but no
570
+ longer matches `user:10`; prefixes ending in `:` (including every
571
+ `queryScope()` prefix) match as before. Only `:` is a segment boundary, so
572
+ raw prefixes built with other separators no longer match by text:
573
+ `invalidate('/api/users')` no longer matches `/api/users/1`, and
574
+ `invalidate('a.b')` no longer matches `a.b.c`. Move such keys to `:`
575
+ delimiters or `queryScope()`.
576
+ - fix(boot): `createSPA({ dataRuntime })` and `hydrateSPA({ dataRuntime })` now
577
+ use the configured runtime consistently. `hydrateSPA` previously seeded the
578
+ custom runtime from the hydration payload while route-component queries read
579
+ the default one, so readers showed a loading state and refetched instead of
580
+ using the hydrated value. Route `preload` hooks (initial route and client
581
+ navigations) also prefetched into the default runtime, so readers of a
582
+ custom runtime never saw the preloaded data.
583
+ - feat(ssr): `escapeHtml()` from `@askrjs/askr/ssr` escapes `&`, `<`, `>`, `"`
584
+ and `'` for request-derived values interpolated into a hand-written
585
+ `document` renderer template. It accepts any value; `null` and `undefined`
586
+ become an empty string. The SSR, SSG and rendering guides now use it.
587
+ - docs: fix examples that failed at runtime. The API overview and core data
588
+ guide no longer call `state()`/`derive()` at module scope, the quick-start,
589
+ resources, core data and resources reference `resource()` examples check `error` before `pending || !value` so a failed first load no
590
+ longer shows "Loading..." forever, and the runtime-enforcement examples now
591
+ actually trigger the documented hook-order and render-mutation errors and
592
+ quote the real message. Doc fences tagged `run=<id>` are now imported and
593
+ exercised in jsdom by `npm run test:checks`
594
+ (`tests/checks/docs/runnable-snippets.test.ts`), not only type-checked.
595
+ - fix(renderer): event handler errors are reported with `reportError()`, which
596
+ dispatches a `window` `error` event, instead of only being logged. This covers
597
+ delegated and direct listeners and `scheduleEventHandler`; the remaining
598
+ handlers for the event still run. Hosts without `reportError()` (Node, jsdom)
599
+ rethrow the error from a microtask, so it arrives as an `uncaughtException`
600
+ and test runners that fail on unhandled errors report it. Errors thrown by
601
+ function-valued (reactive) props now reach the nearest `ErrorBoundary`
602
+ (including one whose direct children contain the binding; bindings in a
603
+ fallback go to the boundary above), or are thrown from the update when there
604
+ is none. Previously they were a development-only warning and silent in
605
+ production.
606
+ - fix(renderer): keyed fast paths no longer catch errors and retry through a
607
+ slower path. A row whose render throws now renders once per update instead of
608
+ up to three times, and the error surfaces once. Production commits no longer
609
+ capture an `Error().stack` for diagnostics.
610
+ - chore(bench): the benchmark workflow runs only the existing tier1 and tier2
611
+ lanes, as one matrix job per tier, instead of 36 copy-pasted steps that also
612
+ invoked the removed `bench:tier3`/`bench:tier4` scripts. The browser-only
613
+ `precise_clock` input and its dead tier3/4 config are removed, artifacts are
614
+ uploaded per tier (`bench-results-stable-tier<N>`), and docs no longer
615
+ describe the deleted lanes, their guardrails, or hydration timings taken from
616
+ them. A `tests/checks` guard fails on bench scripts, configs, or files that
617
+ docs and workflows reference but do not exist.
618
+ - chore(agents): AGENTS.md now explicitly allows maintainer-run release tooling
619
+ (`scripts/publish-order.mjs`) and prefers workflow matrices over copy-pasted
620
+ steps; `tests/checks` fails on any unlisted `scripts/*` file.
621
+ - fix(router): when several page `fallback()`s match a URL, the deepest page
622
+ prefix (counted in segments) now wins on the client and in sync and async
623
+ SSR. Previously the longest prefix string won, so an encoded prefix such as
624
+ `/caf%C3%A9` could outrank a deeper `/café/x`.
625
+ - fix(router): registering two routes that match the same URLs now throws
626
+ `Duplicate route path` instead of silently shadowing the second. Routes are
627
+ compared the way they match: parameter and splat names, trailing slashes and
628
+ percent-encoding of static segments are ignored, a `*` wildcard equals a
629
+ param, and a `fallback()` equals a named splat at its prefix. Each registry
630
+ is checked separately. Declare a template once and use `entries()` for its
631
+ pages.
632
+ - fix(router): `fallback()` inside a parameterized page such as
633
+ `page('/{lang}')` now handles misses under `/en/...` (and receives `lang`)
634
+ instead of matching only the literal `/{lang}/...`.
635
+ - fix(ssg): `invalidationKeys` passed to `route()` were dropped from the
636
+ registry, so incremental generation treated those routes as keyless and
637
+ always rebuilt them. They now apply to every page the route's `entries()`
638
+ generate.
639
+ - fix(boot): error messages no longer point at a nonexistent `createSSR`; they
640
+ name `createSPA`/`hydrateSPA` (and `createIslands`). Removed the unreachable
641
+ redirect branches in `createSPA`/`hydrateSPA`, and sync SSR now matches
642
+ routes against the registry's manifest records like async SSR does.
643
+ - fix(ssr): async render contexts resolve `AsyncLocalStorage` from
644
+ `globalThis.AsyncLocalStorage` or `process.getBuiltinModule('node:async_hooks')`
645
+ instead of `new Function('return require(...)')`. Previously synchronous
646
+ `withRenderContext()` could not accept async callbacks under Node ESM (where
647
+ that loader never resolved `require`), and async render contexts were
648
+ rejected under a CSP without `'unsafe-eval'` and on runtimes without
649
+ `process.versions.node`. Any runtime that provides `AsyncLocalStorage`
650
+ globally or via `process.getBuiltinModule` is now supported; see the SSR
651
+ guide.
652
+ - fix(runtime): `cspNonce()` decides whether a render scope is active from
653
+ scope state instead of matching the text of `readScope()`'s error message.
654
+ - fix(runtime): `state.set()` now throws when called inside a `derive()` or
655
+ `selector()` computation, including recomputes in the derived lane where no
656
+ component is rendering. Previously only render-time recomputes were caught
657
+ (by the render-mutation guard), so a derived-lane write went through
658
+ silently and could loop. A same-value (no-op) `set()` is still allowed.
659
+ - fix(runtime): an error thrown by the renderer while marking reactive props
660
+ dirty is no longer swallowed. Component readers of the source are still
661
+ notified, then the error is rethrown to the writer (or aggregated by the
662
+ scheduler inside a flush). If notifying readers also fails, both errors are
663
+ thrown together as an `AggregateError`.
664
+ - fix(router): `hydrateSPA()` no longer redirects a server-authorized page to
665
+ the login route when the browser cannot resolve the identity itself (for
666
+ example httpOnly-cookie sessions). Apps opt in with the new
667
+ `auth.dehydrate(context)` hook, which selects the minimal identity snapshot
668
+ (`authenticated`, `principal`, `tenant`, `scopes`; never the session)
669
+ serialized into the hydration payload. Hydration uses it for the initial
670
+ route; navigations use `auth.resolve`, or keep the snapshot when no resolver
671
+ is configured. Nothing about the identity is serialized without the hook.
672
+ `RouteAuthOptions.resolve` is now optional.
673
+ - fix(fx): `scheduleTimeout()`, `scheduleIdle()` and `scheduleRetry()` now
674
+ cancel pending work when the component that scheduled them unmounts, as
675
+ documented. Calls made from a mounted component's `task()`, `watch()`
676
+ callback, mount/commit operation or event handler bind to that component's
677
+ lifetime, including portal content (owned by the writer) and handlers
678
+ wrapped by `debounceEvent()`, `throttleEvent()`, `rafEvent()` or
679
+ `scheduleEventHandler()`. Scheduled callbacks and retry attempts run as the
680
+ scheduling component, so work they reschedule (for example a polling loop)
681
+ also stops on unmount, and a stable portal handler follows the writer that
682
+ last rendered it. A synchronous scheduler flush inside a handler no
683
+ longer runs unrelated queued work as that handler's component. Previously no
684
+ cleanup was ever registered and timers fired after unmount. The unreachable
685
+ SSR branches in these helpers are removed, and `scheduleRetry()` settles
686
+ when `fn` throws synchronously or returns a non-promise.
687
+ - fix(fx): `throttle(fn, ms, { leading: false })` waits the full interval
688
+ after an idle gap instead of firing on the next tick, and a throttle without
689
+ a trailing edge no longer retains the last arguments.
690
+ - fix(fx): `debounceEvent({ leading: true })` and the default
691
+ `throttleEvent()` no longer call the handler twice for a single event. The
692
+ trailing edge only runs when another event arrived after the leading call.
693
+ Both now share their edge logic with `debounce()` and `throttle()`, and
694
+ `debounceEvent().flush()` only runs a pending trailing call.
695
+ - fix(foundations): `mergeProps` no longer lets a `base` value of `undefined`
696
+ overwrite an injected prop. Forwarding an optional prop that was not supplied
697
+ (`onClick={props.onClick}`) used to wipe the primitive's handler or ARIA
698
+ attribute; `undefined` now means "not provided". Pass `null` to clear an
699
+ injected prop explicitly; intrinsic `on*` handler props now accept `null` in
700
+ their types. `mergeInteractionProps` still lets the policy own `disabled`, so
701
+ an enabled native policy clears a fixed `disabled` from the user or child.
702
+ - fix(data): an inline `createQuery({ key, fetch })` no longer warns about a
703
+ conflicting shared query definition on every re-render, and no longer keeps
704
+ the first render's `fetch` closure forever. The reader that defines a key
705
+ now replaces its `fetch`, `isConsistent`, and `reconcile` on each render;
706
+ only other readers of the same key with a different definition warn, after
707
+ the current render work settles (a keyed row replacing the owner does not
708
+ warn). When the defining reader unmounts, a remaining reader's definition
709
+ takes over immediately. An in-flight fetch is checked and reconciled with the
710
+ callbacks it started with. `createQueryCollection()` entries are redefined on
711
+ each update, so `retry()` fetches with the entry's current `input`.
712
+ - fix(ssr): text children of HTML `<script>` and `<style>` are written verbatim
713
+ instead of entity-escaped, so `a > b` no longer becomes `a &gt; b` and breaks
714
+ the CSS or JavaScript. In styles every `<` becomes the CSS escape `\3c `; in
715
+ scripts every `</`, `<script` and `<!--` is rewritten (JSON-safe), so content
716
+ cannot close the element or an ancestor. Text stays entity-escaped inside SVG
717
+ or MathML, `<style>` inside `<select>`, and under raw text or RCDATA ancestors
718
+ such as `<noscript>` and `<textarea>`. Element children inside these elements
719
+ now throw during SSR.
720
+ - fix(router): route precedence is decided segment by segment, as documented:
721
+ the first segment where two routes differ picks static > param > wildcard >
722
+ splat, so `/docs/{*rest}` now beats `/{lang}/{page}` for `/docs/intro`
723
+ instead of losing on a summed score. SPA, SSR and SSG share the ordering.
724
+ `RouteRecord.rank` now encodes this order and its numeric values changed.
725
+ - fix(router): static route segments, `fallback()` prefixes and registry
726
+ `basePath` values are compared against decoded URL segments, so routes such
727
+ as `/café` and `/a b` (and fallbacks or registries mounted under them) match
728
+ `/caf%C3%A9` and `/a%20b` on the client, in SSR and in SSG. Malformed
729
+ encodings do not throw.
730
+ - fix(router): the `*` capture of wildcards, catch-alls and fallbacks is now
731
+ percent-decoded like param and splat captures (`café`, not `caf%C3%A9`).
732
+ Encoded separators `%2F` and `%5C` now stay encoded in every capture,
733
+ including params and named splats, which previously decoded `%2F` to `/`:
734
+ `/files/..%2F..%2Fetc` captures `..%2F..%2Fetc`, not `../../etc`. `to()`
735
+ passes kept `%2F`/`%5C` through, so captures round-trip to their URL.
736
+ - fix(renderer,ssr): camelCase SVG presentation props such as
737
+ `strokeDasharray`, `fillOpacity`, `stopColor` and `clipPath` now render as
738
+ their hyphenated attribute names, and `xlinkHref`/`xmlLang`/`xmlSpace`/
739
+ `xmlnsXlink` render as namespaced `xlink:`/`xml:`/`xmlns:` attributes (set
740
+ with `setAttributeNS` on SVG and MathML elements on the client). Previously
741
+ only five SVG names were mapped and the rest were written verbatim, which
742
+ browsers ignore. The mapping also applies to HTML elements, so a prop such
743
+ as `fontSize` or `pointerEvents` on a `<div>` now renders `font-size` or
744
+ `pointer-events` instead of `fontsize`. Custom elements (tag names with a
745
+ `-`) keep their prop names as before. The SVG prop types now list the
746
+ presentation attributes.
747
+ - fix(renderer,ssr): numeric `style` values get a `px` unit on non-unitless
748
+ properties, so `style={{ width: 10 }}` renders `width:10px` instead of the
749
+ invalid `width:10`. Unitless properties (React's list plus
750
+ `font-size-adjust`, `initial-letter` and `math-depth`, with or without a
751
+ vendor prefix), `0` and custom properties are unchanged. `ms`-prefixed names
752
+ such as `msFlexPositive` now render as `-ms-flex-positive`.
753
+ - fix(renderer,ssr): `false` renders `"false"` for the enumerated attributes
754
+ `draggable`, `spellCheck`, `contentEditable` and `writingSuggestions`
755
+ instead of removing them, so
756
+ `<img draggable={false}>` is no longer draggable. Their prop types now accept
757
+ booleans.
758
+ - breaking(data): `defineQuery()` fetchers now receive the input and the abort
759
+ signal as separate arguments, `fetch(input, { signal })`, instead of one
760
+ merged `{ ...input, signal }` object. The merged shape dropped primitive
761
+ inputs (a `QueryDefinition<number, ...>` fetcher only saw `{ signal }`) and
762
+ let the abort signal overwrite an input field named `signal`. Rewrite
763
+ `fetch: ({ id, signal }) => ...` as `fetch: ({ id }, { signal }) => ...`.
764
+ Old-style fetchers with an annotated parameter
765
+ (`({ id, signal }: { id: string; signal: AbortSignal })`) still compile at
766
+ the `defineQuery()` definition but fail to typecheck at the `createQuery()`
767
+ call site; move `signal` to the second argument.
768
+ Inline `createQuery({ key, fetch })` fetchers are unchanged.
769
+ - fix(router): client navigation no longer dead-ends on URLs no registered app
770
+ can render. A `Link` click or `navigate()` to an unmatched same-origin URL, or
771
+ one outside the registry `basePath`, now loads the URL as a document, and
772
+ Back/Forward to an unmatched entry reloads the page instead of leaving the old
773
+ page mounted under the new URL. A `fallback()` route still renders in place.
774
+ Navigating to the already-loaded URL with no route, or a fragment-only
775
+ Back/Forward on such a page, skips the load. A failed Back/Forward render or
776
+ rejected Back/Forward loader now returns with `history.go()` to the entry whose page
777
+ is still rendered instead of overwriting the entry the user landed on; Askr
778
+ stamps an `askrIndex` position into the history state it writes and reloads
779
+ when an entry written by other code makes positions unknown.
780
+ - fix(resources): a resource hydrated from preloaded data keeps its value on
781
+ later re-renders instead of resetting to pending and refetching. The preloaded
782
+ value now seeds the resource, so `refresh()` and `deps` changes also work
783
+ after hydration.
784
+ - fix(ssr): deferred `Resolve` boundaries now render with the request URL, route
785
+ table, base path and params. Previously the streamed boundary saw the root
786
+ path with empty params, so `currentRoute()` and `Link` produced the wrong HTML
787
+ and hydration reported a markup mismatch.
788
+ - fix(data): SSR `prefetchQuery()` no longer throws a `TypeError` in
789
+ development when a query has no registered server handler. The
790
+ skipped-preload warning is tracked per runtime outside the frozen
791
+ `DataRuntime`, so it logs once per query key and resolves `false` as intended.
792
+ - fix(runtime): `derive()` no longer serves a value computed by a previous
793
+ render's closure (for example after a second render in the same flush from
794
+ `watch()` or `task()`, new props, or a local read from another derive). A
795
+ source change evaluates the derive once with the last render's function and
796
+ skips the owner re-render when the value is unchanged; when it changed, the
797
+ owner re-renders and evaluates its new function once more. A render-time
798
+ recompute of `derive()` or `selector()` now notifies downstream readers, so
799
+ derived values in other components no longer stay one update behind.
800
+ - fix(runtime): hook-order enforcement now catches a render that claims fewer
801
+ hooks than the first render, and a slot whose hook kind changes (for example
802
+ `derive()` where the first render called `state()`). Previously only extra
803
+ hooks threw; skipped or swapped hooks passed silently. Messages name hooks by
804
+ their public API (`createQuery()`, `onRouteChange()`, `<For>`). The unreachable
805
+ monotonic index check is removed, and the internal `ComponentInstance`
806
+ field `expectedStateIndices` is replaced by `expectedHookKinds`.
807
+ - fix(renderer): re-renders no longer strip attributes, class tokens and inline
808
+ styles added by other code (focus traps setting `aria-hidden`/`inert`,
809
+ animation libraries setting `style.transform`, tooltips adding `data-*`,
810
+ `classList.add`). Prop reconciliation now diffs against the props Askr last
811
+ applied to each element instead of the live DOM, so only what Askr rendered is
812
+ removed or patched.
813
+
814
+ - fix(ssr): sync `renderToString({ url, registry })`/`renderToStream()` no
815
+ longer follow auth redirects or render a denial marker with an implicit 200.
816
+ Redirect and deny decisions throw the new `SSRAccessDecisionError`, whose
817
+ `decision` matches what `renderRouteRequest()` returns. The sync path also no
818
+ longer starts route loaders: a loader route throws `SSRDataMissingError`
819
+ naming the route and pointing to `renderRouteRequest()`, and abandoned async
820
+ resolution no longer leaks an unhandled rejection.
821
+ - breaking(ssr): sync `renderToString({ url, registry })`/`renderToStream()`
822
+ no longer follow auth redirects or render a denial marker with an implicit 200. Redirect and deny decisions throw the new `SSRAccessDecisionError`, whose
823
+ `decision` matches what `renderRouteRequest()` returns. The sync path also no
824
+ longer runs route loaders: any route that declares a loader, including
825
+ a synchronous one that previously rendered, now throws `SSRDataMissingError`
826
+ naming the route and pointing to `renderRouteRequest()`, before its preload,
827
+ lazy import, or loader starts. Abandoned async resolution no longer leaks an
828
+ unhandled rejection, and lazy routes whose component is already loaded now
829
+ resolve synchronously.
830
+ - fix(runtime): an update loop that trips the scheduler's `MAX_FLUSH_DEPTH`
831
+ guard no longer aborts the flush. The looping task is dropped and its error is
832
+ reported together with earlier task failures, remaining queued work still
833
+ runs, a dropped component update re-renders on its next write, and
834
+ production builds now fail such a loop instead of hanging. Effects,
835
+ `derive()` and `selector()` are now each limited to 50 runs per flush,
836
+ counted across lanes, so a reactive cycle through them (including
837
+ derive-to-derive cycles that previously hung inside one batch) stops at the
838
+ looping entry without stranding sibling work.
839
+ - fix(router): `currentAuth()` no longer falls back to the process-wide client
840
+ identity during server rendering. A server render without request auth now
841
+ sees an anonymous identity, and server-mode route resolution no longer writes
842
+ the client identity, so one request cannot observe another request's user.
843
+ - fix(ssr): rejected `defer()` values no longer serialize the server error
844
+ message into the hydration payload. The client receives a generic reason
845
+ unless the error sets `expose: true`.
846
+ - fix(renderer,ssr): drop `javascript:` and `vbscript:` URLs from `src` and
847
+ `data` attributes (for example `<iframe src>` and `<object data>`) on both
848
+ client and server. `data:`, `blob:` and custom-scheme resource URLs are
849
+ unchanged.
850
+ - fix(data): hydrated or prefetched query data is now consumed by the first
851
+ client reader for its key instead of staying in `runtime.queryData` forever.
852
+ After a `refresh()` and a remount, the query fetches again instead of
853
+ reviving the original server value as fresh, and consumed entries no longer
854
+ accumulate on the default runtime. Server renders still read without
855
+ consuming. Prefetches (including route `preload`) skip keys a mounted query
856
+ already owns and discard results that resolve after a reader mounted. The
857
+ browser keeps at most 50 unread prefetched entries per runtime, evicting the
858
+ oldest; server and SSG payload building is not capped.
859
+
860
+ ## 0.3.1 — 2026-09-12
861
+
862
+ - fix(boot): move the SSR style registry carrier out of the hydration root
863
+ before mounting. `@askrjs/server` prepends the collected styles to the page
864
+ body, so they arrive as the first child of the mount root; that extra element
865
+ made the root's child list disagree with the rendered tree, which both cost
866
+ the app in-place hydration (the server node was replaced rather than adopted)
867
+ and discarded the carried CSS during reconciliation. The carrier now moves to
868
+ `<head>`, where it still applies and no longer participates in reconciliation.
869
+
5
870
  ## 0.3.0 — 2026-09-11
6
871
 
7
872
  - refactor(internal): rename `src/compatibility/` to names that describe what it
@@ -13,6 +878,15 @@
13
878
  - fix(runtime): reuse a context-free default abort reason during component
14
879
  teardown so retained signals do not retain departed component generations.
15
880
 
881
+ ## 0.2.4 — 2026-08-28
882
+
883
+ - feat(router): expose entry-local history state on `RouteSnapshot` as `state`,
884
+ with `hasState` distinguishing an omitted state from an explicit `undefined`.
885
+ Both are required members, so code that constructs a `RouteSnapshot` by hand
886
+ (test fixtures, adapters) must supply them; code that only reads snapshots is
887
+ unaffected.
888
+ - chore: refresh dependencies.
889
+
16
890
  ## 0.2.3 — 2026-08-25
17
891
 
18
892
  - fix(data): make mutation invalidation and reconcile retries generation-safe