@ultimat3/render
Advanced tools
+2
-1
@@ -64,4 +64,5 @@ # @ultimat3/render — boundary | ||
| | Island boot | `el.__x` holds the boot PROMISE, never a boolean. As a flag, a second interaction while the chunk was still loading got a resolved promise back and the replay queue flushed into an island that had not mounted — the events went nowhere and the listeners were already removed. | | ||
| | Island mount markers | `data-x-mounted=""` when `mount()` RESOLVED, `data-x-failed="<message>"` when it rejected — set by the runtime, never by `emitIslandAttributes`, because the server does not know the answer. `el.__x` alone cannot carry this: it is assigned when `import()` is CALLED, so a chunk still downloading and one whose `mount()` threw were the same observable, and telling those apart is what `x shot` gates on. Two attributes rather than two values of one, so `[data-x-mounted]` never counts a failure as a success. The rejection handler RETHROWS — swallowing it resolves `el.__x` and the interaction runtime flushes its replay queue into an island that never mounted, which is the row above reintroduced one layer out. Costs 129 B of shared prelude: `idle` 774, `visible` 846, `interaction` 1,067 (`As of 2026-08-23`, the numbers `DEFAULT_ISLAND_JS_BYTES` is derived from). | | ||
| | Island mount markers | `data-x-mounted=""` when `mount()` RESOLVED, `data-x-failed="<message>"` when it rejected — set by the runtime, never by `emitIslandAttributes`, because the server does not know the answer. `el.__x` alone cannot carry this: it is assigned when `import()` is CALLED, so a chunk still downloading and one whose `mount()` threw were the same observable, and telling those apart is what `x shot` gates on. Two attributes rather than two values of one, so `[data-x-mounted]` never counts a failure as a success. The rejection handler RETHROWS — swallowing it resolves `el.__x` and the interaction runtime flushes its replay queue into an island that never mounted, which is the row above reintroduced one layer out. Costs 129 B of shared prelude: `idle` 774, `visible` 846, `interaction` 1,251 (`As of 2026-08-25`, the numbers `DEFAULT_ISLAND_JS_BYTES` is derived from — `interaction` moved 1,067 -> 1,251 for the replay-target row below, and neither of the other two changed). | | ||
| | A runtime that calls `boot` | TERMINATES the chain, because `boot` rethrows. `idle` and `visible` end in `.catch(hush)`; `interaction` passes `off` as the rejection arm of its `then`. A bare `boot(el)` produced a fresh rejected promise per call — one unhandled rejection per user event on an island whose `mount()` threw — and, on `interaction`, left `done` false, the listeners attached and the queue growing by one retained `Event` (each with a live `target`) per click, for an island that will never mount. Nothing is lost by swallowing here: the DOM already carries the failure as `data-x-failed`, which is the row above and the documented observable. `hydrate-runtime.test.ts` runs all three against a real module; Bun's runner fails a test on an unhandled rejection, so the omission reds the suite by itself. | | ||
| | Where `interaction` replays | `aim(el, ev)` in `hydrate.ts`, never `ev.target`. Every island's `mount` opens with `el.textContent = ''` — the documented idiom, and what `settings`, `feed` and `like` all do — so the node the visitor pressed is DETACHED by the time the replay runs and `ev.target.dispatchEvent(c)` reached nothing: the first press did nothing and the second worked, which reads as a slow network and is never filed as a bug. `examples/dummy/apps/web/app/posts/[id]/page.tsx` declares `hydrate: 'idle'` in writing to avoid it. The runtime CAN tell the two mounts apart — `el.contains(ev.target)` AFTER the mount is the exact question — so this is a repair and not a refusal: refusing the pairing would delete a strategy that works today for a takeover-style island (`contact-sales.island.tsx` attaches to the server's own form and replaces nothing). Kept → the original target. Replaced → `document.elementFromPoint(ev.clientX, ev.clientY)`, which is where the event would land had the visitor pressed a moment later. The island ROOT is the last resort and never the repair: Solid's delegated listener sits on `document` and walks UP from the target (`solid-js/web`'s `eventHandler`), so a handler on a CHILD of the root is never visited and dispatching at the root fixes nothing for the canonical island. A hit landing outside this island falls back to the root too — synthesizing a click on an element the visitor never pressed is worse than losing the replay. `typeof ev.clientX === 'number'`, never `ev.clientX || ev.clientY`: (0, 0) is a coordinate. `hydrate-replay.test.ts` holds it, and it is a separate file because `hydrate-runtime.test.ts`'s element is BOTH the island root and every event's target — the two answers are the same node there, which is how this survived. | | ||
| | `idle`'s deadline | `IDLE_HYDRATE_TIMEOUT_MS`, interpolated INTO the runtime string. Exported because a second reader has to agree — `x shot` waits before it photographs, and a settle shorter than this deadline reports an unhydrated page for one that hydrates perfectly. A constant the emitted string restates instead of reading is worse than no constant. | | ||
@@ -68,0 +69,0 @@ | Route truth | `registry.ts`. Never keep a second route list anywhere, and never a second *matcher*: this package's `matchRoute` was deleted in 2026-08 with zero consumers, because `@ultimat3/http`'s trie (`stages.ts`) is the one that serves requests and two matchers with different precedence rules is two answers to "which route is this?". `routeFor` is an exact-path `Map` lookup, not a pattern matcher. | |
+5
-5
| { | ||
| "name": "@ultimat3/render", | ||
| "version": "13.0.0", | ||
| "version": "14.0.0", | ||
| "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.", | ||
@@ -39,8 +39,8 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@ultimat3/cache": "13.0.0", | ||
| "@ultimat3/core": "13.0.0", | ||
| "@ultimat3/i18n": "13.0.0", | ||
| "@ultimat3/seo": "13.0.0", | ||
| "@ultimat3/cache": "14.0.0", | ||
| "@ultimat3/core": "14.0.0", | ||
| "@ultimat3/i18n": "14.0.0", | ||
| "@ultimat3/seo": "14.0.0", | ||
| "sass": "1.102.0" | ||
| } | ||
| } |
+17
-10
@@ -201,16 +201,16 @@ # 🖼 @ultimat3/render | ||
| | `settings.island.tsx`, the heaviest island this repo ships | 17,797 | | ||
| | one directive's hydration runtime at `hydrate: 'idle'` | 615 | | ||
| | the same at `'interaction'`, which is what an island route declaring no `hydrate` gets | 881 | | ||
| | one directive's hydration runtime at `hydrate: 'idle'` | 774 | | ||
| | the same at `'interaction'`, which is what an island route declaring no `hydrate` gets | 1,251 | | ||
| 17,797 + 881 = **18,678** — the heaviest island this repo ships, plus the runtime an app pays | ||
| 17,797 + 1,251 = **19,048** — the heaviest island this repo ships, plus the runtime an app pays | ||
| without writing a number down. `DEFAULT_ISLAND_HYDRATE` is `'interaction'` | ||
| ([`route.ts:33`](src/route.ts)), applied at `:253` to any island route that states no `hydrate`, so | ||
| `idle`'s 615 is the cheaper case and not the one a budget has to clear. | ||
| `idle`'s 774 is the cheaper case and not the one a budget has to clear. | ||
| The default is **20,480** (20kb), which is not that number rounded: the next whole kilobyte above | ||
| it is 19,456, and clearing today's worst island by 778 bytes is a ceiling the next line anyone | ||
| writes breaks. 20kb leaves 1,802 B, and stays under 2× 18,678 — so a route that bundles the same | ||
| it is 19,456, and clearing today's worst island by 408 bytes is a ceiling the next line anyone | ||
| writes breaks. 20kb leaves 1,432 B, and stays under 2× 19,048 — so a route that bundles the same | ||
| island twice is still refused. All three clauses are assertions in | ||
| [`modes.test.ts`](src/modes.test.ts)'s `DEFAULT_ISLAND_JS_BYTES` block, against the measured | ||
| table above; a default that stopped clearing the floor, or stopped being a ceiling, is red. | ||
| [`island-budget.test.ts`](src/island-budget.test.ts)'s `DEFAULT_ISLAND_JS_BYTES` block, against the | ||
| measured table above; a default that stopped clearing the floor, or stopped being a ceiling, is red. | ||
@@ -222,3 +222,5 @@ It was **4kb** until `As of 2026-08`, sized from `contact-sales.island.tsx` — 875 B of chunk, and | ||
| baseline and not against it. (Its second number was wrong too — one directive's hydration runtime | ||
| is 615 B at `idle` and 881 B at `interaction`, never 1,019.) | ||
| was 615 B at `idle` and 881 B at `interaction` when that default was set, never 1,019. The table | ||
| above is what it measures today: the runtime has grown three times since, for the mount markers, | ||
| for terminating the chain `boot` starts, and for aiming the replay.) | ||
@@ -390,3 +392,8 @@ Still a ceiling and not a pass: exceeding it is `X_BUDGET_EXCEEDED`, naming the island. An island | ||
| - **`hydrate: 'interaction'`** replays the event that woke the island; without replay the | ||
| first click on a cold island is silently lost. | ||
| first click on a cold island is silently lost. It replays onto a node the mount left standing — | ||
| the original target when the mount kept it, otherwise whatever `elementFromPoint` now answers for | ||
| a pointer event, otherwise the island root. An island's `mount` opens with `el.textContent = ''`, | ||
| so the pressed node is usually gone by the time the replay runs, and dispatching at it reached | ||
| nothing: `hydrate: 'interaction'` is usable with a replacing island, and was not until | ||
| `As of 2026-08-25`. | ||
| - **`hydrate: 'never'`** emits no attributes beyond the marker and no runtime — the `site/` | ||
@@ -393,0 +400,0 @@ 0kb default is mechanical, not aspirational. A page that renders an island anyway is |
+25
-1
@@ -151,2 +151,23 @@ /** | ||
| // | ||
| // `aim` is WHERE it is re-dispatched, and it is not `ev.target`. An island's `mount` opens with | ||
| // `el.textContent = ''` — the documented idiom, and what `settings`, `feed` and `like` all do — so | ||
| // by the time the replay runs, the node the visitor actually pressed has been detached and a | ||
| // `dispatchEvent` on it reaches nothing: "the button does nothing on the first press, and works on | ||
| // the second", which is indistinguishable from a slow network and is never reported as a bug. | ||
| // | ||
| // The runtime CAN tell the two mounts apart, per event, and that is what makes a repair possible | ||
| // instead of a refusal: `el.contains(ev.target)` after the mount answers "did this mount keep the | ||
| // node I caught the event on". Kept → replay there, which is what a takeover-style island | ||
| // (`contact-sales.island.tsx` attaches to the server's own form) needs. | ||
| // | ||
| // Replaced → the honest target is where the event would land NOW, so a pointer event is | ||
| // hit-tested again with `elementFromPoint`. That is the same answer the browser would have given | ||
| // had the visitor pressed a moment later, and it reaches a fresh descendant's own handler — | ||
| // dispatching at the island ROOT does not, because Solid's delegated listener sits on `document` | ||
| // and walks UP from the target (`solid-js/web`'s `eventHandler`), so a handler on a child of the | ||
| // root is never visited. The root is the last resort, not the repair: it is where an event with no | ||
| // coordinates goes (a `keydown` has no `clientX`), and where a hit landing outside this island goes | ||
| // — synthesizing a click on an element the visitor never pressed is worse than losing the replay. | ||
| // `typeof` and not `ev.clientX||ev.clientY`, because (0, 0) is a coordinate. | ||
| // | ||
| // `off` is BOTH arms of the `then`, and the rejection arm is the reason it is a named function. | ||
@@ -161,2 +182,5 @@ // `boot` rethrows on purpose (see the prelude), so `el.__x` holds a rejected promise from the | ||
| const RUNTIME_INTERACTION = ` | ||
| function aim(el,ev){var t=ev.target;if(t&&el.contains(t))return t; | ||
| var x=ev.clientX,h=typeof x==='number'?document.elementFromPoint(x,ev.clientY):null; | ||
| return h&&el.contains(h)?h:el} | ||
| each('[data-x-hydrate="interaction"]',function(el){ | ||
@@ -168,3 +192,3 @@ var evs=(el.getAttribute('data-x-events')||'click').split(' '); | ||
| boot(el).then(function(){var r=q;off(); | ||
| r.forEach(function(ev){var c=new ev.constructor(ev.type,ev);ev.target.dispatchEvent(c)})},off)}; | ||
| r.forEach(function(ev){var c=new ev.constructor(ev.type,ev);aim(el,ev).dispatchEvent(c)})},off)}; | ||
| evs.forEach(function(n){el.addEventListener(n,on,true)})}) | ||
@@ -171,0 +195,0 @@ `.trim(); |
+8
-6
@@ -228,8 +228,8 @@ /** | ||
| * | ||
| * The number: 17,797 (the heaviest island this repo actually ships) + 1,067 (`hydrateRuntimeBytes` | ||
| * The number: 17,797 (the heaviest island this repo actually ships) + 1,251 (`hydrateRuntimeBytes` | ||
| * for one directive at `DEFAULT_ISLAND_HYDRATE`, which is `'interaction'` — `route.ts:33`, applied | ||
| * at `:253` to any island route declaring no `hydrate`) = **18,864**. That is the worst case an | ||
| * at `:253` to any island route declaring no `hydrate`) = **19,048**. That is the worst case an | ||
| * app reaches without writing a number down. 20,480 is NOT that rounded up — the next whole | ||
| * kilobyte above it is 19,456 — it is one whole kB further, leaving 1,616 B of headroom and still | ||
| * under 2x 18,864, so a route bundling the same island twice is refused. `island-budget.test.ts` | ||
| * kilobyte above it is 19,456 — it is one whole kB further, leaving 1,432 B of headroom and still | ||
| * under 2x 19,048, so a route bundling the same island twice is refused. `island-budget.test.ts` | ||
| * asserts all three. `idle` costs 774 and `visible` 846, so an island route that declares its | ||
@@ -242,4 +242,6 @@ * strategy pays less; the default is what the budget has to clear. | ||
| * `interaction` — when each runtime learned to TERMINATE the promise chain `boot` starts rather | ||
| * than emit one unhandled rejection per user event. The headroom absorbed both and the conclusion | ||
| * is unchanged, which is the point of stating the | ||
| * than emit one unhandled rejection per user event. `interaction` alone grew a third time on | ||
| * 2026-08-25 (+184 B, `aim`), when the replay learned that the node it was dispatching at had been | ||
| * detached by the mount it was waiting for. The headroom absorbed all three and the conclusion is | ||
| * unchanged, which is the point of stating the | ||
| * arithmetic here rather than the answer alone. It is not | ||
@@ -246,0 +248,0 @@ * derived from Solid's own size on purpose — this package may not import or name `solid-js` |
254745
1.88%4156
0.63%407
1.75%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated