mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
perf(analytics): keep closed blocks of past readings cached across syncs
Every sync marked all of an account's analytics cells stale, so the slice re-read the pre-fold remainder (every reading older than five years, folded per source and day, the slice's largest statement on a long history) and the day view re-read its past history blocks, although a sync of today's readings cannot change either. A cache key can now name the instant its value's readings end before (closedBlockKeyPart). invalidateUserMeasurements takes touchedFrom, the earliest measuredAt a write created, changed or removed, and leaves such blocks in place when it is not earlier; without it every block goes as before. The batch route passes it from the identities it wrote. The pre-fold remainder moves into its own module and is cached as a closed block; the day view's past history blocks carry their end, with a day of margin for a night across midnight. The batch route's two up-front reads move into batch-stored-rows.ts with the once-per-batch key rule they share.
This commit is contained in:
1 parent
901cd884e8
commit
f5d6710670
12 files changed
+493
-279
No files matched your search
Vendored
+11
-11
@@ -88,15 +88,15 @@ export interface ServerCacheStats {
|
||||
* fence — invalidation flips `invalidated` and detaches the slot from
|
||||
* `pending`, so (a) the builder's `.then` skips the `set()` and (b) the
|
||||
* next read starts a fresh build instead of joining the stale one.
|
||||
* Callers already awaiting the detached promise still receive its value
|
||||
* (equivalent to having read a moment before the write); it just never
|
||||
* enters the cache.
|
||||
* Callers already awaiting the detached promise still get its value (as
|
||||
* if read just before the write); it never enters the cache.
|
||||
*/
|
||||
interface PendingBuild<T> {
|
||||
promise: Promise<T>;
|
||||
invalidated: boolean;
|
||||
}
|
||||
|
||||
type KeyFilter = (key: string) => boolean; // closed blocks, `invalidate.ts`
|
||||
export class ServerCache<T> {
|
||||
private readonly map = new Map<
|
||||
string,
|
||||
@@ -124,9 +124,9 @@ export class ServerCache<T> {
|
||||
}
|
||||
|
||||
/** Fence + detach every in-flight builder under `prefix`. */
|
||||
private detachPendingByPrefix(prefix: string): void {
|
||||
private detachPendingByPrefix(prefix: string, spare?: KeyFilter): void {
|
||||
for (const [key, slot] of this.pending) {
|
||||
if (key.startsWith(prefix)) {
|
||||
if (key.startsWith(prefix) && !spare?.(key)) {
|
||||
slot.invalidated = true;
|
||||
this.pending.delete(key);
|
||||
}
|
||||
@@ -283,17 +283,17 @@ export class ServerCache<T> {
|
||||
* so the next read is a clean miss — identical to the old evict
|
||||
* behaviour).
|
||||
*/
|
||||
markStaleByPrefix(prefix: string): number {
|
||||
markStaleByPrefix(prefix: string, spare?: KeyFilter): number {
|
||||
// v1.16.9 — fence in-flight builders too: a refresh that started
|
||||
// before the write would otherwise complete after this mark and
|
||||
// re-cache the pre-write body as FRESH for the full TTL. Detaching
|
||||
// keeps the SWR contract — the marked entry still serves stale, and
|
||||
// the next read kicks off a fresh (post-write) recompute.
|
||||
this.detachPendingByPrefix(prefix);
|
||||
this.detachPendingByPrefix(prefix, spare);
|
||||
let marked = 0;
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of this.map) {
|
||||
if (key.startsWith(prefix)) {
|
||||
if (key.startsWith(prefix) && !spare?.(key)) {
|
||||
entry.expiresAt = now;
|
||||
marked += 1;
|
||||
}
|
||||
@@ -320,12 +320,12 @@ export class ServerCache<T> {
|
||||
* entire user-bucket from a cache without having to enumerate every
|
||||
* possible cache-key variant the user might have warmed.
|
||||
*/
|
||||
deleteByPrefix(prefix: string): number {
|
||||
deleteByPrefix(prefix: string, spare?: KeyFilter): number {
|
||||
// v1.16.9 — fence in-flight builders under the prefix (see `delete`).
|
||||
this.detachPendingByPrefix(prefix);
|
||||
this.detachPendingByPrefix(prefix, spare);
|
||||
let removed = 0;
|
||||
for (const key of this.map.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
if (key.startsWith(prefix) && !spare?.(key)) {
|
||||
const entry = this.map.get(key);
|
||||
this.map.delete(key);
|
||||
this.totalWeight -= entry?.weight ?? 0;
|
||||
|
||||
Reference in new issue
Block a user