diff --git a/.jules/bolt.md b/.jules/bolt.md index 1c3beb4..0639513 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -2,3 +2,8 @@ **Learning:** `useMemo` can significantly boost the performance of long list or grid components (like `PhotoGrid` component) that otherwise gets re-rendered on simple interactions like navigating the image lightbox. Running standard formatting `pnpm format` applies huge changes across entire directories, so it's generally best to format and clean up ONLY modified files to avoid huge diff pollution in code review. **Action:** Be extremely cautious of running format/lint commands at the root codebase; specify only modified directories or files, or avoid formatting if it cascades globally. Focus on targeted memoization where state changes dictate expensive array mapping operations. + +## 2024-05-28 - [Deduplicate concurrent API requests via Promise caching] + +**Learning:** When using `Promise.all` to fetch dependent data in parallel (e.g., getting multiple subpages and standalone albums that internally call a shared `getAlbums` function), the internal cache might not be populated in time. This leads to redundant concurrent network requests to the upstream server. +**Action:** Implement Promise deduplication (request coalescing) by caching the pending Promise itself (e.g., in a `this.pendingPromise` class field) rather than just the final result. Return the pending Promise to subsequent callers until it resolves, ensuring only one network request is made. diff --git a/lib/immich.ts b/lib/immich.ts index d3ba881..f1c7e5d 100644 --- a/lib/immich.ts +++ b/lib/immich.ts @@ -72,6 +72,7 @@ export interface SubpageSummary { class ImmichClient { private hasLoggedAlbums = false; + private pendingAlbumsPromise: Promise | null = null; private get config() { return getConfig(); @@ -152,57 +153,69 @@ class ImmichClient { const cached = cache.get(cacheKey); if (cached) return cached; - const all = await this.request('/albums?shared=true'); - if (!all) return []; - - const allowedIds = new Set(this.config.albums); - const filtered = all - .filter((album) => allowedIds.has(album.id)) - .map((album) => { - const name = this.config.albumOverrides[album.id] ?? album.albumName; - return { - ...album, - albumName: name, - slug: slugify(name), - }; - }); - - // Log album summary on first load so admins can see what's published - if (!this.hasLoggedAlbums) { - this.hasLoggedAlbums = true; - console.log('\n[Lightbox] Published albums:'); - console.log('─'.repeat(80)); - - // Log standalone albums - const standaloneIds = new Set(this.config.standaloneAlbums); - const standalone = filtered.filter((a) => standaloneIds.has(a.id)); - if (standalone.length > 0) { - console.log(' Standalone:'); - for (const a of standalone) { - console.log(` 📷 ${a.albumName}`); - console.log(` URL: /${a.slug} • ${a.assetCount} photos • ID: ${a.id}`); - } - } - - // Log subpage groupings - for (const sp of this.config.subpages) { - const spAlbums = filtered.filter((a) => sp.albumIds.includes(a.id)); - console.log(` 📁 ${sp.name} (/${sp.slug}):`); - for (const a of spAlbums) { - console.log(` 📷 ${a.albumName}`); - console.log(` URL: /${sp.slug}/${a.slug} • ${a.assetCount} photos`); - } - } - - const missing = this.config.albums.filter((id) => !all.some((a) => a.id === id)); - if (missing.length > 0) { - console.warn(` ⚠️ Unknown album IDs: ${missing.join(', ')}`); - } - console.log('─'.repeat(80) + '\n'); + if (this.pendingAlbumsPromise) { + return this.pendingAlbumsPromise; } - cache.set(cacheKey, filtered, this.config.cacheTtl); - return filtered; + this.pendingAlbumsPromise = (async () => { + try { + const all = await this.request('/albums?shared=true'); + if (!all) return []; + + const allowedIds = new Set(this.config.albums); + const filtered = all + .filter((album) => allowedIds.has(album.id)) + .map((album) => { + const name = this.config.albumOverrides[album.id] ?? album.albumName; + return { + ...album, + albumName: name, + slug: slugify(name), + }; + }); + + // Log album summary on first load so admins can see what's published + if (!this.hasLoggedAlbums) { + this.hasLoggedAlbums = true; + console.log('\n[Lightbox] Published albums:'); + console.log('─'.repeat(80)); + + // Log standalone albums + const standaloneIds = new Set(this.config.standaloneAlbums); + const standalone = filtered.filter((a) => standaloneIds.has(a.id)); + if (standalone.length > 0) { + console.log(' Standalone:'); + for (const a of standalone) { + console.log(` 📷 ${a.albumName}`); + console.log(` URL: /${a.slug} • ${a.assetCount} photos • ID: ${a.id}`); + } + } + + // Log subpage groupings + for (const sp of this.config.subpages) { + const spAlbums = filtered.filter((a) => sp.albumIds.includes(a.id)); + console.log(` 📁 ${sp.name} (/${sp.slug}):`); + for (const a of spAlbums) { + console.log(` 📷 ${a.albumName}`); + console.log(` URL: /${sp.slug}/${a.slug} • ${a.assetCount} photos`); + } + } + + const missing = this.config.albums.filter((id) => !all.some((a) => a.id === id)); + if (missing.length > 0) { + console.warn(` ⚠️ Unknown album IDs: ${missing.join(', ')}`); + } + console.log('─'.repeat(80) + '\n'); + } + + cache.set(cacheKey, filtered, this.config.cacheTtl); + return filtered; + } finally { + this.pendingAlbumsPromise = null; + } + })(); + + return this.pendingAlbumsPromise; } /**