@rsc-kit/core 0.26.0 → 0.27.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.
@@ -6,7 +6,7 @@
6
6
  // name, through the same refresh() a page calls itself. One stream per tab
7
7
  // however many regions, reopened only when the set of names changes, closed
8
8
  // while the tab is hidden and caught up when it is seen again.
9
- import { refresh } from "./router";
9
+ import { refresh, refreshOnChange } from "./router";
10
10
  const CHANGES_PATH = "/_rsc/changes";
11
11
  const registrations = new Set();
12
12
  let source = null;
@@ -28,7 +28,16 @@ function listen() {
28
28
  if (listening || typeof document === "undefined")
29
29
  return;
30
30
  listening = true;
31
- document.addEventListener("visibilitychange", () => document.hidden ? close() : schedule());
31
+ document.addEventListener("visibilitychange", () => {
32
+ if (!document.hidden)
33
+ return schedule();
34
+ // Hidden tabs do not hold a connection. Nothing is missed: seen again,
35
+ // the stream reopens at the versions this tab holds and every name that
36
+ // moved meanwhile is reported at once.
37
+ if (source)
38
+ note("refreshOn paused: tab hidden. It catches up when the tab is shown.");
39
+ close();
40
+ });
32
41
  window.addEventListener("online", schedule);
33
42
  }
34
43
  /** Many regions register in one commit; one reconcile serves them all. */
@@ -70,8 +79,23 @@ function reconcile() {
70
79
  failures = 0;
71
80
  if (source && key === openedWith)
72
81
  return;
82
+ if (import.meta.env?.DEV) {
83
+ const lines = [...registrations].map(({ target, names }) => ` ${target}: ${Object.entries(names)
84
+ .map(([name, [version]]) => `${name} @${version}`)
85
+ .join(", ")}`);
86
+ note(`refreshOn watching, by region:\n${lines.join("\n")}`);
87
+ }
73
88
  open(key, entries);
74
89
  }
90
+ /**
91
+ * Development only: what the page refreshes on, and why it stopped. A region
92
+ * that declared refreshOn but is missing from the list rendered no names -
93
+ * the server said why, once, in its own log.
94
+ */
95
+ function note(message) {
96
+ if (import.meta.env?.DEV)
97
+ console.info("[rsc-kit] " + message);
98
+ }
75
99
  function open(key, entries) {
76
100
  close();
77
101
  openedWith = key;
@@ -112,15 +136,18 @@ function close() {
112
136
  source = null;
113
137
  openedWith = "";
114
138
  }
139
+ /** Regions to refresh this tick, each with the changes that moved it: `stock@5`. */
115
140
  let pending = null;
116
- /** A version moved: every region live on the name at another version refreshes, once per tick. */
141
+ /** A version moved: every region on the name at another version refreshes, once per tick. */
117
142
  function apply(name, version) {
118
143
  for (const entry of registrations) {
119
144
  const held = entry.names[name];
120
145
  if (!held || held[0] === version)
121
146
  continue;
122
147
  held[0] = version;
123
- (pending ??= new Set()).add(entry.target);
148
+ const changes = (pending ??= new Map()).get(entry.target) ?? new Set();
149
+ changes.add(name + "@" + version);
150
+ pending.set(entry.target, changes);
124
151
  }
125
152
  if (!pending)
126
153
  return;
@@ -130,9 +157,15 @@ function apply(name, version) {
130
157
  if (!targets)
131
158
  return;
132
159
  // 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(() => { });
160
+ if (targets.has("page")) {
161
+ void refresh("page").catch(() => { });
162
+ return;
163
+ }
164
+ // Saying which change asked lets every tab asking about the same one
165
+ // share a single render of a shared section.
166
+ for (const [target, changes] of targets) {
167
+ void refreshOnChange(target, [...changes].sort().join(",")).catch(() => { });
168
+ }
136
169
  });
137
170
  }
138
171
  /** For tests. */
@@ -1 +1 @@
1
- {"version":3,"file":"changesStore.js","sourceRoot":"","sources":["../../src/js/changesStore.ts"],"names":[],"mappings":"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","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"]}
1
+ {"version":3,"file":"changesStore.js","sourceRoot":"","sources":["../../src/js/changesStore.ts"],"names":[],"mappings":"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","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, refreshOnChange } 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 if (!document.hidden) return schedule();\n\n // Hidden tabs do not hold a connection. Nothing is missed: seen again,\n // the stream reopens at the versions this tab holds and every name that\n // moved meanwhile is reported at once.\n if (source)\n note(\n \"refreshOn paused: tab hidden. It catches up when the tab is shown.\",\n );\n close();\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 if (import.meta.env?.DEV) {\n const lines = [...registrations].map(\n ({ target, names }) =>\n ` ${target}: ${Object.entries(names)\n .map(([name, [version]]) => `${name} @${version}`)\n .join(\", \")}`,\n );\n\n note(`refreshOn watching, by region:\\n${lines.join(\"\\n\")}`);\n }\n\n open(key, entries);\n}\n\n/**\n * Development only: what the page refreshes on, and why it stopped. A region\n * that declared refreshOn but is missing from the list rendered no names -\n * the server said why, once, in its own log.\n */\nfunction note(message: string): void {\n if (import.meta.env?.DEV) console.info(\"[rsc-kit] \" + message);\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\n/** Regions to refresh this tick, each with the changes that moved it: `stock@5`. */\nlet pending: Map<string, Set<string>> | null = null;\n\n/** A version moved: every region 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\n const changes =\n (pending ??= new Map()).get(entry.target) ?? new Set<string>();\n\n changes.add(name + \"@\" + version);\n pending.set(entry.target, changes);\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 if (targets.has(\"page\")) {\n void refresh(\"page\").catch(() => {});\n\n return;\n }\n\n // Saying which change asked lets every tab asking about the same one\n // share a single render of a shared section.\n for (const [target, changes] of targets) {\n void refreshOnChange(target, [...changes].sort().join(\",\")).catch(\n () => {},\n );\n }\n });\n}\n\n/** For tests. */\nexport function resetChanges(): void {\n registrations.clear();\n close();\n failures = 0;\n pending = null;\n}\n"]}
@@ -139,7 +139,7 @@ export declare function applyRevalidated(target: string, tree: ReactNode): void;
139
139
  * form leaves the layouts alone, which is what makes it cheap and also why a
140
140
  * count living in a layout will not move until you ask for 'all'.
141
141
  */
142
- export declare function refresh(target?: string): Promise<void>;
142
+ export declare function refresh(target?: string, changedBy?: string): Promise<void>;
143
143
  /**
144
144
  * Fetch a page's payload ahead of the navigation that may ask for it.
145
145
  *
@@ -957,7 +957,7 @@ export function applyRevalidated(target, tree) {
957
957
  * form leaves the layouts alone, which is what makes it cheap and also why a
958
958
  * count living in a layout will not move until you ask for 'all'.
959
959
  */
960
- export async function refresh(target = "page") {
960
+ export async function refresh(target = "page", changedBy) {
961
961
  const url = window.location.pathname + window.location.search;
962
962
  // Asked because the data moved on; what was fetched or held before is
963
963
  // from before.
@@ -968,6 +968,9 @@ export async function refresh(target = "page") {
968
968
  "X-RSC": "true",
969
969
  "X-RSC-Version": version,
970
970
  "X-RSC-Revalidate": target,
971
+ // Which change asked: tabs asking about the same one share a render
972
+ // of a shared section. Only ever set by the watcher.
973
+ ...(changedBy ? { "X-RSC-Changed": changedBy } : {}),
971
974
  },
972
975
  });
973
976
  if (!response.ok) {