@mapled/next 0.8.0 → 0.9.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/README.md +33 -0
- package/dist/live-pages.d.ts +4 -0
- package/dist/live-pages.js +142 -0
- package/dist/server.d.ts +32 -9
- package/dist/server.js +61 -16
- package/dist/watch.d.ts +7 -0
- package/dist/watch.js +12 -4
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -375,6 +375,38 @@ export function UpdateNotice() {
|
|
|
375
375
|
|
|
376
376
|
Outside React, `watchRelease({ onPublish })` from `@mapled/next` is the same watch as a plain function — it returns the function that stops it.
|
|
377
377
|
|
|
378
|
+
### Pages Router
|
|
379
|
+
|
|
380
|
+
A site on `pages/` mounts the route as an API route and renders the component from `@mapled/next/live/pages` in its custom App:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
// Pages Router: pages/api/mapled/release.ts
|
|
384
|
+
import { createPagesReleaseHandler } from "@mapled/next/server";
|
|
385
|
+
|
|
386
|
+
export default createPagesReleaseHandler({ key: process.env.MAPLED_KEY! });
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// Pages Router: pages/_app.tsx
|
|
391
|
+
import type { AppProps } from "next/app";
|
|
392
|
+
import { MapledLive } from "@mapled/next/live/pages";
|
|
393
|
+
|
|
394
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
395
|
+
return (
|
|
396
|
+
<>
|
|
397
|
+
<Component {...pageProps} />
|
|
398
|
+
<MapledLive />
|
|
399
|
+
</>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
On a publish it replaces the page with itself through `next/router`, so the page's data runs again and it re-renders with the new props in place: scroll position and client state stay. A `#hash` in the address goes: while there is one, a replace to the same page runs no data, and every render the router makes scrolls back to the hash's anchor. For the router it is a navigation to the same URL, so `routeChangeStart` and `routeChangeComplete` fire — a pageview counter listening to them counts it too. A publish that lands while the reader is on the way to another page, or to an anchor on this one, doesn't cancel that navigation: it waits for the page they land on and refreshes that one. The first check counts too: when the release it finds went live after the page was requested — a publish between `getServerSideProps` and that check, or while a tab opened in the background waited to be shown — the page refreshes, and `onPublish` gets `previous` as `null` (which release the page read, the tab can't tell). A page requested within ten seconds after a publish may refresh once for nothing. `endpoint`, `interval` and `onPublish` work as above. (`<MapledLive />` from `@mapled/next/live` would reload the whole tab here.)
|
|
405
|
+
|
|
406
|
+
- **Pages that follow Publish read in `getServerSideProps`.** The Pages Router has no cache tag the route could share with a page, so `createPagesReleaseHandler` answers the live release as Mapled has it, and it moves the moment someone publishes. A page that reads in `getServerSideProps` reads that same release, so the refresh always renders the new content. Keep such a page out of shared caches — Next.js sends it uncached unless you set `Cache-Control` yourself.
|
|
407
|
+
- A `getStaticProps` page can't keep up: it is a copy Next.js regenerates on its own schedule — when its `revalidate` window runs out, or when a webhook of yours calls `res.revalidate` — and the route can't wait for that. A tab that refreshes before the copy does gets the old copy back and won't hear of this publish again. Read the pages that should follow Publish in `getServerSideProps` instead.
|
|
408
|
+
- Reads a page makes in the browser aren't part of its data: reload them in `onPublish`.
|
|
409
|
+
|
|
378
410
|
## Preview drafts
|
|
379
411
|
|
|
380
412
|
Mount the preview routes:
|
|
@@ -509,6 +541,7 @@ Preview (drafts on the site) is built on Next.js draft mode and ships with this
|
|
|
509
541
|
- `createRevalidateHandler({ secret, tags? })` — App Router `POST` handler
|
|
510
542
|
- `createPreviewHandler({ apiUrl?, appUrl? })`, `createExitPreviewHandler()` — App Router `GET` handlers of the preview routes
|
|
511
543
|
- `createReleaseHandler({ key, apiUrl?, ttl?, revalidate? })` — App Router `GET` handler of the release route; `<MapledLive endpoint? interval? onPublish? />` from `@mapled/next/live` and `watchRelease({ endpoint?, interval?, onPublish })` ask it
|
|
544
|
+
- `createPagesReleaseHandler({ key, apiUrl?, ttl? })` — Pages Router API route of the release route (the default export of `pages/api/mapled/release.ts`); `<MapledLive endpoint? interval? onPublish? />` from `@mapled/next/live/pages` asks it
|
|
512
545
|
- `verifySignature(secret, body, signature)` — if you'd rather build your own handler
|
|
513
546
|
|
|
514
547
|
The delivery key only reads published, public content — safe to use anywhere.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/** Open tabs of a Pages Router site follow Publish. Render it once, in
|
|
3
|
+
the custom App:
|
|
4
|
+
|
|
5
|
+
pages/_app.tsx:
|
|
6
|
+
import { MapledLive } from "@mapled/next/live/pages";
|
|
7
|
+
<><Component {...pageProps} /><MapledLive /></>
|
|
8
|
+
|
|
9
|
+
It asks the release route (`createPagesReleaseHandler` from
|
|
10
|
+
"@mapled/next/server", mounted at pages/api/mapled/release) every few
|
|
11
|
+
seconds and, when a publish lands, runs the page's data again through
|
|
12
|
+
the router, in place — scroll position and client state stay. A
|
|
13
|
+
module of its own, so an App Router bundle never pulls in
|
|
14
|
+
`next/router`, nor a Pages one `next/navigation`. */
|
|
15
|
+
import { useRouter } from "next/router";
|
|
16
|
+
import { useEffect, useRef } from "react";
|
|
17
|
+
import { watchFrom } from "./watch.js";
|
|
18
|
+
/** The Pages Router's way to re-run a page's data: a replace to where the
|
|
19
|
+
tab already is. Two details make it an actual refresh — the client
|
|
20
|
+
keeps `getStaticProps` data it fetched once, so its cache is skipped;
|
|
21
|
+
and while the address has a #hash, the router takes any replace to
|
|
22
|
+
the same path — with the hash or without it — for a hash change that
|
|
23
|
+
runs no data, and every render it makes scrolls back to the hash's
|
|
24
|
+
anchor. So the hash is left first, without scrolling, and stays gone:
|
|
25
|
+
putting it back would throw the reader back to the anchor. */
|
|
26
|
+
function refresh(router) {
|
|
27
|
+
const at = router.asPath;
|
|
28
|
+
const cut = at.indexOf("#");
|
|
29
|
+
const path = cut === -1 ? at : at.slice(0, cut);
|
|
30
|
+
const run = () => router.replace(path, undefined, { scroll: false, unstable_skipClientCache: true });
|
|
31
|
+
(cut === -1
|
|
32
|
+
? run()
|
|
33
|
+
: // false: another navigation took over — the tab is somewhere else now
|
|
34
|
+
router.replace(path, undefined, { scroll: false, shallow: true }).then((ok) => ok && run()))
|
|
35
|
+
// the router has reported it (routeChangeError), and a page it can't load it loads in full
|
|
36
|
+
.catch(() => false);
|
|
37
|
+
}
|
|
38
|
+
const landed = (trip, now) => !trip.moving && now !== trip.from;
|
|
39
|
+
/** Mapled stamps a release when its publish starts, not when it commits
|
|
40
|
+
(a big one takes seconds), and its clock and the site's differ a
|
|
41
|
+
little: a release stamped up to this long before the page was asked
|
|
42
|
+
for may still be newer than what the page read. */
|
|
43
|
+
const MARGIN_MS = 10_000;
|
|
44
|
+
/** Whether `first` went live after this document was asked for — then
|
|
45
|
+
the page may have read the release before it: a publish between
|
|
46
|
+
getServerSideProps and the first check, or while a tab opened in the
|
|
47
|
+
background waited to be shown before asking. Compared on the servers'
|
|
48
|
+
clock, which the route's answer tells (`Date`, plus `Age` from a CDN),
|
|
49
|
+
so a browser whose clock is off changes nothing; an answer without
|
|
50
|
+
`Date` leaves the browser's own. Every error of the estimate — a
|
|
51
|
+
second's rounding in `Date`, the time the answer took to arrive —
|
|
52
|
+
makes a refresh likelier, never rarer; a page asked for within the
|
|
53
|
+
margin after a publish refreshes once for nothing. */
|
|
54
|
+
function newerThanPage(first, headers) {
|
|
55
|
+
const askedAt = globalThis.performance?.timeOrigin;
|
|
56
|
+
if (askedAt === undefined)
|
|
57
|
+
return false;
|
|
58
|
+
const date = Date.parse(headers.get("date") ?? "");
|
|
59
|
+
const offset = Number.isNaN(date) ? 0 : date + (Number(headers.get("age")) || 0) * 1000 - Date.now();
|
|
60
|
+
return Date.parse(first.publishedAt) > askedAt + offset - MARGIN_MS;
|
|
61
|
+
}
|
|
62
|
+
export function MapledLive({ endpoint, interval, onPublish }) {
|
|
63
|
+
// the Pages Router hands out a new router object on every render: the
|
|
64
|
+
// watch reads the latest one and restarts only for a new route or pace
|
|
65
|
+
const router = useRouter();
|
|
66
|
+
const latest = useRef({ router, onPublish });
|
|
67
|
+
const trip = useRef({ moving: false, from: null, owed: false });
|
|
68
|
+
/** the newest release the page has started from or heard of — a watch
|
|
69
|
+
restarted for a new route or pace starts from it, not from the time */
|
|
70
|
+
const followed = useRef(undefined);
|
|
71
|
+
/** runs a refresh owed by a publish mid-navigation, once the page it
|
|
72
|
+
led to has rendered — the router's end-of-navigation event and
|
|
73
|
+
React's effect with the new router object come in either order */
|
|
74
|
+
const settle = () => {
|
|
75
|
+
const now = latest.current.router;
|
|
76
|
+
if (trip.current.owed && landed(trip.current, now)) {
|
|
77
|
+
trip.current.owed = false;
|
|
78
|
+
refresh(now);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
latest.current = { router, onPublish };
|
|
83
|
+
settle();
|
|
84
|
+
}, [router, onPublish]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const { events } = latest.current.router;
|
|
87
|
+
const start = () => {
|
|
88
|
+
trip.current.moving = true;
|
|
89
|
+
trip.current.from = latest.current.router;
|
|
90
|
+
};
|
|
91
|
+
const end = () => {
|
|
92
|
+
trip.current.moving = false;
|
|
93
|
+
settle();
|
|
94
|
+
};
|
|
95
|
+
// a cancelled navigation is not an end: what cancelled it is itself a
|
|
96
|
+
// navigation, which has started, or a hash change, or leaves the page
|
|
97
|
+
const failed = (err) => {
|
|
98
|
+
if (!err?.cancelled)
|
|
99
|
+
end();
|
|
100
|
+
};
|
|
101
|
+
// a jump to an anchor has no routeChangeStart, but the router awaits
|
|
102
|
+
// its render all the same
|
|
103
|
+
events.on("routeChangeStart", start);
|
|
104
|
+
events.on("hashChangeStart", start);
|
|
105
|
+
events.on("routeChangeComplete", end);
|
|
106
|
+
events.on("routeChangeError", failed);
|
|
107
|
+
events.on("hashChangeComplete", end);
|
|
108
|
+
return () => {
|
|
109
|
+
events.off("routeChangeStart", start);
|
|
110
|
+
events.off("hashChangeStart", start);
|
|
111
|
+
events.off("routeChangeComplete", end);
|
|
112
|
+
events.off("routeChangeError", failed);
|
|
113
|
+
events.off("hashChangeComplete", end);
|
|
114
|
+
};
|
|
115
|
+
}, []);
|
|
116
|
+
useEffect(() => watchFrom({
|
|
117
|
+
endpoint,
|
|
118
|
+
interval,
|
|
119
|
+
onPublish: (next, previous) => {
|
|
120
|
+
followed.current = next.release;
|
|
121
|
+
const { router: now, onPublish: custom } = latest.current;
|
|
122
|
+
if (custom)
|
|
123
|
+
custom(next, previous);
|
|
124
|
+
else if (!landed(trip.current, now))
|
|
125
|
+
trip.current.owed = true;
|
|
126
|
+
else {
|
|
127
|
+
trip.current.owed = false;
|
|
128
|
+
refresh(now);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// the first answer names what is live, not what the page read:
|
|
133
|
+
// one that went live after the page was asked for counts as a
|
|
134
|
+
// publish, with no `previous` — which release the page read, the
|
|
135
|
+
// tab can't tell
|
|
136
|
+
(first, headers) => {
|
|
137
|
+
const from = followed.current ?? (newerThanPage(first, headers) ? null : first.release);
|
|
138
|
+
followed.current = Math.max(first.release, followed.current ?? 0);
|
|
139
|
+
return from;
|
|
140
|
+
}), [endpoint, interval]);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -25,19 +25,42 @@ export declare function createRevalidateHandler(options: {
|
|
|
25
25
|
client fetch. */
|
|
26
26
|
tags?: string[];
|
|
27
27
|
}): (req: Request) => Promise<Response>;
|
|
28
|
-
|
|
29
|
-
the release this site serves right now, for `<MapledLive />` from
|
|
30
|
-
"@mapled/next/live" to ask about. The pointer is read under the
|
|
31
|
-
"mapled" tag like the content is, so it moves when the publish webhook
|
|
32
|
-
refreshes the cache and not before — a tab that refreshes on it
|
|
33
|
-
renders the new release, never the old one again. One answer is reused
|
|
34
|
-
for `ttl` seconds, in this instance and at the CDN in front of it:
|
|
35
|
-
however many tabs ask, Mapled hears from the site a few times a minute. */
|
|
36
|
-
export declare function createReleaseHandler(options: {
|
|
28
|
+
type ReleaseOptions = {
|
|
37
29
|
key: string;
|
|
38
30
|
apiUrl?: string;
|
|
39
31
|
/** Seconds one answer is reused (default 5, at least 1). */
|
|
40
32
|
ttl?: number;
|
|
33
|
+
};
|
|
34
|
+
/** GET handler for the release route of an App Router site (mount at
|
|
35
|
+
app/api/mapled/release/route.ts): the release this site serves right
|
|
36
|
+
now, for `<MapledLive />` from "@mapled/next/live" to ask about. The
|
|
37
|
+
pointer is read under the "mapled" tag like the content is, so it
|
|
38
|
+
moves when the publish webhook refreshes the cache and not before — a
|
|
39
|
+
tab that refreshes on it renders the new release, never the old one
|
|
40
|
+
again. */
|
|
41
|
+
export declare function createReleaseHandler(options: ReleaseOptions & {
|
|
41
42
|
/** ISR window of the pointer, the same as a read's (default 3600 — the webhook keeps it fresh). */
|
|
42
43
|
revalidate?: number | false;
|
|
43
44
|
}): (req: Request) => Promise<Response>;
|
|
45
|
+
/** What the Pages Router hands an API route: `NextApiRequest` and
|
|
46
|
+
`NextApiResponse` are these and more. */
|
|
47
|
+
type PagesApiRequest = {
|
|
48
|
+
method?: string;
|
|
49
|
+
headers: Record<string, string | string[] | undefined>;
|
|
50
|
+
};
|
|
51
|
+
type PagesApiResponse = {
|
|
52
|
+
statusCode: number;
|
|
53
|
+
setHeader(name: string, value: string): unknown;
|
|
54
|
+
end(body?: string): unknown;
|
|
55
|
+
};
|
|
56
|
+
/** The release route of a Pages Router site — the default export of
|
|
57
|
+
pages/api/mapled/release.ts, for `<MapledLive />` from
|
|
58
|
+
"@mapled/next/live/pages" to ask about. The Pages Router has no cache
|
|
59
|
+
tag a route could share with a page, so the pointer is Mapled's live
|
|
60
|
+
release, read every `ttl` seconds: it moves the moment someone
|
|
61
|
+
publishes. A page that reads in `getServerSideProps` reads that same
|
|
62
|
+
release, and a tab refreshed on it renders the new content; a
|
|
63
|
+
`getStaticProps` page is a copy Next regenerates on its own schedule,
|
|
64
|
+
which the pointer can't wait for (README, «Live updates»). */
|
|
65
|
+
export declare function createPagesReleaseHandler(options: ReleaseOptions): (req: PagesApiRequest, res: PagesApiResponse) => Promise<void>;
|
|
66
|
+
export {};
|
package/dist/server.js
CHANGED
|
@@ -99,15 +99,13 @@ export function createRevalidateHandler(options) {
|
|
|
99
99
|
return Response.json({ revalidated: true });
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
however many tabs ask, Mapled hears from the site a few times a minute. */
|
|
110
|
-
export function createReleaseHandler(options) {
|
|
102
|
+
/** The release route, whichever router mounts it: one answer reused for
|
|
103
|
+
`ttl` seconds, in this instance and at the CDN in front of it —
|
|
104
|
+
however many tabs ask, Mapled hears from the site a few times a
|
|
105
|
+
minute. `next` is how the pointer is fetched: under the content's
|
|
106
|
+
cache tag in the App Router, as is in the Pages Router, which has no
|
|
107
|
+
such cache. */
|
|
108
|
+
function releaseAnswers(options, next) {
|
|
111
109
|
if (!options.key)
|
|
112
110
|
throw new Error("Mapled: a delivery key is required.");
|
|
113
111
|
const base = (options.apiUrl ?? "https://api.mapled.io").replace(/\/+$/, "");
|
|
@@ -121,7 +119,7 @@ export function createReleaseHandler(options) {
|
|
|
121
119
|
try {
|
|
122
120
|
const init = {
|
|
123
121
|
headers: { "x-mapled-key": options.key },
|
|
124
|
-
next
|
|
122
|
+
...(next ? { next } : {}),
|
|
125
123
|
};
|
|
126
124
|
const res = await fetch(`${base}/v1/delivery/release`, init);
|
|
127
125
|
if (!res.ok)
|
|
@@ -146,9 +144,8 @@ export function createReleaseHandler(options) {
|
|
|
146
144
|
askedAt = Date.now();
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const asked = req.headers.get("if-none-match");
|
|
147
|
+
/** `asked` — the tab's If-None-Match */
|
|
148
|
+
return async function answer(asked) {
|
|
152
149
|
if (Date.now() - askedAt >= ttlMs) {
|
|
153
150
|
loading ??= load().finally(() => {
|
|
154
151
|
loading = null;
|
|
@@ -156,15 +153,63 @@ export function createReleaseHandler(options) {
|
|
|
156
153
|
await loading;
|
|
157
154
|
}
|
|
158
155
|
if (!held) {
|
|
159
|
-
return
|
|
156
|
+
return { status: 502, headers: { "cache-control": "no-store" }, body: { error: "The live release isn't available right now." } };
|
|
160
157
|
}
|
|
161
158
|
// the CDN's copy lives no longer than this instance's
|
|
162
159
|
const left = Math.max(1, Math.ceil((ttlMs - (Date.now() - askedAt)) / 1000));
|
|
163
160
|
const etag = `W/"r${held.release ?? 0}"`;
|
|
164
161
|
const headers = { "cache-control": `public, max-age=0, s-maxage=${left}`, etag };
|
|
165
162
|
if (asked && asked.split(",").some((t) => t.trim().replace(/^W\//, "") === etag.slice(2))) {
|
|
166
|
-
return
|
|
163
|
+
return { status: 304, headers };
|
|
164
|
+
}
|
|
165
|
+
return { status: 200, headers, body: held };
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** GET handler for the release route of an App Router site (mount at
|
|
169
|
+
app/api/mapled/release/route.ts): the release this site serves right
|
|
170
|
+
now, for `<MapledLive />` from "@mapled/next/live" to ask about. The
|
|
171
|
+
pointer is read under the "mapled" tag like the content is, so it
|
|
172
|
+
moves when the publish webhook refreshes the cache and not before — a
|
|
173
|
+
tab that refreshes on it renders the new release, never the old one
|
|
174
|
+
again. */
|
|
175
|
+
export function createReleaseHandler(options) {
|
|
176
|
+
const answer = releaseAnswers(options, { revalidate: options.revalidate ?? 3600, tags: ["mapled"] });
|
|
177
|
+
return async function GET(req) {
|
|
178
|
+
// read first: a handler that looks at its request is never prerendered
|
|
179
|
+
const { status, headers, body } = await answer(req.headers.get("if-none-match"));
|
|
180
|
+
return status === 304 ? new Response(null, { status, headers }) : Response.json(body, { status, headers });
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The release route of a Pages Router site — the default export of
|
|
184
|
+
pages/api/mapled/release.ts, for `<MapledLive />` from
|
|
185
|
+
"@mapled/next/live/pages" to ask about. The Pages Router has no cache
|
|
186
|
+
tag a route could share with a page, so the pointer is Mapled's live
|
|
187
|
+
release, read every `ttl` seconds: it moves the moment someone
|
|
188
|
+
publishes. A page that reads in `getServerSideProps` reads that same
|
|
189
|
+
release, and a tab refreshed on it renders the new content; a
|
|
190
|
+
`getStaticProps` page is a copy Next regenerates on its own schedule,
|
|
191
|
+
which the pointer can't wait for (README, «Live updates»). */
|
|
192
|
+
export function createPagesReleaseHandler(options) {
|
|
193
|
+
const answer = releaseAnswers(options);
|
|
194
|
+
return async function handler(req, res) {
|
|
195
|
+
const method = req.method ?? "GET";
|
|
196
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
197
|
+
res.statusCode = 405;
|
|
198
|
+
res.setHeader("allow", "GET, HEAD");
|
|
199
|
+
res.end();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const asked = req.headers["if-none-match"];
|
|
203
|
+
const { status, headers, body } = await answer(Array.isArray(asked) ? asked.join(",") : (asked ?? null));
|
|
204
|
+
res.statusCode = status;
|
|
205
|
+
for (const [name, value] of Object.entries(headers))
|
|
206
|
+
res.setHeader(name, value);
|
|
207
|
+
if (body === undefined) {
|
|
208
|
+
res.end();
|
|
209
|
+
return;
|
|
167
210
|
}
|
|
168
|
-
|
|
211
|
+
// written as is: res.json would swap the ETag for a hash of the body
|
|
212
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
213
|
+
res.end(method === "HEAD" ? undefined : JSON.stringify(body));
|
|
169
214
|
};
|
|
170
215
|
}
|
package/dist/watch.d.ts
CHANGED
|
@@ -21,3 +21,10 @@ export type WatchOptions = {
|
|
|
21
21
|
comes after it. A hidden tab doesn't ask — it checks the moment it is
|
|
22
22
|
shown again — and a route that fails is asked less and less often. */
|
|
23
23
|
export declare function watchRelease(options: WatchOptions): () => void;
|
|
24
|
+
/** The watch behind `watchRelease`, for a caller that can tell more about
|
|
25
|
+
where to start than the first answer can. `from` gets the first
|
|
26
|
+
release the route names, with the answer's headers, and returns the
|
|
27
|
+
release the page is on — the first answer's own number to start from
|
|
28
|
+
it, an older one (or null: not known) to hand the first answer to
|
|
29
|
+
`onPublish` as well. Not part of the package's API. */
|
|
30
|
+
export declare function watchFrom(options: WatchOptions, from?: (first: Release, headers: Headers) => number | null): () => void;
|
package/dist/watch.js
CHANGED
|
@@ -22,6 +22,15 @@ function pointer(body) {
|
|
|
22
22
|
comes after it. A hidden tab doesn't ask — it checks the moment it is
|
|
23
23
|
shown again — and a route that fails is asked less and less often. */
|
|
24
24
|
export function watchRelease(options) {
|
|
25
|
+
return watchFrom(options);
|
|
26
|
+
}
|
|
27
|
+
/** The watch behind `watchRelease`, for a caller that can tell more about
|
|
28
|
+
where to start than the first answer can. `from` gets the first
|
|
29
|
+
release the route names, with the answer's headers, and returns the
|
|
30
|
+
release the page is on — the first answer's own number to start from
|
|
31
|
+
it, an older one (or null: not known) to hand the first answer to
|
|
32
|
+
`onPublish` as well. Not part of the package's API. */
|
|
33
|
+
export function watchFrom(options, from) {
|
|
25
34
|
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
26
35
|
const everyMs = Math.max(2, options.interval ?? 10) * 1000;
|
|
27
36
|
const doc = typeof document === "undefined" ? null : document;
|
|
@@ -54,10 +63,9 @@ export function watchRelease(options) {
|
|
|
54
63
|
}
|
|
55
64
|
else {
|
|
56
65
|
failures = 0;
|
|
57
|
-
if (seen === undefined)
|
|
58
|
-
seen = now ? now.release : null;
|
|
59
|
-
|
|
60
|
-
else if (now && now.release > (seen ?? 0)) {
|
|
66
|
+
if (seen === undefined)
|
|
67
|
+
seen = now ? (from ? from(now, res.headers) : now.release) : null;
|
|
68
|
+
if (now && now.release > (seen ?? 0)) {
|
|
61
69
|
// releases only grow (a rollback is a new one): an answer from a
|
|
62
70
|
// server that hasn't caught up yet never looks like news
|
|
63
71
|
news = { next: now, previous: seen };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"./live": {
|
|
32
32
|
"types": "./dist/live.d.ts",
|
|
33
33
|
"default": "./dist/live.js"
|
|
34
|
+
},
|
|
35
|
+
"./live/pages": {
|
|
36
|
+
"types": "./dist/live-pages.d.ts",
|
|
37
|
+
"default": "./dist/live-pages.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|