@rsc-kit/core 0.24.3 → 0.26.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/dist/changed.d.ts +113 -0
- package/dist/changed.js +399 -0
- package/dist/changed.js.map +1 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +28 -0
- package/dist/conformance.js.map +1 -1
- package/dist/events.d.ts +25 -9
- package/dist/events.js.map +1 -1
- package/dist/headers.d.ts +2 -0
- package/dist/headers.js +2 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.js +15 -5
- package/dist/host.js.map +1 -1
- package/dist/js/Changes.d.ts +11 -0
- package/dist/js/Changes.js +19 -0
- package/dist/js/Changes.js.map +1 -0
- package/dist/js/changesStore.d.ts +9 -0
- package/dist/js/changesStore.js +145 -0
- package/dist/js/changesStore.js.map +1 -0
- package/dist/js/refreshOn.d.ts +21 -0
- package/dist/js/refreshOn.js +53 -0
- package/dist/js/refreshOn.js.map +1 -0
- package/dist/js/section.d.ts +10 -1
- package/dist/js/section.js +20 -4
- package/dist/js/section.js.map +1 -1
- package/dist/js/useEvents.d.ts +11 -5
- package/dist/js/useEvents.js +70 -21
- package/dist/js/useEvents.js.map +1 -1
- package/dist/request.d.ts +11 -0
- package/dist/request.js +17 -0
- package/dist/request.js.map +1 -1
- package/dist/routes.d.ts +31 -0
- package/dist/routes.js.map +1 -1
- package/dist/testHost.d.ts +22 -0
- package/dist/testHost.js +23 -0
- package/dist/testHost.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/vite.js +60 -7
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a rendered region is live on, handed to the tab's watcher.
|
|
3
|
+
*
|
|
4
|
+
* Renders nothing. Rendered by `RefreshOn` beside the region it describes,
|
|
5
|
+
* so it arrives in the same payload - with the page, and again with the
|
|
6
|
+
* region each time it is refreshed, carrying the versions that render saw.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Changes({ target, names, }: {
|
|
9
|
+
target: string;
|
|
10
|
+
names: Record<string, [number, string]>;
|
|
11
|
+
}): null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { register } from "./changesStore";
|
|
4
|
+
/**
|
|
5
|
+
* What a rendered region is live on, handed to the tab's watcher.
|
|
6
|
+
*
|
|
7
|
+
* Renders nothing. Rendered by `RefreshOn` beside the region it describes,
|
|
8
|
+
* so it arrives in the same payload - with the page, and again with the
|
|
9
|
+
* region each time it is refreshed, carrying the versions that render saw.
|
|
10
|
+
*/
|
|
11
|
+
export function Changes({ target, names, }) {
|
|
12
|
+
const key = JSON.stringify(names);
|
|
13
|
+
useEffect(() => register({
|
|
14
|
+
target,
|
|
15
|
+
names: JSON.parse(key),
|
|
16
|
+
}), [target, key]);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=Changes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Changes.js","sourceRoot":"","sources":["../../src/js/Changes.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,EACtB,MAAM,EACN,KAAK,GAIN;IACC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAElC,SAAS,CACP,GAAG,EAAE,CACH,QAAQ,CAAC;QACP,MAAM;QACN,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAqC;KAC3D,CAAC,EACJ,CAAC,MAAM,EAAE,GAAG,CAAC,CACd,CAAC;IAEF,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { register } from \"./changesStore\";\n\n/**\n * What a rendered region is live on, handed to the tab's watcher.\n *\n * Renders nothing. Rendered by `RefreshOn` beside the region it describes,\n * so it arrives in the same payload - with the page, and again with the\n * region each time it is refreshed, carrying the versions that render saw.\n */\nexport function Changes({\n target,\n names,\n}: {\n target: string;\n names: Record<string, [number, string]>;\n}) {\n const key = JSON.stringify(names);\n\n useEffect(\n () =>\n register({\n target,\n names: JSON.parse(key) as Record<string, [number, string]>,\n }),\n [target, key],\n );\n\n return null;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The names a region is live on, as the server rendered them: version at render and signature. */
|
|
2
|
+
export interface Registration {
|
|
3
|
+
target: string;
|
|
4
|
+
names: Record<string, [version: number, signature: string]>;
|
|
5
|
+
}
|
|
6
|
+
/** Register what a rendered region is live on; the returned function forgets it. */
|
|
7
|
+
export declare function register(entry: Registration): () => void;
|
|
8
|
+
/** For tests. */
|
|
9
|
+
export declare function resetChanges(): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The tab's side of live sections: what the page is showing, on one stream.
|
|
2
|
+
//
|
|
3
|
+
// Every section or page rendered with `live` registers its names here, with
|
|
4
|
+
// the version each was at and its signature. The union is what the stream
|
|
5
|
+
// watches; a version that moved refreshes exactly the regions live on that
|
|
6
|
+
// name, through the same refresh() a page calls itself. One stream per tab
|
|
7
|
+
// however many regions, reopened only when the set of names changes, closed
|
|
8
|
+
// while the tab is hidden and caught up when it is seen again.
|
|
9
|
+
import { refresh } from "./router";
|
|
10
|
+
const CHANGES_PATH = "/_rsc/changes";
|
|
11
|
+
const registrations = new Set();
|
|
12
|
+
let source = null;
|
|
13
|
+
let openedWith = "";
|
|
14
|
+
let settle;
|
|
15
|
+
let retry;
|
|
16
|
+
let failures = 0;
|
|
17
|
+
let listening = false;
|
|
18
|
+
/** Register what a rendered region is live on; the returned function forgets it. */
|
|
19
|
+
export function register(entry) {
|
|
20
|
+
registrations.add(entry);
|
|
21
|
+
schedule();
|
|
22
|
+
return () => {
|
|
23
|
+
registrations.delete(entry);
|
|
24
|
+
schedule();
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function listen() {
|
|
28
|
+
if (listening || typeof document === "undefined")
|
|
29
|
+
return;
|
|
30
|
+
listening = true;
|
|
31
|
+
document.addEventListener("visibilitychange", () => document.hidden ? close() : schedule());
|
|
32
|
+
window.addEventListener("online", schedule);
|
|
33
|
+
}
|
|
34
|
+
/** Many regions register in one commit; one reconcile serves them all. */
|
|
35
|
+
function schedule() {
|
|
36
|
+
listen();
|
|
37
|
+
clearTimeout(settle);
|
|
38
|
+
settle = setTimeout(reconcile, 20);
|
|
39
|
+
}
|
|
40
|
+
/** Every name any region is live on, at the lowest version held - the one furthest behind decides. */
|
|
41
|
+
function union() {
|
|
42
|
+
const held = new Map();
|
|
43
|
+
for (const { names } of registrations) {
|
|
44
|
+
for (const [name, [version, signature]] of Object.entries(names)) {
|
|
45
|
+
const seen = held.get(name);
|
|
46
|
+
if (!seen || version < seen[0])
|
|
47
|
+
held.set(name, [version, signature]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...held]
|
|
51
|
+
.map(([name, [version, signature]]) => [
|
|
52
|
+
name,
|
|
53
|
+
version,
|
|
54
|
+
signature,
|
|
55
|
+
])
|
|
56
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : 1));
|
|
57
|
+
}
|
|
58
|
+
function reconcile() {
|
|
59
|
+
const entries = union();
|
|
60
|
+
if (entries.length === 0 ||
|
|
61
|
+
(typeof document !== "undefined" && document.hidden) ||
|
|
62
|
+
typeof EventSource === "undefined") {
|
|
63
|
+
close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const key = JSON.stringify(entries);
|
|
67
|
+
// Something changed: a region came or went, or one re-rendered at a new
|
|
68
|
+
// version. Either way the set is new, and a failed stream gets a fresh try.
|
|
69
|
+
if (key !== openedWith)
|
|
70
|
+
failures = 0;
|
|
71
|
+
if (source && key === openedWith)
|
|
72
|
+
return;
|
|
73
|
+
open(key, entries);
|
|
74
|
+
}
|
|
75
|
+
function open(key, entries) {
|
|
76
|
+
close();
|
|
77
|
+
openedWith = key;
|
|
78
|
+
const stream = new EventSource(CHANGES_PATH + "?w=" + encodeURIComponent(JSON.stringify(entries)));
|
|
79
|
+
source = stream;
|
|
80
|
+
stream.onopen = () => {
|
|
81
|
+
failures = 0;
|
|
82
|
+
};
|
|
83
|
+
stream.onmessage = (event) => {
|
|
84
|
+
let moved;
|
|
85
|
+
try {
|
|
86
|
+
moved = JSON.parse(event.data);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (typeof moved.name === "string" && typeof moved.version === "number")
|
|
92
|
+
apply(moved.name, moved.version);
|
|
93
|
+
};
|
|
94
|
+
stream.onerror = () => {
|
|
95
|
+
// CONNECTING: the browser is retrying a dropped connection itself. CLOSED:
|
|
96
|
+
// an error answer - a 403 for names signed by another deploy, a 502 - and
|
|
97
|
+
// only a new stream comes back. Three in a row and the page is left alone
|
|
98
|
+
// until something re-renders and registers afresh.
|
|
99
|
+
if (stream.readyState !== EventSource.CLOSED)
|
|
100
|
+
return;
|
|
101
|
+
stream.close();
|
|
102
|
+
if (source === stream)
|
|
103
|
+
source = null;
|
|
104
|
+
if (++failures > 3)
|
|
105
|
+
return;
|
|
106
|
+
retry = setTimeout(() => open(openedWith, entries), Math.min(30_000, 1000 * 2 ** (failures - 1)));
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function close() {
|
|
110
|
+
clearTimeout(retry);
|
|
111
|
+
source?.close();
|
|
112
|
+
source = null;
|
|
113
|
+
openedWith = "";
|
|
114
|
+
}
|
|
115
|
+
let pending = null;
|
|
116
|
+
/** A version moved: every region live on the name at another version refreshes, once per tick. */
|
|
117
|
+
function apply(name, version) {
|
|
118
|
+
for (const entry of registrations) {
|
|
119
|
+
const held = entry.names[name];
|
|
120
|
+
if (!held || held[0] === version)
|
|
121
|
+
continue;
|
|
122
|
+
held[0] = version;
|
|
123
|
+
(pending ??= new Set()).add(entry.target);
|
|
124
|
+
}
|
|
125
|
+
if (!pending)
|
|
126
|
+
return;
|
|
127
|
+
queueMicrotask(() => {
|
|
128
|
+
const targets = pending;
|
|
129
|
+
pending = null;
|
|
130
|
+
if (!targets)
|
|
131
|
+
return;
|
|
132
|
+
// The page re-renders its sections with it, so refreshing both is twice.
|
|
133
|
+
const run = targets.has("page") ? ["page"] : [...targets];
|
|
134
|
+
for (const target of run)
|
|
135
|
+
void refresh(target).catch(() => { });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** For tests. */
|
|
139
|
+
export function resetChanges() {
|
|
140
|
+
registrations.clear();
|
|
141
|
+
close();
|
|
142
|
+
failures = 0;
|
|
143
|
+
pending = null;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=changesStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesStore.js","sourceRoot":"","sources":["../../src/js/changesStore.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,+DAA+D;AAE/D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAQnC,MAAM,YAAY,GAAG,eAAe,CAAC;AAErC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAgB,CAAC;AAE9C,IAAI,MAAM,GAAuB,IAAI,CAAC;AACtC,IAAI,UAAU,GAAG,EAAE,CAAC;AACpB,IAAI,MAAiD,CAAC;AACtD,IAAI,KAAgD,CAAC;AACrD,IAAI,QAAQ,GAAG,CAAC,CAAC;AACjB,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB,oFAAoF;AACpF,MAAM,UAAU,QAAQ,CAAC,KAAmB;IAC1C,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACzB,QAAQ,EAAE,CAAC;IAEX,OAAO,GAAG,EAAE;QACV,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,MAAM;IACb,IAAI,SAAS,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAEzD,SAAS,GAAG,IAAI,CAAC;IACjB,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,CACjD,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CACvC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC9C,CAAC;AAED,0EAA0E;AAC1E,SAAS,QAAQ;IACf,MAAM,EAAE,CAAC;IACT,YAAY,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,sGAAsG;AACtG,SAAS,KAAK;IACZ,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;IAEjD,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,aAAa,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAE5B,IAAI,CAAC,IAAI,IAAI,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC;SACb,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,EAA4B,EAAE,CAAC;QAC/D,IAAI;QACJ,OAAO;QACP,SAAS;KACV,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,SAAS;IAChB,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IAExB,IACE,OAAO,CAAC,MAAM,KAAK,CAAC;QACpB,CAAC,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC;QACpD,OAAO,WAAW,KAAK,WAAW,EAClC,CAAC;QACD,KAAK,EAAE,CAAC;QAER,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAEpC,wEAAwE;IACxE,4EAA4E;IAC5E,IAAI,GAAG,KAAK,UAAU;QAAE,QAAQ,GAAG,CAAC,CAAC;IACrC,IAAI,MAAM,IAAI,GAAG,KAAK,UAAU;QAAE,OAAO;IAEzC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,OAAmC;IAC5D,KAAK,EAAE,CAAC;IACR,UAAU,GAAG,GAAG,CAAC;IAEjB,MAAM,MAAM,GAAG,IAAI,WAAW,CAC5B,YAAY,GAAG,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CACnE,CAAC;IAEF,MAAM,GAAG,MAAM,CAAC;IAEhB,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE;QACnB,QAAQ,GAAG,CAAC,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,CAAC,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE;QACzC,IAAI,KAA4C,CAAC;QAEjD,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAc,CAGtC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QAED,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;YACrE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC,CAAC;IAEF,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE;QACpB,2EAA2E;QAC3E,0EAA0E;QAC1E,0EAA0E;QAC1E,mDAAmD;QACnD,IAAI,MAAM,CAAC,UAAU,KAAK,WAAW,CAAC,MAAM;YAAE,OAAO;QAErD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,IAAI,MAAM,KAAK,MAAM;YAAE,MAAM,GAAG,IAAI,CAAC;QACrC,IAAI,EAAE,QAAQ,GAAG,CAAC;YAAE,OAAO;QAE3B,KAAK,GAAG,UAAU,CAChB,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAC7C,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,KAAK;IACZ,YAAY,CAAC,KAAK,CAAC,CAAC;IACpB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,GAAG,IAAI,CAAC;IACd,UAAU,GAAG,EAAE,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,GAAuB,IAAI,CAAC;AAEvC,kGAAkG;AAClG,SAAS,KAAK,CAAC,IAAY,EAAE,OAAe;IAC1C,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAE/B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO;YAAE,SAAS;QAE3C,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;QAClB,CAAC,OAAO,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,cAAc,CAAC,GAAG,EAAE;QAClB,MAAM,OAAO,GAAG,OAAO,CAAC;QAExB,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,yEAAyE;QACzE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;QAE1D,KAAK,MAAM,MAAM,IAAI,GAAG;YAAE,KAAK,OAAO,CAAC,MAAe,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,YAAY;IAC1B,aAAa,CAAC,KAAK,EAAE,CAAC;IACtB,KAAK,EAAE,CAAC;IACR,QAAQ,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC","sourcesContent":["// The tab's side of live sections: what the page is showing, on one stream.\n//\n// Every section or page rendered with `live` registers its names here, with\n// the version each was at and its signature. The union is what the stream\n// watches; a version that moved refreshes exactly the regions live on that\n// name, through the same refresh() a page calls itself. One stream per tab\n// however many regions, reopened only when the set of names changes, closed\n// while the tab is hidden and caught up when it is seen again.\n\nimport { refresh } from \"./router\";\n\n/** The names a region is live on, as the server rendered them: version at render and signature. */\nexport interface Registration {\n target: string;\n names: Record<string, [version: number, signature: string]>;\n}\n\nconst CHANGES_PATH = \"/_rsc/changes\";\n\nconst registrations = new Set<Registration>();\n\nlet source: EventSource | null = null;\nlet openedWith = \"\";\nlet settle: ReturnType<typeof setTimeout> | undefined;\nlet retry: ReturnType<typeof setTimeout> | undefined;\nlet failures = 0;\nlet listening = false;\n\n/** Register what a rendered region is live on; the returned function forgets it. */\nexport function register(entry: Registration): () => void {\n registrations.add(entry);\n schedule();\n\n return () => {\n registrations.delete(entry);\n schedule();\n };\n}\n\nfunction listen(): void {\n if (listening || typeof document === \"undefined\") return;\n\n listening = true;\n document.addEventListener(\"visibilitychange\", () =>\n document.hidden ? close() : schedule(),\n );\n window.addEventListener(\"online\", schedule);\n}\n\n/** Many regions register in one commit; one reconcile serves them all. */\nfunction schedule(): void {\n listen();\n clearTimeout(settle);\n settle = setTimeout(reconcile, 20);\n}\n\n/** Every name any region is live on, at the lowest version held - the one furthest behind decides. */\nfunction union(): [string, number, string][] {\n const held = new Map<string, [number, string]>();\n\n for (const { names } of registrations) {\n for (const [name, [version, signature]] of Object.entries(names)) {\n const seen = held.get(name);\n\n if (!seen || version < seen[0]) held.set(name, [version, signature]);\n }\n }\n\n return [...held]\n .map(([name, [version, signature]]): [string, number, string] => [\n name,\n version,\n signature,\n ])\n .sort((a, b) => (a[0] < b[0] ? -1 : 1));\n}\n\nfunction reconcile(): void {\n const entries = union();\n\n if (\n entries.length === 0 ||\n (typeof document !== \"undefined\" && document.hidden) ||\n typeof EventSource === \"undefined\"\n ) {\n close();\n\n return;\n }\n\n const key = JSON.stringify(entries);\n\n // Something changed: a region came or went, or one re-rendered at a new\n // version. Either way the set is new, and a failed stream gets a fresh try.\n if (key !== openedWith) failures = 0;\n if (source && key === openedWith) return;\n\n open(key, entries);\n}\n\nfunction open(key: string, entries: [string, number, string][]): void {\n close();\n openedWith = key;\n\n const stream = new EventSource(\n CHANGES_PATH + \"?w=\" + encodeURIComponent(JSON.stringify(entries)),\n );\n\n source = stream;\n\n stream.onopen = () => {\n failures = 0;\n };\n\n stream.onmessage = (event: MessageEvent) => {\n let moved: { name?: unknown; version?: unknown };\n\n try {\n moved = JSON.parse(event.data as string) as {\n name?: unknown;\n version?: unknown;\n };\n } catch {\n return;\n }\n\n if (typeof moved.name === \"string\" && typeof moved.version === \"number\")\n apply(moved.name, moved.version);\n };\n\n stream.onerror = () => {\n // CONNECTING: the browser is retrying a dropped connection itself. CLOSED:\n // an error answer - a 403 for names signed by another deploy, a 502 - and\n // only a new stream comes back. Three in a row and the page is left alone\n // until something re-renders and registers afresh.\n if (stream.readyState !== EventSource.CLOSED) return;\n\n stream.close();\n if (source === stream) source = null;\n if (++failures > 3) return;\n\n retry = setTimeout(\n () => open(openedWith, entries),\n Math.min(30_000, 1000 * 2 ** (failures - 1)),\n );\n };\n}\n\nfunction close(): void {\n clearTimeout(retry);\n source?.close();\n source = null;\n openedWith = \"\";\n}\n\nlet pending: Set<string> | null = null;\n\n/** A version moved: every region live on the name at another version refreshes, once per tick. */\nfunction apply(name: string, version: number): void {\n for (const entry of registrations) {\n const held = entry.names[name];\n\n if (!held || held[0] === version) continue;\n\n held[0] = version;\n (pending ??= new Set()).add(entry.target);\n }\n\n if (!pending) return;\n\n queueMicrotask(() => {\n const targets = pending;\n\n pending = null;\n if (!targets) return;\n\n // The page re-renders its sections with it, so refreshing both is twice.\n const run = targets.has(\"page\") ? [\"page\"] : [...targets];\n\n for (const target of run) void refresh(target as never).catch(() => {});\n });\n}\n\n/** For tests. */\nexport function resetChanges(): void {\n registrations.clear();\n close();\n failures = 0;\n pending = null;\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** What a section or page refreshes on: names, or a function of its props that makes them. */
|
|
3
|
+
export type RefreshOnList<P> = string[] | ((props: P) => string[] | Promise<string[]>);
|
|
4
|
+
/**
|
|
5
|
+
* The names a region refreshes on, read for this render and signed for the tab.
|
|
6
|
+
*
|
|
7
|
+
* A server component, rendered beside the region: it works out the names
|
|
8
|
+
* from the props, asks the source what version each is at, and hands them to
|
|
9
|
+
* the client as `Changes`. Under its own Suspense boundary, so the ask never
|
|
10
|
+
* holds up the region's paint - the data is what the visitor is waiting on,
|
|
11
|
+
* not the version numbers behind it.
|
|
12
|
+
*
|
|
13
|
+
* Renders nothing when the source cannot answer - a backend that is down
|
|
14
|
+
* for this one call. The region still renders; it just does not refresh on
|
|
15
|
+
* its names until the next time it is rendered.
|
|
16
|
+
*/
|
|
17
|
+
export declare function RefreshOn<P>({ target, refreshOn, props, }: {
|
|
18
|
+
target: string;
|
|
19
|
+
refreshOn: RefreshOnList<P>;
|
|
20
|
+
props: P;
|
|
21
|
+
}): ReactNode;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Suspense, createElement } from "react";
|
|
2
|
+
import { currentPageProps } from "../request";
|
|
3
|
+
import { sign, versionSource } from "../changed";
|
|
4
|
+
import { Changes } from "./Changes";
|
|
5
|
+
/**
|
|
6
|
+
* The names a region refreshes on, read for this render and signed for the tab.
|
|
7
|
+
*
|
|
8
|
+
* A server component, rendered beside the region: it works out the names
|
|
9
|
+
* from the props, asks the source what version each is at, and hands them to
|
|
10
|
+
* the client as `Changes`. Under its own Suspense boundary, so the ask never
|
|
11
|
+
* holds up the region's paint - the data is what the visitor is waiting on,
|
|
12
|
+
* not the version numbers behind it.
|
|
13
|
+
*
|
|
14
|
+
* Renders nothing when the source cannot answer - a backend that is down
|
|
15
|
+
* for this one call. The region still renders; it just does not refresh on
|
|
16
|
+
* its names until the next time it is rendered.
|
|
17
|
+
*/
|
|
18
|
+
export function RefreshOn({ target, refreshOn, props, }) {
|
|
19
|
+
return createElement(Suspense, { fallback: null }, createElement(Resolve, { target, refreshOn, props }));
|
|
20
|
+
}
|
|
21
|
+
const warned = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* Never throws. An error here would become an error row inside the region's
|
|
24
|
+
* payload, and the browser decodes that as the region failing - a blank
|
|
25
|
+
* document for a version lookup that did not go through. Said once per
|
|
26
|
+
* region instead, and the region renders without refreshing on anything.
|
|
27
|
+
*/
|
|
28
|
+
async function Resolve({ target, refreshOn, props, }) {
|
|
29
|
+
try {
|
|
30
|
+
// The page's params and searchParams, under whatever the page passed: a
|
|
31
|
+
// section rendered by its page is given nothing, and alone is given these.
|
|
32
|
+
const input = { ...currentPageProps(), ...props };
|
|
33
|
+
const named = typeof refreshOn === "function" ? await refreshOn(input) : refreshOn;
|
|
34
|
+
const unique = [...new Set(named)].filter((name) => typeof name === "string" && name !== "");
|
|
35
|
+
if (unique.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
// -1 is a version nothing is at, so every name comes back with its current version.
|
|
38
|
+
const versions = await versionSource().changed(Object.fromEntries(unique.map((name) => [name, -1])), 0);
|
|
39
|
+
const signed = {};
|
|
40
|
+
for (const name of unique)
|
|
41
|
+
signed[name] = [versions[name] ?? 0, await sign(name)];
|
|
42
|
+
return createElement(Changes, { target, names: signed });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!warned.has(target)) {
|
|
46
|
+
warned.add(target);
|
|
47
|
+
console.warn(`[rsc-kit] what ${JSON.stringify(target)} refreshes on could not be read, so it will not: ` +
|
|
48
|
+
(error instanceof Error ? error.message : String(error)));
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=refreshOn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refreshOn.js","sourceRoot":"","sources":["../../src/js/refreshOn.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAI,EAC3B,MAAM,EACN,SAAS,EACT,KAAK,GAKN;IACC,OAAO,aAAa,CAClB,QAAQ,EACR,EAAE,QAAQ,EAAE,IAAI,EAAE,EAClB,aAAa,CAAC,OAAgB,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAC9D,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;AAEjC;;;;;GAKG;AACH,KAAK,UAAU,OAAO,CAAI,EACxB,MAAM,EACN,SAAS,EACT,KAAK,GAKN;IACC,IAAI,CAAC;QACH,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,KAAK,GAAG,EAAE,GAAG,gBAAgB,EAAE,EAAE,GAAI,KAAgB,EAAO,CAAC;QACnE,MAAM,KAAK,GACT,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CACvC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,CAClD,CAAC;QAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAErC,oFAAoF;QACpF,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC,OAAO,CAC5C,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EACpD,CAAC,CACF,CAAC;QACF,MAAM,MAAM,GAA+B,EAAE,CAAC;QAE9C,KAAK,MAAM,IAAI,IAAI,MAAM;YACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEzD,OAAO,aAAa,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CACV,kBAAkB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,mDAAmD;gBACzF,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC3D,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import { Suspense, createElement } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { currentPageProps } from \"../request\";\nimport { sign, versionSource } from \"../changed\";\nimport type { SignedName } from \"../changed\";\nimport { Changes } from \"./Changes\";\n\n/** What a section or page refreshes on: names, or a function of its props that makes them. */\nexport type RefreshOnList<P> =\n string[] | ((props: P) => string[] | Promise<string[]>);\n\n/**\n * The names a region refreshes on, read for this render and signed for the tab.\n *\n * A server component, rendered beside the region: it works out the names\n * from the props, asks the source what version each is at, and hands them to\n * the client as `Changes`. Under its own Suspense boundary, so the ask never\n * holds up the region's paint - the data is what the visitor is waiting on,\n * not the version numbers behind it.\n *\n * Renders nothing when the source cannot answer - a backend that is down\n * for this one call. The region still renders; it just does not refresh on\n * its names until the next time it is rendered.\n */\nexport function RefreshOn<P>({\n target,\n refreshOn,\n props,\n}: {\n target: string;\n refreshOn: RefreshOnList<P>;\n props: P;\n}): ReactNode {\n return createElement(\n Suspense,\n { fallback: null },\n createElement(Resolve as never, { target, refreshOn, props }),\n );\n}\n\nconst warned = new Set<string>();\n\n/**\n * Never throws. An error here would become an error row inside the region's\n * payload, and the browser decodes that as the region failing - a blank\n * document for a version lookup that did not go through. Said once per\n * region instead, and the region renders without refreshing on anything.\n */\nasync function Resolve<P>({\n target,\n refreshOn,\n props,\n}: {\n target: string;\n refreshOn: RefreshOnList<P>;\n props: P;\n}): Promise<ReactNode> {\n try {\n // The page's params and searchParams, under whatever the page passed: a\n // section rendered by its page is given nothing, and alone is given these.\n const input = { ...currentPageProps(), ...(props as object) } as P;\n const named =\n typeof refreshOn === \"function\" ? await refreshOn(input) : refreshOn;\n const unique = [...new Set(named)].filter(\n (name) => typeof name === \"string\" && name !== \"\",\n );\n\n if (unique.length === 0) return null;\n\n // -1 is a version nothing is at, so every name comes back with its current version.\n const versions = await versionSource().changed(\n Object.fromEntries(unique.map((name) => [name, -1])),\n 0,\n );\n const signed: Record<string, SignedName> = {};\n\n for (const name of unique)\n signed[name] = [versions[name] ?? 0, await sign(name)];\n\n return createElement(Changes, { target, names: signed });\n } catch (error) {\n if (!warned.has(target)) {\n warned.add(target);\n console.warn(\n `[rsc-kit] what ${JSON.stringify(target)} refreshes on could not be read, so it will not: ` +\n (error instanceof Error ? error.message : String(error)),\n );\n }\n\n return null;\n }\n}\n"]}
|
package/dist/js/section.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
-
|
|
2
|
+
import type { RefreshOnList } from "./refreshOn";
|
|
3
|
+
export interface SectionOptions<P> {
|
|
4
|
+
/**
|
|
5
|
+
* What this section refreshes on: names for the data it shows, or a
|
|
6
|
+
* function of the section's props - the page's params and searchParams -
|
|
7
|
+
* that makes them. When one is said to have changed, the section refreshes.
|
|
8
|
+
*/
|
|
9
|
+
refreshOn?: RefreshOnList<P>;
|
|
10
|
+
}
|
|
11
|
+
export declare function section<P extends Record<string, unknown>>(name: string, Component: ComponentType<P>, options?: SectionOptions<P>): ComponentType<P>;
|
|
3
12
|
/**
|
|
4
13
|
* The component inside a section module, without its boundary.
|
|
5
14
|
*
|
package/dist/js/section.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createElement } from "react";
|
|
1
|
+
import { Fragment, createElement } from "react";
|
|
2
2
|
import { SlotBoundary } from "./SlotBoundary";
|
|
3
|
+
import { RefreshOn } from "./refreshOn";
|
|
3
4
|
/**
|
|
4
5
|
* Mark a region of a page as separately refreshable.
|
|
5
6
|
*
|
|
@@ -14,6 +15,13 @@ import { SlotBoundary } from "./SlotBoundary";
|
|
|
14
15
|
* can address: `Rsc::revalidate('orders')` re-renders this and nothing else,
|
|
15
16
|
* and the answer replaces it in place.
|
|
16
17
|
*
|
|
18
|
+
* With `refreshOn`, it also says what it depends on, and refreshes itself when
|
|
19
|
+
* that changes - from a webhook, a job, another visitor - with nothing polling:
|
|
20
|
+
*
|
|
21
|
+
* export default section('repos', Repos, {
|
|
22
|
+
* refreshOn: ({ params }) => [`team:${params.team}:repos`],
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
17
25
|
* The name has to be unique within the page. It does not have to be unique in
|
|
18
26
|
* the app: the component is carried on the wrapper it returns, so the server
|
|
19
27
|
* resolves a section through the module the route declares rather than by
|
|
@@ -22,17 +30,25 @@ import { SlotBoundary } from "./SlotBoundary";
|
|
|
22
30
|
*/
|
|
23
31
|
/** Where the unwrapped component hangs off the wrapper section() returns. */
|
|
24
32
|
const INNER = Symbol.for('@rsc-kit/core.section-component');
|
|
25
|
-
export function section(name, Component) {
|
|
33
|
+
export function section(name, Component, options = {}) {
|
|
34
|
+
const { refreshOn } = options;
|
|
35
|
+
// The names render inside the boundary, with the component, so a refresh -
|
|
36
|
+
// which renders the inside alone - carries the versions it saw.
|
|
37
|
+
const Inner = refreshOn
|
|
38
|
+
? function RefreshOnSection(props) {
|
|
39
|
+
return createElement(Fragment, null, createElement(RefreshOn, { key: 'refreshOn', target: name, refreshOn, props }), createElement(Component, { key: 'section', ...props }));
|
|
40
|
+
}
|
|
41
|
+
: Component;
|
|
26
42
|
// The boundary wraps the component here rather than at the call site, so a
|
|
27
43
|
// page renders a section exactly like anything else.
|
|
28
44
|
function Section(props) {
|
|
29
45
|
// createElement cannot line its overloads up with a component generic
|
|
30
46
|
// over its own props. The call is the ordinary one.
|
|
31
|
-
return createElement(SlotBoundary, { name }, createElement(
|
|
47
|
+
return createElement(SlotBoundary, { name }, createElement(Inner, props));
|
|
32
48
|
}
|
|
33
49
|
Section.displayName = `Section(${name})`;
|
|
34
50
|
// Reached through the route's own module, never through a shared map.
|
|
35
|
-
Section[INNER] =
|
|
51
|
+
Section[INNER] = Inner;
|
|
36
52
|
return Section;
|
|
37
53
|
}
|
|
38
54
|
/**
|
package/dist/js/section.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../src/js/section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"section.js","sourceRoot":"","sources":["../../src/js/section.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,6EAA6E;AAC7E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAW5D,MAAM,UAAU,OAAO,CACrB,IAAY,EACZ,SAA2B,EAC3B,OAAO,GAAsB,EAAE;IAE/B,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAE9B,2EAA2E;IAC3E,gEAAgE;IAChE,MAAM,KAAK,GAAqB,SAAS;QACvC,CAAC,CAAC,SAAS,gBAAgB,CAAC,KAAQ;YAChC,OAAO,aAAa,CAClB,QAAQ,EACR,IAAI,EACJ,aAAa,CAAC,SAAiC,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAW,CAAC,EAC/G,aAAa,CAAC,SAAiC,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,GAAI,KAAgB,EAAW,CAAC,CACpG,CAAC;QACJ,CAAC;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,2EAA2E;IAC3E,qDAAqD;IACrD,SAAS,OAAO,CAAC,KAAQ;QACvB,sEAAsE;QACtE,oDAAoD;QACpD,OAAO,aAAa,CAClB,YAA+C,EAC/C,EAAE,IAAI,EAAE,EACR,aAAa,CAAC,KAA6B,EAAE,KAAc,CAAC,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,WAAW,GAAG,WAAW,IAAI,GAAG,CAAC;IACzC,sEAAsE;IACrE,OAA8C,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;IAE/D,OAAO,OAA2B,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAiB;IAEjB,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAErD,OAAQ,QAA+C,CAAC,KAAK,CAEhD,CAAC;AAChB,CAAC","sourcesContent":["import { Fragment, createElement } from \"react\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport { SlotBoundary } from \"./SlotBoundary\";\nimport { RefreshOn } from \"./refreshOn\";\nimport type { RefreshOnList } from \"./refreshOn\";\n\n/**\n * Mark a region of a page as separately refreshable.\n *\n * Deliberately not a client module. The boundary it wraps things in is one,\n * but the things it wraps are server components — an async component that\n * fetches its own data. Marking this file \"use client\" would turn every one of\n * them into a client reference, which async components cannot be.\n *\n * export default section('orders', async function Orders() { ... })\n *\n * The page renders it like any component. What it buys is a name the server\n * can address: `Rsc::revalidate('orders')` re-renders this and nothing else,\n * and the answer replaces it in place.\n *\n * With `refreshOn`, it also says what it depends on, and refreshes itself when\n * that changes - from a webhook, a job, another visitor - with nothing polling:\n *\n * export default section('repos', Repos, {\n * refreshOn: ({ params }) => [`team:${params.team}:repos`],\n * })\n *\n * The name has to be unique within the page. It does not have to be unique in\n * the app: the component is carried on the wrapper it returns, so the server\n * resolves a section through the module the route declares rather than by\n * looking the name up in a registry every section in the app also writes to.\n * Two pages may both call theirs 'stats'.\n */\n\n/** Where the unwrapped component hangs off the wrapper section() returns. */\nconst INNER = Symbol.for('@rsc-kit/core.section-component');\n\nexport interface SectionOptions<P> {\n /**\n * What this section refreshes on: names for the data it shows, or a\n * function of the section's props - the page's params and searchParams -\n * that makes them. When one is said to have changed, the section refreshes.\n */\n refreshOn?: RefreshOnList<P>;\n}\n\nexport function section<P extends Record<string, unknown>>(\n name: string,\n Component: ComponentType<P>,\n options: SectionOptions<P> = {},\n): ComponentType<P> {\n const { refreshOn } = options;\n\n // The names render inside the boundary, with the component, so a refresh -\n // which renders the inside alone - carries the versions it saw.\n const Inner: ComponentType<P> = refreshOn\n ? function RefreshOnSection(props: P): ReactNode {\n return createElement(\n Fragment,\n null,\n createElement(RefreshOn as ComponentType<never>, { key: 'refreshOn', target: name, refreshOn, props } as never),\n createElement(Component as ComponentType<never>, { key: 'section', ...(props as object) } as never),\n );\n }\n : Component;\n\n // The boundary wraps the component here rather than at the call site, so a\n // page renders a section exactly like anything else.\n function Section(props: P): ReactNode {\n // createElement cannot line its overloads up with a component generic\n // over its own props. The call is the ordinary one.\n return createElement(\n SlotBoundary as ComponentType<{ name: string }>,\n { name },\n createElement(Inner as ComponentType<never>, props as never),\n );\n }\n\n Section.displayName = `Section(${name})`;\n // Reached through the route's own module, never through a shared map.\n (Section as unknown as Record<symbol, unknown>)[INNER] = Inner;\n\n return Section as ComponentType<P>;\n}\n\n/**\n * The component inside a section module, without its boundary.\n *\n * Revalidation renders this rather than the wrapper: the client swaps what is\n * inside the boundary, so returning the wrapper would nest a new boundary\n * inside the old one on every refresh.\n *\n * Takes the module's own export rather than a name. A name-keyed registry is\n * written to by every section in the app at bundle load — the generated entry\n * imports them all eagerly — so a lookup by name reached any page's region from\n * any url, bounded only by whatever guard happened to sit on the url asked for.\n */\nexport function sectionComponent(\n exported: unknown,\n): ComponentType<Record<string, unknown>> | undefined {\n if (typeof exported !== 'function') return undefined;\n\n return (exported as unknown as Record<symbol, unknown>)[INNER] as\n | ComponentType<Record<string, unknown>>\n | undefined;\n}\n"]}
|
package/dist/js/useEvents.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import type { MessageOf, NamedMessageOf } from "../events.js";
|
|
2
|
+
import type { EventsAt, EventsUrl } from "../routes.js";
|
|
1
3
|
export type EventsStatus = "connecting" | "open" | "closed";
|
|
2
|
-
export interface EventsOptions<T> {
|
|
4
|
+
export interface EventsOptions<T, E extends string | undefined = string | undefined> {
|
|
3
5
|
/** Off, and nothing connects. For a stream that waits on an id. */
|
|
4
6
|
enabled?: boolean;
|
|
5
7
|
/** Listen to one named event rather than the unnamed stream. */
|
|
6
|
-
event?:
|
|
8
|
+
event?: E;
|
|
7
9
|
/** Every message, as it arrives. */
|
|
8
10
|
onMessage?: (message: T) => void;
|
|
9
11
|
/**
|
|
10
|
-
* The connection failed or dropped.
|
|
11
|
-
* this is for a notice, not a retry
|
|
12
|
+
* The connection failed or dropped. The hook reconnects on its own, so
|
|
13
|
+
* this is for a notice, not a retry.
|
|
12
14
|
*/
|
|
13
15
|
onError?: (error: Event) => void;
|
|
14
16
|
/** How many messages `all` keeps. Default 100; 0 keeps none. */
|
|
@@ -22,4 +24,8 @@ export interface EventsState<T> {
|
|
|
22
24
|
/** Close the stream for good; the hook will not reconnect. */
|
|
23
25
|
close: () => void;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
/** What the hook receives from `Y`'s yields: the unnamed messages, or those named `E`. */
|
|
28
|
+
type Received<Y, E> = unknown extends Y ? unknown : E extends string ? NamedMessageOf<Y, E> : MessageOf<Y>;
|
|
29
|
+
export declare function useEvents<const U extends EventsUrl, const E extends string | undefined = undefined>(url: U, options?: EventsOptions<Received<EventsAt<U>, E>, E>): EventsState<Received<EventsAt<U>, E>>;
|
|
30
|
+
export declare function useEvents<T>(url: EventsUrl, options?: EventsOptions<T>): EventsState<T>;
|
|
31
|
+
export {};
|
package/dist/js/useEvents.js
CHANGED
|
@@ -4,14 +4,27 @@
|
|
|
4
4
|
*
|
|
5
5
|
* const { latest, status } = useEvents(`/api/orders/${id}/events`)
|
|
6
6
|
*
|
|
7
|
+
* A message is typed from the route: the build records what each `events()`
|
|
8
|
+
* route yields, so `latest` is what the generator sends, and a route that
|
|
9
|
+
* changes it fails the typecheck here rather than in the browser. Another
|
|
10
|
+
* site's stream, or one written without `events()`, is `unknown`; name its
|
|
11
|
+
* type with `useEvents<Message>(url)`.
|
|
12
|
+
*
|
|
7
13
|
* Subscribes in an effect, so it is safe in a component that also renders on
|
|
8
|
-
* the server. The browser's EventSource reconnects on its own
|
|
9
|
-
* with Last-Event-ID when the route yielded ids
|
|
14
|
+
* the server. The browser's EventSource reconnects on its own after a dropped
|
|
15
|
+
* connection and resumes with Last-Event-ID when the route yielded ids. What
|
|
16
|
+
* it does not survive is an error answer - a 502 while a deploy restarts, a
|
|
17
|
+
* proxy's timeout page - after which it closes for good; the hook opens a new
|
|
18
|
+
* one, backing off from a second to thirty, and at once when the browser comes
|
|
19
|
+
* back online. `status` says where it is; `close()` stops it.
|
|
20
|
+
*
|
|
10
21
|
* `onMessage` is for a store that already holds the value - TanStack's
|
|
11
22
|
* setQueryData, SWR's mutate, a setState - and `latest` for when the hook is
|
|
12
23
|
* the store.
|
|
13
24
|
*/
|
|
14
25
|
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
|
26
|
+
/** The longest wait between attempts after the browser gave up. */
|
|
27
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
15
28
|
export function useEvents(url, options = {}) {
|
|
16
29
|
const { enabled = true, event, keep = 100 } = options;
|
|
17
30
|
// Effect Events: the connection is opened once per url, and the handlers
|
|
@@ -24,16 +37,17 @@ export function useEvents(url, options = {}) {
|
|
|
24
37
|
const [all, setAll] = useState([]);
|
|
25
38
|
const [status, setStatus] = useState(enabled ? "connecting" : "closed");
|
|
26
39
|
const [error, setError] = useState(null);
|
|
27
|
-
|
|
40
|
+
// Ends this url's connection and any retry waiting to open the next one.
|
|
41
|
+
const stop = useRef(null);
|
|
28
42
|
useEffect(() => {
|
|
29
43
|
if (!enabled || typeof EventSource === "undefined") {
|
|
30
44
|
setStatus("closed");
|
|
31
45
|
return;
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
let es = null;
|
|
48
|
+
let retry;
|
|
49
|
+
let failures = 0;
|
|
50
|
+
let stopped = false;
|
|
37
51
|
const receive = (e) => {
|
|
38
52
|
let message;
|
|
39
53
|
try {
|
|
@@ -49,20 +63,55 @@ export function useEvents(url, options = {}) {
|
|
|
49
63
|
: [...prev, message]);
|
|
50
64
|
onMessage(message);
|
|
51
65
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
setStatus(
|
|
66
|
+
const connect = () => {
|
|
67
|
+
clearTimeout(retry);
|
|
68
|
+
retry = undefined;
|
|
69
|
+
const source = new EventSource(url);
|
|
70
|
+
es = source;
|
|
71
|
+
setStatus("connecting");
|
|
72
|
+
source.onopen = () => {
|
|
73
|
+
failures = 0;
|
|
74
|
+
setError(null);
|
|
75
|
+
setStatus("open");
|
|
76
|
+
};
|
|
77
|
+
source.onerror = (e) => {
|
|
78
|
+
setError(e);
|
|
79
|
+
onError(e);
|
|
80
|
+
// Still CONNECTING: the browser is retrying a dropped connection
|
|
81
|
+
// itself. CLOSED: it gave up, and only a new EventSource comes back.
|
|
82
|
+
if (source.readyState !== EventSource.CLOSED) {
|
|
83
|
+
setStatus("connecting");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
source.close();
|
|
87
|
+
if (stopped)
|
|
88
|
+
return;
|
|
89
|
+
setStatus("connecting");
|
|
90
|
+
retry = setTimeout(connect, Math.min(MAX_BACKOFF_MS, 1000 * 2 ** failures++));
|
|
91
|
+
};
|
|
92
|
+
if (event)
|
|
93
|
+
source.addEventListener(event, receive);
|
|
94
|
+
else
|
|
95
|
+
source.onmessage = receive;
|
|
96
|
+
};
|
|
97
|
+
// Back online: try now rather than at the end of the backoff.
|
|
98
|
+
const online = () => {
|
|
99
|
+
if (!stopped && retry !== undefined)
|
|
100
|
+
connect();
|
|
101
|
+
};
|
|
102
|
+
connect();
|
|
103
|
+
window.addEventListener("online", online);
|
|
104
|
+
const end = () => {
|
|
105
|
+
stopped = true;
|
|
106
|
+
clearTimeout(retry);
|
|
107
|
+
es?.close();
|
|
108
|
+
window.removeEventListener("online", online);
|
|
58
109
|
};
|
|
59
|
-
|
|
60
|
-
es.addEventListener(event, receive);
|
|
61
|
-
else
|
|
62
|
-
es.onmessage = receive;
|
|
110
|
+
stop.current = end;
|
|
63
111
|
return () => {
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
end();
|
|
113
|
+
if (stop.current === end)
|
|
114
|
+
stop.current = null;
|
|
66
115
|
};
|
|
67
116
|
}, [url, enabled, event, keep]);
|
|
68
117
|
return {
|
|
@@ -71,8 +120,8 @@ export function useEvents(url, options = {}) {
|
|
|
71
120
|
status,
|
|
72
121
|
error,
|
|
73
122
|
close: () => {
|
|
74
|
-
|
|
75
|
-
|
|
123
|
+
stop.current?.();
|
|
124
|
+
stop.current = null;
|
|
76
125
|
setStatus("closed");
|
|
77
126
|
},
|
|
78
127
|
};
|
package/dist/js/useEvents.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEvents.js","sourceRoot":"","sources":["../../src/js/useEvents.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb
|
|
1
|
+
{"version":3,"file":"useEvents.js","sourceRoot":"","sources":["../../src/js/useEvents.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAsCpE,mEAAmE;AACnE,MAAM,cAAc,GAAG,MAAM,CAAC;AAO9B,MAAM,UAAU,SAAS,CACvB,GAAW,EACX,OAAO,GAAqB,EAAE;IAE9B,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC;IACtD,yEAAyE;IACzE,oEAAoE;IACpE,qEAAqE;IACrE,kCAAkC;IAClC,MAAM,SAAS,GAAG,cAAc,CAAC,CAAC,OAAU,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,cAAc,CAAC,CAAC,KAAY,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAE3E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC,CAAC;IACrD,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAM,EAAE,CAAC,CAAC;IACxC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAClC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAClC,CAAC;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,yEAAyE;IACzE,MAAM,IAAI,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IAE/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,IAAI,OAAO,WAAW,KAAK,WAAW,EAAE,CAAC;YACnD,SAAS,CAAC,QAAQ,CAAC,CAAC;YAEpB,OAAO;QACT,CAAC;QAED,IAAI,EAAE,GAAuB,IAAI,CAAC;QAClC,IAAI,KAAgD,CAAC;QACrD,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,MAAM,OAAO,GAAG,CAAC,CAAe,EAAE,EAAE;YAClC,IAAI,OAAU,CAAC;YAEf,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAM,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,CAAC,CAAC,IAAS,CAAC;YACxB,CAAC;YAED,SAAS,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,IAAI,GAAG,CAAC;gBACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACd,IAAI,CAAC,MAAM,IAAI,IAAI;oBACjB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;oBAC7B,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CACvB,CAAC;YACJ,SAAS,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC,CAAC;QAEF,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,GAAG,SAAS,CAAC;YAElB,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC;YAEpC,EAAE,GAAG,MAAM,CAAC;YACZ,SAAS,CAAC,YAAY,CAAC,CAAC;YAExB,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE;gBACnB,QAAQ,GAAG,CAAC,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACf,SAAS,CAAC,MAAM,CAAC,CAAC;YACpB,CAAC,CAAC;YACF,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,EAAE;gBACrB,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACZ,OAAO,CAAC,CAAC,CAAC,CAAC;gBAEX,iEAAiE;gBACjE,qEAAqE;gBACrE,IAAI,MAAM,CAAC,UAAU,KAAK,WAAW,CAAC,MAAM,EAAE,CAAC;oBAC7C,SAAS,CAAC,YAAY,CAAC,CAAC;oBAExB,OAAO;gBACT,CAAC;gBAED,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,IAAI,OAAO;oBAAE,OAAO;gBAEpB,SAAS,CAAC,YAAY,CAAC,CAAC;gBACxB,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC;YAChF,CAAC,CAAC;YAEF,IAAI,KAAK;gBAAE,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAwB,CAAC,CAAC;;gBAC/D,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC;QAClC,CAAC,CAAC;QAEF,8DAA8D;QAC9D,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,IAAI,CAAC,OAAO,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC;QAEF,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAE1C,MAAM,GAAG,GAAG,GAAG,EAAE;YACf,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,EAAE,EAAE,KAAK,EAAE,CAAC;YACZ,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC,CAAC;QAEF,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC;QAEnB,OAAO,GAAG,EAAE;YACV,GAAG,EAAE,CAAC;YACN,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG;gBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAChD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAEhC,OAAO;QACL,MAAM;QACN,GAAG;QACH,MAAM;QACN,KAAK;QACL,KAAK,EAAE,GAAG,EAAE;YACV,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,SAAS,CAAC,QAAQ,CAAC,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\n/**\n * A stream of server-sent events, as state.\n *\n * const { latest, status } = useEvents(`/api/orders/${id}/events`)\n *\n * A message is typed from the route: the build records what each `events()`\n * route yields, so `latest` is what the generator sends, and a route that\n * changes it fails the typecheck here rather than in the browser. Another\n * site's stream, or one written without `events()`, is `unknown`; name its\n * type with `useEvents<Message>(url)`.\n *\n * Subscribes in an effect, so it is safe in a component that also renders on\n * the server. The browser's EventSource reconnects on its own after a dropped\n * connection and resumes with Last-Event-ID when the route yielded ids. What\n * it does not survive is an error answer - a 502 while a deploy restarts, a\n * proxy's timeout page - after which it closes for good; the hook opens a new\n * one, backing off from a second to thirty, and at once when the browser comes\n * back online. `status` says where it is; `close()` stops it.\n *\n * `onMessage` is for a store that already holds the value - TanStack's\n * setQueryData, SWR's mutate, a setState - and `latest` for when the hook is\n * the store.\n */\n\nimport { useEffect, useEffectEvent, useRef, useState } from \"react\";\nimport type { MessageOf, NamedMessageOf } from \"../events.js\";\nimport type { EventsAt, EventsUrl } from \"../routes.js\";\n\nexport type EventsStatus = \"connecting\" | \"open\" | \"closed\";\n\nexport interface EventsOptions<T, E extends string | undefined = string | undefined> {\n /** Off, and nothing connects. For a stream that waits on an id. */\n enabled?: boolean;\n /** Listen to one named event rather than the unnamed stream. */\n event?: E;\n /** Every message, as it arrives. */\n onMessage?: (message: T) => void;\n /**\n * The connection failed or dropped. The hook reconnects on its own, so\n * this is for a notice, not a retry.\n */\n onError?: (error: Event) => void;\n /** How many messages `all` keeps. Default 100; 0 keeps none. */\n keep?: number;\n}\n\nexport interface EventsState<T> {\n latest: T | null;\n all: T[];\n status: EventsStatus;\n error: Event | null;\n /** Close the stream for good; the hook will not reconnect. */\n close: () => void;\n}\n\n/** What the hook receives from `Y`'s yields: the unnamed messages, or those named `E`. */\ntype Received<Y, E> = unknown extends Y\n ? unknown\n : E extends string\n ? NamedMessageOf<Y, E>\n : MessageOf<Y>;\n\n/** The longest wait between attempts after the browser gave up. */\nconst MAX_BACKOFF_MS = 30_000;\n\nexport function useEvents<const U extends EventsUrl, const E extends string | undefined = undefined>(\n url: U,\n options?: EventsOptions<Received<EventsAt<U>, E>, E>,\n): EventsState<Received<EventsAt<U>, E>>;\nexport function useEvents<T>(url: EventsUrl, options?: EventsOptions<T>): EventsState<T>;\nexport function useEvents<T>(\n url: string,\n options: EventsOptions<T> = {},\n): EventsState<T> {\n const { enabled = true, event, keep = 100 } = options;\n // Effect Events: the connection is opened once per url, and the handlers\n // it calls see the callbacks of the latest render without being the\n // effect's dependencies - which would reopen the connection on every\n // render that passed a new arrow.\n const onMessage = useEffectEvent((message: T) => options.onMessage?.(message));\n const onError = useEffectEvent((event: Event) => options.onError?.(event));\n\n const [latest, setLatest] = useState<T | null>(null);\n const [all, setAll] = useState<T[]>([]);\n const [status, setStatus] = useState<EventsStatus>(\n enabled ? \"connecting\" : \"closed\",\n );\n const [error, setError] = useState<Event | null>(null);\n // Ends this url's connection and any retry waiting to open the next one.\n const stop = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n if (!enabled || typeof EventSource === \"undefined\") {\n setStatus(\"closed\");\n\n return;\n }\n\n let es: EventSource | null = null;\n let retry: ReturnType<typeof setTimeout> | undefined;\n let failures = 0;\n let stopped = false;\n\n const receive = (e: MessageEvent) => {\n let message: T;\n\n try {\n message = JSON.parse(e.data) as T;\n } catch {\n message = e.data as T;\n }\n\n setLatest(message);\n if (keep > 0)\n setAll((prev) =>\n prev.length >= keep\n ? [...prev.slice(1), message]\n : [...prev, message],\n );\n onMessage(message);\n };\n\n const connect = () => {\n clearTimeout(retry);\n retry = undefined;\n\n const source = new EventSource(url);\n\n es = source;\n setStatus(\"connecting\");\n\n source.onopen = () => {\n failures = 0;\n setError(null);\n setStatus(\"open\");\n };\n source.onerror = (e) => {\n setError(e);\n onError(e);\n\n // Still CONNECTING: the browser is retrying a dropped connection\n // itself. CLOSED: it gave up, and only a new EventSource comes back.\n if (source.readyState !== EventSource.CLOSED) {\n setStatus(\"connecting\");\n\n return;\n }\n\n source.close();\n if (stopped) return;\n\n setStatus(\"connecting\");\n retry = setTimeout(connect, Math.min(MAX_BACKOFF_MS, 1000 * 2 ** failures++));\n };\n\n if (event) source.addEventListener(event, receive as EventListener);\n else source.onmessage = receive;\n };\n\n // Back online: try now rather than at the end of the backoff.\n const online = () => {\n if (!stopped && retry !== undefined) connect();\n };\n\n connect();\n window.addEventListener(\"online\", online);\n\n const end = () => {\n stopped = true;\n clearTimeout(retry);\n es?.close();\n window.removeEventListener(\"online\", online);\n };\n\n stop.current = end;\n\n return () => {\n end();\n if (stop.current === end) stop.current = null;\n };\n }, [url, enabled, event, keep]);\n\n return {\n latest,\n all,\n status,\n error,\n close: () => {\n stop.current?.();\n stop.current = null;\n setStatus(\"closed\");\n },\n };\n}\n"]}
|
package/dist/request.d.ts
CHANGED
|
@@ -234,3 +234,14 @@ export declare function requestReadWhere(): string[];
|
|
|
234
234
|
* asked for.
|
|
235
235
|
*/
|
|
236
236
|
export declare function searchParams(): Promise<URLSearchParams>;
|
|
237
|
+
/**
|
|
238
|
+
* The props the page is being rendered with, for whatever renders inside it.
|
|
239
|
+
*
|
|
240
|
+
* A section is rendered by its page with whatever the page passed, which is
|
|
241
|
+
* usually nothing, and alone - on a refresh - with the page's own props. Its
|
|
242
|
+
* refreshOn is a function of the page's params either way, so the engine notes
|
|
243
|
+
* them here and the section reads them back, whichever way it was rendered.
|
|
244
|
+
*/
|
|
245
|
+
export declare function notePageProps(props: Record<string, unknown>): void;
|
|
246
|
+
/** The page's props as noted by the engine; empty outside a page render. */
|
|
247
|
+
export declare function currentPageProps(): Record<string, unknown>;
|