Skip to content
Open
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
59 changes: 43 additions & 16 deletions packages/metro-file-map/src/watchers/NativeWatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @format
*/

import type {WatcherBackendChangeEvent} from '../flow-types';
import type {FSWatcher} from 'node:fs';

import {AbstractWatcher} from './AbstractWatcher';
Expand Down Expand Up @@ -46,6 +47,11 @@ const RECRAWL_EVENT = 'recrawl';
export default class NativeWatcher extends AbstractWatcher {
#fsWatcher: ?FSWatcher;

/**
* Promise chain to emit events in the order they were received.
*/
#emitQueue: Promise<void> = Promise.resolve();

static isSupported(): boolean {
return platform() === 'darwin';
}
Expand All @@ -59,7 +65,7 @@ export default class NativeWatcher extends AbstractWatcher {
...
}>,
) {
if (!NativeWatcher.isSupported) {
if (!NativeWatcher.isSupported()) {
throw new Error('This watcher can only be used on macOS');
}
super(dir, opts);
Expand All @@ -76,9 +82,25 @@ export default class NativeWatcher extends AbstractWatcher {
recursive: true,
},
(event, relativePath) => {
this._handleEvent(event, relativePath).catch(error => {
this.emitError(error);
});
// Start handling immediately so that stats are gathered concurrently
// and as close as possible to the event, but emit in arrival order.
const settled = this.#handleEvent(event, relativePath).then(
change => ({change, error: null}),
(error: Error) => ({change: null, error}),
);
this.#emitQueue = this.#emitQueue
.then(async () => {
const {change, error} = await settled;
if (error != null) {
this.emitError(error);
} else if (change != null) {
this.emitFileEvent(change);
}
})
.catch(error => {
// Only reached if emitting threw
this.emitError(error);
});
},
);

Expand All @@ -95,7 +117,14 @@ export default class NativeWatcher extends AbstractWatcher {
}
}

async _handleEvent(event: string, relativePath: string) {
/**
* Resolve a raw `fs.watch` event into the event to emit for it, or `null` if
* it should be dropped.
*/
async #handleEvent(
event: string,
relativePath: string,
): Promise<?Omit<WatcherBackendChangeEvent, 'root'>> {
const absolutePath = path.resolve(this.root, relativePath);
if (this.doIgnore(relativePath)) {
debug(
Expand All @@ -104,7 +133,7 @@ export default class NativeWatcher extends AbstractWatcher {
relativePath,
this.root,
);
return;
return null;
}
debug(
'Handling event "%s" on %s (root: %s)',
Expand All @@ -119,11 +148,11 @@ export default class NativeWatcher extends AbstractWatcher {

// Ignore files of an unrecognized type
if (!type) {
return;
return null;
}

if (!includedByGlob(type, this.globs, this.dot, relativePath)) {
return;
return null;
}

// For directory "rename" events, notify that we need a recrawl since we
Expand All @@ -136,29 +165,27 @@ export default class NativeWatcher extends AbstractWatcher {
'Directory rename detected on %s, requesting recrawl',
relativePath,
);
this.emitFileEvent({
return {
event: RECRAWL_EVENT,
relativePath,
});
return;
};
}

this.emitFileEvent({
return {
event: TOUCH_EVENT,
relativePath,
metadata: {
type,
modifiedTime: stat.mtime.getTime(),
size: stat.size,
},
});
};
} catch (error) {
if (error?.code !== 'ENOENT') {
this.emitError(error);
return;
throw error;
}

this.emitFileEvent({event: DELETE_EVENT, relativePath});
return {event: DELETE_EVENT, relativePath};
}
}
}
Loading