166 lines
4.3 KiB
TypeScript
166 lines
4.3 KiB
TypeScript
import { accountHistory } from '../../investment-sync-plan';
|
|
import { confirmedOperationRanges, type OperationCoverage,
|
|
type OperationInterval,
|
|
operationIntervalsOverlap,
|
|
operationsSyncPriority,
|
|
planOperationRead,
|
|
uncoveredOperationIntervals } from './operations-sync-plan';
|
|
|
|
const UNKNOWN_AS_OF_MS = 0;
|
|
|
|
type WorkState = {
|
|
errorCode: null | string;
|
|
id: string;
|
|
intervals: OperationInterval[];
|
|
priority: number;
|
|
state: string;
|
|
};
|
|
|
|
type CoverageAccount = { accountId: string; openedOn: null | string };
|
|
|
|
// Pure derivation of the ledger read view: what was requested, what is
|
|
// confirmed, what is missing, and which sync work is currently relevant.
|
|
export function deriveReadCoverage(input: {
|
|
accounts: CoverageAccount[];
|
|
connectionMembershipAt: Date | null;
|
|
coverage: OperationCoverage[];
|
|
from: Date;
|
|
instrumentUid?: string;
|
|
movingEnd: boolean;
|
|
now: Date;
|
|
to: Date;
|
|
work: WorkState[];
|
|
}) {
|
|
const targets = input.accounts.flatMap((account) =>
|
|
input.from < input.to
|
|
? [
|
|
{
|
|
accountId: account.accountId,
|
|
from: input.from,
|
|
to: input.to,
|
|
instrumentUid: input.instrumentUid,
|
|
},
|
|
]
|
|
: [],
|
|
);
|
|
const plan = planOperationRead(targets, input.coverage, input.now, {
|
|
movingEnd: input.movingEnd,
|
|
});
|
|
const confirmed = confirmedOperationRanges(plan.intervals, input.coverage);
|
|
const missing = uncoveredOperationIntervals(
|
|
plan.intervals,
|
|
confirmed.map((range) => ({
|
|
...range,
|
|
instrumentUid: input.instrumentUid,
|
|
})),
|
|
);
|
|
const available = Boolean(input.connectionMembershipAt);
|
|
const complete = available && !missing.length;
|
|
const relevant = input.work.filter((work) =>
|
|
work.intervals.some((interval) =>
|
|
targets.some((target) => operationIntervalsOverlap(interval, target)),
|
|
),
|
|
);
|
|
const work =
|
|
relevant.find((work) => work.state === 'running') ??
|
|
relevant.find((work) => work.state === 'failed') ??
|
|
null;
|
|
const sources = input.coverage.filter((row) =>
|
|
targets.some(
|
|
(target) =>
|
|
operationIntervalsOverlap(row, target) &&
|
|
(!row.instrumentUid || row.instrumentUid === target.instrumentUid),
|
|
),
|
|
);
|
|
const lastSuccessfulAt =
|
|
sources
|
|
.map((row) => row.fetchedAt)
|
|
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
|
|
(complete ? input.connectionMembershipAt : null) ??
|
|
null;
|
|
const history = planOperationRead(
|
|
accountHistory(input.accounts, input.now),
|
|
input.coverage,
|
|
input.now,
|
|
{ movingEnd: true },
|
|
);
|
|
const knownBoundary = input.accounts.every(
|
|
(account) => account.openedOn !== null,
|
|
);
|
|
return {
|
|
targets,
|
|
plan,
|
|
confirmed,
|
|
missing,
|
|
available,
|
|
complete,
|
|
work,
|
|
lastSuccessfulAt,
|
|
history,
|
|
knownBoundary,
|
|
};
|
|
}
|
|
|
|
export function coverageAsOf(
|
|
plan: ReturnType<typeof planOperationRead>,
|
|
accountIds: string[],
|
|
): null | string {
|
|
if (
|
|
accountIds.length &&
|
|
accountIds.every((accountId) =>
|
|
plan.intervals.some(
|
|
(interval) =>
|
|
interval.accountId === accountId && interval.asOf,
|
|
),
|
|
)
|
|
) {
|
|
return new Date(
|
|
Math.min(
|
|
...accountIds.map((accountId) =>
|
|
Math.max(
|
|
...plan.intervals
|
|
.filter((interval) => interval.accountId === accountId)
|
|
.map((interval) => interval.asOf?.valueOf() ?? UNKNOWN_AS_OF_MS),
|
|
),
|
|
),
|
|
),
|
|
).toISOString();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function historyState(
|
|
knownBoundary: boolean,
|
|
history: ReturnType<typeof planOperationRead>,
|
|
work: WorkState[],
|
|
coverageCount: number,
|
|
): 'complete' | 'failed' | 'loading' | 'missing' | 'partial' {
|
|
if (
|
|
work.some(
|
|
(work) => work.state === 'running' && work.priority > operationsSyncPriority.screen,
|
|
)
|
|
) {
|
|
return 'loading';
|
|
}
|
|
if (
|
|
work.some(
|
|
(work) => work.state === 'failed' && work.priority > operationsSyncPriority.screen,
|
|
)
|
|
) {
|
|
return 'failed';
|
|
}
|
|
if (
|
|
knownBoundary &&
|
|
history.intervals.every(
|
|
(interval) => interval.availability === 'complete',
|
|
)
|
|
) {
|
|
return 'complete';
|
|
}
|
|
return coverageCount ? 'partial' : 'missing';
|
|
}
|
|
|
|
export function serializeRange(range: { accountId: string; from: Date; to: Date }) {
|
|
return { accountId: range.accountId, from: range.from.toISOString(), to: range.to.toISOString() };
|
|
}
|