Skip to content

Commit

Permalink
[Flight] Add read-only fs methods (facebook#20412)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gaearon authored and koto committed Jun 15, 2021
1 parent 445ef2f commit 1da3756
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 10 deletions.
253 changes: 243 additions & 10 deletions packages/react-fs/src/ReactFilesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ const Rejected = 2;
type PendingRecord = {|
status: 0,
value: Wakeable,
cache: Array<mixed>,
cache: null,
|};

type ResolvedRecord<T> = {|
status: 1,
value: T,
cache: Array<mixed>,
cache: null | Array<mixed>,
|};

type RejectedRecord = {|
status: 2,
value: mixed,
cache: Array<mixed>,
cache: null,
|};

type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
Expand All @@ -41,7 +41,7 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
const record: Record<T> = {
status: Pending,
value: thenable,
cache: [],
cache: null,
};
thenable.then(
value => {
Expand All @@ -62,9 +62,10 @@ function createRecordFromThenable<T>(thenable: Thenable<T>): Record<T> {
return record;
}

function readRecordValue<T>(record: Record<T>): T {
function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
if (record.status === Resolved) {
return record.value;
// This is just a type refinement.
return record;
} else {
throw record.value;
}
Expand All @@ -91,7 +92,122 @@ function checkPathInDev(path: string) {
}
}

function createReadFileCache(): Map<string, Record<Buffer>> {
function createAccessMap(): Map<string, Array<number | Record<void>>> {
return new Map();
}

export function access(path: string, mode?: number): void {
checkPathInDev(path);
if (mode == null) {
mode = 0; // fs.constants.F_OK
}
const map = unstable_getCacheForType(createAccessMap);
let accessCache = map.get(path);
if (!accessCache) {
accessCache = [];
map.set(path, accessCache);
}
let record;
for (let i = 0; i < accessCache.length; i += 2) {
const cachedMode: number = (accessCache[i]: any);
if (mode === cachedMode) {
const cachedRecord: Record<void> = (accessCache[i + 1]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.access(path, mode);
record = createRecordFromThenable(thenable);
accessCache.push(mode, record);
}
readRecord(record); // No return value.
}

function createLstatMap(): Map<string, Array<boolean | Record<mixed>>> {
return new Map();
}

export function lstat(path: string, options?: {bigint?: boolean}): mixed {
checkPathInDev(path);
let bigint = false;
if (options && options.bigint) {
bigint = true;
}
const map = unstable_getCacheForType(createLstatMap);
let lstatCache = map.get(path);
if (!lstatCache) {
lstatCache = [];
map.set(path, lstatCache);
}
let record;
for (let i = 0; i < lstatCache.length; i += 2) {
const cachedBigint: boolean = (lstatCache[i]: any);
if (bigint === cachedBigint) {
const cachedRecord: Record<void> = (lstatCache[i + 1]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.lstat(path, {bigint});
record = createRecordFromThenable(thenable);
lstatCache.push(bigint, record);
}
const stats = readRecord(record).value;
return stats;
}

function createReaddirMap(): Map<
string,
Array<string | boolean | Record<mixed>>,
> {
return new Map();
}

export function readdir(
path: string,
options?: string | {encoding?: string, withFileTypes?: boolean},
): mixed {
checkPathInDev(path);
let encoding = 'utf8';
let withFileTypes = false;
if (typeof options === 'string') {
encoding = options;
} else if (options != null) {
if (options.encoding) {
encoding = options.encoding;
}
if (options.withFileTypes) {
withFileTypes = true;
}
}
const map = unstable_getCacheForType(createReaddirMap);
let readdirCache = map.get(path);
if (!readdirCache) {
readdirCache = [];
map.set(path, readdirCache);
}
let record;
for (let i = 0; i < readdirCache.length; i += 3) {
const cachedEncoding: string = (readdirCache[i]: any);
const cachedWithFileTypes: boolean = (readdirCache[i + 1]: any);
if (encoding === cachedEncoding && withFileTypes === cachedWithFileTypes) {
const cachedRecord: Record<void> = (readdirCache[i + 2]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.readdir(path, {encoding, withFileTypes});
record = createRecordFromThenable(thenable);
readdirCache.push(encoding, withFileTypes, record);
}
const files = readRecord(record).value;
return files;
}

function createReadFileMap(): Map<string, Record<Buffer>> {
return new Map();
}

Expand All @@ -106,15 +222,16 @@ export function readFile(
signal?: mixed, // We'll have our own signal
},
): string | Buffer {
const map = unstable_getCacheForType(createReadFileCache);
checkPathInDev(path);
const map = unstable_getCacheForType(createReadFileMap);
let record = map.get(path);
if (!record) {
const thenable = fs.readFile(path);
record = createRecordFromThenable(thenable);
map.set(path, record);
}
const buffer: Buffer = readRecordValue(record);
const resolvedRecord = readRecord(record);
const buffer: Buffer = resolvedRecord.value;
if (!options) {
return buffer;
}
Expand All @@ -136,7 +253,7 @@ export function readFile(
if (typeof encoding !== 'string') {
return buffer;
}
const textCache = record.cache;
const textCache = resolvedRecord.cache || (resolvedRecord.cache = []);
for (let i = 0; i < textCache.length; i += 2) {
if (textCache[i] === encoding) {
return (textCache[i + 1]: any);
Expand All @@ -146,3 +263,119 @@ export function readFile(
textCache.push(encoding, text);
return text;
}

function createReadlinkMap(): Map<string, Array<string | Record<mixed>>> {
return new Map();
}

export function readlink(
path: string,
options?: string | {encoding?: string},
): mixed {
checkPathInDev(path);
let encoding = 'utf8';
if (typeof options === 'string') {
encoding = options;
} else if (options != null) {
if (options.encoding) {
encoding = options.encoding;
}
}
const map = unstable_getCacheForType(createReadlinkMap);
let readlinkCache = map.get(path);
if (!readlinkCache) {
readlinkCache = [];
map.set(path, readlinkCache);
}
let record;
for (let i = 0; i < readlinkCache.length; i += 2) {
const cachedEncoding: string = (readlinkCache[i]: any);
if (encoding === cachedEncoding) {
const cachedRecord: Record<void> = (readlinkCache[i + 1]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.readlink(path, {encoding});
record = createRecordFromThenable(thenable);
readlinkCache.push(encoding, record);
}
const linkString = readRecord(record).value;
return linkString;
}

function createRealpathMap(): Map<string, Array<string | Record<mixed>>> {
return new Map();
}

export function realpath(
path: string,
options?: string | {encoding?: string},
): mixed {
checkPathInDev(path);
let encoding = 'utf8';
if (typeof options === 'string') {
encoding = options;
} else if (options != null) {
if (options.encoding) {
encoding = options.encoding;
}
}
const map = unstable_getCacheForType(createRealpathMap);
let realpathCache = map.get(path);
if (!realpathCache) {
realpathCache = [];
map.set(path, realpathCache);
}
let record;
for (let i = 0; i < realpathCache.length; i += 2) {
const cachedEncoding: string = (realpathCache[i]: any);
if (encoding === cachedEncoding) {
const cachedRecord: Record<void> = (realpathCache[i + 1]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.realpath(path, {encoding});
record = createRecordFromThenable(thenable);
realpathCache.push(encoding, record);
}
const resolvedPath = readRecord(record).value;
return resolvedPath;
}

function createStatMap(): Map<string, Array<boolean | Record<mixed>>> {
return new Map();
}

export function stat(path: string, options?: {bigint?: boolean}): mixed {
checkPathInDev(path);
let bigint = false;
if (options && options.bigint) {
bigint = true;
}
const map = unstable_getCacheForType(createStatMap);
let statCache = map.get(path);
if (!statCache) {
statCache = [];
map.set(path, statCache);
}
let record;
for (let i = 0; i < statCache.length; i += 2) {
const cachedBigint: boolean = (statCache[i]: any);
if (bigint === cachedBigint) {
const cachedRecord: Record<void> = (statCache[i + 1]: any);
record = cachedRecord;
break;
}
}
if (!record) {
const thenable = fs.stat(path, {bigint});
record = createRecordFromThenable(thenable);
statCache.push(bigint, record);
}
const stats = readRecord(record).value;
return stats;
}
34 changes: 34 additions & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ declare function __webpack_chunk_load__(id: string): Promise<mixed>;
declare function __webpack_require__(id: string): any;

declare module 'fs/promises' {
declare var access: (path: string, mode?: number) => Promise<void>;
declare var lstat: (
path: string,
options?: ?{bigint?: boolean},
) => Promise<mixed>;
declare var readdir: (
path: string,
options?:
| ?string
| {
encoding?: ?string,
withFileTypes?: ?boolean,
},
) => Promise<Buffer>;
declare var readFile: (
path: string,
options?:
Expand All @@ -79,6 +93,26 @@ declare module 'fs/promises' {
encoding?: ?string,
},
) => Promise<Buffer>;
declare var readlink: (
path: string,
options?:
| ?string
| {
encoding?: ?string,
},
) => Promise<mixed>;
declare var realpath: (
path: string,
options?:
| ?string
| {
encoding?: ?string,
},
) => Promise<mixed>;
declare var stat: (
path: string,
options?: ?{bigint?: boolean},
) => Promise<mixed>;
}
declare module 'pg' {
declare var Pool: (
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false;
// const HAS_SIDE_EFFECTS_ON_IMPORT = true;
const importSideEffects = Object.freeze({
fs: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
'fs/promises': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
path: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
'prop-types/checkPropTypes': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface': HAS_NO_SIDE_EFFECTS_ON_IMPORT,
Expand Down

0 comments on commit 1da3756

Please sign in to comment.