Skip to content

fix: Avoid swap-file races when multiple plugin instances share a tadaOutputLocation#420

Open
macrozone wants to merge 1 commit into
0no-co:mainfrom
macrozone:fix/swap-write-race
Open

fix: Avoid swap-file races when multiple plugin instances share a tadaOutputLocation#420
macrozone wants to merge 1 commit into
0no-co:mainfrom
macrozone:fix/swap-write-race

Conversation

@macrozone

Copy link
Copy Markdown

Summary

Fixes 0no-co/gql.tada#571.

When several TS projects configure the plugin with the same tadaOutputLocation (e.g. a monorepo where ~10 tsconfigs extend one shared fragment), each plugin instance regenerates the same typings file. swapWrite used a single shared swap-file path (<output>.tmp) for all of them, so concurrent regenerations raced:

  1. Instance A and B both write <output>.d.ts.tmp.
  2. A renames it into place.
  3. B's rename fails with ENOENT, and its clean-up unlink throws ENOENT as well — which is the error that surfaces:
Failed to write the typings file to "<...>/api-env.d.ts":
ENOENT: no such file or directory, unlink '<...>/api-env.d.ts.tmp'

After that, the in-editor tada types stay stale/broken until a manual CLI regeneration + TS server restart.

Fix

  • The swap-file name is now unique per process and write (<output>.<pid>.<n>.tmp), covering both separate tsserver processes and multiple plugin instances within one process. rename stays atomic, so concurrent writers simply last-write-win without ever leaving a broken output.
  • Swap-file clean-up in the rename error path is best-effort (.catch(() => {})), so a genuine rename failure is reported instead of being masked by the unlink error.

Test

Added test/unit/swapWrite.test.ts with a 20-way concurrent write test. Against the previous implementation it reproduces the exact error from the issue:

Caused by: Error: ENOENT: no such file or directory, unlink '.../introspection.d.ts.tmp'

With the fix, all writers resolve, the target contains one full write, and no .tmp files are left behind.

🤖 Generated with Claude Code

…aOutputLocation

Multiple TS projects (e.g. in a monorepo extending a shared tsconfig)
can point their plugin instances at the same tadaOutputLocation. All
instances used the same swap-file path ('<output>.tmp'), so concurrent
regenerations raced: one instance renames the swap-file into place
while another still expects it, failing its rename and then throwing
ENOENT from the clean-up unlink — leaving the typings stale until a
manual regeneration.

The swap-file name is now unique per process and write, and clean-up
no longer masks the original rename error.

Fixes 0no-co/gql.tada#571

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f24debb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@0no-co/graphqlsp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@macrozone

Copy link
Copy Markdown
Author

thx fable...

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left one small note.

// If the file exists, we write to a swap-file, then rename (i.e. move)
// the file into place. No try-catch around `writeFile` for proper
// directory/permission errors
const tempTarget = target + '.tmp';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still races on the first write: if the output doesn't exist yet, concurrent instances all take this direct writeFile path. Can we use the unique swap+rename path for missing targets too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ts-plugin: ENOENT unlink '<output>.d.ts.tmp' when multiple TS projects share one tadaOutputLocation (monorepo)

2 participants