@rsc-kit/core 0.18.0 → 0.18.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/dist/js/LoadingBoundary.d.ts +5 -0
- package/dist/js/LoadingBoundary.js +19 -0
- package/dist/js/LoadingBoundary.js.map +1 -0
- package/dist/js/fallbackReport.js +10 -8
- package/dist/js/fallbackReport.js.map +1 -1
- package/dist/js/useEvents.js +9 -7
- package/dist/js/useEvents.js.map +1 -1
- package/dist/js/usePolling.js +38 -37
- package/dist/js/usePolling.js.map +1 -1
- package/dist/prerender.js +1 -0
- package/dist/prerender.js.map +1 -1
- package/dist/routeSchema.d.ts +49 -0
- package/dist/routeSchema.js.map +1 -1
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +27 -3
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +175 -38
- package/dist/vite.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// The engine's own Suspense: the one a loading.tsx becomes.
|
|
4
|
+
//
|
|
5
|
+
// A component, and a named one, for the report and nothing else. When a read
|
|
6
|
+
// is caught at a boundary during a server render, the component stack says
|
|
7
|
+
// whose boundary it was - and the stack is the only thing that does. It
|
|
8
|
+
// used to be inferred from what stood two frames outside the nearest
|
|
9
|
+
// Suspense, which held for a first render and broke on a PPR resume: React
|
|
10
|
+
// leaves server components out of that stack, so a <Suspense> the developer
|
|
11
|
+
// wrote as the first thing in a page sat directly under the engine's
|
|
12
|
+
// segment boundary and was reported as a loading.tsx catching the whole
|
|
13
|
+
// segment - a warning nobody could make go away, on every request. Now the
|
|
14
|
+
// engine's boundary is the one rendered by LoadingBoundary, and only that.
|
|
15
|
+
import { Suspense } from "react";
|
|
16
|
+
export function LoadingBoundary({ fallback, children, }) {
|
|
17
|
+
return _jsx(Suspense, { fallback: fallback, children: children });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=LoadingBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadingBoundary.js","sourceRoot":"","sources":["../../src/js/LoadingBoundary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,4DAA4D;AAC5D,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,qEAAqE;AACrE,2EAA2E;AAC3E,4EAA4E;AAC5E,qEAAqE;AACrE,wEAAwE;AACxE,2EAA2E;AAC3E,2EAA2E;AAC3E,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAEjD,MAAM,UAAU,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,GAIT;IACC,OAAO,KAAC,QAAQ,IAAC,QAAQ,EAAE,QAAQ,YAAG,QAAQ,GAAY,CAAC;AAC7D,CAAC","sourcesContent":["\"use client\";\n\n// The engine's own Suspense: the one a loading.tsx becomes.\n//\n// A component, and a named one, for the report and nothing else. When a read\n// is caught at a boundary during a server render, the component stack says\n// whose boundary it was - and the stack is the only thing that does. It\n// used to be inferred from what stood two frames outside the nearest\n// Suspense, which held for a first render and broke on a PPR resume: React\n// leaves server components out of that stack, so a <Suspense> the developer\n// wrote as the first thing in a page sat directly under the engine's\n// segment boundary and was reported as a loading.tsx catching the whole\n// segment - a warning nobody could make go away, on every request. Now the\n// engine's boundary is the one rendered by LoadingBoundary, and only that.\nimport { Suspense, type ReactNode } from \"react\";\n\nexport function LoadingBoundary({\n fallback,\n children,\n}: {\n fallback: ReactNode;\n children: ReactNode;\n}) {\n return <Suspense fallback={fallback}>{children}</Suspense>;\n}\n"]}
|
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
* arrives, and a boundary closer to the read would keep the rest painted.
|
|
10
10
|
*
|
|
11
11
|
* The component stack says which: frames run from the component outward,
|
|
12
|
-
* and the engine
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and
|
|
12
|
+
* and the engine renders the boundary a loading.tsx becomes through a
|
|
13
|
+
* component named for it, so the frame just outside the nearest Suspense
|
|
14
|
+
* is LoadingBoundary exactly when the boundary is the engine's. Only that
|
|
15
|
+
* frame, and only that name: the check used to accept SegmentBoundary two
|
|
16
|
+
* frames out, which held for a first render and misfired on a PPR resume,
|
|
17
|
+
* where React leaves server components out of the stack and a developer's
|
|
18
|
+
* own <Suspense> at the top of a page sat directly under the segment
|
|
19
|
+
* boundary - a warning on every request that nothing could make go away.
|
|
16
20
|
*/
|
|
17
|
-
const ENGINE_BOUNDARY = /^\s*at
|
|
21
|
+
const ENGINE_BOUNDARY = /^\s*at LoadingBoundary\b/;
|
|
18
22
|
export function caughtByLoading(componentStack) {
|
|
19
23
|
const frames = (componentStack ?? "")
|
|
20
24
|
.split("\n")
|
|
@@ -22,9 +26,7 @@ export function caughtByLoading(componentStack) {
|
|
|
22
26
|
const at = frames.findIndex((line) => /^\s*at Suspense\b/.test(line));
|
|
23
27
|
if (at === -1)
|
|
24
28
|
return false;
|
|
25
|
-
return frames
|
|
26
|
-
.slice(at + 1, at + 3)
|
|
27
|
-
.some((line) => ENGINE_BOUNDARY.test(line));
|
|
29
|
+
return ENGINE_BOUNDARY.test(frames[at + 1] ?? "");
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Whether a render error is the consumer cancelling, not the app failing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallbackReport.js","sourceRoot":"","sources":["../../src/js/fallbackReport.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fallbackReport.js","sourceRoot":"","sources":["../../src/js/fallbackReport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD,MAAM,UAAU,eAAe,CAC7B,cAAyC;IAEzC,MAAM,MAAM,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;SAClC,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtE,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAE5B,OAAO,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,SAAS,GACb,8EAA8E,CAAC;AAEjF,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,KAAK,YAAY,YAAY;QAAE,OAAO,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC;IAEtE,MAAM,OAAO,GACX,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,IAAI,KAAK;QAC/D,CAAC,CAAC,MAAM,CAAE,KAA8B,CAAC,OAAO,CAAC;QACjD,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,EAAE,CAAC;IAEX,OAAO,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Whether a read React caught at a boundary fell through to a loading.tsx.\n *\n * A component that reads the query string under a <Suspense> the developer\n * wrote is the designed path: the fallback is stored, the browser fills it,\n * and nothing needs saying. The same read with nothing closer than a\n * loading.tsx - a boundary the engine put there for the whole segment - is\n * worth a line, because the whole segment shows the fallback until the query\n * arrives, and a boundary closer to the read would keep the rest painted.\n *\n * The component stack says which: frames run from the component outward,\n * and the engine renders the boundary a loading.tsx becomes through a\n * component named for it, so the frame just outside the nearest Suspense\n * is LoadingBoundary exactly when the boundary is the engine's. Only that\n * frame, and only that name: the check used to accept SegmentBoundary two\n * frames out, which held for a first render and misfired on a PPR resume,\n * where React leaves server components out of the stack and a developer's\n * own <Suspense> at the top of a page sat directly under the segment\n * boundary - a warning on every request that nothing could make go away.\n */\nconst ENGINE_BOUNDARY = /^\\s*at LoadingBoundary\\b/;\n\nexport function caughtByLoading(\n componentStack: string | null | undefined,\n): boolean {\n const frames = (componentStack ?? \"\")\n .split(\"\\n\")\n .filter((line) => /^\\s*at /.test(line));\n const at = frames.findIndex((line) => /^\\s*at Suspense\\b/.test(line));\n\n if (at === -1) return false;\n\n return ENGINE_BOUNDARY.test(frames[at + 1] ?? \"\");\n}\n\n/**\n * Whether a render error is the consumer cancelling, not the app failing.\n *\n * React's server renderer reports an abort as an error, and the reason it\n * gives when the stream was simply cancelled - a browser that left the page\n * mid-stream, a prefetch abandoned, a proxy that closed - is its own fixed\n * message. Logged, it reads as a fault in the page, and the page had none:\n * \"[rsc-kit:ssr] Error: The render was aborted by the server without a\n * reason.\" on a dev console, every so often, for nothing. The same message\n * from the payload renderer means the same thing.\n */\nconst CANCELLED =\n /^The render was aborted by the server (?:without a reason|with a promise)\\.$/;\n\nexport function cancelledByConsumer(error: unknown): boolean {\n if (error instanceof DOMException) return error.name === \"AbortError\";\n\n const message =\n typeof error === \"object\" && error !== null && \"message\" in error\n ? String((error as { message: unknown }).message)\n : typeof error === \"string\"\n ? error\n : \"\";\n\n return CANCELLED.test(message);\n}\n"]}
|
package/dist/js/useEvents.js
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
* setQueryData, SWR's mutate, a setState - and `latest` for when the hook is
|
|
12
12
|
* the store.
|
|
13
13
|
*/
|
|
14
|
-
import { useEffect, useRef, useState } from "react";
|
|
14
|
+
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
|
15
15
|
export function useEvents(url, options = {}) {
|
|
16
16
|
const { enabled = true, event, keep = 100 } = options;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// Effect Events: the connection is opened once per url, and the handlers
|
|
18
|
+
// it calls see the callbacks of the latest render without being the
|
|
19
|
+
// effect's dependencies - which would reopen the connection on every
|
|
20
|
+
// render that passed a new arrow.
|
|
21
|
+
const onMessage = useEffectEvent((message) => options.onMessage?.(message));
|
|
22
|
+
const onError = useEffectEvent((event) => options.onError?.(event));
|
|
21
23
|
const [latest, setLatest] = useState(null);
|
|
22
24
|
const [all, setAll] = useState([]);
|
|
23
25
|
const [status, setStatus] = useState(enabled ? "connecting" : "closed");
|
|
@@ -45,12 +47,12 @@ export function useEvents(url, options = {}) {
|
|
|
45
47
|
setAll((prev) => prev.length >= keep
|
|
46
48
|
? [...prev.slice(1), message]
|
|
47
49
|
: [...prev, message]);
|
|
48
|
-
onMessage
|
|
50
|
+
onMessage(message);
|
|
49
51
|
};
|
|
50
52
|
es.onopen = () => setStatus("open");
|
|
51
53
|
es.onerror = (e) => {
|
|
52
54
|
setError(e);
|
|
53
|
-
onError
|
|
55
|
+
onError(e);
|
|
54
56
|
// EventSource reconnects on its own; CLOSED means it gave up.
|
|
55
57
|
setStatus(es.readyState === EventSource.CLOSED ? "closed" : "connecting");
|
|
56
58
|
};
|
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;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useEvents.js","sourceRoot":"","sources":["../../src/js/useEvents.ts"],"names":[],"mappings":"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","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 * 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 and resumes\n * with Last-Event-ID when the route yielded ids; `status` says where it is.\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\";\n\nexport type EventsStatus = \"connecting\" | \"open\" | \"closed\";\n\nexport interface EventsOptions<T> {\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?: string;\n /** Every message, as it arrives. */\n onMessage?: (message: T) => void;\n /**\n * The connection failed or dropped. EventSource reconnects on its own, so\n * this is for a notice, not a retry; `status` says whether it gave up.\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\nexport function useEvents<T = unknown>(\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 const source = useRef<EventSource | null>(null);\n\n useEffect(() => {\n if (!enabled || typeof EventSource === \"undefined\") {\n setStatus(\"closed\");\n\n return;\n }\n\n const es = new EventSource(url);\n\n source.current = es;\n setStatus(\"connecting\");\n setError(null);\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 es.onopen = () => setStatus(\"open\");\n es.onerror = (e) => {\n setError(e);\n onError(e);\n // EventSource reconnects on its own; CLOSED means it gave up.\n setStatus(es.readyState === EventSource.CLOSED ? \"closed\" : \"connecting\");\n };\n\n if (event) es.addEventListener(event, receive as EventListener);\n else es.onmessage = receive;\n\n return () => {\n es.close();\n source.current = null;\n };\n }, [url, enabled, event, keep]);\n\n return {\n latest,\n all,\n status,\n error,\n close: () => {\n source.current?.close();\n source.current = null;\n setStatus(\"closed\");\n },\n };\n}\n"]}
|
package/dist/js/usePolling.js
CHANGED
|
@@ -29,23 +29,9 @@
|
|
|
29
29
|
* reuses the query you wrote - and is cacheable. Start here when you have
|
|
30
30
|
* no change feed yet; see useEvents when you do.
|
|
31
31
|
*/
|
|
32
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
32
|
+
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
33
33
|
export function usePolling(read, options) {
|
|
34
34
|
const { every, enabled = true, whenHidden = false } = options;
|
|
35
|
-
const latest = useRef({
|
|
36
|
-
read,
|
|
37
|
-
onData: options.onData,
|
|
38
|
-
until: options.until,
|
|
39
|
-
onSettled: options.onSettled,
|
|
40
|
-
onError: options.onError,
|
|
41
|
-
});
|
|
42
|
-
latest.current = {
|
|
43
|
-
read,
|
|
44
|
-
onData: options.onData,
|
|
45
|
-
until: options.until,
|
|
46
|
-
onSettled: options.onSettled,
|
|
47
|
-
onError: options.onError,
|
|
48
|
-
};
|
|
49
35
|
const [data, setData] = useState(null);
|
|
50
36
|
const [error, setError] = useState(null);
|
|
51
37
|
const failures = useRef(0);
|
|
@@ -55,6 +41,41 @@ export function usePolling(read, options) {
|
|
|
55
41
|
// refresh() or a change of inputs. Kept in a ref as well as state, so the
|
|
56
42
|
// interval callback sees it without a re-render in between.
|
|
57
43
|
const isSettled = useRef(false);
|
|
44
|
+
// One read, seeing the read function and callbacks of the latest render.
|
|
45
|
+
// An Effect Event rather than a ref of the latest props: it is what React
|
|
46
|
+
// provides for exactly this - a function the interval calls that must not
|
|
47
|
+
// be a dependency of the interval, and must not go stale - and the
|
|
48
|
+
// convention this package tells apps to use.
|
|
49
|
+
const perform = useEffectEvent(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const next = await read();
|
|
52
|
+
setData(next);
|
|
53
|
+
setError(null);
|
|
54
|
+
failures.current = 0;
|
|
55
|
+
options.onData?.(next);
|
|
56
|
+
if (options.until?.(next)) {
|
|
57
|
+
isSettled.current = true;
|
|
58
|
+
setStatus("settled");
|
|
59
|
+
options.onSettled?.(next);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
failures.current += 1;
|
|
64
|
+
setError(e);
|
|
65
|
+
options.onError?.(e, { failures: failures.current });
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
inFlight.current = null;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Stable, so a component can hand it to a button without re-rendering
|
|
72
|
+
// subscribers on every poll. It only ever runs from an effect's interval or
|
|
73
|
+
// an event handler, never during render, which is what an Effect Event
|
|
74
|
+
// asks of its callers. `perform` is deliberately not a dependency: an
|
|
75
|
+
// Effect Event is never one - React hands out a fresh wrapper per render,
|
|
76
|
+
// every one of which calls the latest implementation - and listing it
|
|
77
|
+
// would recreate refresh on every render, re-run the interval effect that
|
|
78
|
+
// depends on refresh, and poll on each render forever.
|
|
58
79
|
const refresh = useCallback(async () => {
|
|
59
80
|
// Never two at once: a slow answer and a fast interval would otherwise
|
|
60
81
|
// pile reads up, and the last to land wins whether or not it was newest.
|
|
@@ -65,29 +86,9 @@ export function usePolling(read, options) {
|
|
|
65
86
|
isSettled.current = false;
|
|
66
87
|
setStatus("reading");
|
|
67
88
|
}
|
|
68
|
-
inFlight.current = (
|
|
69
|
-
try {
|
|
70
|
-
const next = await latest.current.read();
|
|
71
|
-
setData(next);
|
|
72
|
-
setError(null);
|
|
73
|
-
failures.current = 0;
|
|
74
|
-
latest.current.onData?.(next);
|
|
75
|
-
if (latest.current.until?.(next)) {
|
|
76
|
-
isSettled.current = true;
|
|
77
|
-
setStatus("settled");
|
|
78
|
-
latest.current.onSettled?.(next);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
failures.current += 1;
|
|
83
|
-
setError(e);
|
|
84
|
-
latest.current.onError?.(e, { failures: failures.current });
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
inFlight.current = null;
|
|
88
|
-
}
|
|
89
|
-
})();
|
|
89
|
+
inFlight.current = perform();
|
|
90
90
|
return inFlight.current;
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above
|
|
91
92
|
}, []);
|
|
92
93
|
useEffect(() => {
|
|
93
94
|
if (!enabled) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePolling.js","sourceRoot":"","sources":["../../src/js/usePolling.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AA8BjE,MAAM,UAAU,UAAU,CACxB,IAAsB,EACtB,OAA0B;IAE1B,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,CAAC;QACpB,IAAI;QACJ,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;IAEH,MAAM,CAAC,OAAO,GAAG;QACf,IAAI;QACJ,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC;IAEF,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC,CAAC;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAU,IAAI,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAClC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAC7B,CAAC;IACF,MAAM,QAAQ,GAAG,MAAM,CAAuB,IAAI,CAAC,CAAC;IACpD,qEAAqE;IACrE,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAEhC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,uEAAuE;QACvE,yEAAyE;QACzE,IAAI,QAAQ,CAAC,OAAO;YAAE,OAAO,QAAQ,CAAC,OAAO,CAAC;QAE9C,wEAAwE;QACxE,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,SAAS,CAAC,OAAO,GAAG,KAAK,CAAC;YAC1B,SAAS,CAAC,SAAS,CAAC,CAAC;QACvB,CAAC;QAED,QAAQ,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC7B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;gBAEzC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACf,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC;gBACrB,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;gBAE9B,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;oBACjC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;oBACzB,SAAS,CAAC,SAAS,CAAC,CAAC;oBACrB,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;gBACtB,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACZ,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,CAAC;oBAAS,CAAC;gBACT,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,SAAS,CAAC,MAAM,CAAC,CAAC;YAElB,OAAO;QACT,CAAC;QAED,IAAI,KAAK,GAA0C,IAAI,CAAC;QAExD,SAAS,CAAC,OAAO,GAAG,KAAK,CAAC;QAE1B,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,IAAI,KAAK;gBAAE,OAAO;YAElB,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,KAAK,OAAO,EAAE,CAAC;YACf,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;gBACvB,IAAI,CAAC,SAAS,CAAC,OAAO;oBAAE,KAAK,OAAO,EAAE,CAAC;YACzC,CAAC,EAAE,KAAK,CAAC,CAAC;QACZ,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,KAAK;gBAAE,aAAa,CAAC,KAAK,CAAC,CAAC;YAEhC,KAAK,GAAG,IAAI,CAAC;QACf,CAAC,CAAC;QACF,MAAM,YAAY,GAAG,GAAG,EAAE;YACxB,IAAI,QAAQ,CAAC,eAAe,KAAK,QAAQ,EAAE,CAAC;gBAC1C,IAAI,EAAE,CAAC;gBACP,SAAS,CAAC,QAAQ,CAAC,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACN,KAAK,EAAE,CAAC;YACV,CAAC;QACH,CAAC,CAAC;QAEF,IACE,UAAU;YACV,OAAO,QAAQ,KAAK,WAAW;YAC/B,QAAQ,CAAC,eAAe,KAAK,QAAQ;YAErC,KAAK,EAAE,CAAC;;YACL,SAAS,CAAC,QAAQ,CAAC,CAAC;QAEzB,IAAI,CAAC,UAAU,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YACnD,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,GAAG,EAAE;YACV,IAAI,EAAE,CAAC;YACP,IAAI,CAAC,UAAU,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACnD,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;YACjE,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAE1C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAC1C,CAAC","sourcesContent":["\"use client\";\n\n/**\n * A value read again on an interval, as state - until it settles, if asked.\n *\n * const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n *\n * // Until a job is done, then re-render the page that showed it.\n * usePolling(() => fetchQuery(jobStatus, [id]), {\n * every: 2_000,\n * until: (job) => job.state === 'done' || job.state === 'failed',\n * onSettled: () => refresh('page'),\n * })\n *\n * The result is the data: whatever `read` returns is what `data` holds, and\n * a query's Cache-Control lets a CDN absorb a thousand tabs polling the same\n * thing into one origin request per interval. Settling is separate and\n * explicit: `until` says when a read is the last one, and `onSettled` fires\n * once on that read - so a page that wants to re-render through the server\n * path that built it calls refresh('page') there, and a page that wants the\n * value in hand reads `data`. Which of those is the page's to decide, not\n * the hook's.\n *\n * Pauses while the tab is hidden, never overlaps two reads, and `refresh()`\n * reads now - including after it settled, which starts it again.\n *\n * Polling against server-sent events: a stream sends bytes only when\n * something changed and arrives at once, but holds a connection per tab\n * and needs a source of change to yield from. Polling needs neither - it\n * reuses the query you wrote - and is cacheable. Start here when you have\n * no change feed yet; see useEvents when you do.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface PollingOptions<T> {\n /** Milliseconds between reads. */\n every: number;\n enabled?: boolean;\n /** Every answer, as it arrives. */\n onData?: (data: T) => void;\n /** When a read is the last one: polling stops, and the answer is settled. */\n until?: (data: T) => boolean;\n /** Once, with the answer `until` accepted. */\n onSettled?: (data: T) => void;\n /**\n * Every read that failed. The next interval still reads; `error` is the\n * state. `failures` counts the failed reads in a row, so a third one can be\n * a toast where the first was a blip — a success resets it.\n */\n onError?: (error: unknown, info: { failures: number }) => void;\n /** Keep reading while the tab is hidden. Off by default. */\n whenHidden?: boolean;\n}\n\nexport interface PollingState<T> {\n data: T | null;\n error: unknown;\n status: \"idle\" | \"reading\" | \"paused\" | \"settled\";\n /** Read now, outside the interval - and start again after settling. */\n refresh: () => Promise<void>;\n}\n\nexport function usePolling<T>(\n read: () => Promise<T>,\n options: PollingOptions<T>,\n): PollingState<T> {\n const { every, enabled = true, whenHidden = false } = options;\n const latest = useRef({\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n });\n\n latest.current = {\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n };\n\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const failures = useRef(0);\n const [status, setStatus] = useState<PollingState<T>[\"status\"]>(\n enabled ? \"reading\" : \"idle\",\n );\n const inFlight = useRef<Promise<void> | null>(null);\n // Settled is a stop: the interval is cleared and stays cleared until\n // refresh() or a change of inputs. Kept in a ref as well as state, so the\n // interval callback sees it without a re-render in between.\n const isSettled = useRef(false);\n\n const refresh = useCallback(async () => {\n // Never two at once: a slow answer and a fast interval would otherwise\n // pile reads up, and the last to land wins whether or not it was newest.\n if (inFlight.current) return inFlight.current;\n\n // A read after settling is asked for: refresh() starts the clock again.\n if (isSettled.current) {\n isSettled.current = false;\n setStatus(\"reading\");\n }\n\n inFlight.current = (async () => {\n try {\n const next = await latest.current.read();\n\n setData(next);\n setError(null);\n failures.current = 0;\n latest.current.onData?.(next);\n\n if (latest.current.until?.(next)) {\n isSettled.current = true;\n setStatus(\"settled\");\n latest.current.onSettled?.(next);\n }\n } catch (e) {\n failures.current += 1;\n setError(e);\n latest.current.onError?.(e, { failures: failures.current });\n } finally {\n inFlight.current = null;\n }\n })();\n\n return inFlight.current;\n }, []);\n\n useEffect(() => {\n if (!enabled) {\n setStatus(\"idle\");\n\n return;\n }\n\n let timer: ReturnType<typeof setInterval> | null = null;\n\n isSettled.current = false;\n\n const start = () => {\n if (timer) return;\n\n setStatus(\"reading\");\n void refresh();\n timer = setInterval(() => {\n if (!isSettled.current) void refresh();\n }, every);\n };\n const stop = () => {\n if (timer) clearInterval(timer);\n\n timer = null;\n };\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") {\n stop();\n setStatus(\"paused\");\n } else {\n start();\n }\n };\n\n if (\n whenHidden ||\n typeof document === \"undefined\" ||\n document.visibilityState !== \"hidden\"\n )\n start();\n else setStatus(\"paused\");\n\n if (!whenHidden && typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n\n return () => {\n stop();\n if (!whenHidden && typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n }, [enabled, every, whenHidden, refresh]);\n\n return { data, error, status, refresh };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"usePolling.js","sourceRoot":"","sources":["../../src/js/usePolling.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * A value read again on an interval, as state - until it settles, if asked.\n *\n * const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n *\n * // Until a job is done, then re-render the page that showed it.\n * usePolling(() => fetchQuery(jobStatus, [id]), {\n * every: 2_000,\n * until: (job) => job.state === 'done' || job.state === 'failed',\n * onSettled: () => refresh('page'),\n * })\n *\n * The result is the data: whatever `read` returns is what `data` holds, and\n * a query's Cache-Control lets a CDN absorb a thousand tabs polling the same\n * thing into one origin request per interval. Settling is separate and\n * explicit: `until` says when a read is the last one, and `onSettled` fires\n * once on that read - so a page that wants to re-render through the server\n * path that built it calls refresh('page') there, and a page that wants the\n * value in hand reads `data`. Which of those is the page's to decide, not\n * the hook's.\n *\n * Pauses while the tab is hidden, never overlaps two reads, and `refresh()`\n * reads now - including after it settled, which starts it again.\n *\n * Polling against server-sent events: a stream sends bytes only when\n * something changed and arrives at once, but holds a connection per tab\n * and needs a source of change to yield from. Polling needs neither - it\n * reuses the query you wrote - and is cacheable. Start here when you have\n * no change feed yet; see useEvents when you do.\n */\n\nimport { useCallback, useEffect, useEffectEvent, useRef, useState } from \"react\";\n\nexport interface PollingOptions<T> {\n /** Milliseconds between reads. */\n every: number;\n enabled?: boolean;\n /** Every answer, as it arrives. */\n onData?: (data: T) => void;\n /** When a read is the last one: polling stops, and the answer is settled. */\n until?: (data: T) => boolean;\n /** Once, with the answer `until` accepted. */\n onSettled?: (data: T) => void;\n /**\n * Every read that failed. The next interval still reads; `error` is the\n * state. `failures` counts the failed reads in a row, so a third one can be\n * a toast where the first was a blip — a success resets it.\n */\n onError?: (error: unknown, info: { failures: number }) => void;\n /** Keep reading while the tab is hidden. Off by default. */\n whenHidden?: boolean;\n}\n\nexport interface PollingState<T> {\n data: T | null;\n error: unknown;\n status: \"idle\" | \"reading\" | \"paused\" | \"settled\";\n /** Read now, outside the interval - and start again after settling. */\n refresh: () => Promise<void>;\n}\n\nexport function usePolling<T>(\n read: () => Promise<T>,\n options: PollingOptions<T>,\n): PollingState<T> {\n const { every, enabled = true, whenHidden = false } = options;\n\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const failures = useRef(0);\n const [status, setStatus] = useState<PollingState<T>[\"status\"]>(\n enabled ? \"reading\" : \"idle\",\n );\n const inFlight = useRef<Promise<void> | null>(null);\n // Settled is a stop: the interval is cleared and stays cleared until\n // refresh() or a change of inputs. Kept in a ref as well as state, so the\n // interval callback sees it without a re-render in between.\n const isSettled = useRef(false);\n\n // One read, seeing the read function and callbacks of the latest render.\n // An Effect Event rather than a ref of the latest props: it is what React\n // provides for exactly this - a function the interval calls that must not\n // be a dependency of the interval, and must not go stale - and the\n // convention this package tells apps to use.\n const perform = useEffectEvent(async (): Promise<void> => {\n try {\n const next = await read();\n\n setData(next);\n setError(null);\n failures.current = 0;\n options.onData?.(next);\n\n if (options.until?.(next)) {\n isSettled.current = true;\n setStatus(\"settled\");\n options.onSettled?.(next);\n }\n } catch (e) {\n failures.current += 1;\n setError(e);\n options.onError?.(e, { failures: failures.current });\n } finally {\n inFlight.current = null;\n }\n });\n\n // Stable, so a component can hand it to a button without re-rendering\n // subscribers on every poll. It only ever runs from an effect's interval or\n // an event handler, never during render, which is what an Effect Event\n // asks of its callers. `perform` is deliberately not a dependency: an\n // Effect Event is never one - React hands out a fresh wrapper per render,\n // every one of which calls the latest implementation - and listing it\n // would recreate refresh on every render, re-run the interval effect that\n // depends on refresh, and poll on each render forever.\n const refresh = useCallback(async () => {\n // Never two at once: a slow answer and a fast interval would otherwise\n // pile reads up, and the last to land wins whether or not it was newest.\n if (inFlight.current) return inFlight.current;\n\n // A read after settling is asked for: refresh() starts the clock again.\n if (isSettled.current) {\n isSettled.current = false;\n setStatus(\"reading\");\n }\n\n inFlight.current = perform();\n\n return inFlight.current;\n // eslint-disable-next-line react-hooks/exhaustive-deps -- see above\n }, []);\n\n useEffect(() => {\n if (!enabled) {\n setStatus(\"idle\");\n\n return;\n }\n\n let timer: ReturnType<typeof setInterval> | null = null;\n\n isSettled.current = false;\n\n const start = () => {\n if (timer) return;\n\n setStatus(\"reading\");\n void refresh();\n timer = setInterval(() => {\n if (!isSettled.current) void refresh();\n }, every);\n };\n const stop = () => {\n if (timer) clearInterval(timer);\n\n timer = null;\n };\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") {\n stop();\n setStatus(\"paused\");\n } else {\n start();\n }\n };\n\n if (\n whenHidden ||\n typeof document === \"undefined\" ||\n document.visibilityState !== \"hidden\"\n )\n start();\n else setStatus(\"paused\");\n\n if (!whenHidden && typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n\n return () => {\n stop();\n if (!whenHidden && typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n }, [enabled, every, whenHidden, refresh]);\n\n return { data, error, status, refresh };\n}\n"]}
|