@ultimat3/render 25.0.0 → 25.2.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
@@ -113,7 +113,7 @@ axiom 6). Never `cli` (upward).
113
113
  | 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. |
114
114
  | 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. |
115
115
  | 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. |
116
- | Router: nothing is sent twice | the router follows no redirect (`redirect: 'manual'`; the server hands it over as `204` + `x-ultimate-location`), never re-submits a POST (a failure is `ultimate:navigation-error`), and hands a 2xx non-page answer to the browser from the bytes it already has. A 4xx/5xx non-page answer (an offline worker's empty 503) is never handed over (it became a `blob:` URL): a POST fails, a GET is loaded by the browser — the ONE deliberate re-send, as there is nothing to show (`responseVerdict`). Its cache never answers with a non-2xx, a hand-over, or a `no-store` page older than 5 s, and is emptied by any POST (when sent AND when it settles), `onClientWrite`, `onRescope` and a `BroadcastChannel` message from another tab. |
116
+ | Router: nothing is sent twice | every router fetch is `cache: 'no-cache'` — the browser's HTTP cache never answers, the router's own is the only reuse layer (#693; `navigation-fetch.test.ts`, `client-navigation-cache.e2e.test.ts`). The router follows no redirect (`redirect: 'manual'`; the server hands it over as `204` + `x-ultimate-location`), never re-submits a POST (a failure is `ultimate:navigation-error`), and hands a 2xx non-page answer to the browser from the bytes it already has. A 4xx/5xx non-page answer (an offline worker's empty 503) is never handed over (it became a `blob:` URL): a POST fails, a GET is loaded by the browser — the ONE deliberate re-send, as there is nothing to show (`responseVerdict`). Its cache never answers with a non-2xx, a hand-over, or a `no-store` page older than 5 s, and is emptied by any POST (when sent AND when it settles), `onClientWrite`, `onRescope` and a `BroadcastChannel` message from another tab. |
117
117
 
118
118
  Cross-package: `@ultimat3/pwa` consumes route descriptors as **data**, never by import.
119
119
  Keep `RouteDescriptor` additive — removing a field breaks `sw.js` generation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "25.0.0",
3
+ "version": "25.2.0",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,11 +39,11 @@
39
39
  "test": "bun test"
40
40
  },
41
41
  "dependencies": {
42
- "@ultimat3/cache": "25.0.0",
43
- "@ultimat3/core": "25.0.0",
44
- "@ultimat3/http": "25.0.0",
45
- "@ultimat3/i18n": "25.0.0",
46
- "@ultimat3/seo": "25.0.0",
42
+ "@ultimat3/cache": "25.2.0",
43
+ "@ultimat3/core": "25.2.0",
44
+ "@ultimat3/http": "25.2.0",
45
+ "@ultimat3/i18n": "25.2.0",
46
+ "@ultimat3/seo": "25.2.0",
47
47
  "sass": "1.105.1"
48
48
  }
49
49
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The client router's one request: the headers the server's navigation gate reads (purpose, the
3
3
  * document's `<app>:<surface>`, its principal, its build on a GET), `redirect: 'manual'` so no
4
- * redirect is followed behind the router's back, and the answer read ONCE into an `Answer` — the
5
- * page's text, or a non-page's bytes to hand over (a prefetch's are dropped unread).
4
+ * redirect is followed behind the router's back, `cache: 'no-cache'` so the browser's HTTP cache
5
+ * never answers, and the answer read ONCE into an `Answer` — the page's text, or a non-page's
6
+ * bytes to hand over (a prefetch's are dropped unread).
6
7
  */
7
8
 
8
9
  import {
@@ -50,8 +51,16 @@ export function fetchDocument(
50
51
  // GET only: a skewed GET is refused before any route runs; a POST is never refused mid-submit.
51
52
  const build = metaOf(doc, CLIENT_BUILD_META);
52
53
  if ((init.method ?? 'GET') === 'GET' && build !== null) headers[CLIENT_BUILD_META] = build;
54
+ // `no-cache`: the browser's HTTP cache never answers — a `stale-while-revalidate` page would be
55
+ // swapped in stale after a write, a sign-out or a deploy (#693); the router's cache is the only one.
53
56
  return win
54
- .fetch(url, { ...init, credentials: 'same-origin', redirect: 'manual', headers })
57
+ .fetch(url, {
58
+ ...init,
59
+ cache: 'no-cache',
60
+ credentials: 'same-origin',
61
+ redirect: 'manual',
62
+ headers,
63
+ })
55
64
  .then(async (response) => {
56
65
  const contentType = response.headers.get('content-type') ?? '';
57
66
  const opaqueRedirect = response.type === 'opaqueredirect';