mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(cache): keep the closed-block key helpers in a leaf module
Readers that tag a cell as a closed block no longer import the invalidation module, which several tests replace wholesale. The cache route test counts the slice's pre-fold cell apart from the route's two cells, and the provenance test reads the shared pick's ORDER BY over its grouped rows.
This commit is contained in:
1 parent
4c86b68363
commit
83661319d2
7 files changed
+48
-36
No files matched your search
@@ -28,7 +28,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { closedBlockKeyPart } from "@/lib/cache/invalidate";
|
||||
import { closedBlockKeyPart } from "@/lib/cache/closed-block";
|
||||
import { caches, type ServerCache } from "@/lib/cache/server-cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { dayWeightedRowsSql, foldMeanSql } from "@/lib/measurements/day-mean";
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
closedBlockKeyPart,
|
||||
closedBlockUntouched,
|
||||
invalidateUserMeasurements,
|
||||
} from "../invalidate";
|
||||
import { closedBlockKeyPart, closedBlockUntouched } from "../closed-block";
|
||||
import { invalidateUserMeasurements } from "../invalidate";
|
||||
import { caches, type ServerCache } from "../server-cache";
|
||||
|
||||
const analytics = caches.analytics as ServerCache<{ v: number }>;
|
||||
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* The key part that marks an analytics entry as a closed block: a value that
|
||||
* depends only on readings measured before `before`. Such a block is the
|
||||
* same however many syncs land after it, as long as none of them touches a
|
||||
* reading that old, so a measurement write that says how far back it
|
||||
* reached (`touchedFrom`) leaves it in place. Every other invalidation, and
|
||||
* every write that does not say, drops it as before.
|
||||
*/
|
||||
export function closedBlockKeyPart(before: Date | number): string {
|
||||
const ms = typeof before === "number" ? before : before.getTime();
|
||||
return `before:${ms}`;
|
||||
}
|
||||
|
||||
const CLOSED_BLOCK_RE = /\|before:(\d+)(?:\||$)/;
|
||||
|
||||
/**
|
||||
* Whether `key` is a closed block that a write reaching back to
|
||||
* `touchedFrom` cannot have changed. Exported for its test.
|
||||
*/
|
||||
export function closedBlockUntouched(key: string, touchedFrom: Date): boolean {
|
||||
const match = CLOSED_BLOCK_RE.exec(key);
|
||||
return match !== null && Number(match[1]) <= touchedFrom.getTime();
|
||||
}
|
||||
Vendored
+1
-24
@@ -20,6 +20,7 @@
|
||||
|
||||
import { forgetCoverageProbe } from "@/lib/rollups/coverage-inflight";
|
||||
|
||||
import { closedBlockUntouched } from "./closed-block";
|
||||
import { caches } from "./server-cache";
|
||||
|
||||
/**
|
||||
@@ -48,30 +49,6 @@ export function invalidateUserDashboardSnapshot(userId: string): void {
|
||||
caches.analytics.deleteByPrefix(dashboardSnapshotCacheKey(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* The key part that marks an analytics entry as a closed block: a value that
|
||||
* depends only on readings measured before `before`. Such a block is the
|
||||
* same however many syncs land after it, as long as none of them touches a
|
||||
* reading that old, so a measurement write that says how far back it
|
||||
* reached (`touchedFrom`) leaves it in place. Every other invalidation, and
|
||||
* every write that does not say, drops it as before.
|
||||
*/
|
||||
export function closedBlockKeyPart(before: Date | number): string {
|
||||
const ms = typeof before === "number" ? before : before.getTime();
|
||||
return `before:${ms}`;
|
||||
}
|
||||
|
||||
const CLOSED_BLOCK_RE = /\|before:(\d+)(?:\||$)/;
|
||||
|
||||
/**
|
||||
* Whether `key` is a closed block that a write reaching back to
|
||||
* `touchedFrom` cannot have changed. Exported for its test.
|
||||
*/
|
||||
export function closedBlockUntouched(key: string, touchedFrom: Date): boolean {
|
||||
const match = CLOSED_BLOCK_RE.exec(key);
|
||||
return match !== null && Number(match[1]) <= touchedFrom.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate every cache that may reflect a user's measurement set.
|
||||
*
|
||||
|
||||
@@ -31,7 +31,7 @@ import type { MeasurementType } from "@/generated/prisma/enums";
|
||||
|
||||
import type { DayNotable } from "@/lib/day/contract";
|
||||
import { readLocalDailyStats, type DailyStats } from "@/lib/day/daily-stats";
|
||||
import { closedBlockKeyPart } from "@/lib/cache/invalidate";
|
||||
import { closedBlockKeyPart } from "@/lib/cache/closed-block";
|
||||
import { caches, type ServerCache } from "@/lib/cache/server-cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { startOfLocalDayKey } from "@/lib/tz/local-day";
|
||||
|
||||
@@ -115,10 +115,10 @@ describe("the provenance query orders like the canonical pick", () => {
|
||||
`ORDER BY date_trunc('day', "measured_at"), (${rank}), "source"::text`,
|
||||
),
|
||||
);
|
||||
// The shared pick groups to (type, day, source) first and orders the
|
||||
// groups by the same keys, the UTC day named `d`.
|
||||
expect(squash(canonicalMeasurementsCte(rank))).toContain(
|
||||
squash(
|
||||
`ORDER BY "type", date_trunc('day', "measured_at"), (${rank}), "source"::text`,
|
||||
),
|
||||
squash(`ORDER BY "type", d, (${rank}), "source"::text`),
|
||||
);
|
||||
// Values are bound, never spliced.
|
||||
expect(userId).toBe("u1");
|
||||
|
||||
@@ -117,8 +117,21 @@ function countBuildsPerKey(cache: {
|
||||
return builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* The slice's pre-fold remainder is a cell of its own in the same bucket (a
|
||||
* closed block, `pre-fold-remainder.ts`), built when the slice takes the
|
||||
* rollup path; it is counted apart from the route's two cells.
|
||||
*/
|
||||
function preFoldBuildsOf(builds: Map<string, number>, userId: string) {
|
||||
return [...builds.entries()]
|
||||
.filter(([k]) => k.startsWith(`${userId}|pre-fold-remainder|`))
|
||||
.reduce((n, [, c]) => n + c, 0);
|
||||
}
|
||||
|
||||
function buildsOf(builds: Map<string, number>, userId: string) {
|
||||
const entries = [...builds.entries()].filter(([k]) => k.startsWith(userId));
|
||||
const entries = [...builds.entries()].filter(
|
||||
([k]) => k.startsWith(userId) && !k.includes("|pre-fold-remainder|"),
|
||||
);
|
||||
return {
|
||||
defaultBody: entries
|
||||
.filter(([k]) => k.startsWith(`${userId}|default|`))
|
||||
@@ -230,7 +243,9 @@ describe("v1.4.34 IW-G — server-cache route integration", () => {
|
||||
summaries: 2,
|
||||
keys: 2,
|
||||
});
|
||||
expect(caches.analytics.stats().misses).toBe(4);
|
||||
expect(caches.analytics.stats().misses).toBe(
|
||||
4 + preFoldBuildsOf(builds, user.id),
|
||||
);
|
||||
|
||||
// Sanity: the body reflects the new write.
|
||||
const body = (await third.json()) as {
|
||||
|
||||
Reference in new issue
Block a user