Skip to content

Commit 269b194

Browse files
committed
Process theme image before caching
1 parent cf81f37 commit 269b194

File tree

1 file changed

+59
-47
lines changed

1 file changed

+59
-47
lines changed

src/api/favicon.ts

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { catchError, map, share, switchMap, tap } from "rxjs/operators";
3434
export async function getFavicon(
3535
req: Request,
3636
res: Response,
37-
services: Services
37+
services: Services,
3838
) {
3939
const url = new URL(req.url, `http://${req.headers.host}`);
4040
const { redis } = services;
@@ -58,30 +58,30 @@ export async function getFavicon(
5858
size,
5959
dpr: dpr || 1,
6060
theme,
61-
}))
61+
})),
6262
);
6363

6464
const cachedImage$ = params$.pipe(
6565
switchMap(({ url, size, dpr, theme }) => {
6666
const key = { url, size, dpr, theme };
6767
// Skip cache in development if Redis is not available
68-
if (process.env.NODE_ENV === 'development' && !redis) {
68+
if (process.env.NODE_ENV === "development" && !redis) {
6969
return of(null);
7070
}
7171
return from(getCachedImage(key, redis));
7272
}),
73-
share()
73+
share(),
7474
);
7575

7676
const [cached$, uncached$] = partition(
7777
cachedImage$,
78-
(cachedImage): cachedImage is IconMetadata => cachedImage != null
78+
(cachedImage): cachedImage is IconMetadata => cachedImage != null,
7979
);
8080

8181
const response$ = merge(
8282
combineLatest([cached$, params$]).pipe(
8383
switchMap(([cachedImage, params]) =>
84-
cachedFaviconResponse$(params, cachedImage, services, defer)
84+
cachedFaviconResponse$(params, cachedImage, services, defer),
8585
),
8686
tap(({ expiry, objectKey }) => {
8787
const faviconHost = process.env.RAYCAST_FAVICON_HOST;
@@ -91,35 +91,27 @@ export async function getFavicon(
9191
res.set(responseHeaders({ expiry }));
9292
res.redirect(`https://${faviconHost}/${objectKey}`);
9393
}
94-
})
94+
}),
9595
),
9696
combineLatest([uncached$, params$]).pipe(
9797
switchMap(([_, params]) =>
9898
combineLatest([
9999
of(params),
100100
uncachedFaviconResponse$(params, services, defer),
101-
])
101+
]),
102102
),
103103
tap(async ([{ size, dpr, url, theme }, result]) => {
104104
if (result.found) {
105-
let { blob, expiry } = result;
106-
107-
// Apply theme-based processing if needed
108-
try {
109-
blob = await processImageForTheme(blob, theme, url);
110-
} catch (error) {
111-
console.error('Error processing image for theme:', error);
112-
}
113-
105+
const { blob, expiry } = result;
114106
res.type(blob.type);
115107
const buffer = await blob.arrayBuffer();
116108
res.set(responseHeaders({ size: blob.size, expiry }));
117109
res.send(Buffer.from(buffer));
118110
} else {
119111
res.status(404).send("Not found");
120112
}
121-
})
122-
)
113+
}),
114+
),
123115
);
124116

125117
try {
@@ -136,10 +128,15 @@ export async function getFavicon(
136128
}
137129

138130
function cachedFaviconResponse$(
139-
params: { url: URL; size: SizeParam; dpr: DevicePixelRatioParam; theme?: ThemeParam },
131+
params: {
132+
url: URL;
133+
size: SizeParam;
134+
dpr: DevicePixelRatioParam;
135+
theme?: ThemeParam;
136+
},
140137
icon: IconMetadata,
141138
services: Services,
142-
defer: (work: Promise<any>) => void
139+
defer: (work: Promise<any>) => void,
143140
) {
144141
const { redis } = services;
145142
return combineLatest([of(params), of(icon)]).pipe(
@@ -151,16 +148,16 @@ function cachedFaviconResponse$(
151148
{
152149
lastAccess: new Date(),
153150
},
154-
redis
155-
)
151+
redis,
152+
),
156153
);
157154
}
158155
}),
159156
switchMap(([_, cachedImage]) => {
160157
const { objectKey } = cachedImage;
161158
return combineLatest([of(cachedImage), of(objectKey)]);
162159
}),
163-
map(([{ expiry }, objectKey]) => ({ expiry, objectKey }))
160+
map(([{ expiry }, objectKey]) => ({ expiry, objectKey })),
164161
);
165162
}
166163

@@ -172,59 +169,74 @@ function uncachedFaviconResponse$(
172169
theme?: ThemeParam;
173170
},
174171
services: Services,
175-
defer: (work: Promise<any>) => void
172+
defer: (work: Promise<any>) => void,
176173
): Observable<{ found: true; blob: Blob; expiry: Date } | { found: false }> {
177174
const loadResult$ = of(params).pipe(
178175
switchMap(({ url, size, dpr, theme }) =>
179176
loadIconsForValidatedURL$(url, size, dpr, theme),
180177
),
181-
share()
178+
share(),
182179
);
183180

184181
const [foundIcon$, notFoundIcon$] = partition(
185182
loadResult$,
186183
(loadResult): loadResult is { icon: Icon; foundIcons: IconSource[] } =>
187-
loadResult.icon != null
184+
loadResult.icon != null,
188185
);
189186

190187
return merge(
191188
combineLatest([foundIcon$, of(params)]).pipe(
192-
switchMap(([{ icon, foundIcons }, { url, size, dpr, theme }]) => {
189+
switchMap(async ([{ icon, foundIcons }, { url, size, dpr, theme }]) => {
193190
const { image } = icon;
194-
const { blob, expiry } = image;
191+
let { blob, expiry } = image;
192+
193+
try {
194+
blob = await processImageForTheme(blob, theme, url);
195+
} catch (error) {
196+
console.error(
197+
"Error processing image for theme before caching:",
198+
error,
199+
);
200+
}
201+
202+
const processedIcon = {
203+
...icon,
204+
image: { ...image, blob },
205+
};
206+
195207
const key = { url, size, dpr, theme };
196-
defer(cacheFavicon(key, icon, services));
197-
return of({ found: true, blob, expiry } as const);
198-
})
208+
defer(cacheFavicon(key, processedIcon, services));
209+
return { found: true, blob, expiry } as const;
210+
}),
199211
),
200212
combineLatest([notFoundIcon$, of(params)]).pipe(
201213
switchMap(([_, params]) => {
202214
const { url, size, dpr } = params;
203215
return of({ found: false } as const);
204-
})
205-
)
216+
}),
217+
),
206218
);
207219
}
208220

209221
function loadIconsForValidatedURL$(
210222
url: URL,
211223
size: SizeParam,
212224
dpr: DevicePixelRatioParam,
213-
theme?: ThemeParam
225+
theme?: ThemeParam,
214226
): Observable<IconLoadResult> {
215227
const results$ = of({ url, size, theme }).pipe(
216228
switchMap(({ url, size, theme }) =>
217229
combineLatest([
218230
loadFaviconIco$(url),
219231
loadFaviconFromHTMLPage$(url, size, dpr, theme),
220-
])
221-
)
232+
]),
233+
),
222234
);
223235

224236
return combineLatest([of(url), of(theme), results$]).pipe(
225237
map(([url, theme, [favicon, page]]) =>
226-
bestResult(url, { favicon: favicon, page: page }, theme)
227-
)
238+
bestResult(url, { favicon: favicon, page: page }, theme),
239+
),
228240
);
229241
}
230242

@@ -240,7 +252,7 @@ export function getURLParam$(url: URL) {
240252
throw new APIError(400, "missing_url", "Missing 'url' query parameter");
241253
}
242254
return urlParam;
243-
})
255+
}),
244256
);
245257
}
246258

@@ -262,9 +274,9 @@ export function getSizeParam$(url: URL) {
262274
throw new APIError(
263275
400,
264276
"invalid_size",
265-
`Invalid 'size' query parameter. Valid sizes are ${allSizes.join(", ")}`
277+
`Invalid 'size' query parameter. Valid sizes are ${allSizes.join(", ")}`,
266278
);
267-
})
279+
}),
268280
);
269281
}
270282

@@ -282,7 +294,7 @@ export function getDevicePixelRatioParam$(url: URL) {
282294
throw new APIError(
283295
400,
284296
"invalid_dpr",
285-
`Invalid 'dpr' query parameter. This should be a number`
297+
`Invalid 'dpr' query parameter. This should be a number`,
286298
);
287299
}
288300

@@ -292,7 +304,7 @@ export function getDevicePixelRatioParam$(url: URL) {
292304
}),
293305
catchError(() => {
294306
throw makeInternalError();
295-
})
307+
}),
296308
);
297309
}
298310

@@ -311,9 +323,9 @@ export function getThemeParam$(url: URL) {
311323
throw new APIError(
312324
400,
313325
"invalid_theme",
314-
"Invalid 'theme' query parameter. Valid values are 'light' or 'dark'"
326+
"Invalid 'theme' query parameter. Valid values are 'light' or 'dark'",
315327
);
316-
})
328+
}),
317329
);
318330
}
319331

@@ -331,6 +343,6 @@ export function parsedAndValidatedURL$(urlString: string) {
331343
map(validatedURL),
332344
catchError(() => {
333345
throw new APIError(400, "invalid_url", "Invalid 'url' query parameter");
334-
})
346+
}),
335347
);
336348
}

0 commit comments

Comments
 (0)