Skip to content

Commit ff011cd

Browse files
betegonclaude
andcommitted
refactor(init): convert readFiles, fileExistsBatch, applyPatchset to async
Use fs.promises throughout for non-blocking I/O. readFiles and fileExistsBatch now process paths in parallel via Promise.all. applyPatchset remains sequential since patches may depend on prior creates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a30448e commit ff011cd

File tree

1 file changed

+111
-78
lines changed

1 file changed

+111
-78
lines changed

src/lib/init/local-ops.ts

Lines changed: 111 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -436,59 +436,82 @@ async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
436436
return { ok: true, data: { entries } };
437437
}
438438

439-
function readFiles(payload: ReadFilesPayload): LocalOpResult {
440-
const { cwd, params } = payload;
441-
const maxBytes = params.maxBytes ?? MAX_FILE_BYTES;
442-
const files: Record<string, string | null> = {};
443-
444-
for (const filePath of params.paths) {
445-
try {
446-
const absPath = safePath(cwd, filePath);
447-
const stat = fs.statSync(absPath);
448-
let content: string;
449-
if (stat.size > maxBytes) {
450-
// Read only up to maxBytes
439+
async function readSingleFile(
440+
cwd: string,
441+
filePath: string,
442+
maxBytes: number
443+
): Promise<string | null> {
444+
try {
445+
const absPath = safePath(cwd, filePath);
446+
const stat = await fs.promises.stat(absPath);
447+
let content: string;
448+
if (stat.size > maxBytes) {
449+
const fh = await fs.promises.open(absPath, "r");
450+
try {
451451
const buffer = Buffer.alloc(maxBytes);
452-
const fd = fs.openSync(absPath, "r");
453-
try {
454-
fs.readSync(fd, buffer, 0, maxBytes, 0);
455-
} finally {
456-
fs.closeSync(fd);
457-
}
452+
await fh.read(buffer, 0, maxBytes, 0);
458453
content = buffer.toString("utf-8");
459-
} else {
460-
content = fs.readFileSync(absPath, "utf-8");
454+
} finally {
455+
await fh.close();
461456
}
457+
} else {
458+
content = await fs.promises.readFile(absPath, "utf-8");
459+
}
462460

463-
// Minify JSON files by stripping whitespace/formatting
464-
if (filePath.endsWith(".json")) {
465-
try {
466-
content = JSON.stringify(JSON.parse(content));
467-
} catch {
468-
// Not valid JSON (truncated, JSONC, etc.) — send as-is
469-
}
461+
// Minify JSON files by stripping whitespace/formatting
462+
if (filePath.endsWith(".json")) {
463+
try {
464+
content = JSON.stringify(JSON.parse(content));
465+
} catch {
466+
// Not valid JSON (truncated, JSONC, etc.) — send as-is
470467
}
471-
472-
files[filePath] = content;
473-
} catch {
474-
files[filePath] = null;
475468
}
469+
470+
return content;
471+
} catch {
472+
return null;
473+
}
474+
}
475+
476+
async function readFiles(payload: ReadFilesPayload): Promise<LocalOpResult> {
477+
const { cwd, params } = payload;
478+
const maxBytes = params.maxBytes ?? MAX_FILE_BYTES;
479+
480+
const results = await Promise.all(
481+
params.paths.map(async (filePath) => {
482+
const content = await readSingleFile(cwd, filePath, maxBytes);
483+
return [filePath, content] as const;
484+
})
485+
);
486+
487+
const files: Record<string, string | null> = {};
488+
for (const [filePath, content] of results) {
489+
files[filePath] = content;
476490
}
477491

478492
return { ok: true, data: { files } };
479493
}
480494

481-
function fileExistsBatch(payload: FileExistsBatchPayload): LocalOpResult {
495+
async function fileExistsBatch(
496+
payload: FileExistsBatchPayload
497+
): Promise<LocalOpResult> {
482498
const { cwd, params } = payload;
483-
const exists: Record<string, boolean> = {};
484499

485-
for (const filePath of params.paths) {
486-
try {
487-
const absPath = safePath(cwd, filePath);
488-
exists[filePath] = fs.existsSync(absPath);
489-
} catch {
490-
exists[filePath] = false;
491-
}
500+
const results = await Promise.all(
501+
params.paths.map(async (filePath) => {
502+
try {
503+
const absPath = safePath(cwd, filePath);
504+
await fs.promises.access(absPath);
505+
return [filePath, true] as const;
506+
} catch {
507+
return [filePath, false] as const;
508+
}
509+
})
510+
);
511+
512+
const exists: Record<string, boolean> = {};
513+
for (const [filePath, found] of results) {
514+
exists[filePath] = found;
492515
}
493516

494517
return { ok: true, data: { exists } };
@@ -626,24 +649,56 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
626649
* indentation style is detected and preserved. For `create` actions, a default
627650
* of 2-space indentation is used.
628651
*/
629-
function resolvePatchContent(
652+
async function resolvePatchContent(
630653
absPath: string,
631654
patch: ApplyPatchsetPayload["params"]["patches"][number]
632-
): string {
655+
): Promise<string> {
633656
if (!patch.path.endsWith(".json")) {
634657
return patch.patch;
635658
}
636659
if (patch.action === "modify") {
637-
const existing = fs.readFileSync(absPath, "utf-8");
660+
const existing = await fs.promises.readFile(absPath, "utf-8");
638661
return prettyPrintJson(patch.patch, detectJsonIndent(existing));
639662
}
640663
return prettyPrintJson(patch.patch, DEFAULT_JSON_INDENT);
641664
}
642665

643-
function applyPatchset(
666+
type Patch = ApplyPatchsetPayload["params"]["patches"][number];
667+
668+
const VALID_PATCH_ACTIONS = new Set(["create", "modify", "delete"]);
669+
670+
async function applySinglePatch(absPath: string, patch: Patch): Promise<void> {
671+
switch (patch.action) {
672+
case "create": {
673+
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
674+
const content = await resolvePatchContent(absPath, patch);
675+
await fs.promises.writeFile(absPath, content, "utf-8");
676+
break;
677+
}
678+
case "modify": {
679+
const content = await resolvePatchContent(absPath, patch);
680+
await fs.promises.writeFile(absPath, content, "utf-8");
681+
break;
682+
}
683+
case "delete": {
684+
try {
685+
await fs.promises.unlink(absPath);
686+
} catch (err) {
687+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
688+
throw err;
689+
}
690+
}
691+
break;
692+
}
693+
default:
694+
break;
695+
}
696+
}
697+
698+
async function applyPatchset(
644699
payload: ApplyPatchsetPayload,
645700
dryRun?: boolean
646-
): LocalOpResult {
701+
): Promise<LocalOpResult> {
647702
if (dryRun) {
648703
return applyPatchsetDryRun(payload);
649704
}
@@ -653,56 +708,34 @@ function applyPatchset(
653708
// Phase 1: Validate all paths and actions before writing anything
654709
for (const patch of params.patches) {
655710
safePath(cwd, patch.path);
656-
if (!["create", "modify", "delete"].includes(patch.action)) {
711+
if (!VALID_PATCH_ACTIONS.has(patch.action)) {
657712
return {
658713
ok: false,
659714
error: `Unknown patch action: "${patch.action}" for path "${patch.path}"`,
660715
};
661716
}
662717
}
663718

664-
// Phase 2: Apply patches
719+
// Phase 2: Apply patches (sequential — later patches may depend on earlier creates)
665720
const applied: Array<{ path: string; action: string }> = [];
666721

667722
for (const patch of params.patches) {
668723
const absPath = safePath(cwd, patch.path);
669724

670-
switch (patch.action) {
671-
case "create": {
672-
const dir = path.dirname(absPath);
673-
fs.mkdirSync(dir, { recursive: true });
674-
const content = resolvePatchContent(absPath, patch);
675-
fs.writeFileSync(absPath, content, "utf-8");
676-
applied.push({ path: patch.path, action: "create" });
677-
break;
678-
}
679-
case "modify": {
680-
if (!fs.existsSync(absPath)) {
681-
return {
682-
ok: false,
683-
error: `Cannot modify "${patch.path}": file does not exist`,
684-
data: { applied },
685-
};
686-
}
687-
const content = resolvePatchContent(absPath, patch);
688-
fs.writeFileSync(absPath, content, "utf-8");
689-
applied.push({ path: patch.path, action: "modify" });
690-
break;
691-
}
692-
case "delete": {
693-
if (fs.existsSync(absPath)) {
694-
fs.unlinkSync(absPath);
695-
}
696-
applied.push({ path: patch.path, action: "delete" });
697-
break;
698-
}
699-
default:
725+
if (patch.action === "modify") {
726+
try {
727+
await fs.promises.access(absPath);
728+
} catch {
700729
return {
701730
ok: false,
702-
error: `Unknown patch action: "${patch.action}" for path "${patch.path}"`,
731+
error: `Cannot modify "${patch.path}": file does not exist`,
703732
data: { applied },
704733
};
734+
}
705735
}
736+
737+
await applySinglePatch(absPath, patch);
738+
applied.push({ path: patch.path, action: patch.action });
706739
}
707740

708741
return { ok: true, data: { applied } };

0 commit comments

Comments
 (0)