Skip to content
Draft
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
16 changes: 16 additions & 0 deletions .changeset/soften-await-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"oxlint-plugin-react-doctor": patch
"react-doctor": patch
---

Soften await parallelization messaging to acknowledge uncertainty

The `server-sequential-independent-await` and `async-await-in-loop` rules now use conditional language that acknowledges parallelization only helps when work is truly independent. The messages explicitly mention the requirements: no shared queues, transactions, ordering constraints, or synchronous work behind async facades.

**Changed messages:**
- `server-sequential-independent-await`: Now says "These awaits appear independent, but parallelization only improves performance when work doesn't share queues, transactions, or resources. Verify independence, then consider \`Promise.all([...])\`."
- `async-await-in-loop`: Now says "This loop runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Verify before applying..."

**Detection unchanged:** The rules still fire on the same patterns. No new exemptions were added. The fix addresses messaging accuracy, not detection scope.

Fixes #1840
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@
"id": "async-await-in-loop",
"title": "await inside a loop",
"severity": "warn",
"recommendation": "Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",
"recommendation": "If the work is truly independent (no shared queues, transactions, ordering requirements, or synchronous facades), consider parallelizing with `await Promise.all(items.map(...))`. Verify independence before applying.",
"category": "Performance",
"framework": "global",
"tags": ["test-noise"],
Expand Down Expand Up @@ -12388,7 +12388,7 @@
"id": "server-sequential-independent-await",
"title": "Sequential independent awaits",
"severity": "warn",
"recommendation": "These two awaits don't depend on each other. Wrap them in `Promise.all([...])` so they run at the same time.",
"recommendation": "If these awaits perform truly independent asynchronous work (no shared queues, transactions, or resources), wrap them in `Promise.all([...])` to run them concurrently. Verify independence before applying.",
"category": "Bugs",
"framework": "global",
"tags": ["server-action", "test-noise"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -871,4 +871,46 @@ describe("js-performance/async-await-in-loop — regressions", () => {
expect(result.parseErrors).toEqual([]);
expect(result.diagnostics.length).toBeGreaterThan(0);
});

it("flags serialized-queue method calls where syntactic independence cannot prove runtime serialization (#1840)", () => {
const result = runRule(
asyncAwaitInLoop,
`class Repository {
private tail: Promise<void> = Promise.resolve();
private serialize<T>(operation: () => Promise<T>): Promise<T> {
const result = this.tail.then(operation, operation);
this.tail = result.then(() => undefined, () => undefined);
return result;
}
runAsync(id: string): Promise<void> {
return this.serialize(async () => { this.writes.push(id); });
}
}
async function writeBatch(repository: Repository, ids: string[]): Promise<void> {
for (const id of ids) {
await repository.runAsync(id);
}
}`,
);
expect(result.parseErrors).toEqual([]);
expect(result.diagnostics.length).toBeGreaterThan(0);
});

it("flags async facades over synchronous work where syntactic await cannot prove asynchrony (#1840)", () => {
const result = runRule(
asyncAwaitInLoop,
`const files = new Set(['a', 'b']);
const cache = { exists: async (path: string) => files.has(path) };
async function readExisting(paths: string[]): Promise<string[]> {
const found: string[] = [];
for (const path of paths) {
if (!(await cache.exists(path))) continue;
found.push(path);
}
return found;
}`,
);
expect(result.parseErrors).toEqual([]);
expect(result.diagnostics.length).toBeGreaterThan(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ export const asyncAwaitInLoop = defineRule({
severity: "warn",
tags: ["test-noise"],
recommendation:
"Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",
"If the work is truly independent (no shared queues, transactions, ordering requirements, or synchronous facades), consider parallelizing with `await Promise.all(items.map(...))`. Verify independence before applying.",
create: (context: RuleContext) => {
let hasTestLibraryImport = false;
const inspectLoop = (
Expand Down Expand Up @@ -1345,7 +1345,7 @@ export const asyncAwaitInLoop = defineRule({
if (firstAwait) {
context.report({
node: firstAwait,
message: `This makes the ${label} slow because each await runs one after another, so collect the independent calls & run them together with \`await Promise.all(items.map(...))\``,
message: `This ${label} runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Verify before applying \`await Promise.all(items.map(...))\`.`,
});
}
};
Expand Down Expand Up @@ -1399,7 +1399,7 @@ export const asyncAwaitInLoop = defineRule({
const message =
methodName === "forEach"
? "Async callback in .forEach silently drops every await, so the work never finishes before the loop moves on. Use a `for…of` loop, or `await Promise.all(items.map(async (item) => {...}))`"
: `Async callback in .${methodName} runs the awaits one after another, so it is slow. Use \`await Promise.all(items.map(async (item) => {...}))\` to run them at the same time`;
: `Async callback in .${methodName} runs awaits sequentially. Parallelization may improve performance if work is truly independent (no shared queues, transactions, or ordering requirements). Consider \`await Promise.all(items.map(async (item) => {...}))\`.`;
context.report({ node: firstAwait, message });
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,45 @@ export default async function Page() {
const user = await fetchUser();
const posts = await fetchPosts();
return null;
}`,
);
expect(result.parseErrors).toEqual([]);
expect(result.diagnostics.length).toBeGreaterThan(0);
});

it("flags serialized-queue method calls where syntactic independence cannot prove runtime independence (#1840)", () => {
const result = runRule(
serverSequentialIndependentAwait,
`class Repository {
private tail: Promise<void> = Promise.resolve();
private serialize<T>(operation: () => Promise<T>): Promise<T> {
const result = this.tail.then(operation, operation);
this.tail = result.then(() => undefined, () => undefined);
return result;
}
private runSqliteRead(kind: string): Promise<string[]> {
return this.serialize(async () => [kind]);
}
async readSnapshot(): Promise<string[]> {
const collectionRows = await this.runSqliteRead('collection');
const binderRows = await this.runSqliteRead('binders');
return [...collectionRows, ...binderRows];
}
}`,
);
expect(result.parseErrors).toEqual([]);
expect(result.diagnostics.length).toBeGreaterThan(0);
});

it("flags async facades over synchronous work where syntactic await cannot prove asynchrony (#1840)", () => {
const result = runRule(
serverSequentialIndependentAwait,
`const files = new Set(['a', 'b']);
const cache = { exists: async (path: string) => files.has(path) };
async function readMetadata(): Promise<string[]> {
const hasConfig = await cache.exists('config.json');
const hasManifest = await cache.exists('manifest.json');
return [hasConfig ? 'config' : '', hasManifest ? 'manifest' : ''].filter(Boolean);
}`,
);
expect(result.parseErrors).toEqual([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export const serverSequentialIndependentAwait = defineRule({
severity: "warn",
tags: ["test-noise"],
recommendation:
"These two awaits don't depend on each other. Wrap them in `Promise.all([...])` so they run at the same time.",
"If these awaits perform truly independent asynchronous work (no shared queues, transactions, or resources), wrap them in `Promise.all([...])` to run them concurrently. Verify independence before applying.",
create: (context: RuleContext) => {
const inspectStatements = (statements: EsTreeNode[]): void => {
for (let statementIndex = 0; statementIndex < statements.length - 1; statementIndex++) {
Expand Down Expand Up @@ -242,7 +242,7 @@ export const serverSequentialIndependentAwait = defineRule({
context.report({
node: nextStatement,
message:
"This await doesn't use the previous result, so your users wait twice as long for nothing.",
"These awaits appear independent, but parallelization only improves performance when work doesn't share queues, transactions, or resources. Verify independence, then consider `Promise.all([...])`.",
});
// Skip past the next so we don't double-report a chain.
statementIndex++;
Expand Down
Loading