import { alertSearchCursorsDb, smsAlertSearchCursorsDb, type AlertSearchCursorsStore, } from "../db/alertSearchCursorsSqlite.js"; import { isAlertSearchCursorUlid } from "./params.js"; import type { AlertSearchSourceResult, RetrieveAlertSearchResult, } from "./retrieve.js"; import type { AlertSearchChannel, EndorserAlertSearchData, PartnerAlertSearchData, } from "./types.js"; export type StoredAlertSearchCursors = { endorserAfterId?: string; partnerAfterDate?: string; }; export type CursorAdvanceResult = { endorserAdvanced: boolean; partnerAdvanced: boolean; endorserAfterId: string | null; partnerAfterAt: string | null; }; const CURSOR_STORES: Record = { fcm: alertSearchCursorsDb, sms: smsAlertSearchCursorsDb, }; const COMPLETE_OUTCOMES = new Set(["success"]); function isCompleteOutcome(outcome: string): boolean { return COMPLETE_OUTCOMES.has(outcome); } /** MAX ULID across Endorser alertSearch identifier fields. */ export function maxEndorserAlertSearchUlid( data: EndorserAlertSearchData ): string | undefined { const ids: string[] = []; for (const row of data.claims) { if (isAlertSearchCursorUlid(row.id)) ids.push(row.id); } for (const row of data.personalPlanContributions) { if (isAlertSearchCursorUlid(row.id)) ids.push(row.id); } for (const row of data.trackedPlanClaims) { if (isAlertSearchCursorUlid(row.id)) ids.push(row.id); } for (const row of data.trackedPlanUpdates) { if (isAlertSearchCursorUlid(row.jwtId)) ids.push(row.jwtId); } for (const row of data.plansNearby) { if (isAlertSearchCursorUlid(row.jwtId)) ids.push(row.jwtId); } if (ids.length === 0) return undefined; ids.sort(); return ids[ids.length - 1]; } /** MAX profilesNearby.updatedAt (ISO). Not rowId. */ export function maxPartnerUpdatedAt( data: PartnerAlertSearchData ): string | undefined { let best: string | undefined; let bestMs = Number.NEGATIVE_INFINITY; for (const row of data.profilesNearby) { if (typeof row.updatedAt !== "string" || row.updatedAt.length === 0) { continue; } const ms = Date.parse(row.updatedAt); if (Number.isNaN(ms)) continue; if (ms > bestMs) { bestMs = ms; best = row.updatedAt; } } return best; } export function nextEndorserCursorFromResult( endorser: AlertSearchSourceResult ): string | undefined { if (!isCompleteOutcome(endorser.outcome)) return undefined; return maxEndorserAlertSearchUlid(endorser.data); } export function nextPartnerCursorFromResult( partner: AlertSearchSourceResult ): string | undefined { if (!isCompleteOutcome(partner.outcome)) return undefined; return maxPartnerUpdatedAt(partner.data); } /** * Load stored cursors for a retrieval query. * Omits missing values so first run sends no afterId / afterDate / "0". */ export async function loadAlertSearchCursors( userId: string, channel: AlertSearchChannel = "fcm" ): Promise { const row = await CURSOR_STORES[channel].get(userId); const out: StoredAlertSearchCursors = {}; if (row?.endorserAfterId && isAlertSearchCursorUlid(row.endorserAfterId)) { out.endorserAfterId = row.endorserAfterId; } if (row?.partnerAfterAt && row.partnerAfterAt.length > 0) { out.partnerAfterDate = row.partnerAfterAt; } return out; } /** * Advance each cursor independently after retrieveAlertSearch. * Does not persist on empty, pagination, or error outcomes. */ export async function advanceAlertSearchCursors( userId: string, result: RetrieveAlertSearchResult, channel: AlertSearchChannel = "fcm" ): Promise { const store = CURSOR_STORES[channel]; let endorserAdvanced = false; let partnerAdvanced = false; const nextEndorser = nextEndorserCursorFromResult(result.endorser); if (nextEndorser !== undefined) { await store.setEndorserAfterId(userId, nextEndorser); endorserAdvanced = true; } const nextPartner = nextPartnerCursorFromResult(result.partner); if (nextPartner !== undefined) { await store.setPartnerAfterAt(userId, nextPartner); partnerAdvanced = true; } const stored = await store.get(userId); return { endorserAdvanced, partnerAdvanced, endorserAfterId: stored?.endorserAfterId ?? null, partnerAfterAt: stored?.partnerAfterAt ?? null, }; }