@zenfs/core
Version:
A filesystem, anywhere
235 lines (234 loc) • 9.11 kB
JavaScript
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;
}