metro-file-map 0.81.0 → 0.81.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +10 -7
  2. package/src/Watcher.js +35 -34
  3. package/src/Watcher.js.flow +41 -62
  4. package/src/cache/DiskCacheManager.d.ts +4 -3
  5. package/src/cache/DiskCacheManager.js +71 -13
  6. package/src/cache/DiskCacheManager.js.flow +91 -17
  7. package/src/flow-types.d.ts +51 -25
  8. package/src/flow-types.js.flow +140 -28
  9. package/src/index.d.ts +5 -1
  10. package/src/index.js +269 -345
  11. package/src/index.js.flow +311 -418
  12. package/src/lib/FileProcessor.js +164 -0
  13. package/src/lib/FileProcessor.js.flow +243 -0
  14. package/src/lib/TreeFS.js +39 -2
  15. package/src/lib/TreeFS.js.flow +63 -7
  16. package/src/lib/sorting.js +27 -0
  17. package/src/lib/sorting.js.flow +35 -0
  18. package/src/{lib/MutableHasteMap.js → plugins/HastePlugin.js} +139 -48
  19. package/src/{lib/MutableHasteMap.js.flow → plugins/HastePlugin.js.flow} +173 -63
  20. package/src/plugins/MockPlugin.js +171 -0
  21. package/src/plugins/MockPlugin.js.flow +205 -0
  22. package/src/{lib → plugins/haste}/DuplicateHasteCandidatesError.js +1 -1
  23. package/src/{lib → plugins/haste}/DuplicateHasteCandidatesError.js.flow +2 -2
  24. package/src/plugins/haste/HasteConflictsError.js +56 -0
  25. package/src/plugins/haste/HasteConflictsError.js.flow +62 -0
  26. package/src/plugins/haste/computeConflicts.js +73 -0
  27. package/src/plugins/haste/computeConflicts.js.flow +105 -0
  28. package/src/watchers/AbstractWatcher.js +76 -0
  29. package/src/watchers/AbstractWatcher.js.flow +92 -0
  30. package/src/watchers/{NodeWatcher.js → FallbackWatcher.js} +112 -71
  31. package/src/watchers/{NodeWatcher.js.flow → FallbackWatcher.js.flow} +125 -99
  32. package/src/watchers/NativeWatcher.js +109 -0
  33. package/src/watchers/NativeWatcher.js.flow +136 -0
  34. package/src/watchers/WatchmanWatcher.js +70 -63
  35. package/src/watchers/WatchmanWatcher.js.flow +67 -79
  36. package/src/watchers/common.js +9 -61
  37. package/src/watchers/common.js.flow +19 -90
  38. package/src/worker.js +14 -28
  39. package/src/worker.js.flow +5 -23
  40. package/src/lib/DuplicateError.js +0 -14
  41. package/src/lib/DuplicateError.js.flow +0 -22
  42. package/src/lib/MockMap.js +0 -22
  43. package/src/lib/MockMap.js.flow +0 -31
  44. package/src/watchers/FSEventsWatcher.js +0 -179
  45. package/src/watchers/FSEventsWatcher.js.flow +0 -231
  46. /package/src/{lib → plugins/haste}/getPlatformExtension.js +0 -0
  47. /package/src/{lib → plugins/haste}/getPlatformExtension.js.flow +0 -0
  48. /package/src/{getMockName.js → plugins/mocks/getMockName.js} +0 -0
  49. /package/src/{getMockName.js.flow → plugins/mocks/getMockName.js.flow} +0 -0
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "metro-file-map",
3
- "version": "0.81.0",
3
+ "version": "0.81.2",
4
4
  "description": "[Experimental] - 🚇 File crawling, watching and mapping for Metro",
5
5
  "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./package.json": "./package.json",
9
+ "./private/*": "./src/*.js",
10
+ "./src": "./src/index.js",
11
+ "./src/*.js": "./src/*.js",
12
+ "./src/*": "./src/*.js"
13
+ },
6
14
  "repository": {
7
15
  "type": "git",
8
16
  "url": "git@github.com:facebook/metro.git"
@@ -13,24 +21,19 @@
13
21
  },
14
22
  "license": "MIT",
15
23
  "dependencies": {
16
- "anymatch": "^3.0.3",
17
24
  "debug": "^2.2.0",
18
25
  "fb-watchman": "^2.0.0",
19
26
  "flow-enums-runtime": "^0.0.6",
20
27
  "graceful-fs": "^4.2.4",
21
28
  "invariant": "^2.2.4",
22
- "jest-worker": "^29.6.3",
29
+ "jest-worker": "^29.7.0",
23
30
  "micromatch": "^4.0.4",
24
- "node-abort-controller": "^3.1.1",
25
31
  "nullthrows": "^1.1.1",
26
32
  "walker": "^1.0.7"
27
33
  },
28
34
  "devDependencies": {
29
35
  "slash": "^3.0.0"
30
36
  },
31
- "optionalDependencies": {
32
- "fsevents": "^2.3.2"
33
- },
34
37
  "engines": {
35
38
  "node": ">=18.18"
36
39
  }
package/src/Watcher.js CHANGED
@@ -7,10 +7,12 @@ exports.Watcher = void 0;
7
7
  var _node = _interopRequireDefault(require("./crawlers/node"));
8
8
  var _watchman = _interopRequireDefault(require("./crawlers/watchman"));
9
9
  var _common = require("./watchers/common");
10
- var _FSEventsWatcher = _interopRequireDefault(
11
- require("./watchers/FSEventsWatcher")
10
+ var _FallbackWatcher = _interopRequireDefault(
11
+ require("./watchers/FallbackWatcher")
12
+ );
13
+ var _NativeWatcher = _interopRequireDefault(
14
+ require("./watchers/NativeWatcher")
12
15
  );
13
- var _NodeWatcher = _interopRequireDefault(require("./watchers/NodeWatcher"));
14
16
  var _WatchmanWatcher = _interopRequireDefault(
15
17
  require("./watchers/WatchmanWatcher")
16
18
  );
@@ -60,8 +62,8 @@ class Watcher extends _events.default {
60
62
  async crawl() {
61
63
  this._options.perfLogger?.point("crawl_start");
62
64
  const options = this._options;
63
- const ignore = (filePath) =>
64
- options.ignore(filePath) ||
65
+ const ignoreForCrawl = (filePath) =>
66
+ options.ignoreForCrawl(filePath) ||
65
67
  path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
66
68
  const crawl = options.useWatchman ? _watchman.default : _node.default;
67
69
  let crawler = crawl === _watchman.default ? "watchman" : "node";
@@ -73,7 +75,7 @@ class Watcher extends _events.default {
73
75
  includeSymlinks: options.enableSymlinks,
74
76
  extensions: options.extensions,
75
77
  forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
76
- ignore,
78
+ ignore: ignoreForCrawl,
77
79
  onStatus: (status) => {
78
80
  this.emit("status", status);
79
81
  },
@@ -123,17 +125,17 @@ class Watcher extends _events.default {
123
125
  }
124
126
  }
125
127
  async watch(onChange) {
126
- const { extensions, ignorePattern, useWatchman } = this._options;
128
+ const { extensions, ignorePatternForWatch, useWatchman } = this._options;
127
129
  const WatcherImpl = useWatchman
128
130
  ? _WatchmanWatcher.default
129
- : _FSEventsWatcher.default.isSupported()
130
- ? _FSEventsWatcher.default
131
- : _NodeWatcher.default;
132
- let watcher = "node";
131
+ : _NativeWatcher.default.isSupported()
132
+ ? _NativeWatcher.default
133
+ : _FallbackWatcher.default;
134
+ let watcher = "fallback";
133
135
  if (WatcherImpl === _WatchmanWatcher.default) {
134
136
  watcher = "watchman";
135
- } else if (WatcherImpl === _FSEventsWatcher.default) {
136
- watcher = "fsevents";
137
+ } else if (WatcherImpl === _NativeWatcher.default) {
138
+ watcher = "native";
137
139
  }
138
140
  debug(`Using watcher: ${watcher}`);
139
141
  this._options.perfLogger?.annotate({
@@ -145,39 +147,38 @@ class Watcher extends _events.default {
145
147
  const createWatcherBackend = (root) => {
146
148
  const watcherOptions = {
147
149
  dot: true,
148
- glob: [
150
+ globs: [
149
151
  "**/package.json",
150
152
  "**/" + this._options.healthCheckFilePrefix + "*",
151
153
  ...extensions.map((extension) => "**/*." + extension),
152
154
  ],
153
- ignored: ignorePattern,
155
+ ignored: ignorePatternForWatch,
154
156
  watchmanDeferStates: this._options.watchmanDeferStates,
155
157
  };
156
158
  const watcher = new WatcherImpl(root, watcherOptions);
157
- return new Promise((resolve, reject) => {
159
+ return new Promise(async (resolve, reject) => {
158
160
  const rejectTimeout = setTimeout(
159
161
  () => reject(new Error("Failed to start watch mode.")),
160
162
  MAX_WAIT_TIME
161
163
  );
162
- watcher.once("ready", () => {
163
- clearTimeout(rejectTimeout);
164
- watcher.on("all", (type, filePath, root, metadata) => {
165
- const basename = path.basename(filePath);
166
- if (basename.startsWith(this._options.healthCheckFilePrefix)) {
167
- if (type === _common.ADD_EVENT || type === _common.CHANGE_EVENT) {
168
- debug(
169
- "Observed possible health check cookie: %s in %s",
170
- filePath,
171
- root
172
- );
173
- this._handleHealthCheckObservation(basename);
174
- }
175
- return;
164
+ watcher.onFileEvent((change) => {
165
+ const basename = path.basename(change.relativePath);
166
+ if (basename.startsWith(this._options.healthCheckFilePrefix)) {
167
+ if (change.event === _common.TOUCH_EVENT) {
168
+ debug(
169
+ "Observed possible health check cookie: %s in %s",
170
+ change.relativePath,
171
+ root
172
+ );
173
+ this._handleHealthCheckObservation(basename);
176
174
  }
177
- onChange(type, filePath, root, metadata);
178
- });
179
- resolve(watcher);
175
+ return;
176
+ }
177
+ onChange(change);
180
178
  });
179
+ await watcher.startWatching();
180
+ clearTimeout(rejectTimeout);
181
+ resolve(watcher);
181
182
  });
182
183
  };
183
184
  this._backends = await Promise.all(
@@ -192,7 +193,7 @@ class Watcher extends _events.default {
192
193
  resolveHealthCheck();
193
194
  }
194
195
  async close() {
195
- await Promise.all(this._backends.map((watcher) => watcher.close()));
196
+ await Promise.all(this._backends.map((watcher) => watcher.stopWatching()));
196
197
  this._activeWatcher = null;
197
198
  }
198
199
  async checkHealth(timeout) {
@@ -9,22 +9,22 @@
9
9
  */
10
10
 
11
11
  import type {
12
- ChangeEventMetadata,
13
12
  Console,
14
13
  CrawlerOptions,
15
14
  FileData,
16
15
  Path,
17
16
  PerfLogger,
17
+ WatcherBackend,
18
+ WatcherBackendChangeEvent,
18
19
  WatchmanClocks,
19
20
  } from './flow-types';
20
21
  import type {WatcherOptions as WatcherBackendOptions} from './watchers/common';
21
- import type {AbortSignal} from 'node-abort-controller';
22
22
 
23
23
  import nodeCrawl from './crawlers/node';
24
24
  import watchmanCrawl from './crawlers/watchman';
25
- import {ADD_EVENT, CHANGE_EVENT} from './watchers/common';
26
- import FSEventsWatcher from './watchers/FSEventsWatcher';
27
- import NodeWatcher from './watchers/NodeWatcher';
25
+ import {TOUCH_EVENT} from './watchers/common';
26
+ import FallbackWatcher from './watchers/FallbackWatcher';
27
+ import NativeWatcher from './watchers/NativeWatcher';
28
28
  import WatchmanWatcher from './watchers/WatchmanWatcher';
29
29
  import EventEmitter from 'events';
30
30
  import * as fs from 'fs';
@@ -50,8 +50,8 @@ type WatcherOptions = {
50
50
  extensions: $ReadOnlyArray<string>,
51
51
  forceNodeFilesystemAPI: boolean,
52
52
  healthCheckFilePrefix: string,
53
- ignore: string => boolean,
54
- ignorePattern: RegExp,
53
+ ignoreForCrawl: string => boolean,
54
+ ignorePatternForWatch: RegExp,
55
55
  previousState: CrawlerOptions['previousState'],
56
56
  perfLogger: ?PerfLogger,
57
57
  roots: $ReadOnlyArray<string>,
@@ -61,11 +61,6 @@ type WatcherOptions = {
61
61
  watchmanDeferStates: $ReadOnlyArray<string>,
62
62
  };
63
63
 
64
- interface WatcherBackend {
65
- getPauseReason(): ?string;
66
- close(): Promise<void>;
67
- }
68
-
69
64
  let nextInstanceId = 0;
70
65
 
71
66
  export type HealthCheckResult =
@@ -92,8 +87,8 @@ export class Watcher extends EventEmitter {
92
87
  this._options.perfLogger?.point('crawl_start');
93
88
 
94
89
  const options = this._options;
95
- const ignore = (filePath: string) =>
96
- options.ignore(filePath) ||
90
+ const ignoreForCrawl = (filePath: string) =>
91
+ options.ignoreForCrawl(filePath) ||
97
92
  path.basename(filePath).startsWith(this._options.healthCheckFilePrefix);
98
93
  const crawl = options.useWatchman ? watchmanCrawl : nodeCrawl;
99
94
  let crawler = crawl === watchmanCrawl ? 'watchman' : 'node';
@@ -107,7 +102,7 @@ export class Watcher extends EventEmitter {
107
102
  includeSymlinks: options.enableSymlinks,
108
103
  extensions: options.extensions,
109
104
  forceNodeFilesystemAPI: options.forceNodeFilesystemAPI,
110
- ignore,
105
+ ignore: ignoreForCrawl,
111
106
  onStatus: status => {
112
107
  this.emit('status', status);
113
108
  },
@@ -163,28 +158,21 @@ export class Watcher extends EventEmitter {
163
158
  }
164
159
  }
165
160
 
166
- async watch(
167
- onChange: (
168
- type: string,
169
- filePath: string,
170
- root: string,
171
- metadata: ChangeEventMetadata,
172
- ) => void,
173
- ) {
174
- const {extensions, ignorePattern, useWatchman} = this._options;
161
+ async watch(onChange: (change: WatcherBackendChangeEvent) => void) {
162
+ const {extensions, ignorePatternForWatch, useWatchman} = this._options;
175
163
 
176
- // WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
164
+ // WatchmanWatcher > NativeWatcher > FallbackWatcher
177
165
  const WatcherImpl = useWatchman
178
166
  ? WatchmanWatcher
179
- : FSEventsWatcher.isSupported()
180
- ? FSEventsWatcher
181
- : NodeWatcher;
167
+ : NativeWatcher.isSupported()
168
+ ? NativeWatcher
169
+ : FallbackWatcher;
182
170
 
183
- let watcher = 'node';
171
+ let watcher = 'fallback';
184
172
  if (WatcherImpl === WatchmanWatcher) {
185
173
  watcher = 'watchman';
186
- } else if (WatcherImpl === FSEventsWatcher) {
187
- watcher = 'fsevents';
174
+ } else if (WatcherImpl === NativeWatcher) {
175
+ watcher = 'native';
188
176
  }
189
177
  debug(`Using watcher: ${watcher}`);
190
178
  this._options.perfLogger?.annotate({string: {watcher}});
@@ -193,7 +181,7 @@ export class Watcher extends EventEmitter {
193
181
  const createWatcherBackend = (root: Path): Promise<WatcherBackend> => {
194
182
  const watcherOptions: WatcherBackendOptions = {
195
183
  dot: true,
196
- glob: [
184
+ globs: [
197
185
  // Ensure we always include package.json files, which are crucial for
198
186
  /// module resolution.
199
187
  '**/package.json',
@@ -201,44 +189,35 @@ export class Watcher extends EventEmitter {
201
189
  '**/' + this._options.healthCheckFilePrefix + '*',
202
190
  ...extensions.map(extension => '**/*.' + extension),
203
191
  ],
204
- ignored: ignorePattern,
192
+ ignored: ignorePatternForWatch,
205
193
  watchmanDeferStates: this._options.watchmanDeferStates,
206
194
  };
207
- const watcher = new WatcherImpl(root, watcherOptions);
195
+ const watcher: WatcherBackend = new WatcherImpl(root, watcherOptions);
208
196
 
209
- return new Promise((resolve, reject) => {
197
+ return new Promise(async (resolve, reject) => {
210
198
  const rejectTimeout = setTimeout(
211
199
  () => reject(new Error('Failed to start watch mode.')),
212
200
  MAX_WAIT_TIME,
213
201
  );
214
202
 
215
- watcher.once('ready', () => {
216
- clearTimeout(rejectTimeout);
217
- watcher.on(
218
- 'all',
219
- (
220
- type: string,
221
- filePath: string,
222
- root: string,
223
- metadata: ChangeEventMetadata,
224
- ) => {
225
- const basename = path.basename(filePath);
226
- if (basename.startsWith(this._options.healthCheckFilePrefix)) {
227
- if (type === ADD_EVENT || type === CHANGE_EVENT) {
228
- debug(
229
- 'Observed possible health check cookie: %s in %s',
230
- filePath,
231
- root,
232
- );
233
- this._handleHealthCheckObservation(basename);
234
- }
235
- return;
236
- }
237
- onChange(type, filePath, root, metadata);
238
- },
239
- );
240
- resolve(watcher);
203
+ watcher.onFileEvent(change => {
204
+ const basename = path.basename(change.relativePath);
205
+ if (basename.startsWith(this._options.healthCheckFilePrefix)) {
206
+ if (change.event === TOUCH_EVENT) {
207
+ debug(
208
+ 'Observed possible health check cookie: %s in %s',
209
+ change.relativePath,
210
+ root,
211
+ );
212
+ this._handleHealthCheckObservation(basename);
213
+ }
214
+ return;
215
+ }
216
+ onChange(change);
241
217
  });
218
+ await watcher.startWatching();
219
+ clearTimeout(rejectTimeout);
220
+ resolve(watcher);
242
221
  });
243
222
  };
244
223
 
@@ -256,7 +235,7 @@ export class Watcher extends EventEmitter {
256
235
  }
257
236
 
258
237
  async close() {
259
- await Promise.all(this._backends.map(watcher => watcher.close()));
238
+ await Promise.all(this._backends.map(watcher => watcher.stopWatching()));
260
239
  this._activeWatcher = null;
261
240
  }
262
241
 
@@ -12,7 +12,7 @@ import type {
12
12
  BuildParameters,
13
13
  CacheData,
14
14
  CacheManager,
15
- FileData,
15
+ CacheManagerWriteOptions,
16
16
  } from '../flow-types';
17
17
 
18
18
  export interface DiskCacheConfig {
@@ -31,7 +31,8 @@ export class DiskCacheManager implements CacheManager {
31
31
  getCacheFilePath(): string;
32
32
  read(): Promise<CacheData | null>;
33
33
  write(
34
- dataSnapshot: CacheData,
35
- {changed, removed}: Readonly<{changed: FileData; removed: FileData}>,
34
+ getSnapshot: () => CacheData,
35
+ opts: CacheManagerWriteOptions,
36
36
  ): Promise<void>;
37
+ end(): Promise<void>;
37
38
  }
@@ -7,22 +7,42 @@ exports.DiskCacheManager = void 0;
7
7
  var _rootRelativeCacheKeys = _interopRequireDefault(
8
8
  require("../lib/rootRelativeCacheKeys")
9
9
  );
10
- var _gracefulFs = require("graceful-fs");
10
+ var _fs = require("fs");
11
11
  var _os = require("os");
12
12
  var _path = _interopRequireDefault(require("path"));
13
+ var _timers = require("timers");
13
14
  var _v = require("v8");
14
15
  function _interopRequireDefault(e) {
15
16
  return e && e.__esModule ? e : { default: e };
16
17
  }
18
+ const debug = require("debug")("Metro:FileMapCache");
17
19
  const DEFAULT_PREFIX = "metro-file-map";
18
20
  const DEFAULT_DIRECTORY = (0, _os.tmpdir)();
21
+ const DEFAULT_AUTO_SAVE_DEBOUNCE_MS = 5000;
19
22
  class DiskCacheManager {
20
- constructor({ buildParameters, cacheDirectory, cacheFilePrefix }) {
21
- this._cachePath = DiskCacheManager.getCacheFilePath(
23
+ #autoSaveOpts;
24
+ #cachePath;
25
+ #debounceTimeout = null;
26
+ #writePromise = Promise.resolve();
27
+ #hasUnwrittenChanges = false;
28
+ #tryWrite;
29
+ #stopListening;
30
+ constructor(
31
+ { buildParameters },
32
+ { autoSave = {}, cacheDirectory, cacheFilePrefix }
33
+ ) {
34
+ this.#cachePath = DiskCacheManager.getCacheFilePath(
22
35
  buildParameters,
23
36
  cacheFilePrefix,
24
37
  cacheDirectory
25
38
  );
39
+ if (autoSave) {
40
+ const { debounceMs = DEFAULT_AUTO_SAVE_DEBOUNCE_MS } =
41
+ autoSave === true ? {} : autoSave;
42
+ this.#autoSaveOpts = {
43
+ debounceMs,
44
+ };
45
+ }
26
46
  }
27
47
  static getCacheFilePath(buildParameters, cacheFilePrefix, cacheDirectory) {
28
48
  const { rootDirHash, relativeConfigHash } = (0,
@@ -35,13 +55,11 @@ class DiskCacheManager {
35
55
  );
36
56
  }
37
57
  getCacheFilePath() {
38
- return this._cachePath;
58
+ return this.#cachePath;
39
59
  }
40
60
  async read() {
41
61
  try {
42
- return (0, _v.deserialize)(
43
- (0, _gracefulFs.readFileSync)(this._cachePath)
44
- );
62
+ return (0, _v.deserialize)(await _fs.promises.readFile(this.#cachePath));
45
63
  } catch (e) {
46
64
  if (e?.code === "ENOENT") {
47
65
  return null;
@@ -49,13 +67,53 @@ class DiskCacheManager {
49
67
  throw e;
50
68
  }
51
69
  }
52
- async write(dataSnapshot, { changed, removed }) {
53
- if (changed.size > 0 || removed.size > 0) {
54
- (0, _gracefulFs.writeFileSync)(
55
- this._cachePath,
56
- (0, _v.serialize)(dataSnapshot)
57
- );
70
+ async write(
71
+ getSnapshot,
72
+ { changedSinceCacheRead, eventSource, onWriteError }
73
+ ) {
74
+ const tryWrite = (this.#tryWrite = () => {
75
+ this.#writePromise = this.#writePromise
76
+ .then(async () => {
77
+ if (!this.#hasUnwrittenChanges) {
78
+ return;
79
+ }
80
+ const data = getSnapshot();
81
+ this.#hasUnwrittenChanges = false;
82
+ await _fs.promises.writeFile(
83
+ this.#cachePath,
84
+ (0, _v.serialize)(data)
85
+ );
86
+ debug("Written cache to %s", this.#cachePath);
87
+ })
88
+ .catch(onWriteError);
89
+ return this.#writePromise;
90
+ });
91
+ if (this.#autoSaveOpts) {
92
+ const autoSave = this.#autoSaveOpts;
93
+ this.#stopListening?.();
94
+ this.#stopListening = eventSource.onChange(() => {
95
+ this.#hasUnwrittenChanges = true;
96
+ if (this.#debounceTimeout) {
97
+ this.#debounceTimeout.refresh();
98
+ } else {
99
+ this.#debounceTimeout = (0, _timers.setTimeout)(
100
+ () => tryWrite(),
101
+ autoSave.debounceMs
102
+ ).unref();
103
+ }
104
+ });
105
+ }
106
+ if (changedSinceCacheRead) {
107
+ this.#hasUnwrittenChanges = true;
108
+ await tryWrite();
109
+ }
110
+ }
111
+ async end() {
112
+ if (this.#debounceTimeout) {
113
+ (0, _timers.clearTimeout)(this.#debounceTimeout);
58
114
  }
115
+ this.#stopListening?.();
116
+ await this.#tryWrite?.();
59
117
  }
60
118
  }
61
119
  exports.DiskCacheManager = DiskCacheManager;
@@ -12,38 +12,59 @@
12
12
  import type {
13
13
  BuildParameters,
14
14
  CacheData,
15
- CacheDelta,
16
15
  CacheManager,
16
+ CacheManagerFactoryOptions,
17
+ CacheManagerWriteOptions,
17
18
  } from '../flow-types';
18
19
 
19
20
  import rootRelativeCacheKeys from '../lib/rootRelativeCacheKeys';
20
- import {readFileSync, writeFileSync} from 'graceful-fs';
21
+ import {promises as fsPromises} from 'fs';
21
22
  import {tmpdir} from 'os';
22
23
  import path from 'path';
24
+ import {Timeout, clearTimeout, setTimeout} from 'timers';
23
25
  import {deserialize, serialize} from 'v8';
24
26
 
27
+ const debug = require('debug')('Metro:FileMapCache');
28
+
29
+ type AutoSaveOptions = $ReadOnly<{
30
+ debounceMs: number,
31
+ }>;
32
+
25
33
  type DiskCacheConfig = {
26
- buildParameters: BuildParameters,
34
+ autoSave?: Partial<AutoSaveOptions> | boolean,
27
35
  cacheFilePrefix?: ?string,
28
36
  cacheDirectory?: ?string,
29
37
  };
30
38
 
31
39
  const DEFAULT_PREFIX = 'metro-file-map';
32
40
  const DEFAULT_DIRECTORY = tmpdir();
41
+ const DEFAULT_AUTO_SAVE_DEBOUNCE_MS = 5000;
33
42
 
34
43
  export class DiskCacheManager implements CacheManager {
35
- _cachePath: string;
36
-
37
- constructor({
38
- buildParameters,
39
- cacheDirectory,
40
- cacheFilePrefix,
41
- }: DiskCacheConfig) {
42
- this._cachePath = DiskCacheManager.getCacheFilePath(
44
+ +#autoSaveOpts: ?AutoSaveOptions;
45
+ +#cachePath: string;
46
+ #debounceTimeout: ?Timeout = null;
47
+ #writePromise: Promise<void> = Promise.resolve();
48
+ #hasUnwrittenChanges: boolean = false;
49
+ #tryWrite: ?() => Promise<void>;
50
+ #stopListening: ?() => void;
51
+
52
+ constructor(
53
+ {buildParameters}: CacheManagerFactoryOptions,
54
+ {autoSave = {}, cacheDirectory, cacheFilePrefix}: DiskCacheConfig,
55
+ ) {
56
+ this.#cachePath = DiskCacheManager.getCacheFilePath(
43
57
  buildParameters,
44
58
  cacheFilePrefix,
45
59
  cacheDirectory,
46
60
  );
61
+
62
+ // Normalise auto-save options.
63
+ if (autoSave) {
64
+ const {debounceMs = DEFAULT_AUTO_SAVE_DEBOUNCE_MS} =
65
+ autoSave === true ? {} : autoSave;
66
+ this.#autoSaveOpts = {debounceMs};
67
+ }
47
68
  }
48
69
 
49
70
  static getCacheFilePath(
@@ -63,12 +84,12 @@ export class DiskCacheManager implements CacheManager {
63
84
  }
64
85
 
65
86
  getCacheFilePath(): string {
66
- return this._cachePath;
87
+ return this.#cachePath;
67
88
  }
68
89
 
69
90
  async read(): Promise<?CacheData> {
70
91
  try {
71
- return deserialize(readFileSync(this._cachePath));
92
+ return deserialize(await fsPromises.readFile(this.#cachePath));
72
93
  } catch (e) {
73
94
  if (e?.code === 'ENOENT') {
74
95
  // Cache file not found - not considered an error.
@@ -80,11 +101,64 @@ export class DiskCacheManager implements CacheManager {
80
101
  }
81
102
 
82
103
  async write(
83
- dataSnapshot: CacheData,
84
- {changed, removed}: CacheDelta,
104
+ getSnapshot: () => CacheData,
105
+ {
106
+ changedSinceCacheRead,
107
+ eventSource,
108
+ onWriteError,
109
+ }: CacheManagerWriteOptions,
85
110
  ): Promise<void> {
86
- if (changed.size > 0 || removed.size > 0) {
87
- writeFileSync(this._cachePath, serialize(dataSnapshot));
111
+ // Initialise a writer function using a promise queue to ensure writes are
112
+ // sequenced.
113
+ const tryWrite = (this.#tryWrite = () => {
114
+ this.#writePromise = this.#writePromise
115
+ .then(async () => {
116
+ if (!this.#hasUnwrittenChanges) {
117
+ return;
118
+ }
119
+ const data = getSnapshot();
120
+ this.#hasUnwrittenChanges = false;
121
+ await fsPromises.writeFile(this.#cachePath, serialize(data));
122
+ debug('Written cache to %s', this.#cachePath);
123
+ })
124
+ .catch(onWriteError);
125
+ return this.#writePromise;
126
+ });
127
+
128
+ // Set up auto-save on changes, if enabled.
129
+ if (this.#autoSaveOpts) {
130
+ const autoSave = this.#autoSaveOpts;
131
+ this.#stopListening?.();
132
+ this.#stopListening = eventSource.onChange(() => {
133
+ this.#hasUnwrittenChanges = true;
134
+ if (this.#debounceTimeout) {
135
+ this.#debounceTimeout.refresh();
136
+ } else {
137
+ this.#debounceTimeout = setTimeout(
138
+ () => tryWrite(),
139
+ autoSave.debounceMs,
140
+ ).unref();
141
+ }
142
+ });
88
143
  }
144
+
145
+ // Write immediately if state has changed since the cache was read.
146
+ if (changedSinceCacheRead) {
147
+ this.#hasUnwrittenChanges = true;
148
+ await tryWrite();
149
+ }
150
+ }
151
+
152
+ async end() {
153
+ // Clear any timers
154
+ if (this.#debounceTimeout) {
155
+ clearTimeout(this.#debounceTimeout);
156
+ }
157
+
158
+ // Remove event listeners
159
+ this.#stopListening?.();
160
+
161
+ // Flush unwritten changes to disk (no-op if no changes)
162
+ await this.#tryWrite?.();
89
163
  }
90
164
  }