Skip to content

Commit a1b1a09

Browse files
author
Eric Wheeler
committed
feat: implement streaming JSON write in safeWriteJson
Refactor safeWriteJson to use stream-json for memory-efficient JSON serialization: - Replace in-memory string creation with streaming pipeline - Add Disassembler and Stringer from stream-json library - Extract streaming logic to a dedicated helper function - Add proper-lockfile and stream-json dependencies This implementation reduces memory usage when writing large JSON objects. Signed-off-by: Eric Wheeler <[email protected]>
1 parent fa14820 commit a1b1a09

File tree

3 files changed

+132
-9
lines changed

3 files changed

+132
-9
lines changed

pnpm-lock.yaml

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,10 @@
361361
"@google/genai": "^0.13.0",
362362
"@mistralai/mistralai": "^1.3.6",
363363
"@modelcontextprotocol/sdk": "^1.9.0",
364+
"@qdrant/js-client-rest": "^1.14.0",
364365
"@roo-code/cloud": "workspace:^",
365366
"@roo-code/telemetry": "workspace:^",
366367
"@roo-code/types": "workspace:^",
367-
"@qdrant/js-client-rest": "^1.14.0",
368368
"@types/lodash.debounce": "^4.0.9",
369369
"@vscode/codicons": "^0.0.36",
370370
"async-mutex": "^0.5.0",
@@ -397,6 +397,7 @@
397397
"pdf-parse": "^1.1.1",
398398
"pkce-challenge": "^4.1.0",
399399
"pretty-bytes": "^6.1.1",
400+
"proper-lockfile": "^4.1.2",
400401
"ps-tree": "^1.2.0",
401402
"puppeteer-chromium-resolver": "^23.0.0",
402403
"puppeteer-core": "^23.4.0",
@@ -406,6 +407,7 @@
406407
"serialize-error": "^11.0.3",
407408
"simple-git": "^3.27.0",
408409
"sound-play": "^1.1.0",
410+
"stream-json": "^1.8.0",
409411
"string-similarity": "^4.0.4",
410412
"strip-ansi": "^7.1.0",
411413
"strip-bom": "^5.0.0",
@@ -435,7 +437,9 @@
435437
"@types/node": "20.x",
436438
"@types/node-cache": "^4.1.3",
437439
"@types/node-ipc": "^9.2.3",
440+
"@types/proper-lockfile": "^4.1.4",
438441
"@types/ps-tree": "^1.1.6",
442+
"@types/stream-json": "^1.7.8",
439443
"@types/string-similarity": "^4.0.2",
440444
"@types/tmp": "^0.2.6",
441445
"@types/turndown": "^5.0.5",

src/utils/safeWriteJson.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as fs from "fs/promises"
2+
import * as fsSync from "fs"
23
import * as path from "path"
34
import * as lockfile from "proper-lockfile"
5+
import Disassembler from "stream-json/Disassembler"
6+
import Stringer from "stream-json/Stringer"
47

58
/**
69
* Safely writes JSON data to a file.
@@ -13,12 +16,8 @@ import * as lockfile from "proper-lockfile"
1316
* @param {any} data - The data to serialize to JSON and write.
1417
* @returns {Promise<void>}
1518
*/
16-
async function safeWriteJson(
17-
filePath: string,
18-
data: any,
19-
replacer?: (key: string, value: any) => any,
20-
space: string | number = 2,
21-
): Promise<void> {
19+
20+
async function safeWriteJson(filePath: string, data: any): Promise<void> {
2221
const absoluteFilePath = path.resolve(filePath)
2322
const lockPath = `${absoluteFilePath}.lock`
2423
let releaseLock = async () => {} // Initialized to a no-op
@@ -59,8 +58,8 @@ async function safeWriteJson(
5958
path.dirname(absoluteFilePath),
6059
`.${path.basename(absoluteFilePath)}.new_${Date.now()}_${Math.random().toString(36).substring(2)}.tmp`,
6160
)
62-
const jsonData = JSON.stringify(data, replacer, space)
63-
await fs.writeFile(actualTempNewFilePath, jsonData, "utf8")
61+
62+
await _streamDataToFile(actualTempNewFilePath, data)
6463

6564
// Step 2: Check if the target file exists. If so, rename it to a backup path.
6665
try {
@@ -159,4 +158,58 @@ async function safeWriteJson(
159158
}
160159
}
161160

161+
/**
162+
* Helper function to stream JSON data to a file.
163+
* @param targetPath The path to write the stream to.
164+
* @param data The data to stream.
165+
* @returns Promise<void>
166+
*/
167+
async function _streamDataToFile(targetPath: string, data: any): Promise<void> {
168+
// Stream data to avoid high memory usage for large JSON objects.
169+
const fileWriteStream = fsSync.createWriteStream(targetPath, { encoding: "utf8" })
170+
const disassembler = Disassembler.disassembler()
171+
// Output will be compact JSON as standard Stringer is used.
172+
const stringer = Stringer.stringer()
173+
174+
return new Promise<void>((resolve, reject) => {
175+
let errorOccurred = false
176+
const handleError = (_streamName: string) => (err: Error) => {
177+
if (!errorOccurred) {
178+
errorOccurred = true
179+
if (!fileWriteStream.destroyed) {
180+
fileWriteStream.destroy(err)
181+
}
182+
reject(err)
183+
}
184+
}
185+
186+
disassembler.on("error", handleError("Disassembler"))
187+
stringer.on("error", handleError("Stringer"))
188+
fileWriteStream.on("error", (err: Error) => {
189+
if (!errorOccurred) {
190+
errorOccurred = true
191+
reject(err)
192+
}
193+
})
194+
195+
fileWriteStream.on("finish", () => {
196+
if (!errorOccurred) {
197+
resolve()
198+
}
199+
})
200+
201+
disassembler.pipe(stringer).pipe(fileWriteStream)
202+
203+
// stream-json's Disassembler might error if `data` is undefined.
204+
// JSON.stringify(undefined) would produce the string "undefined" if it's the root value.
205+
// Writing 'null' is a safer JSON representation for a root undefined value.
206+
if (data === undefined) {
207+
disassembler.write(null)
208+
} else {
209+
disassembler.write(data)
210+
}
211+
disassembler.end()
212+
})
213+
}
214+
162215
export { safeWriteJson }

0 commit comments

Comments
 (0)