From 3ee3461f922f9bb8c8fa1dbaaba234b841a715d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 5 Sep 2026 20:23:08 +0200 Subject: [PATCH] chore(backup): drop the whole-string builder the streaming path replaced Nothing calls it any more: the weekly pass and the off-host job both walk the payload through the streaming writer. Keeping an exported function that builds the entire record as one string invites the next caller to reintroduce the memory profile this branch removes. --- src/lib/export/full-backup-payload.ts | 37 ------------------- src/lib/export/full-backup-stream.ts | 2 +- .../offhost-backup-streaming-memory.test.ts | 2 +- 3 files changed, 2 insertions(+), 39 deletions(-) diff --git a/src/lib/export/full-backup-payload.ts b/src/lib/export/full-backup-payload.ts index 6f1328b98..9aa30beb9 100644 --- a/src/lib/export/full-backup-payload.ts +++ b/src/lib/export/full-backup-payload.ts @@ -661,43 +661,6 @@ export function isDeferredRows(value: unknown): value is DeferredRows { (value as Record)[DEFERRED_ROWS] === true ); } - -/** - * The same payload, already serialised, with the object graph released. - * - * Built through the streaming writer rather than by stringifying a finished - * payload, so the object graph and the string never coexist: the three - * unbounded tables go past a page at a time and every other section is - * released as soon as its JSON exists. What is left is the answer itself, - * which a caller that wants a string necessarily holds. - * - * A caller that does NOT need the string — the weekly job, which only wants - * the compressed ciphertext — should go through `packBackupBlobStreaming` and - * `streamFullBackupJson` directly and never let the whole document exist at - * all. This function is for the callers that genuinely hand a JSON body on. - */ -export async function buildFullBackupJson( - prisma: PrismaClient, - userId: string, - options: FullBackupOptions = {}, -): Promise { - // Imported here rather than at the top of the file: the writer imports this - // module for `buildFullBackupPayload`, and a static edge in both directions - // is a cycle. The call is once per backup, so the cost is nil. - const { streamFullBackupJson } = - await import("@/lib/export/full-backup-stream"); - const pieces: string[] = []; - await streamFullBackupJson( - prisma, - userId, - (chunk) => { - pieces.push(chunk); - }, - options, - ); - return pieces.join(""); -} - /** * Build the canonical full-backup payload for `userId`. Portable exports omit * tombstones and document ciphertext; disaster-recovery payloads preserve diff --git a/src/lib/export/full-backup-stream.ts b/src/lib/export/full-backup-stream.ts index 16de554dd..c5b364aa0 100644 --- a/src/lib/export/full-backup-stream.ts +++ b/src/lib/export/full-backup-stream.ts @@ -1,7 +1,7 @@ /** * The full-backup payload, written out incrementally instead of built. * - * Why it exists. `buildFullBackupJson` has to hold two things at once that + * Why it exists. the old whole-string builder (removed with the streaming path) had to hold two things at once that * both scale with the record: the payload object graph, and the JSON string * `JSON.stringify` makes of it. On a seeded account of 445 000 measurements, * 30 000 mood entries and 60 000 intake events, that pair alone exhausts a diff --git a/tests/integration/offhost-backup-streaming-memory.test.ts b/tests/integration/offhost-backup-streaming-memory.test.ts index 4bb6a7bc4..c5193fd65 100644 --- a/tests/integration/offhost-backup-streaming-memory.test.ts +++ b/tests/integration/offhost-backup-streaming-memory.test.ts @@ -289,7 +289,7 @@ describe("off-host backup under a memory budget", () => { it("would not fit that budget if the object were materialised", async () => { const baseline = liveHeapBytes(); - // Exactly what the job did before, with the step `buildFullBackupJson` + // Exactly what the job did before, with the step the old whole-string builder took // takes internally spelled out: the payload graph, the JSON string it is // stringified into, then a whole-buffer gzip-and-encrypt pass, then a // single put of the finished buffer.