Files changed during startup can be missed until restart
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript
- Domain
- tooling
Research direction
Start in packages/metro-file-map/src/index.js at FileMap.build(), then read WatchmanWatcher.js around the linked watcher setup and inspect the native watcher startup path. Reproduce the race with the provided script using both watcher implementations. Done means files changed during the crawl-to-watcher gap are detected without restarting, including the corresponding @expo/metro-file-map behavior.
Written by the indexing model from the issue text.
Description
A file created or modified while Metro is starting up can be missed entirely - it doesn't exist as far as Metro is concerned until the next restart. This happens with both Watchman and the native watcher.
FileMap.build() crawls, applies the delta, persists the cache and only then starts the watcher:
https://github.com/react/metro/blob/6dbe97956/packages/metro-file-map/src/index.js#L440-L501
Anything that changes after the crawl has read it but before the watcher is listening isn't seen by either.
With Watchman we get a clock from the crawl, but we don't pass it to the watcher - WatchmanWatcher asks for a fresh clock and subscribes since that, so the gap is crawl end to subscription:
The native watcher has no clock at all, and only reports events from when it starts.
It doesn't take anything exotic to hit this. runServer resolves before the file map is built, and anything that generates or copies files alongside metro start (codegen, tsc --watch, asset copying via concurrently) is racing it. I think that's what the original report in #404 was: assets copied by a --watch script started at the same time as the dev server, fixed by starting them separately.
Repro on [email protected], Node 22, macOS - write a new file every 5ms for the first 300ms, then request each one:
const Metro = require('metro');
const fs = require('fs');
const {getDefaultConfig, mergeConfig} = require('metro-config');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
(async () => {
fs.rmSync('late', {recursive: true, force: true});
fs.mkdirSync('late');
const config = mergeConfig(await getDefaultConfig(__dirname), {
watchFolders: [__dirname],
resetCache: true,
resolver: {useWatchman: process.env.WATCHMAN === '1'},
server: {port: 18404},
});
// Write a new file every 5ms for the first 300ms of startup
const t0 = Date.now();
const written = [];
const timer = setInterval(() => {
const t = Date.now() - t0;
if (t > 300) return clearInterval(timer);
fs.writeFileSync(`late/m${t}.js`, 'module.exports = 1;\n');
written.push(t);
}, 5);
const {httpServer} = await Metro.runServer(config);
await sleep(3000);
const missed = [];
for (const t of written) {
fs.writeFileSync('entry.js', `require('./late/m${t}.js');\n`);
await sleep(300);
const res = await fetch('http://localhost:18404/entry.bundle?platform=ios');
await res.text();
if (!res.ok) missed.push(t);
}
console.log(`t0=${t0} written=${written.length} missed=${missed.join(' ') || 'none'}`);
httpServer.close();
process.exit(0);
})();
On a ~2,900 file project, times in ms from t0, crawl and watcher start from DEBUG=Metro:Watcher*:
native crawl 9-30, watcher 35 missed: 23 28
native crawl 11-32, watcher 37 missed: 18
native crawl 12-31, watcher 36 missed: 24 29 36
watchman crawl 76-166, watcher 171 missed: 149 154 167 172 179 185 191 198 203 209 215 222
watchman crawl 76-166, watcher 171 missed: 155 168 173 179 184 190 196 202 208 213 220
watchman crawl 86-180, watcher 185 missed: 168 181 185 191 196 203 209 214 221 227 232 239 245
(The Watchman gap runs ~50ms past "watcher start" because that's logged before the clock + subscribe round trip.)
The windows are small here, but they scale with the crawl and with Watchman latency, and there's nothing to tell you a file was missed.
For Watchman the fix should be straightforward - subscribe since the crawl's clock, which #watch() already has. The native watcher is harder without a clock - I think we want to start it before the crawl, buffer its events, and replay them once the crawl result is applied. Replaying an event for a file the crawl already saw is harmless, since it's just an addOrModify with fresher metadata.
@expo/metro-file-map has the same ordering and the same fresh-clock subscription, so a fix will want porting there too.
- Dominant language
- JavaScript
- Stars
- 5.6k
- Forks
- 699
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 44
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from react/metro
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
react/metro#1927 · 1 comment ·
Maintainers usually reply within 1 day
-
[0.83.x] Backport micromatch removal and dotfile/multipart watcher fix togetherPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 4/5 3-5 days Newbie friendliness 45/100
Maintainers usually reply within 1 day
-
`dependencies is not iterable` / `Cannot read property 'reduce' of undefined`: `FileStore` serves zero-prefixed cache files as hitsPossibly taken @kkkhs claimed this 14 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Async chunk URLs drop the file extension, so .cjs async imports fail with "Requiring unknown module"Possibly taken @dasjideepak claimed this 9 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 74/100
react/metro#1959 · 2 comments · 2 reactions ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
react/metro#1928 · 2 comments · 2 reactions ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
naver/egjs-flicking#971 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
RADAR-base/radar-self-enrolment-ui#118 ·
Maintainers usually reply within 1 day
-
Renderer treats a sub-pixel width difference as a resize, which cancels the `motion()` entranceOpen
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
triage-needed
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
MicrosoftDocs/edge-developer#3970 ·
Maintainers usually reply within 1 day
-
agent/scanner hive/hosted-available-lke648397-260827-5n31 quality testing
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day