fix(workouts): start the windowed workout read after the plain reads beside it

The windowed reader is async, so a Promise.all that listed it first held a rejected promise of it whenever a later element threw while the array was built, and nobody handled that rejection. It now comes last in each of those arrays.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 19:15:58 +02:00
1 parent cec7ce2ad5
commit 6c4c097517
6 files changed
+34 -29

No files matched your search

+5 -5
View File
@@ -909,7 +909,11 @@ async function assessmentsPart(frame: DayFrame): Promise<SectionPart> {
/* ─── workouts, cycle ─────────────────────────────────────────────────────── */
async function workoutsPart(frame: DayFrame): Promise<SectionPart> {
const [stored, account] = await Promise.all([
const [account, stored] = await Promise.all([
prisma.user.findUnique({
where: { id: frame.userId },
select: { sourcePriorityJson: true },
}),
// The windowed reader orders the rows itself.
readWorkoutsAroundRange(
{ from: frame.dayStart, to: frame.dayEnd },
@@ -919,10 +923,6 @@ async function workoutsPart(frame: DayFrame): Promise<SectionPart> {
select: WORKOUT_PICK_SELECT,
}),
),
prisma.user.findUnique({
where: { id: frame.userId },
select: { sourcePriorityJson: true },
}),
]);
// One session synced by two services is one workout on the day, as in the
// workout list it links to, a twin across midnight included.
@@ -1260,15 +1260,7 @@ export async function fetchTrainingSeries(
tz: string,
since: Date,
): Promise<NamedSeries[]> {
const [workouts, account, trimp] = await Promise.all([
readWorkoutsAroundRange({ from: since, to: null }, (bounds) =>
prisma.workout.findMany({
where: { userId, startedAt: bounds },
orderBy: [{ startedAt: "asc" }, { id: "asc" }],
take: WORKOUT_READ_CAP,
select: WORKOUT_PICK_SELECT,
}),
),
const [account, trimp, workouts] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
@@ -1280,6 +1272,14 @@ export async function fetchTrainingSeries(
take: 1000,
select: { day: true, dayTrimp: true },
}),
readWorkoutsAroundRange({ from: since, to: null }, (bounds) =>
prisma.workout.findMany({
where: { userId, startedAt: bounds },
orderBy: [{ startedAt: "asc" }, { id: "asc" }],
take: WORKOUT_READ_CAP,
select: WORKOUT_PICK_SELECT,
}),
),
]);
const sessions = canonicalWorkoutsInRange(
workouts,
+5 -5
View File
@@ -539,7 +539,11 @@ export async function readWorkoutsBlock(
now: number,
): Promise<AggregatedFeatures["workouts"] | undefined> {
const since = new Date(now - WORKOUT_WINDOW_DAYS * MS_PER_DAY);
const [stored, account] = await Promise.all([
const [account, stored] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
readWorkoutsAroundRange({ from: since, to: null }, (bounds) =>
prisma.workout.findMany({
where: { userId, startedAt: bounds },
@@ -555,10 +559,6 @@ export async function readWorkoutsBlock(
},
}),
),
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
]);
// One session synced by two services (or re-sent by HealthKit) is one
// workout: the same collapse the workout list applies, so the count and the
+5 -5
View File
@@ -432,7 +432,11 @@ export async function computeStrainScore(
// The day's workouts + their HR series, with the session identity the
// cross-source picker needs: one run recorded by the watch and synced again
// through a second service is one training impulse, not two.
const [workouts, account] = await Promise.all([
const [account, workouts] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
// Read through the windowed reader: a session whose twin lies across
// midnight is counted once, on the day the list shows it.
readWorkoutsAroundRange({ from: dayStart, to: dayEnd }, (bounds) =>
@@ -450,10 +454,6 @@ export async function computeStrainScore(
take: 100,
}),
),
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
]);
// The watch and the phone relay (or a ring) both write the day's active
+5
View File
@@ -20,6 +20,11 @@
* surfaces then pick over everything read and keep the rows that start inside
* their range: a workout is canonical, or not, the same way on every surface.
*
* The reader is async and calls the callback before it first awaits, so in a
* `Promise.all` it goes after the plain Prisma calls: an element before it
* that threw while the array was being built would leave its rejection
* unhandled.
*
* A read callback may still carry a row cap as a runaway guard. A capped read
* that actually hits its cap has lost rows at one end and is outside this
* guarantee; the caps sit far above any real account's volume.
+5 -5
View File
@@ -151,7 +151,11 @@ export async function buildWorkoutsProjection(
// round trips. The rows come through the windowed reader: a twin just past
// `until`, or a cluster that began just before `since`, decides which row
// of the range is canonical exactly as it does for the detail route.
const [around, userRow] = await Promise.all([
const [userRow, around] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
readWorkoutsAroundRange(range, (bounds) =>
prisma.workout.findMany({
where: {
@@ -179,10 +183,6 @@ export async function buildWorkoutsProjection(
},
}),
),
prisma.user.findUnique({
where: { id: userId },
select: { sourcePriorityJson: true },
}),
]);
const canonical: CanonicalWorkoutRow[] = canonicalWorkoutsInRange(