[Flight] Add read-only fs methods (#20412)
* Don't allocate the inner cache unnecessarily We only need it when we're asking for text. I anticipate I'll want to avoid allocating it in other methods too when it's not strictly necessary. * Add fs.access * Add fs.lstat * Add fs.stat * Add fs.readdir * Add fs.readlink * Add fs.realpath * Rename functions to disambiguate two caches
Dan Abramov committed
Dec 9, 2020 at 21:46 UTC
03126dd087b285789447ab814ee7f3385e1c179d
3 files changed
+278
-10
packages/react-fs/src/ReactFilesystem.js
+243
-10
@@ -20,19 +20,19 @@ const Rejected = 2;
20
type PendingRecord = {|
21
status: 0,
22
value: Wakeable,
23
- cache: Array<mixed>,
23
+ cache: null,
24
|};
25
26
type ResolvedRecord<T> = {|
27
status: 1,
28
value: T,
29
- cache: Array<mixed>,
29
+ cache: null | Array<mixed>,
30
|};
31
32
type RejectedRecord = {|
33
status: 2,
34
value: mixed,
35
- cache: Array<mixed>,
35
+ cache: null,
36
|};
37
38
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
@@ -41,7 +41,7 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
41
const record: Record<T> = {
42
status: Pending,
43
value: thenable,
44
- cache: [],
44
+ cache: null,
45
};
46
thenable.then(
47
value => {
@@ -62,9 +62,10 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
62
return record;
63
}
64
65
-function readRecordValue<T>(record: Record<T>): T {
65
+function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
66
if (record.status === Resolved) {
67
- return record.value;
67
+ // This is just a type refinement.
68
+ return record;
69
} else {
70
throw record.value;
71
}
@@ -91,7 +92,122 @@ function checkPathInDev(path: string) {
92
}
93
}
94
94
-function createReadFileCache(): Map<string, Record<Buffer>> {
95
+function createAccessMap(): Map<string, Array<number | Record<void>>> {
96
+ return new Map();
97
+}
98
+
99
+export function access(path: string, mode?: number): void {
100
+ checkPathInDev(path);
101
+ if (mode == null) {
102
+ mode = 0; // fs.constants.F_OK
103
+ }
104
+ const map = unstable_getCacheForType(createAccessMap);
105
+ let accessCache = map.get(path);
106
+ if (!accessCache) {
107
+ accessCache = [];
108
+ map.set(path, accessCache);
109
+ }
110
+ let record;
111
+ for (let i = 0; i < accessCache.length; i += 2) {
112
+ const cachedMode: number = (accessCache[i]: any);
113
+ if (mode === cachedMode) {
114
+ const cachedRecord: Record<void> = (accessCache[i + 1]: any);
115
+ record = cachedRecord;
116
+ break;
117
+ }
118
+ }
119
+ if (!record) {
120
+ const thenable = fs.access(path, mode);
121
+ record = createRecordFromThenable(thenable);
122
+ accessCache.push(mode, record);
123
+ }
124
+ readRecord(record); // No return value.
125
+}
126
+
127
+function createLstatMap(): Map<string, Array<boolean | Record<mixed>>> {
128
+ return new Map();
129
+}
130
+
131
+export function lstat(path: string, options?: {bigint?: boolean}): mixed {
132
+ checkPathInDev(path);
133
+ let bigint = false;
134
+ if (options && options.bigint) {
135
+ bigint = true;
136
+ }
137
+ const map = unstable_getCacheForType(createLstatMap);
138
+ let lstatCache = map.get(path);
139
+ if (!lstatCache) {
140
+ lstatCache = [];
141
+ map.set(path, lstatCache);
142
+ }
143
+ let record;
144
+ for (let i = 0; i < lstatCache.length; i += 2) {
145
+ const cachedBigint: boolean = (lstatCache[i]: any);
146
+ if (bigint === cachedBigint) {
147
+ const cachedRecord: Record<void> = (lstatCache[i + 1]: any);
148
+ record = cachedRecord;
149
+ break;
150
+ }
151
+ }
152
+ if (!record) {
153
+ const thenable = fs.lstat(path, {bigint});
154
+ record = createRecordFromThenable(thenable);
155
+ lstatCache.push(bigint, record);
156
+ }
157
+ const stats = readRecord(record).value;
158
+ return stats;
159
+}
160
+
161
+function createReaddirMap(): Map<
162
+ string,
163
+ Array<string | boolean | Record<mixed>>,
164
+> {
165
+ return new Map();
166
+}
167
+
168
+export function readdir(
169
+ path: string,
170
+ options?: string | {encoding?: string, withFileTypes?: boolean},
171
+): mixed {
172
+ checkPathInDev(path);
173
+ let encoding = 'utf8';
174
+ let withFileTypes = false;
175
+ if (typeof options === 'string') {
176
+ encoding = options;
177
+ } else if (options != null) {
178
+ if (options.encoding) {
179
+ encoding = options.encoding;
180
+ }
181
+ if (options.withFileTypes) {
182
+ withFileTypes = true;
183
+ }
184
+ }
185
+ const map = unstable_getCacheForType(createReaddirMap);
186
+ let readdirCache = map.get(path);
187
+ if (!readdirCache) {
188
+ readdirCache = [];
189
+ map.set(path, readdirCache);
190
+ }
191
+ let record;
192
+ for (let i = 0; i < readdirCache.length; i += 3) {
193
+ const cachedEncoding: string = (readdirCache[i]: any);
194
+ const cachedWithFileTypes: boolean = (readdirCache[i + 1]: any);
195
+ if (encoding === cachedEncoding && withFileTypes === cachedWithFileTypes) {
196
+ const cachedRecord: Record<void> = (readdirCache[i + 2]: any);
197
+ record = cachedRecord;
198
+ break;
199
+ }
200
+ }
201
+ if (!record) {
202
+ const thenable = fs.readdir(path, {encoding, withFileTypes});
203
+ record = createRecordFromThenable(thenable);
204
+ readdirCache.push(encoding, withFileTypes, record);
205
+ }
206
+ const files = readRecord(record).value;
207
+ return files;
208
+}
209
+
210
+function createReadFileMap(): Map<string, Record<Buffer>> {
211
return new Map();
212
}
213
@@ -106,15 +222,16 @@ export function readFile(
222
signal?: mixed, // We'll have our own signal
223
},
224
): string | Buffer {
109
- const map = unstable_getCacheForType(createReadFileCache);
225
checkPathInDev(path);
226
+ const map = unstable_getCacheForType(createReadFileMap);
227
let record = map.get(path);
228
if (!record) {
229
const thenable = fs.readFile(path);
230
record = createRecordFromThenable(thenable);
231
map.set(path, record);
232
}
117
- const buffer: Buffer = readRecordValue(record);
233
+ const resolvedRecord = readRecord(record);
234
+ const buffer: Buffer = resolvedRecord.value;
235
if (!options) {
236
return buffer;
237
}
@@ -136,7 +253,7 @@ export function readFile(
253
if (typeof encoding !== 'string') {
254
return buffer;
255
}
139
- const textCache = record.cache;
256
+ const textCache = resolvedRecord.cache || (resolvedRecord.cache = []);
257
for (let i = 0; i < textCache.length; i += 2) {
258
if (textCache[i] === encoding) {
259
return (textCache[i + 1]: any);
@@ -146,3 +263,119 @@ export function readFile(
263
textCache.push(encoding, text);
264
return text;
265
}
266
+
267
+function createReadlinkMap(): Map<string, Array<string | Record<mixed>>> {
268
+ return new Map();
269
+}
270
+
271
+export function readlink(
272
+ path: string,
273
+ options?: string | {encoding?: string},
274
+): mixed {
275
+ checkPathInDev(path);
276
+ let encoding = 'utf8';
277
+ if (typeof options === 'string') {
278
+ encoding = options;
279
+ } else if (options != null) {
280
+ if (options.encoding) {
281
+ encoding = options.encoding;
282
+ }
283
+ }
284
+ const map = unstable_getCacheForType(createReadlinkMap);
285
+ let readlinkCache = map.get(path);
286
+ if (!readlinkCache) {
287
+ readlinkCache = [];
288
+ map.set(path, readlinkCache);
289
+ }
290
+ let record;
291
+ for (let i = 0; i < readlinkCache.length; i += 2) {
292
+ const cachedEncoding: string = (readlinkCache[i]: any);
293
+ if (encoding === cachedEncoding) {
294
+ const cachedRecord: Record<void> = (readlinkCache[i + 1]: any);
295
+ record = cachedRecord;
296
+ break;
297
+ }
298
+ }
299
+ if (!record) {
300
+ const thenable = fs.readlink(path, {encoding});
301
+ record = createRecordFromThenable(thenable);
302
+ readlinkCache.push(encoding, record);
303
+ }
304
+ const linkString = readRecord(record).value;
305
+ return linkString;
306
+}
307
+
308
+function createRealpathMap(): Map<string, Array<string | Record<mixed>>> {
309
+ return new Map();
310
+}
311
+
312
+export function realpath(
313
+ path: string,
314
+ options?: string | {encoding?: string},
315
+): mixed {
316
+ checkPathInDev(path);
317
+ let encoding = 'utf8';
318
+ if (typeof options === 'string') {
319
+ encoding = options;
320
+ } else if (options != null) {
321
+ if (options.encoding) {
322
+ encoding = options.encoding;
323
+ }
324
+ }
325
+ const map = unstable_getCacheForType(createRealpathMap);
326
+ let realpathCache = map.get(path);
327
+ if (!realpathCache) {
328
+ realpathCache = [];
329
+ map.set(path, realpathCache);
330
+ }
331
+ let record;
332
+ for (let i = 0; i < realpathCache.length; i += 2) {
333
+ const cachedEncoding: string = (realpathCache[i]: any);
334
+ if (encoding === cachedEncoding) {
335
+ const cachedRecord: Record<void> = (realpathCache[i + 1]: any);
336
+ record = cachedRecord;
337
+ break;
338
+ }
339
+ }
340
+ if (!record) {
341
+ const thenable = fs.realpath(path, {encoding});
342
+ record = createRecordFromThenable(thenable);
343
+ realpathCache.push(encoding, record);
344
+ }
345
+ const resolvedPath = readRecord(record).value;
346
+ return resolvedPath;
347
+}
348
+
349
+function createStatMap(): Map<string, Array<boolean | Record<mixed>>> {
350
+ return new Map();
351
+}
352
+
353
+export function stat(path: string, options?: {bigint?: boolean}): mixed {
354
+ checkPathInDev(path);
355
+ let bigint = false;
356
+ if (options && options.bigint) {
357
+ bigint = true;
358
+ }
359
+ const map = unstable_getCacheForType(createStatMap);
360
+ let statCache = map.get(path);
361
+ if (!statCache) {
362
+ statCache = [];
363
+ map.set(path, statCache);
364
+ }
365
+ let record;
366
+ for (let i = 0; i < statCache.length; i += 2) {
367
+ const cachedBigint: boolean = (statCache[i]: any);
368
+ if (bigint === cachedBigint) {
369
+ const cachedRecord: Record<void> = (statCache[i + 1]: any);
370
+ record = cachedRecord;
371
+ break;
372
+ }
373
+ }
374
+ if (!record) {
375
+ const thenable = fs.stat(path, {bigint});
376
+ record = createRecordFromThenable(thenable);
377
+ statCache.push(bigint, record);
378
+ }
379
+ const stats = readRecord(record).value;
380
+ return stats;
381
+}
scripts/flow/environment.js
+34
@@ -71,6 +71,20 @@ declare function __webpack_chunk_load__(id: string): Promise<mixed>;
71
declare function __webpack_require__(id: string): any;
72
73
declare module 'fs/promises' {
74
+ declare var access: (path: string, mode?: number) => Promise<void>;
75
+ declare var lstat: (
76
+ path: string,
77
+ options?: ?{bigint?: boolean},
78
+ ) => Promise<mixed>;
79
+ declare var readdir: (
80
+ path: string,
81
+ options?:
82
+ | ?string
83
+ | {
84
+ encoding?: ?string,
85
+ withFileTypes?: ?boolean,
86
+ },
87
+ ) => Promise<Buffer>;
88
declare var readFile: (
89
path: string,
90
options?:
@@ -79,6 +93,26 @@ declare module 'fs/promises' {
93
encoding?: ?string,
94
},
95
) => Promise<Buffer>;
96
+ declare var readlink: (
97
+ path: string,
98
+ options?:
99
+ | ?string
100
+ | {
101
+ encoding?: ?string,
102
+ },
103
+ ) => Promise<mixed>;
104
+ declare var realpath: (
105
+ path: string,
106
+ options?:
107
+ | ?string
108
+ | {
109
+ encoding?: ?string,
110
+ },
111
+ ) => Promise<mixed>;
112
+ declare var stat: (
113
+ path: string,
114
+ options?: ?{bigint?: boolean},
115
+ ) => Promise<mixed>;
116
}
117
declare module 'pg' {
118
declare var Pool: (
scripts/rollup/modules.js
+1
@@ -10,6 +10,7 @@ const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false;
10
// const HAS_SIDE_EFFECTS_ON_IMPORT = true;
11
const importSideEffects = Object.freeze({
12
fs: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
13
+ 'fs/promises': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
14
path: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
15
'prop-types/checkPropTypes': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
16
'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,