fix(cache): complete preload coverage + surface silent write failures
Root cause of 'pre-cache done but offline data missing':
1. Preload manifest never listed the services/{prayers,texts,headers,var}
subtrees the Service view dereferences (CommonPrayers->prayers, etc.),
plus only 13 hardcoded commands. Generator now enumerates all of them
(en 1500->1740, ru->1333, cu 1500->2108 files).
2. DataCache.set silently resolved on IDB write failure (quota exceeded),
so preload counted un-stored files as success and reported 100% done.
set() now returns boolean; preload treats failed writes as failures
that feed the existing circuit breaker (visible abort + diag entry).
3. No persistence request: added best-effort navigator.storage.persist()
before preload, diag-logged.
4. Dropped dead manifest weight (menaion-common/add, horologionadd) -
never referenced by any view.
This commit is contained in:
1 parent
4f11e8ca6c
commit
1201e16d5e
4 files changed
+1169
-405
No files matched your search
@@ -123,16 +123,6 @@ function calendarFiles(lang: string): string[] {
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
}
|
||||
|
||||
// Menaion common + add
|
||||
for (let i = 1; i <= 42; i++) {
|
||||
const fp = `/shared/menaion-common/${String(i).padStart(2, '0')}.json`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
}
|
||||
for (let i = 1; i <= 51; i++) {
|
||||
const fp = `/shared/menaion-add/${String(i).padStart(2, '0')}.json`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
}
|
||||
|
||||
// Prayer collections
|
||||
const prayerDirs: [string, string[]][] = [];
|
||||
for (const coll of ['akathists', 'horologion', 'prayer-rule', 'sbornik', 'irmologion']) {
|
||||
@@ -149,19 +139,6 @@ function calendarFiles(lang: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Horologion additions
|
||||
const horAddItems = ['exapostilaria', 'katavasia', 'lamps', 'songs-daily', 'songs-feasts', 'songs-lent',
|
||||
'theotokia-8tones', 'theotokia-dismissal', 'theotokia-sunday', 'trinity-8tones',
|
||||
'trop-common', 'trop-daily', 'trop-lent', 'trop-pentecost', 'trop-sunday'];
|
||||
for (const item of horAddItems) {
|
||||
const fp = `/shared/horologionadd/${item}/full.json`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
}
|
||||
for (const m of ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']) {
|
||||
const fp = `/shared/horologionadd/trop-feasts-${m}/full.json`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
}
|
||||
|
||||
// Parimii
|
||||
const parimiiDir = path.join(DATA_DIR, 'shared', 'parimii');
|
||||
if (fs.existsSync(parimiiDir)) {
|
||||
@@ -191,14 +168,26 @@ function calendarFiles(lang: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Language-specific service commands
|
||||
for (let tone = 1; tone <= 8; tone++) {
|
||||
const fp = `/${lang}/services/commands/Tone${tone}.json`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
// Language-specific service commands (enumerate all on disk)
|
||||
const cmdDir = path.join(DATA_DIR, lang, 'services', 'commands');
|
||||
if (fs.existsSync(cmdDir)) {
|
||||
for (const c of fs.readdirSync(cmdDir)) {
|
||||
if (c.endsWith('.json')) files.push(`/data/${lang}/services/commands/${c}`);
|
||||
}
|
||||
}
|
||||
for (const cmd of ['AfterEach.json', 'Bow.json', 'Prostration.json', 'S1.json', 'S2.json']) {
|
||||
const fp = `/${lang}/services/commands/${cmd}`;
|
||||
if (exists(fp)) files.push(`/data${fp}`);
|
||||
|
||||
// Dereferenceable service subtrees (Services/CommonPrayers -> prayers, etc.)
|
||||
for (const sub of ['prayers', 'texts', 'headers', 'var']) {
|
||||
const subDir = path.join(DATA_DIR, lang, 'services', sub);
|
||||
if (!fs.existsSync(subDir)) continue;
|
||||
const walk = (d: string, rel: string): void => {
|
||||
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
const p = path.join(d, e.name);
|
||||
if (e.isDirectory()) walk(p, `${rel}/${e.name}`);
|
||||
else if (e.name.endsWith('.json')) files.push(`/data/${lang}/services/${sub}${rel}/${e.name}`);
|
||||
}
|
||||
};
|
||||
walk(subDir, '');
|
||||
}
|
||||
|
||||
return files;
|
||||
|
||||
@@ -113,11 +113,12 @@ export class DataCache {
|
||||
|
||||
/**
|
||||
* Store a value in the cache.
|
||||
* Resolves true on success; false when the write failed (e.g. quota).
|
||||
*/
|
||||
static async set(key: string, data: unknown, ttl: number = DEFAULT_TTL_MS): Promise<void> {
|
||||
static async set(key: string, data: unknown, ttl: number = DEFAULT_TTL_MS): Promise<boolean> {
|
||||
try {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const entry: CacheEntry = {
|
||||
@@ -128,11 +129,13 @@ export class DataCache {
|
||||
ttl,
|
||||
};
|
||||
store.put(entry);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(true);
|
||||
tx.onerror = () => resolve(false);
|
||||
tx.onabort = () => resolve(false);
|
||||
});
|
||||
} catch {
|
||||
// Silently fail — cache is best-effort
|
||||
// Write failed (e.g. quota exceeded) — report as failure.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,17 @@ export class OfflineManager {
|
||||
};
|
||||
OfflineManager._progress = progress;
|
||||
|
||||
// Best-effort: request persistent storage so the OS won't evict data
|
||||
// for LRU pressure (and try to lift legacy quota caps where supported).
|
||||
if (navigator.storage?.persist) {
|
||||
try {
|
||||
const granted = await navigator.storage.persist();
|
||||
diagLog('storage-persist', { granted });
|
||||
} catch {
|
||||
diagLog('storage-persist-error');
|
||||
}
|
||||
}
|
||||
|
||||
diagLog('preload-start', {
|
||||
langs: options.languages.join(','),
|
||||
types: options.types.join(','),
|
||||
@@ -225,8 +236,15 @@ export class OfflineManager {
|
||||
} else {
|
||||
data = await resp.json();
|
||||
}
|
||||
await DataCache.set(`url:${file}`, data);
|
||||
success = true;
|
||||
const stored = await DataCache.set(`url:${file}`, data);
|
||||
if (stored) {
|
||||
success = true;
|
||||
} else {
|
||||
// IDB write failed (e.g. quota exceeded) — the cache is not
|
||||
// healthy; count as failure so the circuit breaker can abort
|
||||
// instead of reporting a misleading "done".
|
||||
diagLog('preload-write-fail', { file });
|
||||
}
|
||||
break;
|
||||
} catch {
|
||||
// Retry on network error
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user