Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"docusaurus": "docusaurus",
"start": "npm run generate-icon-types && docusaurus start --no-minify",
"start:current": "npm run generate-icon-types && cross-env DOCUSAURUS_VERSIONS='current' docusaurus start --no-minify",
"build": "npm run generate-icon-types && node --max-old-space-size=8192 --require ./scripts/patch-fs.cjs ./node_modules/@docusaurus/core/bin/docusaurus.mjs build && tsx scripts/split-sitemap.ts && node scripts/generate-robots.js",
"build:current": "npm run generate-icon-types && cross-env DOCUSAURUS_VERSIONS='current' node --max-old-space-size=8192 --require ./scripts/patch-fs.cjs ./node_modules/@docusaurus/core/bin/docusaurus.mjs build && tsx scripts/split-sitemap.ts && node scripts/generate-robots.js",
"build": "npm run generate-icon-types && node --max-old-space-size=8192 --require ./scripts/limit-docusaurus-file-reads.cjs ./node_modules/@docusaurus/core/bin/docusaurus.mjs build && tsx scripts/split-sitemap.ts && node scripts/generate-robots.js",
"build:current": "npm run generate-icon-types && cross-env DOCUSAURUS_VERSIONS='current' node --max-old-space-size=8192 --require ./scripts/limit-docusaurus-file-reads.cjs ./node_modules/@docusaurus/core/bin/docusaurus.mjs build && tsx scripts/split-sitemap.ts && node scripts/generate-robots.js",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand All @@ -32,6 +32,8 @@
"@mdx-js/react": "^3.0.0",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"image-size": "^2.0.2",
"js-yaml": "^4.3.0",
"motion": "^12.38.0",
"prettier": "^3.6.2",
"prism-react-renderer": "^2.3.0",
Expand Down
61 changes: 61 additions & 0 deletions scripts/limit-docusaurus-file-reads.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { createRequire } = require("node:module");

// Resolve through @docusaurus/core, which declares fs-extra directly, instead
// of relying on it being hoisted as an undeclared project dependency.
const requireFromDocusaurus = createRequire(require.resolve("@docusaurus/core/package.json"));
const fsExtra = requireFromDocusaurus("fs-extra");

// Docusaurus has two independent, unbounded read fan-outs: loading Markdown
// across all versions and checking generated route data. Together they can
// exceed Node's Windows open-file ceiling. Bound only the promise-based reads;
// version loading, parsing, route generation, and non-Windows builds stay fully
// parallel. 256 keeps a wide margin below the platform's failure point.
const MAX_CONCURRENT_FILE_READS = 256;

function installReadFileSemaphore(fs, maxConcurrent) {
const originalReadFile = fs.readFile;
const waiters = [];
let availablePermits = maxConcurrent;

function acquire() {
if (availablePermits > 0) {
availablePermits -= 1;
return;
}

return new Promise((resolve) => waiters.push(resolve));
}

function release() {
const next = waiters.shift();
if (next) {
next();
} else {
availablePermits += 1;
}
}

async function readWithPermit(receiver, args) {
await acquire();
try {
return await originalReadFile.apply(receiver, args);
} finally {
release();
}
}

fs.readFile = function readFileWithSemaphore(...args) {
// Preserve callback-style calls; Docusaurus uses the promise form.
if (typeof args[args.length - 1] === "function") {
return originalReadFile.apply(this, args);
}

return readWithPermit(this, args);
};
}

if (process.platform === "win32") {
installReadFileSemaphore(fsExtra, MAX_CONCURRENT_FILE_READS);
}

module.exports = { installReadFileSemaphore };
5 changes: 0 additions & 5 deletions scripts/patch-fs.cjs

This file was deleted.

64 changes: 64 additions & 0 deletions src/lib/read-file-semaphore/__tests__/read-file-semaphore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test } from "node:test";
import assert from "node:assert/strict";

type Callback = (error: null, result: number) => void;
type FileSystem = {
offset: number;
readFile: {
(value: number): Promise<number>;
(value: number, callback: Callback): string;
};
};

const { installReadFileSemaphore } = require("../../../../scripts/limit-docusaurus-file-reads.cjs") as {
installReadFileSemaphore: (fs: { readFile: unknown }, maxConcurrent: number) => void;
};

test("installReadFileSemaphore limits promise reads and preserves callback reads", async () => {
let active = 0;
let peak = 0;

const readFile = function (this: FileSystem, value: number, callback?: Callback): Promise<number> | string {
if (callback) {
setImmediate(() => callback(null, value + this.offset));
return "callback-return";
}

return new Promise<number>((resolve) => {
active += 1;
peak = Math.max(peak, active);
setImmediate(() => {
active -= 1;
resolve(value + this.offset);
});
});
} as FileSystem["readFile"];

const fs: FileSystem = { offset: 10, readFile };
installReadFileSemaphore(fs, 2);

const results = await Promise.all(Array.from({ length: 8 }, (_, index) => fs.readFile(index)));
assert.equal(peak, 2);
assert.deepEqual(results, [10, 11, 12, 13, 14, 15, 16, 17]);

const callbackResult = await new Promise<number>((resolve) => {
const returnValue = fs.readFile(5, (_error, result) => resolve(result));
assert.equal(returnValue, "callback-return");
});
assert.equal(callbackResult, 15);
});

test("installReadFileSemaphore releases a permit after a rejected read", async () => {
const expectedError = new Error("expected");
const fs = {
readFile(value: number) {
return value === 1 ? Promise.reject(expectedError) : Promise.resolve(value);
},
};

installReadFileSemaphore(fs, 1);

const results = await Promise.allSettled([fs.readFile(1), fs.readFile(2)]);
assert.equal(results[0].status, "rejected");
assert.deepEqual(results[1], { status: "fulfilled", value: 2 });
});
Loading