@mapled/next 0.11.0 → 0.11.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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/live.js +100 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -390,6 +390,7 @@ export async function POST(req: Request) {
390
390
  ```
391
391
 
392
392
  - The route answers `{ release, publishedAt }` — the number of publishes and the time of the last one, nothing else — and may be cached by anyone. While Mapled can't be reached it keeps answering the last release it knew.
393
+ - Refreshes ask your site, not the browser's cache. `router.refresh()` asks for the page's Server Components at the same URL every time, and a static page answers with `s-maxage` and `stale-while-revalidate` but no `max-age`: stale for the browser at once, which Chrome and Edge serve anyway while they fetch a fresh copy behind it. So from the first publish a tab hears about, every refresh of the page goes with `cache: "no-cache"`, and your site may still answer 304. That covers the component's own refresh, the one your `onPublish` makes (like the button above), and any your code makes. The layer changes only the cache mode, and only of requests your server reads as a refresh of the page the tab is on. Navigations, server actions, your own requests and any request that sets its own `cache` go as they did. A layer over `fetch`, put in place with the first publish, does it; a `fetch` wrapper of your own keeps working either way. (Before 0.11.1 an open tab showed the release before the one it was told about, from its second publish on.)
393
394
  - Expect a publish to reach an open tab in about ten seconds with the defaults. `<MapledLive interval={3} />` and `createReleaseHandler({ key, ttl: 1 })` make it quicker at the price of more requests to your site.
394
395
  - With a `basePath` in `next.config`, the route answers under it, and so does the default: `<MapledLive />` asks `/docs/api/mapled/release` on a site with `basePath: "/docs"` — Next writes the basePath into the bundle, and the component reads it from there (0.10.0 and later; earlier versions ask `/api/mapled/release`). An `endpoint` you pass is asked as written, basePath included: `<MapledLive endpoint="/docs/live" />`.
395
396
  - Leave route segment config off the release route: `createReleaseHandler` sets how it is cached. `dynamic = "force-static"` turns it into a copy Next builds without the tab's request, and `fetchCache = "force-no-store"` reads the release past the cache tag, so Mapled hears from the route every `ttl`. `dynamic = "force-dynamic"` does the same when the handler's `revalidate` is `false` or `0`; at its default it is harmless. On the webhook route, leave `fetchCache` off too: with `"force-no-store"` every answer names a new cache mark, and the release route may name no release. Middleware that runs on the route has to let the tab's `GET` through: no redirect, rewrite or sign-in wall. `output: "export"` has no server to run the route at all.
package/dist/live.js CHANGED
@@ -22,6 +22,7 @@ export function MapledLive({ endpoint, interval, onPublish }) {
22
22
  endpoint,
23
23
  interval,
24
24
  onPublish: (next, previous) => {
25
+ refreshPastBrowserCache();
25
26
  if (custom.current)
26
27
  custom.current(next, previous);
27
28
  else
@@ -30,3 +31,102 @@ export function MapledLive({ endpoint, interval, onPublish }) {
30
31
  }), [endpoint, interval, router]);
31
32
  return null;
32
33
  }
34
+ /** The layers this module has put over `fetch`. */
35
+ const layers = new WeakSet();
36
+ /** Sends every refresh of the page from now on past the browser's cache:
37
+ ours, the one `onPublish` makes later, and one the site makes for its
38
+ own reasons alike — a request doesn't say whose it is, and a refresh
39
+ asks for fresh content anyway. A layer over `fetch` does it, put in
40
+ place when the first publish lands: Next asks `fetch` when it sends,
41
+ not when it loads. Code that wraps `fetch` later wraps the layer too;
42
+ code that puts back a `fetch` it kept from before gets a new layer
43
+ over it on the next publish. A layer, once there, stays: removing it
44
+ could take someone else's wrapper out with it.
45
+ The layer keeps nothing from one request to the next. A bypass armed
46
+ by each publish and spent by the first request that looked like the
47
+ refresh was spent, again and again, by prefetches it couldn't be told
48
+ from once a `fetch` wrapper had dropped their priority (PR 442). */
49
+ function refreshPastBrowserCache() {
50
+ let below;
51
+ try {
52
+ below = globalThis.fetch;
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ if (typeof below !== "function" || layers.has(below))
58
+ return;
59
+ const layer = function (input, init) {
60
+ return below.call(this, input, needsBypass(input, init) ? bypassing(init) : init);
61
+ };
62
+ layers.add(layer);
63
+ try {
64
+ globalThis.fetch = layer;
65
+ }
66
+ catch {
67
+ // a fetch that may not be replaced: the refresh goes as before
68
+ }
69
+ }
70
+ /** Whether a request is one the site's server reads as the router's
71
+ refresh of the page the tab is on, left to the browser's default
72
+ cache mode:
73
+ - a GET for the page's server components (the `rsc` header) that is
74
+ not a prefetch (no prefetch header);
75
+ - asked from the root of the router's tree: the `refetch` marker at
76
+ the root of `next-router-state-tree`, which `router.refresh()` sets
77
+ in Next 14, 15 and 16;
78
+ - to the tab's own address, the query compared less the `_rsc` Next
79
+ adds. Next 16 asks a route it doesn't know yet from the root too,
80
+ and `?page=2` of this page is another page here.
81
+ Only what the server reads decides, which a `fetch` wrapper can't
82
+ change without breaking the page — not the `priority` the browser
83
+ keeps, which a wrapper drops when it makes a `Request` of Next's
84
+ `(url, init)` or passes one on as fields. So Next 16's full prefetch
85
+ of this page, asked from the root while its segment cache is empty,
86
+ goes past the cache too: the server can't tell it from the refresh.
87
+ A cache mode someone chose (`no-store`, `reload`, `force-cache`) is
88
+ theirs, and it stays. */
89
+ function needsBypass(input, init) {
90
+ try {
91
+ const request = typeof Request === "function" && input instanceof Request ? input : null;
92
+ if ((init?.cache ?? request?.cache ?? "default") !== "default")
93
+ return false;
94
+ const headers = new Headers(init?.headers ?? request?.headers);
95
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
96
+ if (method !== "GET" || headers.get("rsc") !== "1")
97
+ return false;
98
+ if (headers.has("next-router-prefetch") || headers.has("next-router-segment-prefetch"))
99
+ return false;
100
+ const tree = JSON.parse(decodeURIComponent(headers.get("next-router-state-tree") ?? ""));
101
+ if (!Array.isArray(tree) || tree[3] !== "refetch")
102
+ return false;
103
+ const url = new URL(request ? request.url : String(input), location.href);
104
+ return url.origin === location.origin && url.pathname === location.pathname && query(url.search) === query(location.search);
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /** The members of `RequestInit`, which `fetch` reads with getters and
111
+ all: a spread copies only an object's own fields, and a `Request`
112
+ handed on as the init keeps its members on its prototype. */
113
+ const MEMBERS = ["body", "cache", "credentials", "duplex", "headers", "integrity", "keepalive", "method", "mode", "priority", "redirect", "referrer", "referrerPolicy", "signal", "window"];
114
+ /** `init` as `fetch` would read it, with `cache: "no-cache"` — the one
115
+ change the layer makes. */
116
+ function bypassing(init) {
117
+ const read = { ...init };
118
+ for (const key of MEMBERS) {
119
+ const value = init?.[key];
120
+ if (value !== undefined)
121
+ read[key] = value;
122
+ }
123
+ read.cache = "no-cache";
124
+ return read;
125
+ }
126
+ /** A query as the page reads it: Next 14 writes the request's anew, 15
127
+ and 16 append `_rsc` to it as it was. */
128
+ function query(search) {
129
+ const params = new URLSearchParams(search);
130
+ params.delete("_rsc");
131
+ return params.toString();
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapled/next",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Read published Mapled content in a Next.js site \u2014 delivery client, ISR tags, and a revalidation webhook handler.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mapled.io",