@ultimat3/render 29.4.0 → 30.1.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.
package/CLAUDE.md CHANGED
@@ -44,19 +44,19 @@ axiom 6). Never `cli` (upward).
44
44
  | Island declaration | `island({ src })` — a **specifier**, never an import, so a `static` page's graph cannot grow the island's dependencies. Never add an overload that takes a component. |
45
45
  | Island filename | `*.island.tsx`, `ISLAND_EXTENSION` — one spelling. Never widen it. |
46
46
  | Island timing | the route's `hydrate` and nothing else. `hydrate: 'never'` + an island, or an `island()` call below the `defineRoute` that drains it, is `X_ISLAND_NOT_HYDRATED`; `islandNeverDrained(spec)` tells the two causes apart so the `fix:` names exactly one edit. |
47
- | Island node shape | a **branded array** (`IslandNode extends Array<never>`); every walker tests `isIslandNode` BEFORE `Array.isArray`. Solid's `JSX.Element` is an unaugmentable alias whose only object member is `ArrayElement`. Never import solid's union. |
47
+ | Island node shape | a **branded array** (`IslandNode extends Array<never>`); every walker tests `isIslandNode` BEFORE `Array.isArray`. Solid's `JSX.Element` is an unaugmentable alias whose only object member is `ArrayElement`. Never import solid's union. The brands `JSX_NODE`, `ISLAND_NODE`, `HOLE_NODE` are `@ultimat3/core/render-node`'s, imported by `jsx.ts`, `island.ts` and `hole.ts` (`hole.test.ts`) |
48
48
  | Island declaration order | `island()` above `defineRoute`, drained by it (`drainDeclaredIslands`), reachable from `./island` and never from `src/index.ts`. Per MODULE (evaluated once), unlike the per-render collector. |
49
49
  | Derived budget | `registry.ts`, from the surface: `site/` → `20kb`, `app/` → `34kb` (`DEFAULT_ISLAND_JS_BYTES` above `jsBaselineBytes`), calibrated on a Solid island. A declared `budget.js` wins; a `'never'` route gets none. |
50
50
  | `RouteEntry.islands` | filled from `config.islands` at registration and nothing else — `RegisterRouteInput` has no `islands` key. |
51
51
  | Island props | declared, JSON-safe, under `ISLAND_PROPS_MAX_BYTES` (16 KiB; the doc comment carries the arithmetic) — `island-props.ts` is the one gate, a structural walk, never a `JSON.stringify` round trip. Over the cap the cause names the heaviest props and the fix names the endpoint pattern, never "raise the cap". `X_ISLAND_PROPS_INVALID` has a row in `@ultimat3/http`'s table (500). |
52
52
  | A prop lands via `Object.defineProperty` | never `out[key] = v` — `__proto__` (a real own key off `JSON.parse`) would run the prototype setter. |
53
53
  | An attribute alias is a `Map` | never a record (`toString` resolved to a function). `MODE_SPECS[config.render]` is guarded by `Object.hasOwn` for the same reason. |
54
- | An attribute NAME is validated too | `ATTRIBUTE_NAME` in `html.ts` (`/^[A-Za-z_:][-A-Za-z0-9_:.]*$/`), refused as `null`; the `on*` handler check folds case. `head.ts`'s `renderTag` shares the predicate (`isAttributeName`). One predicate, never two. |
54
+ | An attribute NAME is validated too | `ATTRIBUTE_NAME` in `html.ts` (`/^[A-Za-z_:][-A-Za-z0-9_:.]*$/`), refused as `null`; the `on*` handler check folds case. `head-attributes.ts` prints a head tag through the same `attributePair`, one exception: a `link` href reads `safeUrl` as a `src` (raster `data:image/`) and `rel="alternate"` keeps `android-app:`/`ios-app:`. A drop logs `head.attribute_dropped` once per tag and attribute in dev. One rule, never two. |
55
55
  | Which attributes take a URL | `URL_BEARING_ATTRIBUTES` in `html.ts` — core's four plus `data`, `poster`, `ping`, `xlink:href`. `srcdoc` is refused outright (entity-decoded, then parsed as HTML). |
56
56
  | Island collection | per render, `renderToHtml(tree, { islands })`. Never module-global and never on an ambient context. |
57
57
  | A byte count in a message | `formatBytes` from `@ultimat3/core`, never a local one. |
58
58
  | Island bytes | **not this package's**: `@ultimat3/cli`'s `packages/cli/src/budgets.ts` measures the EMITTED document and is the gate. What stays here is the budget grammar (`parseByteBudget`) and `defaultIslandBudget`. |
59
- | The hydration runtime's CSP | `HYDRATE_RUNTIME_BODIES` — every body `hydrateRuntime` can emit (seven subsets, each with and without the hold part — fourteen). It is inline in every document with an island and `@ultimat3/http`'s `script-src` is `'self' 'wasm-unsafe-eval'`, so `@ultimat3/cli`'s `script-csp.ts` hashes this list at boot. `runtimeBody` is the one place the served and hashed text are one string. **Uncovered**: `render-stream.ts`'s per-hole `<script>$X("id")</script>` (unreachable today; the first real hole needs a nonce). |
59
+ | The hydration runtime's CSP | `HYDRATE_RUNTIME_BODIES` — every body `hydrateRuntime` can emit (seven subsets, each with and without the hold part — fourteen). It is inline in every document with an island and `@ultimat3/http`'s `script-src` is `'self' 'wasm-unsafe-eval'`, so `@ultimat3/cli`'s `script-csp.ts` hashes this list at boot. `runtimeBody` is the one place the served and hashed text are one string. A hole's reveal body is the constant `$X()`, hashed through `STREAM_REVEAL_BODIES`, and reachable. |
60
60
  | Island markup | the props `<script>` is emitted INSIDE the wrapper by `render-html.ts`; an assembler only adds `hydrateRuntime(directives)`. |
61
61
  | Island boot | `el.__x` holds the boot PROMISE, never a boolean. |
62
62
  | Island mount markers | `data-x-mounted=""` when `mount()` resolved, `data-x-failed="<message>"` when it rejected — set by the runtime, never by `emitIslandAttributes`. The rejection handler RETHROWS. Prelude sizes `As of 2026-10-05`: `idle` 1,843, `visible` 886, `interaction` 1,728 B (what `DEFAULT_ISLAND_JS_BYTES` derives from). |
@@ -77,7 +77,7 @@ axiom 6). Never `cli` (upward).
77
77
  | "Is this a TTL?" has one reader | `parseTtlMs` in `duration.ts`, below `modes.ts` and `render-isr.ts`. |
78
78
  | A build-time frame reads a throw with `renderThrowable` | `render-html.ts`, `render-static.ts`, `css-modules.ts`, `module-loader.ts`, `route-data.ts`. `bun run error-render` cannot see a value laundered through a local helper. |
79
79
  | `X_ROUTE_LOAD_FAILED` computes its pathname BEFORE the try | `pathnameOf` never throws on a relative `ctx.url`. |
80
- | The ISR key | `isrKey(url, locale)` — the path in http's `canonicalPath` spelling (no base path), the locale in the reserved `__x_locale` parameter, the sorted query. A key no route owns is never stored. The locale is REQUIRED. The time zone is deliberately not a dimension. `toResult` emits `vary: accept-language`, and `surrogate-key`/`cache-tag` from `@ultimat3/cache`'s `surrogateKeys()` over the route's `revalidateTags` — kept only while the response stays shared: http's `applyCacheHeaders` strips both on a rewrite to `private`/`no-store`. `registry.ts`'s `assertPurgeableTags` screens the tags at registration (`X_ROUTE_MODE_INVALID`); the rest of the shared key is `@ultimat3/http`'s `cache-headers` stage. File and mounted routes are both screened |
80
+ | The ISR key | `isrKey(url, locale, origin?)` — the path in http's `canonicalPath` spelling (no base path), the locale in the reserved `__x_locale` parameter, the request's origin in `__x_origin` when the app declares none, then the sorted DECLARED query. Full rule: `docs/history/render.md` |
81
81
  | A bust that lands MID-render | fenced with `@ultimat3/cache`'s `sampleFence({ key, tags })`, taken before `render()` and asked before `store.set`. `registerPath` runs BEFORE the render. |
82
82
  | ISR regeneration | `@ultimat3/core`'s `singleFlight`, `deadlineMs` = `regenerateDeadlineMs` (`DEFAULT_ISR_REGENERATE_DEADLINE_MS`, 30 s; `finiteCount` ≥ 1, no opt-out), `schedule` injectable. Past it the PATH is free; the hung render runs on, and `latest` (by identity) drops its result once a newer run started. `render-isr-flight.test.ts`, on a hand-fired scheduler. |
83
83
  | Which route a stored ISR path is | exact path first, else the MOST SPECIFIC pattern: `route-pattern.ts`'s `compilePattern` (never a second compiler), sorted on `CompiledPattern.specificity`. Never table order: `describePages()` sorts `*` before `:`. |
@@ -93,7 +93,7 @@ axiom 6). Never `cli` (upward).
93
93
  | Stream hole deadline | `DEFAULT_HOLE_TIMEOUT_MS` (15 s), `holeTimeoutMs: null` to opt out, otherwise a whole number ≥ 1. A hole reveals exactly once — promise, rejection or deadline. |
94
94
  | Errors | `errors.ts` subclasses only, but for `navigation-errors.ts` (re-exported; keeps `./client` off the table). Never a bare `Error`, never a bare `TODO`. |
95
95
  | Policy | render checks *presence* only. Evaluation belongs to `@ultimat3/policy`. |
96
- | A gated route is never a cached one | `modes.ts` refuses `policy` on both `static` and `isr` (`X_ROUTE_MODE_INVALID`, `modes.test.ts`). `ssr` is the one gated mode. |
96
+ | A gated route is never a cached one | `modes.ts` refuses `policy` on both `static` and `isr` (`X_ROUTE_MODE_INVALID`, `modes.test.ts`). `ssr` is the one gated mode. Both also RENDER as nobody: `@ultimat3/cli` wraps them in http's `withAnonymousRequest` (`runtime-render-shared-actor.test.ts`). |
97
97
  | Responses | return `RenderResult`. `@ultimat3/http` builds the `Response`. |
98
98
  | Who owns `cache-control` | a mode states intent; `@ultimat3/http`'s `cache-headers` stage decides and may overrule. This package cannot see the actor and never tries. |
99
99
  | Solid | no `solid-js` import anywhere in this package; `type-pins.tsx` satisfies `JSX.Element` structurally. `jsx.ts` builds inert nodes. Islands are compiled by `@ultimat3/cli`'s `solid-loader.ts` with `babel-preset-solid`. |
@@ -112,14 +112,17 @@ axiom 6). Never `cli` (upward).
112
112
  | A package's sheets | carried by BOTH surfaces unless the package claims its directory: `claimStylesheets(import.meta.dir, 'app')` makes every sheet below it that surface's alone, still ordered with the library. `@ultimat3/admin` claims `app` — a `site/` document carried its dashboard CSS (axiom 6). A claim re-classifies sheets already registered and survives `clearStylesheets()`. |
113
113
  | The global layer | the app's `shared/global.scss` `@use`s `@ultimat3/ui/global.scss`, side-effect-imported by `shared/global.ts` (this package may not import `ui`). `x verify` fails with `X_STYLES_GLOBAL_MISSING` when a surface's document defines none. |
114
114
  | Colours | tokens and `data-theme` only. No hex in `head.ts` or any emitted script. |
115
- | An island's `mount` | may return `() => void`, its disposer; the runtime never calls it, `@ultimat3/testing`'s `mountIsland` does. |
115
+ | An island's `mount` | RETURNS its disposer: the router calls it on navigation, `mountIsland` on cleanup. Typed `void` or returning nothing is cli's `X_ISLAND_DISPOSER_MISSING` (`boundaries`). |
116
116
  | `<head>` binding | `head.ts` stays injection-only; `head-seo.ts` is the ONE binding of `HeadRenderers` to `@ultimat3/seo`. |
117
- | Client navigation | `navigation*.ts` — soft navigation over server-rendered documents, NOT a mode: the router renders nothing, it swaps the document the route already rendered. Rules are pure (`navigation-rules.ts`: `linkVerdict`, `formVerdict`, `responseVerdict`, `mayPrefetch`) and unit-tested; the DOM half (`navigation-swap.ts`, `navigation.ts`) is proven in Chrome by `packages/cli/e2e/client-navigation-*.e2e.test.ts`. `./navigation` (`navigation-entry.ts`) is the script the CLI builds; importing it starts it, and a document without `ultimate-navigation` is never intercepted. Route-presented modals (`navigation: 'modal'`): the hash grammar and verdicts are pure (`navigation-modal-rules.ts`), the `<dialog>` is `navigation-modal.ts`, styled by `@ultimat3/ui`'s `global.scss` under `NAVIGATION_MODAL_ATTRIBUTE` (pinned by `navigation-modal-style.test.ts` — render never imports ui), proven by `client-navigation-modal.e2e.test.ts`. |
117
+ | Client navigation | `navigation*.ts` — soft navigation over server-rendered documents, NOT a mode: the router swaps the document the route already rendered. Pure rules in `navigation-rules.ts`, proven in Chrome by `packages/cli/e2e/client-navigation-*.e2e.test.ts`; render never imports ui. Full rule: `docs/history/render.md`. `refresh()`'s differences live in `navigation-refresh.ts`; `scheduleRefresh()` is `refresh-idle.ts`, on `./client`, and never reloads without the router. Full rule: `docs/history/render.md` |
118
118
  | Modal on the click | `navigation-modal-frame.ts`: `modal.loading(url)` in `navigate`'s synchronous prefix opens the frame off `NAVIGATION_MODALS_META`; the answer's meta still decides. Closing aborts (`abandon`); the router `release()`s before its own dismissals; `settle()` in `finally`. Never `showModal()` where popovers exist, never a toggle on `<html>`/`<body>` ([why](../../docs/history/render.md#the-modal-that-opens-on-the-click)) |
119
- | Router ↔ the page's lifecycle | `navigation-scroll.ts` owns `scrollRestoration = 'manual'` and RESTORES the entry's saved offset at start, before its first save (a reload, or Back after a full load); `navigation-tabs.ts` owns the `BroadcastChannel`, closed on `pagehide` (it blocks the back/forward cache), reopened and the cache emptied on a persisted `pageshow`. A sheet `loadStylesheets` appends is `owned` at once, so an aborted navigation's sheets are retired by the next swap. A view transition is tracked by identity: an older one finishing never clears the running one. |
119
+ | Router ↔ the page's lifecycle | `navigation-scroll.ts` owns `scrollRestoration = 'manual'` and RESTORES the entry's saved offset at start, before its first save (a reload, or Back after a full load). Full rule: `docs/history/render.md` |
120
120
  | Router ↔ islands | ONE bootstrap: the swap re-runs the inline hydration runtime, whose `each` visits a root once per tab (`el.__v`). Never add a second mount path for the router. Dispose = call what `el.__x` resolved to. |
121
121
  | Router head diff | only elements a SERVER document put in `<head>` (`documentHead`'s set) may be removed; a script-added one is the tab's. Scripts and stylesheets are never diffed. |
122
- | Router: nothing is sent twice | every fetch `cache: 'no-cache'` (#693), `redirect: 'manual'`, a POST never re-sent; the cache answers only a reusable 2xx and is emptied by every write signal ([full rule](../../docs/history/render.md#router-nothing-is-sent-twice)) |
122
+ | Router: nothing is sent twice | every fetch `cache: 'no-cache'` (#693), `redirect: 'manual'`, a POST never re-sent; the cache answers only a reusable 2xx and is emptied by every write signal; an answer is bounded by `NAVIGATION_TIMEOUT_MS`, past it a network failure, a refresh's QUIET (`navigation-deadline.ts`) ([full rule](../../docs/history/render.md#router-nothing-is-sent-twice)) |
123
+ | `Hole` | `hole.ts` imports only `@ultimat3/core/render-node` and is a branded array like `IslandNode`; every walker tests `isHoleNode` before `Array.isArray`. `render-html.ts`'s `HoleSink` is per render (`renderToHtml(tree, { islands, holes })`); only the CLI's `stream` branch passes one. Sinkless: children inline, so `budgets` weighs a hole's island; under a hole nested holes are inline |
124
+ | `hydrateRuntime(directives, { async })` | async while a hole is open and no boot is in the document; `route-islands.ts`'s `streamScripts` decides. Body and hash unchanged. Proof: `packages/cli/e2e/stream-hole.e2e.test.ts`, which fails without it |
125
+ | A hole's failure | `error` markup, the log line, `reportError(source: 'http')`, `X_TIMEOUT` for a deadline; nothing once the client cancelled |
123
126
 
124
127
  Cross-package: `@ultimat3/pwa` consumes route descriptors as **data**, never by import.
125
128
  Keep `RouteDescriptor` additive — removing a field breaks `sw.js` generation.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  The `route` primitive and the four render modes.
4
4
 
5
+ `bun add @ultimat3/render` · guide: [Routes And Render Modes](https://github.com/developerz-ai/ultimate/wiki/Routes-And-Render-Modes)
6
+
5
7
  | Mode | Behavior | Use |
6
8
  |---|---|---|
7
9
  | `static` | built once, served as a file | marketing, docs |
@@ -34,6 +36,43 @@ cross-linked in. `enumeratePrerender()` answers params, `enumeratePrerenderEntri
34
36
  A key outside the three, a `lastmod` that is neither a string nor a `Date`, or `locales` that is
35
37
  not a list of names is `X_PRERENDER_FAILED`, naming the route file.
36
38
 
39
+ ## `<Hole>`: a subtree that arrives after the first flush
40
+
41
+ `Hole` (from `@ultimat3/render`) marks what a `stream` page does not wait for. Its children are a
42
+ thunk or an async component, started after the shell left, in the request's context.
43
+
44
+ ```tsx
45
+ import { Hole } from '@ultimat3/render';
46
+ import { Skeleton } from '@ultimat3/ui';
47
+ import type { JSX } from 'solid-js';
48
+
49
+ // Yours: the components and the read.
50
+ type View = (props: { rows?: readonly unknown[] }) => JSX.Element;
51
+ declare const Stats: View, StatsUnavailable: View;
52
+ declare const statsQuery: (input: object) => Promise<readonly unknown[]>;
53
+
54
+ const readStats = async () => <Stats rows={await statsQuery({})} />;
55
+
56
+ export function Page() {
57
+ return <Hole fallback={<Skeleton lines={3} />} error={<StatsUnavailable />}>{readStats}</Hole>;
58
+ }
59
+ ```
60
+
61
+ | Case | What happens |
62
+ |---|---|
63
+ | `stream` | `fallback` is in the first flush; holes fill in completion order; the document closes when the last one settles |
64
+ | `ssr`, `static`, `isr`, a test, `budgets` | one flush, so the hole is its children, inline. A hole's island is weighed; a throw is the render's |
65
+ | nested | inline inside the outer hole |
66
+ | a throw, or the 15 s deadline | `error`'s markup (default: hidden, empty), a log line, and `reportError(source: 'http')` |
67
+ | status, redirect | `load`'s: decided before the flush. Under the CLI a `setCookie()`/`setRedirect()` inside a hole is `X_STREAM_ALREADY_FLUSHED`, the hole's error markup |
68
+ | cancellation | each hole's `resolve` gets its own signal: aborted at its deadline (`X_TIMEOUT`) and on a disconnect; the CLI makes it the ambient `useContext().signal` |
69
+ | an idle socket | while a hole is open, `<!---->` every `keepAliveMs` (`DEFAULT_STREAM_KEEPALIVE_MS`, 1 s): a write resets Bun's `idleTimeout` |
70
+
71
+ `holeSink({ enter? })` from `@ultimat3/render/server` is the walk's sink (`renderToHtml(tree, { holes })`),
72
+ for a host assembling its own stream; `enter(signal, run)` is the host's scope for a hole's children.
73
+ `StreamHole` gains `error?` and `after?(outcome)` (asked on every reveal, failed ones too);
74
+ `hydrateRuntime(directives, { async: true })` is the runtime for a document still parsing.
75
+
37
76
  ## `load` is the one server-side data seam
38
77
 
39
78
  Optional, and the only way a page gets data. It runs **once per render** and the result is handed
@@ -68,7 +107,7 @@ returns its data through `withStatus(404, data)`. The same object comes back, so
68
107
  `load`'s type, `meta`'s `data` or the page's props changes; every render mode reads the status off
69
108
  it (`routeStatusOf`), and a 4xx or 5xx is `robots: noindex` by construction, applied by the
70
109
  descriptor's `meta` after the route's own ran. Not a throw: a throw is the framework's error page,
71
- outside the app's shell, and `As of 2026-09-07` that was the only way to a 404 — ai-maxxing's
110
+ outside the app's shell, and `As of 2026-09-07` that was the only way to a 404 — one adopter's
72
111
  `/fleet/nope` rendered the right page and answered 200. A 3xx is `X_ROUTE_STATUS_INVALID`. A
73
112
  redirect is `setRedirect(location, status)` from `@ultimat3/http`, called inside `load`: the page
74
113
  path answers that 3xx before rendering a document, `private, no-store` unless the route declares a
@@ -168,6 +207,15 @@ From code — an island, an app script — `navigate(url, { replace? })`, `refre
168
207
  island imports them from **`@ultimat3/render/client`**, never the barrel — see
169
208
  [Two entry points](#two-entry-points). [Navigating from code](../../wiki/Client-Navigation.md#navigating-from-code).
170
209
 
210
+ `refresh()` is the same page for the same visitor (`navigation-refresh.ts`): no view transition,
211
+ focus kept on the control the visitor was in (`markFocus`/`restoreFocus` in `navigation-dom.ts`: by
212
+ `id`, else `name` in its form, else `[data-row]` + tag + place), no announcement, a navigation in
213
+ flight never aborted for it, nothing under an open modal. `scheduleRefresh()` (`refresh-idle.ts`,
214
+ on `/client`) is "a channel said the page changed": one running, one owed, held while the tab is
215
+ hidden, a dialog or popover is open, a field has focus, text is selected, a navigation runs or
216
+ within `QUIET_MS` (1 s) of input; nothing after a lost session and nothing without the router.
217
+ [Client navigation](../../wiki/Client-Navigation.md#refresh-is-the-same-page).
218
+
171
219
  ## Mode invariants, checked at registration
172
220
 
173
221
  | Mode | Invariant | Error if violated |
@@ -177,7 +225,6 @@ island imports them from **`@ultimat3/render/client`**, never the barrel — see
177
225
  | `ssr` | cannot be prerendered | `X_ROUTE_MODE_INVALID` |
178
226
  | any but `ssr` | declares no `cache` — `static`/`isr` headers are the mode, a `stream` is always `private, no-store` | `X_ROUTE_MODE_INVALID` |
179
227
  | gated (`policy`) | its `cache` is never `public`/`immutable` — one actor's document in a shared cache | `X_ROUTE_MODE_INVALID` |
180
- | `stream` | at least one `<Suspense>` boundary | `X_ROUTE_MODE_INVALID` |
181
228
 
182
229
  Plus surface rules: `site/` allows `static | isr | ssr`, `app/` allows `stream | ssr`, and a `site/` route that opts into hydration without a `budget.js`
183
230
  is a build error.
@@ -400,14 +447,15 @@ type DispatchProps = {
400
447
  };
401
448
  const Picker = (props: { readonly models: readonly Model[] }) => <ul>{props.models.length}</ul>;
402
449
 
403
- export function mount(el: HTMLElement, props: DispatchProps): void {
450
+ export function mount(el: HTMLElement, props: DispatchProps): () => void {
404
451
  const [models, setModels] = createSignal<readonly Model[]>(props.models);
405
452
  if (props.modelsEndpoint !== undefined) {
406
453
  fetch(props.modelsEndpoint, { credentials: 'same-origin' })
407
454
  .then((response) => response.json())
408
455
  .then((rows: readonly Model[]) => setModels(rows));
409
456
  }
410
- render(() => <Picker models={models()} />, el);
457
+ // The disposer, returned: the client router calls it on navigation (X_ISLAND_DISPOSER_MISSING).
458
+ return render(() => <Picker models={models()} />, el);
411
459
  }
412
460
  ```
413
461
 
@@ -509,7 +557,7 @@ side effect. Anything that loads an app's source — `x dev`, `x build`, `server
509
557
  `@ultimat3/render/client` is the **browser** entry — what an island or a page script calls:
510
558
  `navigate`, `refresh`, `openModal`, `closeModal`, `NavigateToOptions`,
511
559
  `NavigationModalPathInvalidError`, `NAVIGATE_EVENT`, `NAVIGATED_EVENT`, `NAVIGATION_ERROR_EVENT`,
512
- `disposeIslands`.
560
+ `disposeIslands`, `scheduleRefresh`.
513
561
  The barrel bundles for the browser but its `sideEffects` keeps `errors.ts` — render's code table,
514
562
  and core's and schema's titles tables behind it — in every chunk that reaches it; this entry's
515
563
  graph is the navigation helpers plus `@ultimat3/core/page` (`client-bundle.test.ts`). `As of
@@ -590,8 +638,10 @@ a job boundary the class is gone and the `code` is what survives — match on th
590
638
  | `ISLAND_HOLD_ATTRIBUTE`, `ISLAND_HOLD_MS`, `ISLAND_HOLD_REVEAL` | a held island's marker, and the cap after which it shows its server markup regardless |
591
639
  | `parseByteBudget`, `defaultIslandBudget` | the `'40kb'` budget grammar, and the ceiling a declared island earns |
592
640
  | `mergeHead`, `renderHead`, `themeScript` | `<head>` merge + the one inlined script; it stamps `data-theme` and, beside it, the fallback itself as `data-theme-default` for `@ultimat3/ui`'s `clearTheme`/`watchOsTheme` |
641
+ | `zoneScript`, `zoneScriptBody`, `ZONE_SCRIPT_MAX_BYTES` | `time.detect`'s inline script, `app/` documents only: writes `x_timezone_detected` from `Intl`'s zone, which `@ultimat3/http` ranks below every chosen zone. Under 200 B; tag and CSP hash from one body |
593
642
  | `clientNavigationTags`, `linkVerdict`, `formVerdict`, `responseVerdict`, `reusable`, `mayPrefetch`, `NAVIGATION_*` (`NAVIGATION_ERROR_EVENT`‡), `NAVIGATE_EVENT`‡, `NAVIGATED_EVENT`‡ | the client router's document tags and its pure rules; the router itself is `@ultimat3/render/navigation`, built and served by the CLI |
594
643
  | `navigate`‡, `refresh`‡, `openModal`‡, `closeModal`‡, `NavigateToOptions`‡, `NavigationModalPathInvalidError`‡ | navigating from code: the router when the page has one, the browser's own load when not. Import from `/client` in an island |
644
+ | `scheduleRefresh`‡ | a `refresh()` once the visitor is idle, coalesced; never a reload |
595
645
  | `disposeIslands` (on `/client` only) | dispose the islands mounted under an element: what the router does on a swap, for an island that drops markup holding other islands |
596
646
  | `modalAddress`, `modalLocation`, `addressOf`, `presentation`, `modalHistory`, `leaveModal`, `NAVIGATION_PRESENTATION_META`, `NAVIGATION_MODAL_ATTRIBUTE` | route-presented modals (`navigation: 'modal'`): the hash grammar, when an answer is a modal, and what leaving one does to history |
597
647
  | `declaresModal`, `parseModalRoutes`, `modalRouteList`, `ModalRoutes`, `NAVIGATION_MODALS_META`, `NAVIGATION_MODAL_BODY_ATTRIBUTE`, `NAVIGATION_MODAL_CLOSE_ATTRIBUTE`, `NAVIGATION_MODAL_SKELETON_ATTRIBUTE` | the modal that opens on the click: the meta naming a surface's modal routes (composed by the server, read by the router) and the frame's attributes, styled by `@ultimat3/ui` |
@@ -621,9 +671,11 @@ a job boundary the class is gone and the `code` is what survives — match on th
621
671
  a tag bust and evicts their renders in flight, where the default `'stale'` answers the old copy
622
672
  once more: the next request on this replica and on each one the broadcast reaches; a replica
623
673
  that misses it serves its copy until `ttl`. `maxStale` bounds how long past its `ttl` a copy is
624
- served (`0`: never; the default under `'purge'`). `query: ['page']` (or `[]`) is the only part
625
- of the query string in the key and in the URL `load` is given (`isrRequestUrl`†); a route that
626
- declares none still keys on the whole query string, and `x verify` says so. **Never stored:** a
674
+ served (`0`: never; the default under `'purge'`). `query: ['page']` is the only part of the
675
+ query string in the key and in the URL `load` is given (`isrRequestUrl`†); a route that declares
676
+ none keys on none. With no declared origin the key carries the request's (`isrKey`†'s third
677
+ argument, `__x_origin`), and with one `load` is given that origin. `isr` and `static` render as
678
+ nobody: no actor, no inbound header, the default zone. Per-visitor means `ssr`. **Never stored:** a
627
679
  5xx (the last good page keeps answering, stale; a 1 s cooldown stops a render per request), a
628
680
  render whose `load` returned `noStore(data)`, one invalidated while it ran, a throw, a redirect.
629
681
  - **A store two processes write needs `IsrStore.tagFence`**† — `sample` / `bump` / an atomic
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "29.4.0",
3
+ "version": "30.1.0",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
+ "keywords": [
6
+ "ultimate",
7
+ "bun",
8
+ "framework",
9
+ "ssr",
10
+ "ssg",
11
+ "isr",
12
+ "streaming",
13
+ "solidjs"
14
+ ],
15
+ "homepage": "https://github.com/developerz-ai/ultimate/wiki/Routes-And-Render-Modes",
5
16
  "license": "MIT",
6
17
  "type": "module",
7
18
  "sideEffects": [
@@ -40,11 +51,11 @@
40
51
  "test": "bun test"
41
52
  },
42
53
  "dependencies": {
43
- "@ultimat3/cache": "29.4.0",
44
- "@ultimat3/core": "29.4.0",
45
- "@ultimat3/http": "29.4.0",
46
- "@ultimat3/i18n": "29.4.0",
47
- "@ultimat3/seo": "29.4.0",
48
- "sass": "1.105.1"
54
+ "@ultimat3/cache": "30.1.0",
55
+ "@ultimat3/core": "30.1.0",
56
+ "@ultimat3/http": "30.1.0",
57
+ "@ultimat3/i18n": "30.1.0",
58
+ "@ultimat3/seo": "30.1.0",
59
+ "sass": "1.105.2"
49
60
  }
50
61
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Every pinned action and query the server serves, as a `<head>` tag:
3
+ * `<meta name="ultimate-pins" content="…">`, read by `@ultimat3/core`'s `renderedActionPin` /
4
+ * `renderedQueryPin` for every browser caller that reaches one by NAME. Principal-free, so every
5
+ * document may carry it — and an app that pins nothing writes no tag and renders the same bytes.
6
+ */
7
+
8
+ import type { ClientPins } from '@ultimat3/core';
9
+ import { CLIENT_PINS_META, encodeClientPins } from '@ultimat3/core';
10
+ import type { HeadTag } from './head';
11
+
12
+ /** `pins` is what the server's registries declare, read per render — `defineApi` may run late. */
13
+ export function clientPinTags(pins: ClientPins): readonly HeadTag[] {
14
+ const content = encodeClientPins(pins);
15
+ if (content === undefined) return [];
16
+ return [
17
+ {
18
+ kind: 'meta',
19
+ key: `meta:${CLIENT_PINS_META}`,
20
+ attrs: { name: CLIENT_PINS_META, content },
21
+ },
22
+ ];
23
+ }
package/src/client.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // `@ultimat3/render/client` — THE browser entry of this package: what an island or a page script
2
2
  // may call, and nothing that drags the rest along. The `.` barrel bare-retains `errors.ts` (render's
3
3
  // whole code table, registered at import, plus core's titles tables); this entry's graph is
4
- // `navigation-api.ts`, `navigation-errors.ts`, `island-dispose.ts`, the pure rules and
5
- // `@ultimat3/core/page`, held there by `client-bundle.test.ts`.
4
+ // `navigation-api.ts`, `navigation-errors.ts`, `island-dispose.ts`, `refresh-idle.ts`, the pure
5
+ // rules and `@ultimat3/core/page`, held there by `client-bundle.test.ts`.
6
6
  //
7
- // import { disposeIslands, navigate, openModal, refresh } from '@ultimat3/render/client';
7
+ // import { navigate, refresh, scheduleRefresh } from '@ultimat3/render/client';
8
8
  //
9
9
  // The same names stay exported from `.` for compatibility; an island imports them from here.
10
10
 
@@ -14,3 +14,5 @@ export type { NavigateToOptions } from './navigation-api';
14
14
  export { closeModal, navigate, openModal, refresh } from './navigation-api';
15
15
  export { NavigationModalPathInvalidError } from './navigation-errors';
16
16
  export { NAVIGATE_EVENT, NAVIGATED_EVENT, NAVIGATION_ERROR_EVENT } from './navigation-rules';
17
+ // A channel said the page changed: re-read it once the visitor is idle (`refresh-idle.ts`).
18
+ export { scheduleRefresh } from './refresh-idle';
@@ -230,7 +230,7 @@ const CHARSET_HEAD = /^\uFEFF?(?:@charset\s+"[^"]*"\s*;\s*)?/u;
230
230
  * decoded as UTF-8 by every browser. It is wrong about a fragment. The registry concatenates
231
231
  * modules verbatim, so every module after the first that holds a `content: '·'` began with a BOM
232
232
  * glued to its first selector — `\uFEFF.dashboard_256ee8e0{display:grid}` — which the browser reads
233
- * as an unparseable selector and drops with its whole rule. Measured on ai-maxxing's home
233
+ * as an unparseable selector and drops with its whole rule. Measured on one adopter's home
234
234
  * stylesheet, 2026-09-06: seven modules, seven first rules gone, the dashboard's grid container
235
235
  * painting `display: block` with only the UA rule in Chrome's matched styles. The bundle is served
236
236
  * `text/css; charset=utf-8` by the route, so no sheet needs to claim its encoding at all.
@@ -0,0 +1,57 @@
1
+ // Single responsibility: the attributes of one `<head>` tag. `attributePair` is the rule, with one
2
+ // exception for what a `link` href may name, and a dropped attribute is said out loud in dev: an
3
+ // icon or an alternate that prints with no href otherwise fails with nothing to read.
4
+
5
+ import { escapeHtml, isLocal, logger, safeUrl } from '@ultimat3/core';
6
+ import type { HeadTag } from './head';
7
+ import { attributePair } from './html';
8
+
9
+ /** A declared app deep link. Spelled exactly: a control character in it is not this scheme. */
10
+ const APP_LINK = /^(?:android-app|ios-app):\/\//i;
11
+
12
+ /** Past this many a row is spreading new keys every render: stop remembering, stop warning. */
13
+ const MAX_WARNED_DROPS = 256;
14
+ const warnedDrops = new Set<string>();
15
+
16
+ /**
17
+ * A `link` href is fetched or read as metadata, never navigated, so `safeUrl` answers it as it does
18
+ * a rendered `src`: a raster `data:image/` stays (an inline favicon), SVG and every other scheme do
19
+ * not. `rel="alternate"` alone is never fetched at all, only read by a crawler, so the two app-link
20
+ * schemes are kept there, on the exact value: `alternate stylesheet` is a fetch.
21
+ */
22
+ function linkHref(tag: HeadTag, value: string): string | null {
23
+ const url = safeUrl(value, 'src');
24
+ if (url !== null) return url;
25
+ const rel = tag.attrs?.['rel'];
26
+ const isAlternate = typeof rel === 'string' && rel.trim().toLowerCase() === 'alternate';
27
+ return isAlternate && APP_LINK.test(value) ? value : null;
28
+ }
29
+
30
+ function pairOf(tag: HeadTag, name: string, value: string | boolean): string | null {
31
+ if (tag.kind !== 'link' || name.toLowerCase() !== 'href' || typeof value !== 'string') {
32
+ return attributePair(name, value);
33
+ }
34
+ const url = linkHref(tag, value);
35
+ return url === null ? null : `${name}="${escapeHtml(url)}"`;
36
+ }
37
+
38
+ /** Names the tag and the attribute, never the value: it is a row's, and this is a log line. */
39
+ function warnDropped(tag: HeadTag, name: string): void {
40
+ if (!isLocal({ fallback: 'production' })) return;
41
+ const seen = `${tag.key}\n${name}`;
42
+ if (warnedDrops.has(seen) || warnedDrops.size >= MAX_WARNED_DROPS) return;
43
+ warnedDrops.add(seen);
44
+ logger.warn('head.attribute_dropped', { tag: tag.key, attribute: name });
45
+ }
46
+
47
+ /** The attribute string of `tag`, leading space included, or `''`. */
48
+ export function headAttributes(tag: HeadTag): string {
49
+ const parts: string[] = [];
50
+ for (const [name, value] of Object.entries(tag.attrs ?? {})) {
51
+ const pair = pairOf(tag, name, value);
52
+ if (pair !== null) parts.push(pair);
53
+ // `false` is how a tag says "not this one", so nothing was lost.
54
+ else if (value !== false) warnDropped(tag, name);
55
+ }
56
+ return parts.length === 0 ? '' : ` ${parts.join(' ')}`;
57
+ }
package/src/head.ts CHANGED
@@ -12,7 +12,8 @@ import type { RouteMeta } from '@ultimat3/seo';
12
12
  import { BudgetExceededError } from './errors';
13
13
  // The raw-text escapers are `html.ts`'s and the text/attribute table is core's `escapeHtml` — a
14
14
  // second one is how a character ends up missing.
15
- import { escapeJsonContent, escapeRawTextContent, isAttributeName } from './html';
15
+ import { headAttributes } from './head-attributes';
16
+ import { escapeJsonContent, escapeRawTextContent } from './html';
16
17
 
17
18
  export type HeadTagKind = 'title' | 'base' | 'meta' | 'link' | 'script' | 'style';
18
19
 
@@ -114,15 +115,9 @@ export function renderHead(tags: readonly HeadTag[]): string {
114
115
  }
115
116
 
116
117
  function renderTag(tag: HeadTag): string {
117
- // The NAME is emitted verbatim before the `=` and is escaped nowhere, so a key carrying a space
118
- // carries a whole second attribute with it — an app spreading a row into `attrs` put a live
119
- // event handler in `<head>`. Same predicate `attributePair` uses, never a second copy.
120
- const attrs = Object.entries(tag.attrs ?? {})
121
- .filter(([name]) => isAttributeName(name))
122
- .map(([name, value]) =>
123
- value === true ? ` ${name}` : ` ${name}="${escapeHtml(String(value))}"`,
124
- )
125
- .join('');
118
+ // `attributePair`, the package's one attribute rule: an app spreading a row into `attrs` reaches
119
+ // this sink exactly as it reaches a JSX spread. A second copy printed `onload` and `javascript:`.
120
+ const attrs = headAttributes(tag);
126
121
  if (VOID_KINDS.has(tag.kind)) return `<${tag.kind}${attrs}>`;
127
122
  const raw = tag.content ?? '';
128
123
  return `<${tag.kind}${attrs}>${contentOf(tag, raw)}</${tag.kind}>`;
package/src/hole.ts ADDED
@@ -0,0 +1,32 @@
1
+ // `<Hole>`: the marker that makes a subtree of a `stream` page arrive after the first flush. It
2
+ // decides nothing, it only names the three trees. `renderNode` calls a component with no view of
3
+ // the walk, so the walker in `render-html.ts` is what sees this node and picks what it means.
4
+
5
+ import { HOLE_NODE } from '@ultimat3/core/render-node';
6
+
7
+ export interface HoleProps {
8
+ /** Markup in the first flush, until the hole fills. Rendered at shell time. */
9
+ readonly fallback: unknown;
10
+ /** Replaces the fallback when `children` throw or miss the deadline. Rendered at shell time. */
11
+ readonly error?: unknown;
12
+ /** Awaited after the first flush. A thunk or an async child is what makes the wait start there. */
13
+ readonly children?: unknown;
14
+ }
15
+
16
+ /**
17
+ * Array-based for the reason `IslandNode` states: `ArrayElement` is the one object shape the
18
+ * configured `JSX.Element` admits, so this is what lets `<Hole>` be an ordinary JSX component.
19
+ */
20
+ export interface HoleNode extends Array<never> {
21
+ readonly [HOLE_NODE]: true;
22
+ readonly props: HoleProps;
23
+ }
24
+
25
+ /** Tested BEFORE `Array.isArray` by the walker: the array branch would render it as nothing. */
26
+ export function isHoleNode(value: unknown): value is HoleNode {
27
+ return typeof value === 'object' && value !== null && HOLE_NODE in value;
28
+ }
29
+
30
+ export function Hole(props: HoleProps): HoleNode {
31
+ return Object.assign([] as never[], { [HOLE_NODE]: true as const, props });
32
+ }
package/src/html.ts CHANGED
@@ -119,8 +119,7 @@ const REFUSED_ATTRIBUTES: ReadonlySet<string> = new Set(['srcdoc']);
119
119
  */
120
120
  const ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
121
121
 
122
- /** Exported for `head.ts`, the package's other attribute sink. One predicate, never two. */
123
- export const isAttributeName = (name: string): boolean => ATTRIBUTE_NAME.test(name);
122
+ const isAttributeName = (name: string): boolean => ATTRIBUTE_NAME.test(name);
124
123
 
125
124
  const cssProperty = (name: string): string =>
126
125
  name.startsWith('--') ? name : name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
package/src/hydrate.ts CHANGED
@@ -295,14 +295,27 @@ export const HYDRATE_RUNTIME_BODIES: readonly string[] = [false, true].flatMap((
295
295
  ),
296
296
  );
297
297
 
298
+ export interface HydrateRuntimeOptions {
299
+ /**
300
+ * Run as soon as the script is parsed. An inline module is otherwise deferred until parsing ends,
301
+ * and a document with a `<Hole>` open is still parsing: its shell's islands would stay dead until
302
+ * the slowest hole closes. The body, so its CSP hash, is the same either way.
303
+ */
304
+ readonly async?: boolean;
305
+ }
306
+
298
307
  /**
299
308
  * Emit only the runtime the page's islands actually use. A page of `never` islands gets
300
309
  * the empty string — an unused strategy must not cost a byte.
301
310
  */
302
- export function hydrateRuntime(directives: readonly IslandDirective[]): string {
311
+ export function hydrateRuntime(
312
+ directives: readonly IslandDirective[],
313
+ options: HydrateRuntimeOptions = {},
314
+ ): string {
303
315
  const needed = requiredStrategies(directives);
304
316
  if (needed.size === 0) return '';
305
- return `<script type="module">${runtimeBody(needed, holds(directives))}</script>`;
317
+ const tag = options.async === true ? '<script type="module" async>' : '<script type="module">';
318
+ return `${tag}${runtimeBody(needed, holds(directives))}</script>`;
306
319
  }
307
320
 
308
321
  /** Rough emitted size of the hydration runtime for a page, for the budget check. */
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export type { AssetExtension, AssetPath, AssetResolver } from './asset';
18
18
  export { ASSET_DIR, ASSET_EXTENSIONS, asset, assetPathProblem, setAssetResolver } from './asset';
19
19
  /** The `<meta name="ultimate-path-style">` core's `actionPath` reads — a non-default style only. */
20
20
  export { clientPathStyleTags } from './client-path-style-tag';
21
+ export { clientPinTags } from './client-pins-tag';
21
22
  /** The `<meta name="ultimate-scope">` core's `pageClient()` reads, on private documents only. */
22
23
  export {
23
24
  clientPersistTags,
@@ -72,6 +73,8 @@ export {
72
73
  themeScriptBody,
73
74
  } from './head';
74
75
  export { seoRenderers } from './head-seo';
76
+ export type { HoleNode, HoleProps } from './hole';
77
+ export { Hole } from './hole';
75
78
  export type { IslandDirective } from './hydrate';
76
79
  export {
77
80
  emitIslandAttributes,
@@ -97,7 +100,7 @@ export type { IslandProps, JsonValue } from './island-props';
97
100
  export { ISLAND_PROPS_MAX_BYTES } from './island-props';
98
101
  export { parseByteBudget } from './islands';
99
102
  export type { JsxComponent, JsxNode, JsxProps } from './jsx';
100
- export { Fragment, h, isJsxNode, JSX_NODE } from './jsx';
103
+ export { Fragment, h, isJsxNode } from './jsx';
101
104
  export type { ModeCheckContext, ModeSpec, RouteShape } from './modes';
102
105
  export {
103
106
  assertModeInvariants,
@@ -167,6 +170,7 @@ export {
167
170
  } from './navigation-rules';
168
171
  export type { ClientNavigationHead } from './navigation-tags';
169
172
  export { clientNavigationTags, modalRouteList } from './navigation-tags';
173
+ export { scheduleRefresh } from './refresh-idle';
170
174
  export type {
171
175
  MountedRouteInput,
172
176
  RegisterRouteInput,
@@ -252,3 +256,4 @@ export {
252
256
  surfaceAllows,
253
257
  surfaceOf,
254
258
  } from './surfaces';
259
+ export { ZONE_SCRIPT_MAX_BYTES, zoneScript, zoneScriptBody } from './zone-script';
@@ -18,6 +18,16 @@ const ISLAND_ATTRIBUTE = 'data-x-island';
18
18
  */
19
19
  const released = new WeakSet<Element>();
20
20
 
21
+ /**
22
+ * A disposer that threw, handed to the platform's `reportError`: it reaches `window`'s `error`
23
+ * listeners (the client-errors script among them) as an uncaught error would, once, and the
24
+ * router's swap carries on. Re-thrown where the platform has none, so it is never swallowed.
25
+ */
26
+ function reportDisposeFailure(error: unknown): void {
27
+ if (typeof globalThis.reportError !== 'function') throw error;
28
+ globalThis.reportError(error);
29
+ }
30
+
21
31
  /**
22
32
  * Disposes every island mounted under `root` — `root` itself included when it is an island's
23
33
  * wrapper — unless `kept` claims it, and answers how many had a boot to settle. For each one the
@@ -29,7 +39,8 @@ const released = new WeakSet<Element>();
29
39
  * the pending boot mounts nothing into markup that left the document — a mount nobody would ever
30
40
  * dispose. Works on a detached subtree, so the order against removing `root` does not matter.
31
41
  * Idempotent per island: one already let go of is skipped and not counted; one `kept` claimed is
32
- * untouched, so a later call may still dispose it.
42
+ * untouched, so a later call may still dispose it. A disposer that throws is reported, not left as
43
+ * an unhandled rejection: `reportDisposeFailure`.
33
44
  */
34
45
  export function disposeIslands(
35
46
  root: Element,
@@ -50,7 +61,12 @@ export function disposeIslands(
50
61
  disposed += 1;
51
62
  el.__x.then(
52
63
  (dispose) => {
53
- if (typeof dispose === 'function') dispose();
64
+ if (typeof dispose !== 'function') return;
65
+ try {
66
+ dispose();
67
+ } catch (error: unknown) {
68
+ reportDisposeFailure(error);
69
+ }
54
70
  },
55
71
  () => undefined,
56
72
  );
package/src/island.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { renderFixShellArg } from '@ultimat3/core';
10
+ import { ISLAND_NODE } from '@ultimat3/core/render-node';
10
11
  import { IslandInvalidError } from './errors';
11
12
  import type { JsonValue } from './island-props';
12
13
  import type { JsxProps } from './jsx';
@@ -18,9 +19,6 @@ import type { JsxProps } from './jsx';
18
19
  */
19
20
  export const ISLAND_EXTENSION = '.island.tsx';
20
21
 
21
- /** Registered globally so two copies of this module agree on what an island node is. */
22
- export const ISLAND_NODE: unique symbol = Symbol.for('ultimate.render.island') as never;
23
-
24
22
  /** Characters that would break out of the `data-x-entry` attribute the specifier lands in. */
25
23
  const UNSAFE_SPECIFIER = /["'`<>\s\\]/;
26
24
 
package/src/jsx.ts CHANGED
@@ -4,8 +4,7 @@
4
4
  * thing that decides what a tree means. This is what the `.tsx` loader compiles every element to.
5
5
  */
6
6
 
7
- /** Registered on the global symbol registry: two copies of this module must agree on a node. */
8
- export const JSX_NODE: unique symbol = Symbol.for('ultimate.render.jsx') as never;
7
+ import { JSX_NODE } from '@ultimat3/core/render-node';
9
8
 
10
9
  export type JsxProps = Readonly<Record<string, unknown>>;
11
10