fix(guests): rebuild consumers on identity switch; repair fallback reads

Codex review round 3 on #1268. Three of these were defects in the round 1-2
fixes themselves.

Consumers holding local feedback state are now rebuilt on an identity switch.
Invalidating queries was not enough: six gallery layouts seed their liked set
behind a mount-only likedSeededRef ('so refetches don't clobber in-session
optimistic toggles') and PhotoLightbox keeps its own copy, so a refetch left
the previous guest's hearts on screen. The provider re-keys its subtree, which
covers all seven without touching them. Deliberately only on a switch away
from an established identity -- remounting on first sign-in would tear down
the gallery under the click that triggered the prompt and drop the pending
action.

The storage fallback now repoints reads. storeGuestIdentity wrote to
sessionStorage when localStorage rejected the real write but left
resolvedStorage on localStorage, so every later read missed: x-guest-token was
never sent and the identity vanished on reload. The fallback looked like it
worked while achieving nothing.

Clearing an identity now notifies this tab. Native storage events fire only in
other documents, so the interceptor dropping a server-rejected identity left
the provider still showing that guest and ensureIdentity() still handing it
out. A same-tab event completes the loop.

Cross-tab adoption resolves pending callers. A tab parked on the prompt
awaiting ensureIdentity() while another tab registers now completes exactly as
register() does, instead of hanging forever and registering a second guest if
the visitor submits the still-open prompt.
This commit is contained in:
Paul Nothaft
2026-09-02 10:22:55 +02:00
parent f3f37a8c77
commit e9babf65e7
2 changed files with 78 additions and 5 deletions
+23 -1
View File
@@ -28,6 +28,9 @@ import type { GuestIdentity } from '../services/guests.service';
const TOKEN_KEY_PREFIX = 'guest_token_';
const IDENTITY_KEY_PREFIX = 'guest_identity_';
/** Same-tab counterpart to the native cross-tab `storage` event. */
export const GUEST_IDENTITY_CLEARED_EVENT = 'picpeak:guest-identity-cleared';
const isBrowser = typeof window !== 'undefined';
/**
@@ -169,7 +172,14 @@ export function storeGuestIdentity(slug: string, identity: GuestIdentity, token:
if (isBrowser) {
try {
if (window.sessionStorage && window.sessionStorage !== storage) {
write(window.sessionStorage);
if (write(window.sessionStorage)) {
// Repoint reads at the store that actually accepted the write.
// Without this the identity is written to sessionStorage while
// getGuestToken() keeps reading localStorage, so x-guest-token is
// never sent and the identity is lost on reload — the fallback
// would look like it worked while achieving nothing.
resolvedStorage = window.sessionStorage;
}
}
} catch {
// No store will take it. The identity lasts as long as this page does,
@@ -235,6 +245,18 @@ export function clearGuestIdentity(slug: string): void {
// Best-effort.
}
}
// `storage` events fire only in OTHER documents, so a clear triggered inside
// this tab (the axios interceptor dropping a server-rejected identity) would
// leave the provider still showing the guest and ensureIdentity() still
// handing it out. Announce it locally too.
if (isBrowser) {
try {
window.dispatchEvent(new CustomEvent(GUEST_IDENTITY_CLEARED_EVENT, { detail: { slug } }));
} catch {
// CustomEvent unavailable — the provider simply refreshes on next mount.
}
}
}
/**