@zenfs/core
Version:
A filesystem, anywhere
258 lines (219 loc) • 7.09 kB
text/typescript
import { ErrnoError } from '../error.js';
import type { File } from '../file.js';
import type { FileSystem, FileSystemMetadata } from '../filesystem.js';
import '../polyfills.js';
import type { Stats } from '../stats.js';
import type { Concrete } from '../utils.js';
export class MutexLock {
protected current = Promise.withResolvers<void>();
protected _isLocked: boolean = true;
public get isLocked(): boolean {
return this._isLocked;
}
public constructor(protected readonly previous?: MutexLock) {}
public async done(): Promise<void> {
await this.previous?.done();
await this.current.promise;
}
public unlock(): void {
this.current.resolve();
this._isLocked = false;
}
public [Symbol.dispose](): void {
this.unlock();
}
}
/**
* @hidden
*/
export class _MutexedFS<T extends FileSystem> implements FileSystem {
/**
* @internal
*/
public _fs!: T;
public async ready(): Promise<void> {
return await this._fs.ready();
}
public metadata(): FileSystemMetadata {
return this._fs.metadata();
}
/**
* The current locks
*/
private currentLock?: MutexLock;
/**
* Adds a lock for a path
*/
protected addLock(): MutexLock {
const lock = new MutexLock(this.currentLock);
this.currentLock = lock;
return lock;
}
/**
* Locks `path` asynchronously.
* If the path is currently locked, waits for it to be unlocked.
* @internal
*/
public async lock(path: string, syscall: string): Promise<MutexLock> {
const previous = this.currentLock;
const lock = this.addLock();
const stack = new Error().stack;
setTimeout(() => {
if (lock.isLocked) {
const error = ErrnoError.With('EDEADLK', path, syscall);
error.stack += stack?.slice('Error'.length);
throw error;
}
}, 5000);
await previous?.done();
return lock;
}
/**
* Locks `path` asynchronously.
* If the path is currently locked, an error will be thrown
* @internal
*/
public lockSync(path: string, syscall: string): MutexLock {
if (this.currentLock?.isLocked) {
throw ErrnoError.With('EBUSY', path, syscall);
}
return this.addLock();
}
/**
* Whether `path` is locked
* @internal
*/
public get isLocked(): boolean {
return !!this.currentLock?.isLocked;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
public async rename(oldPath: string, newPath: string): Promise<void> {
using _ = await this.lock(oldPath, 'rename');
await this._fs.rename(oldPath, newPath);
}
public renameSync(oldPath: string, newPath: string): void {
using _ = this.lockSync(oldPath, 'rename');
return this._fs.renameSync(oldPath, newPath);
}
public async stat(path: string): Promise<Stats> {
using _ = await this.lock(path, 'stat');
return await this._fs.stat(path);
}
public statSync(path: string): Stats {
using _ = this.lockSync(path, 'stat');
return this._fs.statSync(path);
}
public async openFile(path: string, flag: string): Promise<File> {
using _ = await this.lock(path, 'openFile');
const file = await this._fs.openFile(path, flag);
file.fs = this;
return file;
}
public openFileSync(path: string, flag: string): File {
using _ = this.lockSync(path, 'openFile');
const file = this._fs.openFileSync(path, flag);
file.fs = this;
return file;
}
public async createFile(path: string, flag: string, mode: number): Promise<File> {
using _ = await this.lock(path, 'createFile');
const file = await this._fs.createFile(path, flag, mode);
file.fs = this;
return file;
}
public createFileSync(path: string, flag: string, mode: number): File {
using _ = this.lockSync(path, 'createFile');
const file = this._fs.createFileSync(path, flag, mode);
file.fs = this;
return file;
}
public async unlink(path: string): Promise<void> {
using _ = await this.lock(path, 'unlink');
await this._fs.unlink(path);
}
public unlinkSync(path: string): void {
using _ = this.lockSync(path, 'unlink');
return this._fs.unlinkSync(path);
}
public async rmdir(path: string): Promise<void> {
using _ = await this.lock(path, 'rmdir');
await this._fs.rmdir(path);
}
public rmdirSync(path: string): void {
using _ = this.lockSync(path, 'rmdir');
return this._fs.rmdirSync(path);
}
public async mkdir(path: string, mode: number): Promise<void> {
using _ = await this.lock(path, 'mkdir');
await this._fs.mkdir(path, mode);
}
public mkdirSync(path: string, mode: number): void {
using _ = this.lockSync(path, 'mkdir');
return this._fs.mkdirSync(path, mode);
}
public async readdir(path: string): Promise<string[]> {
using _ = await this.lock(path, 'readdir');
return await this._fs.readdir(path);
}
public readdirSync(path: string): string[] {
using _ = this.lockSync(path, 'readdir');
return this._fs.readdirSync(path);
}
public async exists(path: string): Promise<boolean> {
using _ = await this.lock(path, 'exists');
return await this._fs.exists(path);
}
public existsSync(path: string): boolean {
using _ = this.lockSync(path, 'exists');
return this._fs.existsSync(path);
}
public async link(srcpath: string, dstpath: string): Promise<void> {
using _ = await this.lock(srcpath, 'link');
await this._fs.link(srcpath, dstpath);
}
public linkSync(srcpath: string, dstpath: string): void {
using _ = this.lockSync(srcpath, 'link');
return this._fs.linkSync(srcpath, dstpath);
}
public async sync(path: string, data: Uint8Array, stats: Readonly<Stats>): Promise<void> {
using _ = await this.lock(path, 'sync');
await this._fs.sync(path, data, stats);
}
public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
using _ = this.lockSync(path, 'sync');
return this._fs.syncSync(path, data, stats);
}
/* eslint-enable @typescript-eslint/no-unused-vars */
}
/**
* This serializes access to an underlying async filesystem.
* For example, on an OverlayFS instance with an async lower
* directory operations like rename and rmdir may involve multiple
* requests involving both the upper and lower file systems -- they
* are not executed in a single atomic step. OverlayFS uses this
* to avoid having to reason about the correctness of
* multiple requests interleaving.
*
* @privateRemarks
* Instead of extending the passed class, `MutexedFS` stores it internally.
* This is to avoid a deadlock caused when a method calls another one
* The problem is discussed extensively in [#78](https://github.com/zen-fs/core/issues/78)
* Instead of extending `FileSystem`,
* `MutexedFS` implements it in order to make sure all of the methods are passed through
*
* @todo Change `using _` to `using void` pending https://github.com/tc39/proposal-discard-binding
* @internal
*/
export function Mutexed<const T extends Concrete<typeof FileSystem>>(
FS: T
): typeof _MutexedFS<InstanceType<T>> & {
new (...args: ConstructorParameters<T>): _MutexedFS<InstanceType<T>>;
} {
class MutexedFS extends _MutexedFS<InstanceType<T>> {
public constructor(...args: ConstructorParameters<T>) {
super();
this._fs = new FS(...args) as InstanceType<T>;
}
}
return MutexedFS;
}