fix(boot): bring the session and the catalogue back without waiting on /me
A reload drew nothing until the account round trip landed, which on a faraway API is the whole of the wait: 8.8s to the photo on the studio's stage and 8.2s to the library's first row, against 0.9s and 0.3s now (measured with every /api answer held back 8s). The studio's restore effect returned early on `!authReady`. The gate is only needed by the two files that ask a tier question — a RAW, which is a PRO develop, and a catalogue frame, which walks `loadFile` — so it keeps those and lets the session's own JPEG up on the first pass. `restoredRef` stops the second pass, when /me lands, from decoding the frame the first one already showed. The library mounted nothing until /me answered. What the first paint reads is now the answer the last visit got, the way the studio reads its own session back: the frames are already on this disk and have no reason to sit behind a round trip. An answer the API contradicts puts the note back on screen the moment it arrives, and an API that never answers is not written down as this account's standing.
This commit is contained in:
+25
-10
@@ -746,9 +746,16 @@ export function Workspace() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// The photo half of the session, back out of IndexedDB. It waits for the
|
||||
// account: which file may open depends on the tier, and the check reads it
|
||||
// The photo half of the session, back out of IndexedDB. Which file may open
|
||||
// depends on the tier — a RAW is a PRO develop and a catalogue frame is the
|
||||
// same walk (`loadFile`) — but a JPEG of the visitor's own is nobody's
|
||||
// question, and it is what the session almost always holds: it goes up on the
|
||||
// first pass, with /me still in flight. The two that do ask wait for the
|
||||
// account, and the effect runs again the moment it lands (91aeacb).
|
||||
const openLibraryPhotoRef = useRef<((id: string) => Promise<void>) | null>(null);
|
||||
// The session's JPEG is restored once and never twice: that second run would
|
||||
// otherwise re-decode the frame the first pass already put on the stage.
|
||||
const restoredRef = useRef(false);
|
||||
|
||||
// When the stored session is empty but a RAW is still parked in OPFS, the RAW
|
||||
// is what "the last photo" means: develop it again rather than open on
|
||||
@@ -756,25 +763,33 @@ export function Workspace() {
|
||||
// both: the visitor just clicked it, and it is newer than anything the
|
||||
// session holds.
|
||||
useEffect(() => {
|
||||
if (!authReady) return;
|
||||
let alive = true;
|
||||
|
||||
const checkAndLoad = () => {
|
||||
const wanted = new URLSearchParams(window.location.search).get('lib');
|
||||
if (wanted && openLibraryPhotoRef.current) {
|
||||
void openLibraryPhotoRef.current(wanted);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!wanted || !openLibraryPhotoRef.current) return false;
|
||||
// Claimed either way: the frame the visitor just clicked wins over the
|
||||
// session's own, so the session photo must not open under it while the
|
||||
// account is still in flight.
|
||||
if (authReady) void openLibraryPhotoRef.current(wanted);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!checkAndLoad()) {
|
||||
if (!checkAndLoad() && !restoredRef.current) {
|
||||
loadPhoto()
|
||||
.then(async (photo) => {
|
||||
if (!alive) return;
|
||||
const show = (row: { name: string; bytes: Uint8Array }) =>
|
||||
adoptPhoto(row.bytes, row.name, new Blob([row.bytes as BlobPart], { type: 'image/jpeg' }), true);
|
||||
if (photo && !isRawName(photo.name)) return show(photo);
|
||||
if (photo && !isRawName(photo.name)) {
|
||||
restoredRef.current = true;
|
||||
return show(photo);
|
||||
}
|
||||
// Only a RAW is left to restore, and only the account says whether the
|
||||
// engine may develop it: read before /me has answered, a PRO's own
|
||||
// parked RAW is a develop a guest is not offered.
|
||||
if (!authReady) return;
|
||||
restoredRef.current = true;
|
||||
const parked = await readRaw();
|
||||
if (!alive) return;
|
||||
if (!parked) return photo ? show(photo) : undefined;
|
||||
|
||||
@@ -242,19 +242,35 @@ const why = (err: unknown) => {
|
||||
// from a folder that would be read for nothing is not the offer. The gate is
|
||||
// here rather than in the router so the scan never starts for a guest — the
|
||||
// catalogue component below is not mounted at all until the account is known.
|
||||
//
|
||||
// What the first paint reads is the answer the last visit got, the way the studio
|
||||
// reads its own session back (see `loadState`): `/me` is a round trip, and a
|
||||
// screen that draws nothing until it lands draws nothing for as long as the API
|
||||
// is away — the frames the reader was working through are already on this disk
|
||||
// and there is no reason to hold them behind it. The record only ever holds this
|
||||
// visitor's own last answer, and an answer the API then contradicts puts the note
|
||||
// back on screen the moment it arrives.
|
||||
const ACCOUNT_KEY = 'recipescam.library.account';
|
||||
export function Library() {
|
||||
const { t } = useI18n();
|
||||
const [state, setState] = useState<'loading' | 'guest' | 'ready'>('loading');
|
||||
const [state, setState] = useState<'loading' | 'guest' | 'ready'>(
|
||||
() => (recall(ACCOUNT_KEY) as 'guest' | 'ready' | null) ?? 'loading'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
api
|
||||
.me()
|
||||
.then((me) => {
|
||||
if (alive) setState(me?.user ? 'ready' : 'guest');
|
||||
if (!alive) return;
|
||||
const next = me?.user ? 'ready' : 'guest';
|
||||
keep(ACCOUNT_KEY, next);
|
||||
setState(next);
|
||||
})
|
||||
// No answer is treated like no account: the API being away is not a
|
||||
// reason to open the shelves to whoever is on the page.
|
||||
// reason to open the shelves to whoever is on the page. It is also not an
|
||||
// answer, so it does not go in the record — the next visit asks again
|
||||
// rather than reading the outage as this account's standing.
|
||||
.catch(() => {
|
||||
if (alive) setState('guest');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user