UNPKG

@zenfs/core

Version:

A filesystem, anywhere

235 lines (234 loc) 9.11 kB
import { _asyncFSKeys } from './shared.js'; import { withErrno } from 'kerium'; import { crit, debug, err } from 'kerium/log'; import { StoreFS } from '../backends/store/fs.js'; import { isDirectory } from '../internal/inode.js'; import { join } from '../path.js'; /** * Async() implements synchronous methods on an asynchronous file system * * Implementing classes must define `_sync` for the synchronous file system used as a cache. * * Synchronous methods on an asynchronous FS are implemented by performing operations over the in-memory copy, * while asynchronously pipelining them to the backing store. * During loading, the contents of the async file system are preloaded into the synchronous store. * @category Internals */ export function Async(FS) { class AsyncFS extends FS { /** * @deprecated Use {@link sync | `sync`} instead */ async done() { return this.sync(); } /** * @deprecated Use {@link sync | `sync`} instead */ queueDone() { return this.sync(); } _promise = Promise.resolve(); _async(thunk) { this._promise = this._promise.finally(() => thunk()); } _isInitialized = false; /** Tracks how many updates to the sync. cache we skipped during initialization */ _skippedCacheUpdates = 0; constructor(...args) { super(...args); this._patchAsync(); } async ready() { await super.ready(); if (this._isInitialized || this.attributes.has('no_async_preload')) return; await this._promise; this.checkSync(); await this._sync.ready(); // optimization: for 2 storeFS', we copy at a lower abstraction level. if (this._sync instanceof StoreFS && this instanceof StoreFS) { const sync = this._sync.transaction(); const async = this.transaction(); const promises = []; for (const key of await async.keys()) { promises.push(async.get(key).then(data => sync.setSync(key, data))); } await Promise.all(promises); this._isInitialized = true; return; } try { await this.crossCopy('/'); debug(`Skipped ${this._skippedCacheUpdates} updates to the sync cache during initialization`); this._isInitialized = true; } catch (e) { this._isInitialized = false; throw crit(e); } } checkSync() { if (this.attributes.has('no_async_preload')) { throw withErrno('ENOTSUP', 'Sync preloading has been disabled for this async file system'); } if (!this._sync) { throw crit(withErrno('ENOTSUP', 'No sync cache is attached to this async file system')); } } renameSync(oldPath, newPath) { this.checkSync(); this._sync.renameSync(oldPath, newPath); this._async(() => this.rename(oldPath, newPath)); } statSync(path) { this.checkSync(); return this._sync.statSync(path); } touchSync(path, metadata) { this.checkSync(); this._sync.touchSync(path, metadata); this._async(() => this.touch(path, metadata)); } createFileSync(path, options) { this.checkSync(); const result = this._sync.createFileSync(path, options); this._async(() => this.createFile(path, options)); return result; } unlinkSync(path) { this.checkSync(); this._sync.unlinkSync(path); this._async(() => this.unlink(path)); } rmdirSync(path) { this.checkSync(); this._sync.rmdirSync(path); this._async(() => this.rmdir(path)); } mkdirSync(path, options) { this.checkSync(); const result = this._sync.mkdirSync(path, options); this._async(() => this.mkdir(path, options)); return result; } readdirSync(path) { this.checkSync(); return this._sync.readdirSync(path); } linkSync(srcpath, dstpath) { this.checkSync(); this._sync.linkSync(srcpath, dstpath); this._async(() => this.link(srcpath, dstpath)); } async sync() { if (!this.attributes.has('no_async_preload') && this._sync) this._sync.syncSync(); await this._promise.catch(() => { }); } syncSync() { this.checkSync(); this._sync.syncSync(); } existsSync(path) { this.checkSync(); return this._sync.existsSync(path); } readSync(path, buffer, offset, end) { this.checkSync(); this._sync.readSync(path, buffer, offset, end); } writeSync(path, buffer, offset) { this.checkSync(); this._sync.writeSync(path, buffer, offset); this._async(() => this.write(path, buffer, offset)); } streamWrite(path, options) { this.checkSync(); const sync = this._sync.streamWrite(path, options).getWriter(); const async = super.streamWrite(path, options).getWriter(); return new WritableStream({ async write(chunk, controller) { await Promise.all([sync.write(chunk), async.write(chunk)]).catch(controller.error.bind(controller)); }, async close() { await Promise.all([sync.close(), async.close()]); }, async abort(reason) { await Promise.all([sync.abort(reason), async.abort(reason)]); }, }); } /** * @internal */ async crossCopy(path) { this.checkSync(); const stats = await this.stat(path); if (!isDirectory(stats)) { this._sync.createFileSync(path, stats); const buffer = new Uint8Array(stats.size); await this.read(path, buffer, 0, stats.size); this._sync.writeSync(path, buffer, 0); this._sync.touchSync(path, stats); return; } if (path !== '/') { this._sync.mkdirSync(path, stats); this._sync.touchSync(path, stats); } const promises = []; for (const file of await this.readdir(path)) { promises.push(this.crossCopy(join(path, file))); } await Promise.all(promises); } /** * @internal * Patch all async methods to also call their synchronous counterparts unless called from themselves (either sync or async) */ _patchAsync() { const noPatch = ['read', 'readdir', 'stat', 'exists']; const toPatch = _asyncFSKeys.filter(key => !noPatch.includes(key)); for (const key of toPatch) { // TS does not narrow the union based on the key const originalMethod = this[key].bind(this); function isInLoop(depth, error) { if (!error) { error = new Error(); Error.captureStackTrace(error, isInLoop); } if (!error.stack) return false; const stack = error.stack.split('\n').slice(depth).join('\n'); // From the async queue return (stack.includes(`at <computed> [as ${key}]`) || stack.includes(`at async <computed> [as ${key}]`) || stack.includes(`${key}Sync `)); } this[key] = async (...args) => { const result = await originalMethod(...args); if (isInLoop(2)) return result; if (!this._isInitialized) { this._skippedCacheUpdates++; return result; } try { // @ts-expect-error 2556 - The type of `args` is not narrowed this._sync?.[`${key}Sync`]?.(...args); } catch (e) { if (isInLoop(3, e)) return result; e.message += ' (Out of sync!)'; throw err(e); } return result; }; } debug(`Async: patched ${toPatch.length} methods`); } } return AsyncFS; }