export const operationsSyncPriority = { screen: 1, history: 2 } as const; const LAST_INTERVAL_INDEX = -1; const NEXT_MONTH_OFFSET = 1; const FIRST_DAY_OF_MONTH = 1; const EMPTY_INTERVAL_COUNT = 0; const EMPTY_SOURCE_COUNT = 0; export type OperationInterval = { accountId: string; from: Date; instrumentUid?: string; to: Date; }; // Normalize duplicate/overlapping targets before creating a durable identity. // Calendar windows keep each provider walk bounded without changing [from,to). export function operationIntervals( requested: OperationInterval[], ): OperationInterval[] { const groups = new Map(); for (const interval of requested) { // eslint-disable-next-line sonarjs/no-inverted-boolean-check -- Negation also rejects Invalid Date; >= would silently accept NaN. if (!(interval.from < interval.to)) { throw new Error('Invalid operations interval'); } const key = JSON.stringify([ interval.accountId, interval.instrumentUid ?? '', ]); const group = groups.get(key) ?? []; group.push(interval); groups.set(key, group); } const result: OperationInterval[] = []; // eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings. for (const key of [...groups.keys()].sort()) { const merged: OperationInterval[] = []; for (const interval of (groups.get(key) ?? []).sort( (left, right) => left.from.valueOf() - right.from.valueOf(), )) { const previous = merged.at(LAST_INTERVAL_INDEX); if (previous && interval.from <= previous.to) { previous.to = new Date( Math.max(previous.to.valueOf(), interval.to.valueOf()), ); } else merged.push({ ...interval }); } for (const interval of merged) { let from = interval.from; while (from < interval.to) { const nextMonth = new Date( Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + NEXT_MONTH_OFFSET, FIRST_DAY_OF_MONTH), ); const to = new Date( Math.min(nextMonth.valueOf(), interval.to.valueOf()), ); result.push({ ...interval, from, to }); from = to; } } } return result; } export function operationIntervalsOverlap( left: OperationInterval, right: OperationInterval, ): boolean { return ( left.accountId === right.accountId && (!left.instrumentUid || !right.instrumentUid || left.instrumentUid === right.instrumentUid) && left.from < right.to && right.from < left.to ); } export function uncoveredOperationIntervals( requested: OperationInterval[], existing: OperationInterval[], ): OperationInterval[] { return requested.flatMap((interval) => { let fragments = [interval]; for (const covered of existing) { fragments = fragments.flatMap((fragment) => { if ( !operationIntervalsOverlap(fragment, covered) || (covered.instrumentUid && covered.instrumentUid !== fragment.instrumentUid) ) { return [fragment]; } return [ ...(fragment.from < covered.from ? [{ ...fragment, to: covered.from }] : []), ...(covered.to < fragment.to ? [{ ...fragment, from: covered.to }] : []), ]; }); } return fragments; }); } export function confirmedOperationRanges( targets: OperationInterval[], coverage: OperationInterval[], ): Omit[] { return targets.flatMap((target) => coverage .filter((row) => row.accountId === target.accountId && (!row.instrumentUid || row.instrumentUid === target.instrumentUid) && row.from < target.to && target.from < row.to, ) .map((row) => ({ accountId: target.accountId, from: new Date(Math.max(row.from.valueOf(), target.from.valueOf())), to: new Date(Math.min(row.to.valueOf(), target.to.valueOf())), })), ); } export type OperationCoverage = OperationInterval & { asOf: Date; fetchedAt: Date; }; const operationsFreshForMs = 3_600_000; export function planOperationRead( requested: OperationInterval[], coverage: OperationCoverage[], now: Date, options: { force?: boolean; movingEnd?: boolean } = {}, ) { const monthStart = new Date( Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), FIRST_DAY_OF_MONTH), ); const observedEnd = Math.max( ...requested.map((interval) => interval.to.valueOf()), ); const load: OperationInterval[] = []; const intervals = operationIntervals(requested).map((target) => { const sources = coverage.filter( (row) => row.accountId === target.accountId && (!row.instrumentUid || row.instrumentUid === target.instrumentUid) && row.from < target.to && target.from < row.to, ); const ranges = sources.map((row) => ({ ...row, instrumentUid: target.instrumentUid, })); const current = target.from >= monthStart && target.to <= now; const fresh = sources.filter( (row) => now.valueOf() - row.fetchedAt.valueOf() < operationsFreshForMs, ); let effective = target; if (options.movingEnd && target.to.valueOf() === observedEnd) { const latest = sources .filter( (row) => row.asOf.valueOf() === row.to.valueOf() && row.to > target.from, ) .sort((left, right) => right.to.valueOf() - left.to.valueOf())[0]; if (latest && latest.to < target.to) { effective = { ...target, to: latest.to }; } } const missing = uncoveredOperationIntervals([effective], ranges); const complete = missing.length === EMPTY_INTERVAL_COUNT; const stale = current && sources.length > EMPTY_SOURCE_COUNT && uncoveredOperationIntervals( ranges .map((row) => ({ ...target, from: new Date( Math.max(effective.from.valueOf(), row.from.valueOf()), ), to: new Date(Math.min(effective.to.valueOf(), row.to.valueOf())), })) .filter((row) => row.from < row.to), fresh.map((row) => ({ ...row, instrumentUid: target.instrumentUid })), ).length > EMPTY_INTERVAL_COUNT; // The mutable tail is owned by the hourly walk; reads only load what is // missing, never re-verify complete or fresh intervals. if (options.force) load.push(target); else load.push(...missing); // Completeness is bounded by the observed end, never by the new wall clock. return { ...effective, availability: complete ? ('complete' as const) : sources.length ? ('partial' as const) : ('missing' as const), stale, asOf: sources.length ? new Date( Math.min( effective.to.valueOf(), Math.max(...sources.map((row) => row.asOf.valueOf())), ), ) : null, }; }); return { intervals, load: operationIntervals(load) }; }