Files
HealthLog/src/lib/export/deferred-rows.ts
T
Marc-André Bombeck 322652a86e refactor(backup): build the export from per-domain slices
buildFullBackupPayload now asks every domain for its slice at once and
lays the slices out in the file's key order (backup-export-sections.ts),
merging the manifest where it first appears. The measurement, medication,
mood, instance-settings and nutrient readers and serialisers move out of
the payload builder into their domain modules; the records section keeps
its composition and manifest, while labs, illness, allergies, family
history, workouts and documents each export from their own module. The
measurement restore half moves to measurements-restore.ts so the export
graph does not pull in the restore's dependencies.

The file, its counts and their order are unchanged; the golden copies
still match. The measurement completeness guard now reads the domain
module and also checks that every carried column is written back.
2026-10-22 07:46:02 +02:00

55 lines
2.1 KiB
TypeScript

/**
* A bulk table the payload declares but has NOT read.
*
* `buildFullBackupPayload({ deferBulk: true })` puts one of these where the
* row array would sit. The streaming writer recognises it, opens the JSON
* array itself and pulls rows through page by page, so a table with half a
* million rows never exists as an array at all. Every other caller leaves
* `deferBulk` unset and gets the arrays, unchanged.
*
* The marker key is a symbol so it cannot collide with a payload field and so
* the deferred payload can never be handed to `JSON.stringify` by accident and
* come back looking merely empty — `streamFullBackupJson` is the only reader.
*/
const DEFERRED_ROWS = Symbol("healthlog.backup.deferredRows");
export interface DeferredRows {
readonly [DEFERRED_ROWS]: true;
/**
* Open the row source. Called exactly once, by the streaming writer. A row
* is a plain object, or a `StreamedField` (`streamed-field.ts`) for a row
* with one field too large to hold as a string.
*/
rows(): AsyncIterable<unknown>;
}
export function deferredRows(rows: () => AsyncIterable<unknown>): DeferredRows {
return { [DEFERRED_ROWS]: true, rows };
}
/** True when a payload value is a deferred bulk table rather than an array. */
export function isDeferredRows(value: unknown): value is DeferredRows {
return (
typeof value === "object" &&
value !== null &&
(value as Record<symbol, unknown>)[DEFERRED_ROWS] === true
);
}
/**
* How many rows one page of a bulk table carries.
*
* Smaller than the measurement pager's 10 000 on purpose: an intake event
* drags its medication's name along and a mood entry drags its links and its
* day context, so a page of either costs several times what a page of
* measurements costs. The number is a memory budget, not a throughput knob.
*/
export const BULK_PAGE_SIZE = 5_000;
/** Drain an async row source into an array. The materialising arm's only use. */
export async function collect<T>(rows: AsyncIterable<T>): Promise<T[]> {
const out: T[] = [];
for await (const row of rows) out.push(row);
return out;
}