@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.
@@ -0,0 +1,5 @@
1
+ import { type ReactNode } from "react";
2
+ export declare function LoadingBoundary({ fallback, children, }: {
3
+ fallback: ReactNode;
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element;
@@ -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's segment boundaries render through SegmentBoundary or
13
- * SlotBoundary, so the frame just outside the nearest Suspense tells whose
14
- * it is. Only development has the stack; production reports nothing here,
15
- * and the build's own note or warning on the route is the record.
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 (SegmentBoundary|SlotBoundary)\b/;
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;;;;;;;;;;;;;;;GAeG;AACH,MAAM,eAAe,GAAG,yCAAyC,CAAC;AAElE,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,MAAM;SACV,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;SACrB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,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's segment boundaries render through SegmentBoundary or\n * SlotBoundary, so the frame just outside the nearest Suspense tells whose\n * it is. Only development has the stack; production reports nothing here,\n * and the build's own note or warning on the route is the record.\n */\nconst ENGINE_BOUNDARY = /^\\s*at (SegmentBoundary|SlotBoundary)\\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 frames\n .slice(at + 1, at + 3)\n .some((line) => ENGINE_BOUNDARY.test(line));\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"]}
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"]}
@@ -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
- const onMessage = useRef(options.onMessage);
18
- const onError = useRef(options.onError);
19
- onMessage.current = options.onMessage;
20
- onError.current = options.onError;
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.current?.(message);
50
+ onMessage(message);
49
51
  };
50
52
  es.onopen = () => setStatus("open");
51
53
  es.onerror = (e) => {
52
54
  setError(e);
53
- onError.current?.(e);
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
  };
@@ -1 +1 @@
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, 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 const onMessage = useRef(options.onMessage);\n const onError = useRef(options.onError);\n\n onMessage.current = options.onMessage;\n onError.current = options.onError;\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.current?.(message);\n };\n\n es.onopen = () => setStatus(\"open\");\n es.onerror = (e) => {\n setError(e);\n onError.current?.(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"]}
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"]}
@@ -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 = (async () => {
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":"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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, 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"]}
package/dist/prerender.js CHANGED
@@ -51,6 +51,7 @@ const DEFAULT_PRERENDER_CONCURRENCY = 4;
51
51
  const RUNTIME_OWN = new Set([
52
52
  "DefaultRouteError",
53
53
  "DocumentTitle",
54
+ "LoadingBoundary",
54
55
  "PathnameProvider",
55
56
  "RouteErrorBoundary",
56
57
  "SegmentBoundary",