fix(withings): keep the plan re-probe date on a run that only narrowed

A run that skipped remembered types and met one more refusal restamped the
leg's checkedAt, pushing the weekly re-probe of the remembered types out on
every such run. Only a re-probe, or a leg with nothing remembered yet, now
starts the week again.
This commit is contained in:
Marc-André Bombeck committed 2026-10-14 19:46:03 +02:00
1 parent 6feaf6849a
commit c7164be0d3
4 files changed
+51 -2

No files matched your search

@@ -384,6 +384,35 @@ describe("syncUserMeasurements inside the Withings API plan", () => {
expect(runs[1]!.startdates.every((s) => s === fromLastGood)).toBe(true);
});
it("keeps the remembered date when a run that is not the re-probe finds one more refusal", async () => {
const remembered = new Date(Date.now() - 3 * 86_400_000).toISOString();
vi.mocked(prisma.withingsConnection.findUnique).mockResolvedValue(
connection({
version: 1,
measures: {
unavailable: [...WITHINGS_ADVANCED_MEASTYPES],
checkedAt: remembered,
},
}) as never,
);
// The plan starts refusing fat ratio too.
refused = new Set([...WITHINGS_ADVANCED_MEASTYPES, 6]);
await syncUserMeasurements("user-1");
expect(persistedLegs()).toEqual([
{
leg: "measures",
value: {
unavailable: [6, ...WITHINGS_ADVANCED_MEASTYPES].sort(
(a, b) => a - b,
),
checkedAt: remembered,
},
},
]);
});
it("reads the whole gap since the last good sync", async () => {
await syncUserMeasurements("user-1");
+18
View File
@@ -297,6 +297,24 @@ export function planSkipFor<K>(
return leg.unavailable;
}
/**
* The `checkedAt` to store with a run's outcome. Only a re-probe asked for the
* full set, so only a re-probe (or a leg with nothing remembered yet) starts
* the week again. A run that skipped remembered keys and found one more
* refusal keeps the old date: moving it would push the re-probe of the keys it
* never asked for further out on every such run.
*/
export function planCheckedAtFor<K>(
leg: WithingsPlanLegState<K> | undefined,
reprobed: boolean,
now: Date,
): string {
if (reprobed || !leg || leg.unavailable.length === 0) {
return now.toISOString();
}
return leg.checkedAt;
}
/**
* Write one leg of the plan state. A single statement merges the leg into the
* stored object, so the measures, sleep and ECG legs (separate crons, separate
+2 -1
View File
@@ -65,6 +65,7 @@ import {
isPlanReprobeDue,
parseWithingsApiPlanState,
persistWithingsPlanLeg,
planCheckedAtFor,
planSkipFor,
requestWithinPlan,
shouldPersistPlanLeg,
@@ -575,7 +576,7 @@ export async function syncUserSleep(
) {
await persistWithingsPlanLeg(userId, "sleepSummary", {
unavailable: [...skip, ...plan.unavailable],
checkedAt: now.toISOString(),
checkedAt: planCheckedAtFor(sleepPlan, reprobed, now),
});
}
} catch (err) {
+2 -1
View File
@@ -18,6 +18,7 @@ import {
isPlanReprobeDue,
parseWithingsApiPlanState,
persistWithingsPlanLeg,
planCheckedAtFor,
planSkipFor,
requestWithinPlan,
shouldPersistPlanLeg,
@@ -284,7 +285,7 @@ export async function syncUserMeasurements(
try {
await persistWithingsPlanLeg(userId, "measures", {
unavailable: [...skip, ...plan.unavailable].sort((a, b) => a - b),
checkedAt: now.toISOString(),
checkedAt: planCheckedAtFor(planState.measures, reprobed, now),
});
} catch (err) {
// Losing the memory only costs a refused round trip next run.