@askrjs/askr 0.3.1 → 0.4.1

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