From 6c4c097517a9494106aca841ecb4e6b86e8c7191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 19:15:58 +0200 Subject: [PATCH] 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. --- src/lib/day/records.ts | 10 +++++----- src/lib/insights/correlation-channel-series.ts | 18 +++++++++--------- src/lib/insights/feature-blocks.ts | 10 +++++----- src/lib/insights/strain-score.ts | 10 +++++----- src/lib/workouts/canonical-read.ts | 5 +++++ src/lib/workouts/list-read.ts | 10 +++++----- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/lib/day/records.ts b/src/lib/day/records.ts index f490f35b1..35138b89d 100644 --- a/src/lib/day/records.ts +++ b/src/lib/day/records.ts @@ -909,7 +909,11 @@ async function assessmentsPart(frame: DayFrame): Promise { /* ─── workouts, cycle ─────────────────────────────────────────────────────── */ async function workoutsPart(frame: DayFrame): Promise { - 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 { 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. diff --git a/src/lib/insights/correlation-channel-series.ts b/src/lib/insights/correlation-channel-series.ts index ed386cc69..25f971c18 100644 --- a/src/lib/insights/correlation-channel-series.ts +++ b/src/lib/insights/correlation-channel-series.ts @@ -1260,15 +1260,7 @@ export async function fetchTrainingSeries( tz: string, since: Date, ): Promise { - 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, diff --git a/src/lib/insights/feature-blocks.ts b/src/lib/insights/feature-blocks.ts index f501f1825..acd01bf57 100644 --- a/src/lib/insights/feature-blocks.ts +++ b/src/lib/insights/feature-blocks.ts @@ -539,7 +539,11 @@ export async function readWorkoutsBlock( now: number, ): Promise { 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 diff --git a/src/lib/insights/strain-score.ts b/src/lib/insights/strain-score.ts index b5a5240b2..526a2527d 100644 --- a/src/lib/insights/strain-score.ts +++ b/src/lib/insights/strain-score.ts @@ -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 diff --git a/src/lib/workouts/canonical-read.ts b/src/lib/workouts/canonical-read.ts index 22777a630..055eae3bb 100644 --- a/src/lib/workouts/canonical-read.ts +++ b/src/lib/workouts/canonical-read.ts @@ -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. diff --git a/src/lib/workouts/list-read.ts b/src/lib/workouts/list-read.ts index ff4f896d0..5c3181c56 100644 --- a/src/lib/workouts/list-read.ts +++ b/src/lib/workouts/list-read.ts @@ -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(