Files
notification-wakeup-service/src/alertSearch/cursors.ts
T

151 lines
4.3 KiB
TypeScript

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<AlertSearchChannel, AlertSearchCursorsStore> = {
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<EndorserAlertSearchData>
): string | undefined {
if (!isCompleteOutcome(endorser.outcome)) return undefined;
return maxEndorserAlertSearchUlid(endorser.data);
}
export function nextPartnerCursorFromResult(
partner: AlertSearchSourceResult<PartnerAlertSearchData>
): 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<StoredAlertSearchCursors> {
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<CursorAdvanceResult> {
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,
};
}