UNPKG

@zenfs/core

Version:

A filesystem, anywhere

258 lines (219 loc) 7.09 kB
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; }