/** * @module computeSubResourceCacheTTL * * Adaptive Cache TTL for Opportunity Sub-Resources * ================================================= * * Determines how long cached sub-resource data (notes, contacts) should * live before being re-fetched from ConnectWise. * * Sub-resources change less frequently than the opportunity record itself * or its activity feed, so TTLs are longer than the primary cache. The * same activity-signal heuristics are used (expected close date, last * updated, closed status) but with relaxed durations. * * ## Spec * * | # | Condition | TTL (ms) | TTL (human) | Rationale | * |---|-------------------------------------------------------------------|----------|-------------|--------------------------------------------------------------------| * | 1 | `closedFlag` is `true` AND closed > 30 days ago | `null` | Do not cache| Old closed records are rarely accessed. | * | 1b| `closedFlag` is `true` AND closed within 30 days | 300 000 | 5 minutes | Recently-closed records may still be viewed occasionally. | * | 2 | `expectedCloseDate` OR `lastUpdated` within **5 days** | 60 000 | 60 seconds | Active deals — contacts/notes may still change. | * | 3 | `expectedCloseDate` OR `lastUpdated` within **14 days** | 120 000 | 2 minutes | Moderate activity — less likely to change. | * | 4 | Everything else (older than 14 days) | 300 000 | 5 minutes | Low activity — safe to cache longer. | * * ## Evaluation order * * Rules are evaluated top-to-bottom; the first matching rule wins. * * ## Inputs * * Uses the same {@link CacheTTLInput} interface as `computeCacheTTL`. * * ## Output * * Returns `number | null`: * - Positive integer = TTL in **milliseconds**. * - `null` = do **not** cache. */ import type { CacheTTLInput } from "./computeCacheTTL"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** 60 seconds — TTL for high-activity sub-resources (within 5 days). */ export const SUB_TTL_HIGH_ACTIVITY = 60_000; /** 2 minutes — TTL for moderate-activity sub-resources (within 14 days). */ export const SUB_TTL_MODERATE_ACTIVITY = 120_000; /** 5 minutes — TTL for low-activity / stale sub-resources. */ export const SUB_TTL_LOW_ACTIVITY = 300_000; /** 30 days in milliseconds. */ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; /** 5 days in milliseconds. */ const FIVE_DAYS_MS = 5 * 24 * 60 * 60 * 1000; /** 14 days in milliseconds. */ const FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1000; // --------------------------------------------------------------------------- // Algorithm // --------------------------------------------------------------------------- /** * Compute the cache TTL for an opportunity sub-resource (notes, contacts). * * @param input - The opportunity's activity signals. See {@link CacheTTLInput}. * @returns The TTL in milliseconds, or `null` if the data should not be cached. */ export function computeSubResourceCacheTTL( input: CacheTTLInput, ): number | null { const { closedFlag, closedDate, expectedCloseDate, lastUpdated, now = new Date(), } = input; const nowMs = now.getTime(); const isWithinWindow = (date: Date | null, windowMs: number): boolean => { if (!date) return false; return Math.abs(nowMs - date.getTime()) <= windowMs; }; // Rule 1 — Closed records if (closedFlag) { if (isWithinWindow(closedDate, THIRTY_DAYS_MS)) { return SUB_TTL_LOW_ACTIVITY; } return null; } // Rule 2 — High activity (5 days) if ( isWithinWindow(expectedCloseDate, FIVE_DAYS_MS) || isWithinWindow(lastUpdated, FIVE_DAYS_MS) ) { return SUB_TTL_HIGH_ACTIVITY; } // Rule 3 — Moderate activity (14 days) if ( isWithinWindow(expectedCloseDate, FOURTEEN_DAYS_MS) || isWithinWindow(lastUpdated, FOURTEEN_DAYS_MS) ) { return SUB_TTL_MODERATE_ACTIVITY; } // Rule 4 — Low activity / stale return SUB_TTL_LOW_ACTIVITY; }