import { type DefaultOpts, getNow } from './datetime' export type BindingSource = { type: 'dateTime' dataType: 'date' options?: { startOfDay?: boolean offset?: number } } export type Binding = { id: string source: BindingSource bindingType: string type: 'binding' label: string format: { type: string } } const CACHE_TTL = 5000 // 5 seconds const MAX_CACHE_SIZE = 1000 // Prevent unlimited cache growth const CACHE_CLEANUP_INTERVAL = 30000 // 30 seconds type CacheValue = { value: string cachedAt: number accessCount: number lastAccessed: number } class DateSourceCache { private cache = new Map() private cleanupTimer: ReturnType | null = null constructor() { this.scheduleCleanup() } private scheduleCleanup() { if (this.cleanupTimer) { clearTimeout(this.cleanupTimer) } this.cleanupTimer = setTimeout(() => { this.cleanup() this.scheduleCleanup() }, CACHE_CLEANUP_INTERVAL) } private cleanup() { const now = Date.now() const entries = Array.from(this.cache.entries()) // Remove expired entries for (const [key, value] of entries) { if (now - value.cachedAt > CACHE_TTL) { this.cache.delete(key) } } // If still over limit, remove least recently used entries if (this.cache.size > MAX_CACHE_SIZE) { const sortedEntries = Array.from(this.cache.entries()).sort( (a, b) => a[1].lastAccessed - b[1].lastAccessed ) const entriesToRemove = this.cache.size - MAX_CACHE_SIZE for (let i = 0; i < entriesToRemove; i += 1) { this.cache.delete(sortedEntries[i][0]) } } } get(key: string): CacheValue | undefined { const entry = this.cache.get(key) if (entry) { entry.accessCount += 1 entry.lastAccessed = Date.now() } return entry } set(key: string, value: string, cachedAt: number) { this.cache.set(key, { value, cachedAt, accessCount: 1, lastAccessed: Date.now(), }) } has(key: string): boolean { return this.cache.has(key) } clear() { this.cache.clear() } destroy() { if (this.cleanupTimer) { clearTimeout(this.cleanupTimer) this.cleanupTimer = null } this.cache.clear() } } const dateSourceCache = new DateSourceCache() // Create a deterministic cache key based on binding configuration and context function createCacheKey( binding: Binding, isAction: boolean, timezone?: string ): string { const { source } = binding const options = source.options ?? {} // Use binding configuration to create a deterministic key const keyParts = [ binding.id || 'no-id', options.startOfDay ? 'sod' : '', options.offset?.toString() || '0', isAction ? 'action' : 'display', timezone || 'default', ] return keyParts.filter(Boolean).join('|') } // Pre-calculate common offset operations to avoid repeated calculations const OFFSET_CALCULATORS = { 180: (now: any, negative: boolean) => now.plus({ months: negative ? -6 : 6 }), 365: (now: any, negative: boolean) => now.plus({ years: negative ? -1 : 1 }), default: (now: any, offset: number) => { if (offset % 1 === 0) { return now.plus({ days: offset }) } else { return now.plus({ minutes: offset * 24 * 60 }) } }, } export const getDateSourceValue = ( binding: Binding, isAction = false, { now = getNow(), timezone }: DefaultOpts & { timezone?: string } = {} ): string => { const currentTime = Date.now() const cacheKey = createCacheKey(binding, isAction, timezone) // Check cache with improved key strategy if (dateSourceCache.has(cacheKey)) { const cached = dateSourceCache.get(cacheKey)! if (currentTime - cached.cachedAt < CACHE_TTL) { return cached.value } } const { source } = binding const options = source.options ?? {} // Clone the now object to avoid mutation side effects let workingTime = now // Apply transformations in order if (options.startOfDay) { workingTime = workingTime.startOf('day') } if (options.offset) { const absOffset = Math.abs(options.offset) const isNegative = options.offset < 0 if (absOffset === 180) { workingTime = OFFSET_CALCULATORS[180](workingTime, isNegative) } else if (absOffset === 365) { workingTime = OFFSET_CALCULATORS[365](workingTime, isNegative) } else { workingTime = OFFSET_CALCULATORS.default(workingTime, options.offset) } } if (isAction) { workingTime = workingTime.toUTC() } const result = workingTime.toISO() dateSourceCache.set(cacheKey, result, currentTime) return result } // Export cache management functions for testing and cleanup export const clearDateSourceCache = () => dateSourceCache.clear() export const destroyDateSourceCache = () => dateSourceCache.destroy()