@ezdevlol/memfs
Version:
In-memory file-system with Node's fs API.
201 lines (200 loc) • 6.68 kB
JavaScript
import { isReadableStream, promisify, streamToBuffer } from './util';
import { constants } from '../constants';
// AsyncIterator implementation for promises.watch
class FSWatchAsyncIterator {
fs;
path;
options;
watcher;
eventQueue = [];
resolveQueue = [];
finished = false;
abortController;
maxQueue;
overflow;
constructor(fs, path, options = {}) {
this.fs = fs;
this.path = path;
this.options = options;
this.maxQueue = options.maxQueue || 2048;
this.overflow = options.overflow || 'ignore';
this.startWatching();
// Handle AbortSignal
if (options.signal) {
if (options.signal.aborted) {
this.finish();
return;
}
options.signal.addEventListener('abort', () => {
this.finish();
});
}
}
startWatching() {
try {
this.watcher = this.fs.watch(this.path, this.options, (eventType, filename) => {
this.enqueueEvent({ eventType, filename });
});
}
catch (error) {
// If we can't start watching, finish immediately
this.finish();
throw error;
}
}
enqueueEvent(event) {
if (this.finished)
return;
// Handle queue overflow
if (this.eventQueue.length >= this.maxQueue) {
if (this.overflow === 'throw') {
const error = new Error(`Watch queue overflow: more than ${this.maxQueue} events queued`);
this.finish(error);
return;
}
else {
// 'ignore' - drop the oldest event
this.eventQueue.shift();
console.warn(`Watch queue overflow: dropping event due to exceeding maxQueue of ${this.maxQueue}`);
}
}
this.eventQueue.push(event);
// If there's a waiting promise, resolve it
if (this.resolveQueue.length > 0) {
const { resolve } = this.resolveQueue.shift();
const nextEvent = this.eventQueue.shift();
resolve({ value: nextEvent, done: false });
}
}
finish(error) {
if (this.finished)
return;
this.finished = true;
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
// Resolve or reject all pending promises
while (this.resolveQueue.length > 0) {
const { resolve, reject } = this.resolveQueue.shift();
if (error) {
reject(error);
}
else {
resolve({ value: undefined, done: true });
}
}
}
async next() {
if (this.finished) {
return { value: undefined, done: true };
}
// If we have queued events, return one
if (this.eventQueue.length > 0) {
const event = this.eventQueue.shift();
return { value: event, done: false };
}
// Otherwise, wait for the next event
return new Promise((resolve, reject) => {
this.resolveQueue.push({ resolve, reject });
});
}
async return() {
this.finish();
return { value: undefined, done: true };
}
async throw(error) {
this.finish(error);
throw error;
}
[Symbol.asyncIterator]() {
return this;
}
}
export class FsPromises {
fs;
FileHandle;
constants = constants;
cp;
opendir;
statfs;
lutimes;
access;
chmod;
chown;
copyFile;
lchmod;
lchown;
link;
lstat;
mkdir;
mkdtemp;
readdir;
readlink;
realpath;
rename;
rmdir;
rm;
stat;
symlink;
truncate;
unlink;
utimes;
readFile;
appendFile;
open;
writeFile;
watch;
constructor(fs, FileHandle) {
this.fs = fs;
this.FileHandle = FileHandle;
this.cp = promisify(this.fs, 'cp');
this.opendir = promisify(this.fs, 'opendir');
this.statfs = promisify(this.fs, 'statfs');
this.lutimes = promisify(this.fs, 'lutimes');
this.access = promisify(this.fs, 'access');
this.chmod = promisify(this.fs, 'chmod');
this.chown = promisify(this.fs, 'chown');
this.copyFile = promisify(this.fs, 'copyFile');
this.lchmod = promisify(this.fs, 'lchmod');
this.lchown = promisify(this.fs, 'lchown');
this.link = promisify(this.fs, 'link');
this.lstat = promisify(this.fs, 'lstat');
this.mkdir = promisify(this.fs, 'mkdir');
this.mkdtemp = promisify(this.fs, 'mkdtemp');
this.readdir = promisify(this.fs, 'readdir');
this.readlink = promisify(this.fs, 'readlink');
this.realpath = promisify(this.fs, 'realpath');
this.rename = promisify(this.fs, 'rename');
this.rmdir = promisify(this.fs, 'rmdir');
this.rm = promisify(this.fs, 'rm');
this.stat = promisify(this.fs, 'stat');
this.symlink = promisify(this.fs, 'symlink');
this.truncate = promisify(this.fs, 'truncate');
this.unlink = promisify(this.fs, 'unlink');
this.utimes = promisify(this.fs, 'utimes');
this.readFile = (id, options) => {
return promisify(this.fs, 'readFile')(id instanceof this.FileHandle ? id.fd : id, options);
};
this.appendFile = (path, data, options) => {
return promisify(this.fs, 'appendFile')(path instanceof this.FileHandle ? path.fd : path, data, options);
};
this.open = (path, flags = 'r', mode) => {
return promisify(this.fs, 'open', fd => new this.FileHandle(this.fs, fd))(path, flags, mode);
};
this.writeFile = (id, data, options) => {
const dataPromise = isReadableStream(data) ? streamToBuffer(data) : Promise.resolve(data);
return dataPromise.then(data => promisify(this.fs, 'writeFile')(id instanceof this.FileHandle ? id.fd : id, data, options));
};
}
// @ts-expect-error
writeFile = (id, data, options) => {
const dataPromise = isReadableStream(data) ? streamToBuffer(data) : Promise.resolve(data);
return dataPromise.then(data => promisify(this.fs, 'writeFile')(id instanceof this.FileHandle ? id.fd : id, data, options));
};
// @ts-expect-error
watch = (filename, options) => {
const watchOptions = typeof options === 'string' ? { encoding: options } : options || {};
return new FSWatchAsyncIterator(this.fs, filename, watchOptions);
};
}