Skip to content

Commit b6cd1af

Browse files
Eric Wheelerdaniel-lxs
authored andcommitted
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 1bca55e commit b6cd1af

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

src/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@
410410
"pdf-parse": "^1.1.1",
411411
"pkce-challenge": "^5.0.0",
412412
"pretty-bytes": "^7.0.0",
413+
"proper-lockfile": "^4.1.2",
413414
"ps-tree": "^1.2.0",
414415
"puppeteer-chromium-resolver": "^24.0.0",
415416
"puppeteer-core": "^23.4.0",
@@ -419,6 +420,7 @@
419420
"serialize-error": "^12.0.0",
420421
"simple-git": "^3.27.0",
421422
"sound-play": "^1.1.0",
423+
"stream-json": "^1.8.0",
422424
"string-similarity": "^4.0.4",
423425
"strip-ansi": "^7.1.0",
424426
"strip-bom": "^5.0.0",
@@ -446,7 +448,9 @@
446448
"@types/node": "20.x",
447449
"@types/node-cache": "^4.1.3",
448450
"@types/node-ipc": "^9.2.3",
451+
"@types/proper-lockfile": "^4.1.4",
449452
"@types/ps-tree": "^1.1.6",
453+
"@types/stream-json": "^1.7.8",
450454
"@types/string-similarity": "^4.0.2",
451455
"@types/tmp": "^0.2.6",
452456
"@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)