import dayjs from "dayjs"; import type { Duration } from "dayjs/plugin/duration"; import { logger } from "@homarr/log"; import type { createChannelWithLatestAndEvents } from "@homarr/redis"; interface Options> { // Unique key for this request handler queryKey: string; requestAsync: (input: TInput) => Promise; createRedisChannel: ( input: TInput, options: Options, ) => ReturnType>; cacheDuration: Duration; } export const createCachedRequestHandler = >( options: Options, ) => { return { handler: (input: TInput) => { const channel = options.createRedisChannel(input, options); return { async getCachedOrUpdatedDataAsync({ forceUpdate = false }) { const requestNewDataAsync = async () => { const data = await options.requestAsync(input); await channel.publishAndUpdateLastStateAsync(data); return { data, timestamp: new Date(), }; }; if (forceUpdate) { logger.debug( `Cached request handler forced update for channel='${channel.name}' queryKey='${options.queryKey}'`, ); return await requestNewDataAsync(); } const channelData = await channel.getAsync(); const shouldRequestNewData = !channelData || dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds(); if (shouldRequestNewData) { logger.debug( `Cached request handler cache miss for channel='${channel.name}' queryKey='${options.queryKey}' reason='${!channelData ? "no data" : "cache expired"}'`, ); return await requestNewDataAsync(); } logger.debug( `Cached request handler cache hit for channel='${channel.name}' queryKey='${options.queryKey}' expiresAt='${dayjs(channelData.timestamp).add(options.cacheDuration).toISOString()}'`, ); return channelData; }, async invalidateAsync() { logger.debug( `Cached request handler invalidating cache channel='${channel.name}' queryKey='${options.queryKey}'`, ); await this.getCachedOrUpdatedDataAsync({ forceUpdate: true }); }, subscribe(callback: (data: TData) => void) { return channel.subscribe(callback); }, }; }, }; };