@ultimat3/render 27.8.0 → 27.8.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.
package/CLAUDE.md CHANGED
@@ -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)` — pathname, the negotiated locale in the reserved `__x_locale` parameter, then the sorted query. 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. The one shipped emitter of purge keys; file and mounted routes are both screened, so `toResult` never refuses on the request path |
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 |
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 `:`. |
package/README.md CHANGED
@@ -557,7 +557,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
557
557
  | `checkSurfaceBoundary`, `assertSurfaceBoundary`, `surfaceOf` | the hard boundary |
558
558
  | `renderStatic`†, `enumeratePrerender`† | build-time render, content hashing |
559
559
  | `isrController`†, `invalidateAndRevalidate`† | SWR + single-flight + tag triggers |
560
- | `isrKey`†, `isrRequestUrl`†, `undeclaredQuery`† | the one derivation of an `isr` store key, and the request URL narrowed to `revalidate.query` |
560
+ | `isrKey`†, `isrRequestUrl`†, `undeclaredQuery`†, `isrRouteFor`† | the one derivation of an `isr` store key — the path in `@ultimat3/http`'s `canonicalPath` spelling, so every spelling the router answers with a page is that page's one entry — the request URL narrowed to `revalidate.query`, and the route a key reads back as (a key no route owns is never stored) |
561
561
  | `noStore`, `routeNoStoreOf` | a loader's "do not keep this answer" — a mark on the data, like `withStatus`; read by `isr` |
562
562
  | `renderSsr`†, `streamResult`† | the per-request modes |
563
563
  | `renderToHtml`†, `renderComponent`†, `stylesFor`† | the server JSX writer and the surface's css |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "27.8.0",
3
+ "version": "27.8.1",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,11 +40,11 @@
40
40
  "test": "bun test"
41
41
  },
42
42
  "dependencies": {
43
- "@ultimat3/cache": "27.8.0",
44
- "@ultimat3/core": "27.8.0",
45
- "@ultimat3/http": "27.8.0",
46
- "@ultimat3/i18n": "27.8.0",
47
- "@ultimat3/seo": "27.8.0",
43
+ "@ultimat3/cache": "27.8.1",
44
+ "@ultimat3/core": "27.8.1",
45
+ "@ultimat3/http": "27.8.1",
46
+ "@ultimat3/i18n": "27.8.1",
47
+ "@ultimat3/seo": "27.8.1",
48
48
  "sass": "1.105.1"
49
49
  }
50
50
  }
@@ -4,6 +4,7 @@
4
4
  * the controller, which only ever holds the key this file derived.
5
5
  */
6
6
 
7
+ import { canonicalPath } from '@ultimat3/http';
7
8
  import { unlocalizedPath } from '@ultimat3/i18n';
8
9
 
9
10
  /**
@@ -57,6 +58,13 @@ export function undeclaredQuery(url: URL, query: readonly string[] | null): read
57
58
  * The ISR store key for one URL: pathname, the negotiated LOCALE, and the query, **params
58
59
  * sorted**. The URL is `isrRequestUrl`'s — already narrowed to the route's declared parameters.
59
60
  *
61
+ * The pathname is keyed in its ONE spelling — `@ultimat3/http`'s `canonicalPath`, built from the
62
+ * functions the router segments and decodes a request with, never a second normaliser. The router
63
+ * answers `/blog//a/`, `/blog/%61` and `/blog/a` with one page; keyed raw, each was an entry of
64
+ * its own (a 20-letter slug has a million percent-spellings, and the store holds 1,000), and one
65
+ * the route table could not read back (`/blog//a`) was stored with no ttl and no tags, which no
66
+ * bust reached: a withdrawn page stayed up at that spelling for the life of the process.
67
+ *
60
68
  * Exported because deriving it is the caller's job and there may only be ONE derivation — a
61
69
  * server that keyed on `url.pathname` while the store believed it held a whole URL is the shape
62
70
  * of #171. Sorting makes `?a=1&b=2` and `?b=2&a=1` one entry rather than two renders of one page.
@@ -72,7 +80,7 @@ export function isrKey(url: URL, locale: string): string {
72
80
  const params = new URLSearchParams(url.search);
73
81
  params.set(ISR_LOCALE_PARAM, locale);
74
82
  params.sort();
75
- return `${url.pathname}?${params.toString()}`;
83
+ return `${canonicalPath(url.pathname)}?${params.toString()}`;
76
84
  }
77
85
 
78
86
  /**
package/src/render-isr.ts CHANGED
@@ -108,6 +108,24 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
108
108
 
109
109
  const descriptorFor = (key: string): RouteDescriptor | undefined => isrRouteFor(routes(), key);
110
110
 
111
+ /**
112
+ * A key NO route owns is rendered for the request that asked and never stored. Its ttl and tags
113
+ * are its route's, so stored it had neither: fresh for ever, on no tag edge, and found by no
114
+ * bust — the one page a purge could not take down. `@ultimat3/cli` keys every request on the
115
+ * path the router matched, so this is a caller that derived a key some other way. Told once.
116
+ */
117
+ let toldUnowned = false;
118
+ function tellUnowned(path: string): void {
119
+ if (toldUnowned) return;
120
+ toldUnowned = true;
121
+ logger.warn('isr.key.unowned', {
122
+ path,
123
+ cause:
124
+ 'no route in the table matches this key, so it has no ttl and no tags to be revalidated by — it was rendered and not stored',
125
+ fix: 'isrKey(url, locale) // key on the URL the router matched, base path removed; never on a hand-built string',
126
+ });
127
+ }
128
+
111
129
  /**
112
130
  * A rendered page joins the invalidation graph under its route's tags, so `/blog/a` and
113
131
  * `/blog/b` are separately addressable and a `tag.post` bust does not touch `/team`.
@@ -192,7 +210,8 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
192
210
  status: finiteStatus('IsrRenderFn', rendered.status),
193
211
  };
194
212
  const current = latest.get(path) === run;
195
- const unstorable = isServerError(rendered.status) || rendered.noStore === true;
213
+ const unstorable =
214
+ descriptor === undefined || isServerError(rendered.status) || rendered.noStore === true;
196
215
  // Kept only when nothing invalidated the page while it rendered — in this process (the
197
216
  // fence) or in any other (the store's) — and no newer run started. Refused, never
198
217
  // published stale-flagged: the next request re-renders from rows that include the write.
@@ -203,6 +222,8 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
203
222
  // store holds stays (still stale, so the next request tries again); a miss has none, and
204
223
  // answers this 5xx to its own request only.
205
224
  logger.warn('isr.render.unstored', { path, status: String(rendered.status) });
225
+ } else if (descriptor === undefined) {
226
+ tellUnowned(path);
206
227
  } else if (rendered.noStore === true) {
207
228
  // The loader's answer is not a failure, so the copy held is no longer the page.
208
229
  if (current) store.delete(path);
@@ -331,7 +352,9 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
331
352
  ageMs: Math.max(0, now() - entry.generatedAt),
332
353
  });
333
354
  let cached = store.get(path);
334
- if (cached !== undefined && pastMaxStale(cached, policy)) {
355
+ // A copy under a key no route owns (a shared store an earlier release wrote) is not served:
356
+ // nothing could have invalidated it since.
357
+ if (cached !== undefined && (descriptor === undefined || pastMaxStale(cached, policy))) {
335
358
  store.delete(path);
336
359
  cached = undefined;
337
360
  }
package/src/server.ts CHANGED
@@ -47,6 +47,7 @@ export {
47
47
  } from './render-isr';
48
48
  export { DEFAULT_ISR_FAILURE_COOLDOWN_MS } from './render-isr-cooldown';
49
49
  export { ISR_LOCALE_PARAM, isrKey, isrRequestUrl, undeclaredQuery } from './render-isr-key';
50
+ export { isrRouteFor } from './render-isr-routes';
50
51
  export type {
51
52
  IsrEntry,
52
53
  IsrState,