fix(library): one session for the whole tab, and a shelf that does not take the machine
The library is the accounts' shelf, but the tab kept two copies of that fact: the screen read `/me` once as it mounted and then stopped listening, so a visitor who signed in at the studio came back to the panel asking for the login they had just given. One record, read by the studio, the landing page and the shelf alike, is now the only copy — a sign-in anywhere is a sign-in everywhere. - auth.ts (new): the tab's one session record. `useAuth()` reads it through `useSyncExternalStore`, a `/me` already in flight is joined rather than repeated, and a call that fails leaves the record *unknown* rather than signed out, so a cold API never reads as a guest. - TopBar: LIBRARY is a link for a member and a dead button for everyone else. An anchor wearing `aria-disabled` still navigates on a click, and the screen behind it would only send the guest home again — so a guest gets the shape of the button and nothing to press. - Library: the gate is that shared record, not a copy taken at mount. A guest who opens `/library` straight is turned home with `replace`, so a Back press does not walk them into the same wall. The screen is mounted the whole time the tab is open, so the turn is for the address the visitor actually opened: a guest on the studio stays on the studio. - engine/library.ts: the thumbs hand the thread back. Two lanes instead of four, a frame's worth of idle between them, and one job per frame — a second caller joins the decode already running instead of starting another. Verified in Chrome (stubbed API, harness run against both the dev server and the built bundle): a guest gets a dead LIBRARY button and stays on /app; signing in at the studio turns it into the link and opens the shelf, with no second login and no "log in to browse" note; the same after a sign-in on the landing page; /library opened cold as a guest lands on / with Back going to /app; opened cold signed in, the shelf is up. On a folder of 22 frames the open runs 2 decodes at a time (was 5) and repeats no frame (44 decodes, was 56 — 12 frames were read twice); the tiles still come out 22 on the open, 24 by hand, 22 again after a reload.
This commit is contained in:
@@ -1919,6 +1919,39 @@ export async function getPhotoThumbs(ids: string[]): Promise<Map<string, Blob>>
|
||||
return res;
|
||||
}
|
||||
|
||||
// Tiles are made behind a screen somebody is using, so they are not made at the
|
||||
// walk's pace. A tile is a decode, a draw and a JPEG encode — the last two on this
|
||||
// thread — and the job is only ever filling in what the wall will want next, never
|
||||
// what the reader is waiting for: two frames at a time, and the thread handed back
|
||||
// between them. `LANES` four-wide back to back is what made opening a folder
|
||||
// stutter, and it bought nothing: the wall mints the frames in view itself.
|
||||
const THUMB_LANES = 2;
|
||||
|
||||
// The breath between frames. The idle queue where the browser has one — the tile
|
||||
// then waits for a gap rather than making one — and a plain macrotask otherwise;
|
||||
// the timeout is there because an idle queue on a busy page can stay empty.
|
||||
//
|
||||
// ponytail: the walk's own scan is not paced from here and does not stop for a
|
||||
// tile; the two share the thread when a folder is opened mid-read. One budget for
|
||||
// both is the next step if a scan ever runs visibly slower beside a tile job.
|
||||
function breathe(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const idle = (
|
||||
globalThis as {
|
||||
requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number;
|
||||
}
|
||||
).requestIdleCallback;
|
||||
if (idle) idle(() => resolve(), { timeout: 120 });
|
||||
else setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// One frame's tile, in flight. The wall scrolls to a frame and asks for its tile
|
||||
// while the folder's own job is working through that same folder: without this
|
||||
// both would read and decode the same file, which is the same seconds of disk and
|
||||
// the same draw on this thread for one tile. The second caller waits on the first.
|
||||
const inFlight = new Map<string, Promise<Blob | null>>();
|
||||
|
||||
// A RAW or a HEIC hands back a preview that is a whole develop, seconds of it:
|
||||
// shrunk to the tile's size the one time it is paid for and then kept, so every
|
||||
// later visit reads the tile back instead of the file.
|
||||
@@ -1937,7 +1970,15 @@ async function tileOf(id: string, preview: Blob): Promise<Blob> {
|
||||
// ponytail: a TIFF has no preview reader here, so it is decoded whole through the
|
||||
// browser (`makeTile`) — `tiffDevelop` is the way in if a folder of scans ever
|
||||
// makes that wait a real one.
|
||||
export async function makeThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
||||
export function makeThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
||||
const running = inFlight.get(photo.id);
|
||||
if (running) return running;
|
||||
const job = makeOneThumb(photo).finally(() => inFlight.delete(photo.id));
|
||||
inFlight.set(photo.id, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
async function makeOneThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
||||
const kept = await getPhotoThumb(photo.id);
|
||||
if (kept) return kept;
|
||||
if (!photo.handle) return null;
|
||||
@@ -2001,9 +2042,10 @@ export async function missingThumbs(ids: string[]): Promise<string[]> {
|
||||
|
||||
// MAKE THUMBS: every frame of the folder — and, unless told otherwise, of what
|
||||
// lies under it — given a tile, and only the frames the catalogue is short of one.
|
||||
// A tile is a decode, so the frames are read a few at a time, and a job the
|
||||
// caller has moved on from gives up at its next frame: a reader who has walked to
|
||||
// another folder is not waiting for this one.
|
||||
// A tile is a decode, so the frames are read two at a time with the thread handed
|
||||
// back between them (see THUMB_LANES), and a job the caller has moved on from gives
|
||||
// up at its next frame: a reader who has walked to another folder is not waiting
|
||||
// for this one.
|
||||
export async function makeThumbs(
|
||||
folder: LibraryFolder,
|
||||
rel = '',
|
||||
@@ -2025,11 +2067,15 @@ export async function makeThumbs(
|
||||
const queue = [...missing];
|
||||
let made = 0;
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(LANES, queue.length) }, async () => {
|
||||
Array.from({ length: Math.min(THUMB_LANES, queue.length) }, async () => {
|
||||
// The folder the reader just opened gets to paint first: these tiles are what
|
||||
// the wall will want, and the wall itself is what the reader is waiting for.
|
||||
await breathe();
|
||||
while (queue.length) {
|
||||
if (opts.shouldStop?.()) return;
|
||||
const photo = byId.get(queue.shift()!);
|
||||
if (photo && (await makeThumb(photo))) made++;
|
||||
await breathe();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user