mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(healthkit): count a type as arrived only when the batch brought something new
A batch that carried a type with duplicates only moved its lastReceivedAt, lastTrigger and lastBackgroundAt, so a type whose live delivery had stalled kept reading as freshly arrived for as long as a replay, a retry or an older client re-sent its old samples. The arrival ledger now moves those stamps only for a type the batch brought at least one new or changed row for. An unchanged stats: re-post and an hour posted again with its stored value count as duplicates here; the wire keeps answering updated for them. A duplicates-only type stamps the new last_seen_duplicate_at column on its existing row and nothing else. The intake bulk route's dose entry follows the same rule through the shared recorder. lastSyncedAt and healthKitLastSyncedAt are unchanged. GET /api/integrations/healthkit serves the stamp additively as metricFreshness[].lastSeenDuplicateAt, and the Apple Health card names a re-send that came after the last arrival. Refs #1259
This commit is contained in:
1 parent
703c92c776
commit
6a2247e422
25 files changed
+547
-84
No files matched your search
@@ -617,6 +617,7 @@ describe("GET /api/integrations/healthkit — sync health", () => {
|
||||
lastNewSampleAt: null,
|
||||
lastBackgroundAt: backgroundAt.toISOString(),
|
||||
lastBackfillAt: null,
|
||||
lastSeenDuplicateAt: null,
|
||||
});
|
||||
expect(byType.PULSE).toMatchObject({ lastBackgroundAt: null });
|
||||
});
|
||||
|
||||
@@ -657,6 +657,11 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
* and in nothing that starts work.
|
||||
*/
|
||||
let unchangedStatsCount = 0;
|
||||
/**
|
||||
* The `stats:` re-posts answered `updated` without a write because nothing
|
||||
* changed. Not an arrival of their type (#1259).
|
||||
*/
|
||||
const unchangedStatsRows = new Set<Prepared>();
|
||||
let duplicateCount = 0;
|
||||
let failedCount = 0;
|
||||
// v1.44 — subset of `insertedCount` written by the set-based insert.
|
||||
@@ -1050,6 +1055,7 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
if (statsRow && isUnchangedStatsRow(p)) {
|
||||
results[p.index] = { index: p.index, status: "updated" };
|
||||
unchangedStatsCount++;
|
||||
unchangedStatsRows.add(p);
|
||||
pushMergeCandidate(p);
|
||||
continue;
|
||||
}
|
||||
@@ -1103,6 +1109,8 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
let hourInsertedCount = 0;
|
||||
let hourUpdatedCount = 0;
|
||||
const hourLanded: HourBucketWrite[] = [];
|
||||
/** The landed hours that wrote a new or different value (#1259). */
|
||||
const hourChanged = new Set<HourBucketWrite>();
|
||||
if (hourWrites.length > 0 && accountTz) {
|
||||
const { writes, superseded } = combineHourWrites(hourWrites);
|
||||
for (const index of superseded) {
|
||||
@@ -1124,11 +1132,17 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
for (const { write, contributors } of writes) {
|
||||
const status = statuses.get(write.index) ?? "inserted";
|
||||
for (const index of contributors) {
|
||||
results[index] = { index, status };
|
||||
// An hour posted again with the same value reads `updated` on the
|
||||
// wire, as before; only the arrival ledger tells the two apart.
|
||||
results[index] = {
|
||||
index,
|
||||
status: status === "inserted" ? "inserted" : "updated",
|
||||
};
|
||||
if (status === "inserted") hourInsertedCount++;
|
||||
else hourUpdatedCount++;
|
||||
}
|
||||
hourLanded.push(write);
|
||||
if (status !== "unchanged") hourChanged.add(write);
|
||||
}
|
||||
} catch (err) {
|
||||
for (const { contributors } of writes) {
|
||||
@@ -1211,7 +1225,11 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
syncTrigger === "background" || syncTrigger === "push";
|
||||
// v1.42 (#1173) — what this sync brought per type, for the per-type
|
||||
// freshness list on the Apple Health card. Apple Health rows only, and
|
||||
// under the same conditions as the checkpoint beside it.
|
||||
// under the same conditions as the checkpoint beside it. Since #1259 only
|
||||
// a new or changed row makes a type arrive: a duplicate, an unchanged
|
||||
// `stats:` re-post and an hour posted again with its stored value are
|
||||
// carried (`accepted`) but bring nothing (`newSamples`), so a replay or a
|
||||
// retry cannot make a stalled type look live.
|
||||
const arrivals = new Map<MeasurementType, HealthKitTypeArrival>();
|
||||
for (const p of prepared) {
|
||||
if (p.row.source !== "APPLE_HEALTH") continue;
|
||||
@@ -1226,14 +1244,17 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
const type = p.row.type as MeasurementType;
|
||||
const entry = arrivals.get(type) ?? { accepted: 0, newSamples: 0 };
|
||||
entry.accepted += 1;
|
||||
if (status !== "duplicate") entry.newSamples += 1;
|
||||
if (status !== "duplicate" && !unchangedStatsRows.has(p)) {
|
||||
entry.newSamples += 1;
|
||||
}
|
||||
arrivals.set(type, entry);
|
||||
}
|
||||
// The hours the phone posted are an arrival of their type as well.
|
||||
// The hours the phone posted are carried by their type as well, and
|
||||
// bring something only when the slot's value changed.
|
||||
for (const w of hourLanded) {
|
||||
const entry = arrivals.get(w.type) ?? { accepted: 0, newSamples: 0 };
|
||||
entry.accepted += 1;
|
||||
entry.newSamples += 1;
|
||||
if (hourChanged.has(w)) entry.newSamples += 1;
|
||||
arrivals.set(w.type, entry);
|
||||
}
|
||||
if (arrivals.size > 0) {
|
||||
|
||||
@@ -1008,8 +1008,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
// trigger, when a sync last brought a new or changed dose, and when a
|
||||
// background sync last carried them. Entries sent with `source:
|
||||
// "APPLE_HEALTH"` only; a dose taken in the app is not an Apple Health
|
||||
// arrival. A replay answered `duplicate` still counts as an arrival, the
|
||||
// same rule as the batch route.
|
||||
// arrival. A batch of replays answered `duplicate` alone is not an arrival
|
||||
// (#1259): it stamps only `lastSeenDuplicateAt`, the same rule as the batch
|
||||
// route.
|
||||
const doseArrival = { accepted: 0, newSamples: 0 };
|
||||
for (const result of results) {
|
||||
if (entries[result.index]?.source !== "APPLE_HEALTH") continue;
|
||||
|
||||
Reference in new issue
Block a user