feat: add hourly portfolio sync history
This commit is contained in:
@@ -12,6 +12,15 @@ export type ScreenProgress = {
|
||||
children: string[];
|
||||
dispatched: boolean;
|
||||
};
|
||||
export type HourlySection = {
|
||||
accountId?: string;
|
||||
errorCode: null | string;
|
||||
section: 'events' | 'membership' | 'operations' | 'portfolio';
|
||||
};
|
||||
export type HourlyProgress = {
|
||||
result?: 'failed' | 'partial' | 'succeeded';
|
||||
sections: HourlySection[];
|
||||
};
|
||||
@Injectable()
|
||||
export class TbankSyncRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -123,4 +132,122 @@ export class TbankSyncRepository {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// The timer only proposes work; this reservation is the durable fence. The
|
||||
// attempt start (createdAt) is the basis of the next hourly term, including
|
||||
// failed and interrupted attempts.
|
||||
reserveHourly(userId: string, runId: string, hourMs: number) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const connection = await tx.tbankConnection.findFirst({
|
||||
where: { userId, invalidToken: false },
|
||||
select: { revision: true },
|
||||
});
|
||||
if (!connection) return null;
|
||||
const key = {
|
||||
userId,
|
||||
revision: connection.revision,
|
||||
scopeKey: 'hourly',
|
||||
};
|
||||
const previous = await tx.tbankSyncWork.findUnique({
|
||||
where: { userId_revision_scopeKey: key },
|
||||
});
|
||||
const now = new Date();
|
||||
if (previous) {
|
||||
const due = now.valueOf() - previous.createdAt.valueOf() >= hourMs;
|
||||
if (previous.state === 'running') {
|
||||
if (previous.processRunId === runId) return null;
|
||||
// A dead process cannot finish its attempt; interrupt it, but its
|
||||
// start stays the basis of the next term.
|
||||
await tx.tbankSyncWork.update({
|
||||
where: { userId_revision_scopeKey: key },
|
||||
data: { state: 'cancelled', errorCode: null },
|
||||
});
|
||||
if (!due) return null;
|
||||
} else if (!due) return null;
|
||||
}
|
||||
const data = {
|
||||
attemptId: randomUUID(),
|
||||
processRunId: runId,
|
||||
state: 'running',
|
||||
priority: 0,
|
||||
intent: JSON.parse(
|
||||
JSON.stringify({ source: 'hourly' }),
|
||||
) as Prisma.InputJsonObject,
|
||||
progress: { sections: [] } as unknown as Prisma.InputJsonValue,
|
||||
errorCode: null,
|
||||
};
|
||||
const work = await tx.tbankSyncWork.upsert({
|
||||
where: { userId_revision_scopeKey: key },
|
||||
create: { ...key, ...data, id: randomUUID() },
|
||||
update: { ...data, createdAt: now },
|
||||
});
|
||||
return { work, launch: true };
|
||||
});
|
||||
}
|
||||
|
||||
settleHourly(
|
||||
attempt: TbankSyncAttempt,
|
||||
result: 'failed' | 'partial' | 'succeeded',
|
||||
sections: HourlySection[],
|
||||
) {
|
||||
return this.prisma.tbankSyncWork.updateMany({
|
||||
where: {
|
||||
id: attempt.id,
|
||||
attemptId: attempt.attemptId,
|
||||
userId: attempt.userId,
|
||||
revision: attempt.revision,
|
||||
state: 'running',
|
||||
},
|
||||
data: {
|
||||
state: result === 'failed' ? 'failed' : 'complete',
|
||||
errorCode:
|
||||
sections.find((section) => section.errorCode)?.errorCode ?? null,
|
||||
progress: { result, sections },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async hourly(userId: string) {
|
||||
const connection = await this.prisma.tbankConnection.findFirst({
|
||||
where: { userId },
|
||||
select: { revision: true, invalidToken: true },
|
||||
});
|
||||
if (!connection || connection.invalidToken) return null;
|
||||
const work = await this.prisma.tbankSyncWork.findUnique({
|
||||
where: {
|
||||
userId_revision_scopeKey: {
|
||||
userId,
|
||||
revision: connection.revision,
|
||||
scopeKey: 'hourly',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!work) return null;
|
||||
const progress = work.progress as HourlyProgress;
|
||||
let state: 'cancelled' | 'running' | HourlyProgress['result'];
|
||||
if (work.state === 'running') state = 'running';
|
||||
else if (work.state === 'cancelled') state = 'cancelled';
|
||||
else state = progress.result ?? 'failed';
|
||||
return {
|
||||
startedAt: work.createdAt,
|
||||
finishedAt: work.state === 'running' ? null : work.updatedAt,
|
||||
state,
|
||||
sections: progress.sections ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
works(userId: string, ids: string[]) {
|
||||
return this.prisma.tbankSyncWork.findMany({
|
||||
where: { userId, id: { in: ids } },
|
||||
select: { id: true, state: true, errorCode: true },
|
||||
});
|
||||
}
|
||||
|
||||
async workError(userId: string, id: string) {
|
||||
const work = await this.prisma.tbankSyncWork.findFirst({
|
||||
where: { userId, id },
|
||||
select: { state: true, errorCode: true },
|
||||
});
|
||||
return work?.state === 'failed' ? work.errorCode : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user