commit 6f3249a2ad39b13b02074c317ccd35b91aec2142
parent 8b16be4fd4d1b15e6f32580eb0d522d4d7f3236d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 00:49:21 -0400
editor: a site picked in one tab reaches the editor's other tabs (review M1)
The cookie is shared by every tab of the origin, but a tab's `stored` came from
its root layout, which a client-side navigation does not re-render. A pick in
another tab left this tab's picker on the old site over pages that read the new
one, and a client-side visit to a site's page was skipped as already stored.
Every successful write (`choose` and `record`) is now posted on
BroadcastChannel ACTIVE_SITE_CHANNEL ("archilyzer-active-site"; per origin, so
per port, like the cookie's name), and the other tabs call router.refresh(),
which re-renders the layout and the page with the current cookie and drops the
tab's router cache. A channel instance does not receive its own posts, so a tab
does not refresh for its own write. Without BroadcastChannel a tab keeps its
value until its next full load.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 49 insertions(+), 6 deletions(-)
diff --git a/editor/app/components/SiteScopeProvider.tsx b/editor/app/components/SiteScopeProvider.tsx
@@ -10,8 +10,9 @@ import {
useState,
type ReactNode,
} from "react";
-import { usePathname } from "next/navigation";
+import { usePathname, useRouter } from "next/navigation";
import {
+ ACTIVE_SITE_CHANNEL,
ACTIVE_SITE_KEY,
isStorableActiveSite,
siteIdFromPathname,
@@ -27,6 +28,17 @@ import { setActiveSiteAction } from "../lib/activeSiteActions";
// server's value changes (the re-render a cookie write triggers, or a refresh
// after another tab chose), it follows that.
//
+// OTHER TABS. The cookie is shared by every tab of this origin, but a tab's
+// `stored` comes from its root layout, which a client-side navigation does not
+// re-render: a site picked in one tab left the others showing the old one over
+// pages that read the new one. So every successful write is announced on a
+// BroadcastChannel (ACTIVE_SITE_CHANNEL; per origin, so per port, like the
+// cookie's name), and the other tabs answer with router.refresh(): the layout
+// and the page re-render with the cookie as it is now, and the tab's router
+// cache is dropped. A channel does not deliver a message to the instance that
+// posted it, so a tab does not refresh for its own write. A browser without
+// BroadcastChannel keeps each tab's value until its next full load.
+//
// Two effects, and neither rewrites a URL:
// - visiting a site's own page (/sites/<id>/…) records that site, so Dashboard
// and Channels follow — the path already shows it, so there is no flash;
@@ -83,6 +95,27 @@ export function SiteScopeProvider({
storedRef.current = stored;
}, [stored]);
+ // The other tabs (see above): subscribe, and keep the one instance to post on.
+ const router = useRouter();
+ const channel = useRef<BroadcastChannel | null>(null);
+ useEffect(() => {
+ if (typeof BroadcastChannel === "undefined") return;
+ const ch = new BroadcastChannel(ACTIVE_SITE_CHANNEL);
+ channel.current = ch;
+ ch.onmessage = () => router.refresh();
+ return () => {
+ ch.close();
+ if (channel.current === ch) channel.current = null;
+ };
+ }, [router]);
+ const announce = useCallback((value: string) => {
+ try {
+ channel.current?.postMessage(value);
+ } catch {
+ /* closed while unmounting: nothing to tell */
+ }
+ }, []);
+
const choose = useCallback(async (value: string): Promise<boolean> => {
const previous = storedRef.current;
storedRef.current = value;
@@ -95,21 +128,26 @@ export function SiteScopeProvider({
ok = false;
}
setWriting((n) => n - 1);
- if (!ok) {
+ if (ok) {
+ announce(value);
+ } else {
storedRef.current = previous;
setStored(previous);
}
return ok;
- }, []);
+ }, [announce]);
// Write a value the page did not choose — no optimistic state (see above).
const record = useCallback(async (value: string): Promise<boolean> => {
+ let ok = false;
try {
- return await setActiveSiteAction(value);
+ ok = await setActiveSiteAction(value);
} catch {
- return false;
+ ok = false;
}
- }, []);
+ if (ok) announce(value);
+ return ok;
+ }, [announce]);
// Visiting a site's own page records it: once per arrival at that site's
// pages (a re-run of the effect — StrictMode, Fast Refresh — does not write
diff --git a/editor/app/lib/activeSite.ts b/editor/app/lib/activeSite.ts
@@ -33,6 +33,11 @@ export const ACTIVE_SITE_KEY = "activeSite";
// one and a worktree's) keep a selection each, as they did.
export const ACTIVE_SITE_COOKIE = "archilyzer-active-site";
+// The BroadcastChannel a successful write is announced on, so the editor's
+// other tabs refresh (SiteScopeProvider). A channel is per origin, so each port
+// has its own, like the cookie's name.
+export const ACTIVE_SITE_CHANNEL = "archilyzer-active-site";
+
// One year, in seconds (the cookie's `maxAge`).
export const ACTIVE_SITE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;