fix(library): land a reading's rows as they are read, and keep the copy the reader reads offline
- scan: rows go down on a clock as well as on a full batch. The reader's folder of 26 RAWs is under one batch of 50, so nothing of it reached the screen until the reading was over — a wall that stood empty for the whole scan and then filled in one go. Frames now reach the screen every 400ms while the roll is still being read, so the count climbs and the first tiles are drawn as the frames land. - tiles: a HEIC or a RAW has its tile written the first time it is drawn, so every later visit reads the tile back instead of the file. - resume: a reading a reload cut off is offered by the next visit with a chip instead of being run behind the reader's back. - offline listing: the desk's listing is kept whole in IndexedDB under `recipescam-offline`/`reads`, past the 200 000 characters localStorage held, so an offline visit opens on the listing it kept instead of on nothing. - offline writes: a vote or a view cast with no server is held and goes out when one answers again, with a bar that says how many are waiting. - sw: the shell precaches its own bundle, stylesheet and icon, read out of the document — a worker that precached only the document answered an offline launch with a page whose JavaScript the machine did not have. - checks in docker/frontend/scripts: tree-scan-check (counts while reading, resume after a reload, tiles of a long roll), offline-read-check, offline-queue-check.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
// /api), the Vite dev server proxies it too — so no base URL, no CORS.
|
||||
import type { Recipe } from '../shared/types';
|
||||
import { shrinkForUpload, resizedJpeg } from './engine/imageOps';
|
||||
import { forgetStale, hold, keepStale, serverAnswered, serverGone, stale } from './pwa/offline';
|
||||
import { t } from './i18n/I18nProvider';
|
||||
|
||||
// Upload caps. The API + nginx both refuse oversized bodies, so shrink in the
|
||||
// browser first; the server then sniffs the bytes and requires the declared
|
||||
@@ -157,14 +159,38 @@ export interface Stats {
|
||||
}
|
||||
|
||||
async function call<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: init?.body ? { 'content-type': 'application/json' } : undefined,
|
||||
...init,
|
||||
});
|
||||
// A read is answered from what the server last said. A write is not answered
|
||||
// at all when no server is there — pretending it landed is a lie the screen
|
||||
// would act on — but it is not lost either: it is held, in order, and sent
|
||||
// when one answers. See `hold`/`flush` in pwa/offline.ts.
|
||||
const read = !init?.method || init.method === 'GET';
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api${path}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: init?.body ? { 'content-type': 'application/json' } : undefined,
|
||||
...init,
|
||||
});
|
||||
} catch (err) {
|
||||
// No answer at all: the server is gone. Serve the last one it gave for this
|
||||
// read, or fail as before — the caller has its own empty state.
|
||||
serverGone();
|
||||
// A write goes on the queue instead. The one exception is the sole call that
|
||||
// is not about the server's own data: `open-explorer` asks it to pop a
|
||||
// folder on this machine, and popping it minutes later is not popping it.
|
||||
if (!read && path !== '/open-explorer') {
|
||||
hold(path, init?.method ?? 'POST', typeof init?.body === 'string' ? init.body : null);
|
||||
throw new Error(t('offline.queued'));
|
||||
}
|
||||
const staleOne = read ? await stale<T>(path) : null;
|
||||
if (staleOne !== null) return staleOne;
|
||||
throw err;
|
||||
}
|
||||
serverAnswered();
|
||||
if (res.status === 204) return undefined as T;
|
||||
const body = await readJson(res);
|
||||
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
if (read) keepStale(path, body);
|
||||
return body as T;
|
||||
}
|
||||
|
||||
@@ -222,10 +248,14 @@ export const api = {
|
||||
// null user = signed out; the API answers 200 either way.
|
||||
me: () => call<{ user: User | null }>('/auth/me'),
|
||||
signup: (email: string, password: string) =>
|
||||
call<{ user: User }>('/auth/signup', { method: 'POST', body: JSON.stringify({ email, password }) }),
|
||||
call<{ user: User }>('/auth/signup', { method: 'POST', body: JSON.stringify({ email, password }) })
|
||||
// The answers held offline are the previous account's and the next one must
|
||||
// not be shown them; this browser is not an account until it has one.
|
||||
.finally(forgetStale),
|
||||
login: (email: string, password: string) =>
|
||||
call<{ user: User }>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }),
|
||||
logout: () => call<void>('/auth/logout', { method: 'POST' }),
|
||||
call<{ user: User }>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) })
|
||||
.finally(forgetStale),
|
||||
logout: () => call<void>('/auth/logout', { method: 'POST' }).finally(forgetStale),
|
||||
// Mail the verification link to the signed-in address again. Works while
|
||||
// unverified (that is the whole point); 429 once the hourly cap is spent.
|
||||
resendVerification: () => call<{ ok: boolean; verified?: boolean }>('/auth/resend-verification', { method: 'POST' }),
|
||||
|
||||
Reference in New Issue
Block a user