Skip to content

Commit 428e234

Browse files
committed
fix: concurrent file processing
1 parent 4668926 commit 428e234

File tree

4 files changed

+99
-61
lines changed

4 files changed

+99
-61
lines changed

src/cli/save-generation-result.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs';
22
import path from 'path';
33
import * as R from 'ramda';
44
import {ClientGenerationResultFile} from '../schema-to-typescript/config';
5+
import {lock} from '../utils/lock';
56

67
export async function saveGenerationResult({
78
files,
@@ -27,19 +28,21 @@ export async function saveGenerationResult({
2728
...files.map(async ({filename, data}) => {
2829
const fullFilename = path.resolve(outputDirPath, filename);
2930
try {
30-
let exists = false;
31-
try {
32-
const existingContent = await fs.promises.readFile(fullFilename, 'utf8');
33-
exists = true;
34-
if (existingContent === data) {
35-
console.log('[no change] ' + fullFilename);
36-
return;
31+
await lock(`file:${fullFilename}`, async () => {
32+
let exists = false;
33+
try {
34+
const existingContent = await fs.promises.readFile(fullFilename, 'utf8');
35+
exists = true;
36+
if (existingContent === data) {
37+
console.log('[no change] ' + fullFilename);
38+
return;
39+
}
40+
} catch (e) {
41+
// ok
3742
}
38-
} catch (e) {
39-
// ok
40-
}
41-
await fs.promises.writeFile(fullFilename, data);
42-
console.log(`[${exists ? 'updated' : 'created'}] ${fullFilename}`);
43+
await fs.promises.writeFile(fullFilename, data);
44+
console.log(`[${exists ? 'updated' : 'created'}] ${fullFilename}`);
45+
});
4346
} catch (e) {
4447
throw new Error(`Could not save file "${fullFilename}": ${e instanceof Error ? e.message : e}.`);
4548
}

src/schema-to-typescript/common/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function generateClient({
176176
logicalExpression(
177177
'??',
178178
memberExpression(memberExpression(thisExpression(), identifier('constructor')), identifier('name')),
179-
stringLiteral('name')
179+
stringLiteral(name)
180180
)
181181
),
182182
objectProperty(identifier('baseUrl'), stringLiteral(baseUrl ?? servers[0]?.url ?? defaultServerUrl)),

src/utils/lock.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const lockedKeys: Record<string, Promise<unknown>> = {};
2+
3+
export function lock<T>(keyName: string, callback: () => Promise<T>): Promise<T> {
4+
const promise = (lockedKeys[keyName] ?? Promise.resolve())
5+
.then(
6+
() => callback(),
7+
() => callback()
8+
)
9+
.finally(() => {
10+
if (lockedKeys[keyName] === promise) {
11+
delete lockedKeys[keyName];
12+
}
13+
});
14+
lockedKeys[keyName] = promise;
15+
return promise;
16+
}

src/utils/postprocess-files.ts

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import type {ESLint as ESLintClass} from 'eslint';
4+
import {lock} from './lock';
45
import {makeDir} from './make-dir';
56
import {
67
ClientGenerationResultFile,
78
CommonOpenApiClientGeneratorConfigPostprocess
89
} from '../schema-to-typescript/config';
910

11+
let eslintInstance: ESLintClass | null = null;
12+
13+
function getEslintInstance(): ESLintClass {
14+
if (eslintInstance === null) {
15+
// This is an optional dependency, so we require it here to avoid loading it when it's not needed.
16+
// eslint-disable-next-line @typescript-eslint/no-var-requires
17+
const {ESLint} = require('eslint') as {ESLint: typeof ESLintClass};
18+
eslintInstance = new ESLint({
19+
fix: true
20+
});
21+
}
22+
return eslintInstance;
23+
}
24+
1025
export async function postprocessFiles({
1126
files,
1227
config: {eslint: enableEslint} = {},
@@ -25,68 +40,72 @@ export async function postprocessFiles({
2540
directories.add(directory);
2641
}
2742

28-
for (const directory of Array.from(directories)) {
29-
try {
30-
await fs.stat(directory);
31-
} catch (_e) {
32-
const directoryBits = directory.split(path.sep);
33-
let currentDirectory = directoryBits.shift() || '/';
34-
for (;;) {
35-
try {
36-
await fs.stat(currentDirectory);
37-
} catch (e) {
38-
await makeDir(currentDirectory);
39-
directoriesToRemove.unshift(currentDirectory);
40-
}
41-
const subDirectory = directoryBits.shift();
42-
if (!subDirectory) {
43-
break;
43+
await lock('calc:directories', async () => {
44+
for (const directory of Array.from(directories)) {
45+
try {
46+
await fs.stat(directory);
47+
} catch (_e) {
48+
const directoryBits = directory.split(path.sep);
49+
let currentDirectory = directoryBits.shift() || '/';
50+
for (;;) {
51+
try {
52+
await fs.stat(currentDirectory);
53+
} catch (e) {
54+
await makeDir(currentDirectory);
55+
directoriesToRemove.unshift(currentDirectory);
56+
}
57+
const subDirectory = directoryBits.shift();
58+
if (!subDirectory) {
59+
break;
60+
}
61+
currentDirectory = path.join(currentDirectory, subDirectory);
4462
}
45-
currentDirectory = path.join(currentDirectory, subDirectory);
4663
}
4764
}
48-
}
65+
});
4966

67+
const eslint = getEslintInstance();
5068
try {
51-
// This is an optional dependency, so we require it here to avoid loading it when it's not needed.
52-
// eslint-disable-next-line @typescript-eslint/no-var-requires
53-
const {ESLint} = require('eslint') as {ESLint: typeof ESLintClass};
54-
const eslint = new ESLint({
55-
fix: true
56-
});
57-
5869
return await Promise.all(
5970
files.map(async (file) => {
6071
const filePath = path.resolve(outputDirPath, file.filename);
61-
let fileCreated = false;
62-
try {
72+
return await lock(`file:${filePath}`, async () => {
73+
let fileCreated = false;
6374
try {
64-
await fs.stat(filePath);
65-
} catch (_e) {
66-
await fs.writeFile(filePath, file.data);
67-
fileCreated = true;
68-
}
69-
const [result] = await eslint.lintText(file.data, {filePath});
70-
for (const message of result.messages) {
71-
if (message.fatal) {
72-
throw new Error(`Fatal ESLint error in ${file.filename}: ${message.message}`);
75+
try {
76+
await fs.stat(filePath);
77+
} catch (_e) {
78+
await fs.writeFile(filePath, file.data);
79+
fileCreated = true;
80+
}
81+
const [result] = await eslint.lintText(file.data, {filePath});
82+
for (const message of result.messages) {
83+
if (message.fatal) {
84+
throw new Error(`Fatal ESLint error in ${file.filename}: ${message.message}`);
85+
}
86+
}
87+
return {
88+
...file,
89+
data: result.output ?? file.data
90+
};
91+
} finally {
92+
if (fileCreated) {
93+
await fs.unlink(filePath);
7394
}
7495
}
75-
return {
76-
...file,
77-
data: result.output ?? file.data
78-
};
79-
} finally {
80-
if (fileCreated) {
81-
await fs.unlink(filePath);
82-
}
83-
}
96+
});
8497
})
8598
);
8699
} finally {
87-
for (const directory of directoriesToRemove) {
88-
await fs.rmdir(directory);
89-
}
100+
await lock('cleanup:directories', async () => {
101+
for (const directory of directoriesToRemove) {
102+
try {
103+
await fs.rmdir(directory);
104+
} catch (e) {
105+
// Ignore
106+
}
107+
}
108+
});
90109
}
91110
}
92111
return files;

0 commit comments

Comments
 (0)