Skip to content

Commit 03126dd

Browse files
authored
[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
1 parent b51a686 commit 03126dd

File tree

3 files changed

+278
-10
lines changed

3 files changed

+278
-10
lines changed

packages/react-fs/src/ReactFilesystem.js

+243-10
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ const Rejected = 2;
2020
type PendingRecord = {|
2121
status: 0,
2222
value: Wakeable,
23-
cache: Array<mixed>,
23+
cache: null,
2424
|};
2525

2626
type ResolvedRecord<T> = {|
2727
status: 1,
2828
value: T,
29-
cache: Array<mixed>,
29+
cache: null | Array<mixed>,
3030
|};
3131

3232
type RejectedRecord = {|
3333
status: 2,
3434
value: mixed,
35-
cache: Array<mixed>,
35+
cache: null,
3636
|};
3737

3838
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
@@ -41,7 +41,7 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
4141
const record: Record<T> = {
4242
status: Pending,
4343
value: thenable,
44-
cache: [],
44+
cache: null,
4545
};
4646
thenable.then(
4747
value => {
@@ -62,9 +62,10 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
6262
return record;
6363
}
6464

65-
function readRecordValue<T>(record: Record<T>): T {
65+
function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
6666
if (record.status === Resolved) {
67-
return record.value;
67+
// This is just a type refinement.
68+
return record;
6869
} else {
6970
throw record.value;
7071
}
@@ -91,7 +92,122 @@ function checkPathInDev(path: string) {
9192
}
9293
}
9394

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>> {
95211
return new Map();
96212
}
97213

@@ -106,15 +222,16 @@ export function readFile(
106222
signal?: mixed, // We'll have our own signal
107223
},
108224
): string | Buffer {
109-
const map = unstable_getCacheForType(createReadFileCache);
110225
checkPathInDev(path);
226+
const map = unstable_getCacheForType(createReadFileMap);
111227
let record = map.get(path);
112228
if (!record) {
113229
const thenable = fs.readFile(path);
114230
record = createRecordFromThenable(thenable);
115231
map.set(path, record);
116232
}
117-
const buffer: Buffer = readRecordValue(record);
233+
const resolvedRecord = readRecord(record);
234+
const buffer: Buffer = resolvedRecord.value;
118235
if (!options) {
119236
return buffer;
120237
}
@@ -136,7 +253,7 @@ export function readFile(
136253
if (typeof encoding !== 'string') {
137254
return buffer;
138255
}
139-
const textCache = record.cache;
256+
const textCache = resolvedRecord.cache || (resolvedRecord.cache = []);
140257
for (let i = 0; i < textCache.length; i += 2) {
141258
if (textCache[i] === encoding) {
142259
return (textCache[i + 1]: any);
@@ -146,3 +263,119 @@ export function readFile(
146263
textCache.push(encoding, text);
147264
return text;
148265
}
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
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ declare function __webpack_chunk_load__(id: string): Promise<mixed>;
7171
declare function __webpack_require__(id: string): any;
7272

7373
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>;
7488
declare var readFile: (
7589
path: string,
7690
options?:
@@ -79,6 +93,26 @@ declare module 'fs/promises' {
7993
encoding?: ?string,
8094
},
8195
) => 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>;
82116
}
83117
declare module 'pg' {
84118
declare var Pool: (

scripts/rollup/modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false;
1010
// const HAS_SIDE_EFFECTS_ON_IMPORT = true;
1111
const importSideEffects = Object.freeze({
1212
fs: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
13+
'fs/promises': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
1314
path: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
1415
'prop-types/checkPropTypes': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
1516
'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,

0 commit comments

Comments
 (0)