Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Meditor/app/components/SiteScopeProvider.tsx | 50++++++++++++++++++++++++++++++++++++++++++++------
Meditor/app/lib/activeSite.ts | 5+++++
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;