Files
immich-folio/lib/cache.ts

99 lines
2.6 KiB
TypeScript
Raw Normal View History

/**
* Simple in-memory LRU cache for Immich API responses.
* Avoids hammering the Immich server with identical requests.
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
*
* Entries carry two deadlines. `staleAt` is the normal TTL — past it, get()
* reports a miss and callers refetch. `hardExpiresAt` is the point past which
* the data is too old to show at all. Between the two, the entry survives as a
* fallback that getStale() hands out when Immich is unreachable, so a
* restarting server does not turn the gallery into an error page.
*/
interface CacheEntry<T> {
data: T;
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
staleAt: number;
hardExpiresAt: number;
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
const DEFAULT_STALE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
class MemoryCache {
private store = new Map<string, CacheEntry<unknown>>();
private maxEntries = 200;
get size(): number {
return this.store.size;
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
/** Move an entry to the newest end of the LRU order. */
private touch(key: string, entry: CacheEntry<unknown>): void {
this.store.delete(key);
this.store.set(key, entry);
}
/** Fresh data only. Returns null once the TTL has elapsed. */
get<T>(key: string): T | null {
const entry = this.store.get(key);
if (!entry) return null;
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
const now = Date.now();
if (now > entry.hardExpiresAt) {
this.store.delete(key);
return null;
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
// Past the TTL the entry is no longer fresh, but it is deliberately kept
// so getStale() can still use it.
if (now > entry.staleAt) return null;
this.touch(key, entry);
return entry.data as T;
}
/**
* Fresh *or* stale data. Only for the upstream-unreachable path — prefer
* get() everywhere else.
*/
getStale<T>(key: string): T | null {
const entry = this.store.get(key);
if (!entry) return null;
if (Date.now() > entry.hardExpiresAt) {
this.store.delete(key);
return null;
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
this.touch(key, entry);
return entry.data as T;
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
set<T>(key: string, data: T, ttlMs: number, staleMaxAgeMs = DEFAULT_STALE_MAX_AGE_MS): void {
if (this.store.has(key)) {
this.store.delete(key);
} else if (this.store.size >= this.maxEntries) {
const firstKey = this.store.keys().next().value;
if (firstKey) this.store.delete(firstKey);
}
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
const now = Date.now();
this.store.set(key, {
data,
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
staleAt: now + ttlMs,
// Measured from write time, and never shorter than the TTL itself.
hardExpiresAt: now + Math.max(ttlMs, staleMaxAgeMs),
});
}
delete(key: string): void {
this.store.delete(key);
}
clear(): void {
this.store.clear();
}
}
export const cache = new MemoryCache();
Close the gaps left after v0.9.2's outage handling: route tests, stale cache, loading states (#397) * docs: brainstorm resilience/adoption features and plan follow-up work Brainstorm and deep-dive on where Immich Folio breaks under Immich outages and what blocks self-hosted adoption, plus an implementation plan for the remaining gaps after v0.9.2 (route-handler tests, stale-while-error caching, loading skeletons). * test: enable route-handler tests and cover admin guards and image content-type vitest.config.ts only ran lib/__tests__/**, so route handlers were structurally untestable. Two mechanisms live only at the route level and cannot be extracted the way lib/imageSize.ts was: the admin auth guards and the image proxy's Content-Type sanitisation (the stored-XSS fix from c2fa8e7). Both were correct but unverified by any test. Adds a table-driven test over all ten guarded /api/admin/* handlers, including a filesystem walk that fails if a future route is added without a row in the table. Adds a route-level test for /api/image/[id] covering token validation, the six Content-Type rewrite cases, ETag/304, and the 404 vs 503 (ImmichUnavailableError) distinction. Verified both catch real regressions by temporarily removing the admin guards and the Content-Type rewrite and confirming the expected tests fail, then restoring and confirming green again. 134 new tests (244 total, up from 210), tsc 0 errors, build succeeds. * feat: serve stale cache entries while Immich is unavailable v0.9.2 (#394) correctly split "gone" (404/410) from "unavailable" (ImmichUnavailableError), and pages now render a 5xx instead of a false 404 during an outage — but that 5xx still replaced a gallery that had loaded fine seconds earlier. lib/cache.ts deleted expired entries on access, so nothing survived long enough to fall back to. Cache entries now carry two deadlines: staleAt (the existing CACHE_TTL, after which get() still reports a miss) and hardExpiresAt (the new STALE_MAX_AGE, default 24h, after which the entry is dropped entirely). Between the two, getStale() can still return the data. getAlbums(), getAlbum() and getAssetInfo() catch ImmichUnavailableError and fall back to getStale() before propagating. The two MISSING-sentinel writes (definitive 404s) are deliberately left on the short cacheTtl window — a corrected asset ID should take effect after the normal TTL, not sit in the 24h stale window meant for outages. STALE_MAX_AGE=0 disables the fallback. Verified the fallback only engages on a real outage, not on a cache hit at the TTL boundary, and that a definitive 404 still reports missing rather than falling back. 256 tests (up from 244), tsc 0 errors, build succeeds. * feat: add loading skeletons for dynamic gallery routes Every gallery route is force-dynamic and waits on a live Immich round-trip — up to IMMICH_TIMEOUT_MS when the server is struggling — with nothing rendered until it resolves. loading.tsx renders instantly while the page component awaits its data. Not browser-verified against live data: this worktree has no .env.local and only .example content files, so the app boots into setup mode (confirmed the dev server starts cleanly and the setup screen renders, HTTP 200). tsc 0 errors, build succeeds, no new lint errors. * docs: document stale cache window, image token revocation, route-test convention - README: STALE_MAX_AGE env var and a "Behaviour when Immich is unreachable" section (stale-serves-until-STALE_MAX_AGE, 503 not 404, outages never cached). - docs/gallery-config.md: new "Image Token Revocation" subsection under System & Security — un-publishing an album does not revoke already issued image URLs; rotating AUTH_SECRET does. - CLAUDE.md: updated cache description to cover the stale window and MISSING sentinel, and a new code-convention note that route-level logic (guards, header sanitisation) should get a route-level test rather than being extracted into lib/ just to reach the test runner. --------- Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:07:34 +02:00
export { DEFAULT_STALE_MAX_AGE_MS };