Skip to content

Commit baf28ab

Browse files
robhoganfacebook-github-bot
authored andcommitted
Process files before adding them to FileSystem (re-land)
Summary: *This is a second attempt at D42846676 (1a81060), which was backed out due to bad Haste move handling - details in test plan.* `metro-file-map` takes raw crawl or watch results, "processes" them with a worker pool (compute hashes, read symlink targets), and makes metadata available to consumers. Previously, files would be added to the file `Map` before processing, and then additional metadata would be set, and finally (when watching) an event would be emitted. ## Problem This created a potential bug because files briefly exist, accessible in the `FileSystem` interface, in an intermediate state. Consider: 1. `foo.js` exists and has a SHA1 pre-calculated. 2. A change is made to `foo.js`, watcher backend emits a change event. 3. `FileSystem` is updated with the metadata of the unprocessed file, and an async worker starts processing. 4. Metro receives a bundle request for `foo.js`. It [fails hard](https://github.com/facebook/metro/blob/main/packages/metro/src/node-haste/DependencyGraph.js#L250) because the file has no calculated hash. 5. The worker completes and we populate the SHA1, and emit a 'change' event. This also complicates `TreeFS`, because symlink targets aren't guaranteed to be available to it. ## This diff This diff switches things around so that the `FileSystem` implementation is only updated with ready-processed `FileMetaData`, and events are emitted to `metro-file-map` consumers synchronously with updates to `FileSystem`. That means `FileSystem` will expose stale, complete state instead of fresher, incomplete state. Besides the fix above, this is okay and indeed preferable, because: - Two calls to `getSha1()` for the same file are guaranteed to return the same result unless there's been a 'change' event emitted on that file in the meantime. This is much more predictable. - In any case, any file system representation is *always* potentially stale - I/O isn't instant, OS events take time to propagate, so well behaved consumers should already be treating it as such. ## Summary Currently we 1. Remove all deleted files from the Haste map and file map simultaneously. 2. Update new/changed files in the file map (with incomplete metadata). 3. "Process" new/changed files, updating the file map with complete metadata and adding entries to the module map. In this diff: 1. Remove all deleted files from the Haste map and file map simultaneously. 2. Process new/changed files, filling in gaps in metadata and adding entries to the module map. 3. Update new/changed files in the file map (with complete metadata). *(In D42846676 (1a81060), 1 and 2 were reversed)* Changelog: **[Fix]** Race condition where a very recently modified file might have missing metadata. Reviewed By: huntie Differential Revision: D42930236 fbshipit-source-id: 983af37bc8829ebc8b403483177211b578905ffc
1 parent 27c2112 commit baf28ab

4 files changed

Lines changed: 89 additions & 122 deletions

File tree

packages/metro-file-map/src/HasteFS.js

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
Glob,
1515
MutableFileSystem,
1616
Path,
17-
VisitMetadata,
1817
} from './flow-types';
1918

2019
import H from './constants';
@@ -51,33 +50,6 @@ export default class HasteFS implements MutableFileSystem {
5150
this.#files.set(this._normalizePath(filePath), metadata);
5251
}
5352

54-
setVisitMetadata(
55-
filePath: Path,
56-
visitResult: $ReadOnly<VisitMetadata>,
57-
): void {
58-
const metadata = this._getFileData(filePath);
59-
if (!metadata) {
60-
throw new Error('Visited file not found in file map: ' + filePath);
61-
}
62-
metadata[H.VISITED] = 1;
63-
64-
if (visitResult.hasteId != null) {
65-
metadata[H.ID] = visitResult.hasteId;
66-
}
67-
68-
if ('sha1' in visitResult) {
69-
metadata[H.SHA1] = visitResult.sha1;
70-
}
71-
72-
if (visitResult.dependencies != null) {
73-
metadata[H.DEPENDENCIES] = visitResult.dependencies;
74-
}
75-
76-
if (visitResult.symlinkTarget != null) {
77-
metadata[H.SYMLINK] = visitResult.symlinkTarget;
78-
}
79-
}
80-
8153
getSerializableSnapshot(): FileData {
8254
return new Map(
8355
Array.from(this.#files.entries(), ([k, v]: [Path, FileMetaData]) => [
@@ -97,16 +69,6 @@ export default class HasteFS implements MutableFileSystem {
9769
return (fileMetadata && fileMetadata[H.SIZE]) ?? null;
9870
}
9971

100-
getSymlinkTarget(file: Path): ?string {
101-
const fileMetadata = this._getFileData(file);
102-
if (fileMetadata == null) {
103-
return null;
104-
}
105-
return typeof fileMetadata[H.SYMLINK] === 'string'
106-
? fileMetadata[H.SYMLINK]
107-
: null;
108-
}
109-
11072
getType(file: Path): ?('f' | 'l') {
11173
const fileMetadata = this._getFileData(file);
11274
if (fileMetadata == null) {

packages/metro-file-map/src/__tests__/index-test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,17 +1530,20 @@ describe('HasteMap', () => {
15301530

15311531
hm_it('does not emit duplicate change events', async hm => {
15321532
const e = mockEmitters[path.join('/', 'project', 'fruits')];
1533+
mockFs[path.join('/', 'project', 'fruits', 'Tomato.js')] = `
1534+
// Tomato!
1535+
`;
15331536
e.emit(
15341537
'all',
15351538
'change',
1536-
'tomato.js',
1539+
'Tomato.js',
15371540
path.join('/', 'project', 'fruits'),
15381541
MOCK_CHANGE_FILE,
15391542
);
15401543
e.emit(
15411544
'all',
15421545
'change',
1543-
'tomato.js',
1546+
'Tomato.js',
15441547
path.join('/', 'project', 'fruits'),
15451548
MOCK_CHANGE_FILE,
15461549
);
@@ -1808,8 +1811,11 @@ describe('HasteMap', () => {
18081811
);
18091812
});
18101813

1811-
hm_it('ignore directories', async hm => {
1814+
hm_it('ignore directory events (even with file-ish names)', async hm => {
18121815
const e = mockEmitters[path.join('/', 'project', 'fruits')];
1816+
mockFs[path.join('/', 'project', 'fruits', 'tomato.js', 'index.js')] = `
1817+
// Tomato!
1818+
`;
18131819
e.emit(
18141820
'all',
18151821
'change',
@@ -1820,8 +1826,8 @@ describe('HasteMap', () => {
18201826
e.emit(
18211827
'all',
18221828
'change',
1823-
'tomato.js',
1824-
path.join('/', 'project', 'fruits', 'tomato.js', 'index.js'),
1829+
path.join('tomato.js', 'index.js'),
1830+
path.join('/', 'project', 'fruits'),
18251831
MOCK_CHANGE_FILE,
18261832
);
18271833
const {eventsQueue} = await waitForItToChange(hm);

packages/metro-file-map/src/flow-types.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ export interface FileSystem {
161161
getModuleName(file: Path): ?string;
162162
getSerializableSnapshot(): FileData;
163163
getSha1(file: Path): ?string;
164-
getSymlinkTarget(file: Path): ?string;
165164
getType(file: Path): ?('f' | 'l');
166165

167166
matchFiles(pattern: RegExp | string): Array<Path>;
@@ -216,7 +215,6 @@ export interface MutableFileSystem extends FileSystem {
216215
remove(filePath: Path): void;
217216
addOrModify(filePath: Path, fileMetadata: FileMetaData): void;
218217
bulkAddOrModify(addedOrModifiedFiles: FileData): void;
219-
setVisitMetadata(filePath: Path, metadata: $ReadOnly<VisitMetadata>): void;
220218
}
221219

222220
export type Path = string;
@@ -238,13 +236,6 @@ export type ReadOnlyRawModuleMap = $ReadOnly<{
238236
mocks: $ReadOnlyMap<string, Path>,
239237
}>;
240238

241-
export type VisitMetadata = {
242-
hasteId?: string,
243-
sha1?: ?string,
244-
dependencies?: string,
245-
symlinkTarget?: string,
246-
};
247-
248239
export type WatchmanClockSpec =
249240
| string
250241
| $ReadOnly<{scm: $ReadOnly<{'mergebase-with': string}>}>;

0 commit comments

Comments
 (0)