diff --git a/cspell.json b/cspell.json index d1fbbd8..1501382 100644 --- a/cspell.json +++ b/cspell.json @@ -8,6 +8,7 @@ "package-lock.json" ], "words": [ + "cbor", "filesystems", "impvol", "knip", diff --git a/src/impvol.ts b/src/impvol.ts index e5c0233..9abcdc5 100644 --- a/src/impvol.ts +++ b/src/impvol.ts @@ -7,6 +7,7 @@ * @packageDocumentation */ import Debug from 'debug'; +import {type File, type Link, type Node} from 'memfs/lib/node.js'; import { fromBinarySnapshotSync, toBinarySnapshotSync, @@ -20,129 +21,177 @@ import {DEFAULT_HOOKS_PATH} from './paths-cjs.cjs'; import {IMPVOL_URL} from './paths.js'; import {type ImpVolInitData} from './types.js'; -let impVol: ImportableVolume; +const TEMP_FILE = 'impvol.cbor'; -export class ImportableVolume extends Volume { - public static registerHook(this: void, volume?: Volume): ImportableVolume { - if (impVol) { - return impVol; +let tmpDir: string; + +const metadata = new WeakMap< + ImportableVolume, + { + tmp: string; + uint8: Uint8Array; + } +>(); + +/** + * @internal + */ +function update(impvol: ImportableVolume) { + const snapshot = toBinarySnapshotSync({fs: impvol}); + const {tmp, uint8} = metadata.get(impvol)!; + if (!tmp || !uint8) { + throw new ReferenceError('Missing metadata'); + } + writeFileSync(tmp, snapshot); + Atomics.store(uint8, 0, 1); + debug('Updated snapshot'); +} + +function initTempDir(tempDir?: string): string { + let actualTempDir: string; + if (!tempDir) { + if (!tmpDir) { + tmpDir = mkdtempSync(path.join(tmpdir(), 'impvol-')); + debug('Created temp directory at %s', tmpDir); } - registerLoaderHook(); + actualTempDir = tmpDir; + } else { + actualTempDir = tempDir; + } + return actualTempDir; +} + +export class ImportableVolume extends Volume { + constructor( + tempDir?: string, + props?: {Node?: Node; Link?: Link; File?: File}, + ) { + super(props); + const sab = new SharedArrayBuffer(1); + const uint8 = new Uint8Array(sab); + const actualTempDir = initTempDir(tempDir); + const tmp = path.resolve(actualTempDir, TEMP_FILE); + metadata.set(this, {tmp, uint8}); + debug('Created temp file at %s', tmp); + Atomics.store(uint8, 0, 0); + + register(DEFAULT_HOOKS_PATH, { + parentURL: IMPVOL_URL, + data: { + tmp, + sab, + }, + }); + } - impVol = new ImportableVolume(); + public static create( + this: void, + volume?: Volume, + tempDir?: string, + ): ImportableVolume; + public static create( + this: void, + json?: DirectoryJSON, + tempDir?: string, + ): ImportableVolume; + + public static create( + this: void, + volumeOrJson?: Volume | DirectoryJSON, + tempDir?: string, + ): ImportableVolume { + const impVol = new ImportableVolume(tempDir); // clone the volume if it is non-empty - if (volume) { - if (Object.keys(volume.toJSON()).length) { + if (volumeOrJson instanceof Volume) { + if (Object.keys(volumeOrJson.toJSON()).length) { debug('Cloning volume'); - const snapshot = toBinarySnapshotSync({fs: volume}); + const snapshot = toBinarySnapshotSync({fs: volumeOrJson}); fromBinarySnapshotSync(snapshot, {fs: impVol}); - impVol.__update__(); + update(impVol); } else { debug('Refusing to clone empty volume'); } + } else if (volumeOrJson) { + impVol.fromJSON(volumeOrJson); } return impVol; } - /** - * @internal - */ - public __update__() { - const snapshot = toBinarySnapshotSync({fs: this}); - writeFileSync(tmp, snapshot); - Atomics.store(uint8, 0, 1); - debug('Updated snapshot'); - } - public override fromJSON(json: DirectoryJSON, cwd?: string): void { super.fromJSON(json, cwd); - this.__update__(); + update(this); } public override reset(): void { super.reset(); - this.__update__(); + update(this); } } + // overrides private methods such that meaningful filesystem writes trigger an // update on the worker thread. Note that using a `FSWatcher` was attempted, but // memfs' implementation is probably Wrong; the change/rename events are emitted // before the files get fully "written to". - // TODO: probably need more here, but this is a start. Also: consider doing // something else. Object.assign(ImportableVolume.prototype, { writeFileBase(this: ImportableVolume, ...args: unknown[]): unknown { // @ts-expect-error private // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const retval = Volume.prototype.writeFileBase.call( + const returnValue = Volume.prototype.writeFileBase.call( this, ...args, ) as unknown; - void this.__update__(); - return retval; + update(this); + return returnValue; }, unlinkBase(this: ImportableVolume, ...args: unknown[]): unknown { // @ts-expect-error private // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const retval = Volume.prototype.unlinkBase.call(this, ...args) as unknown; - void this.__update__(); - return retval; + const returnValue = Volume.prototype.unlinkBase.call( + this, + ...args, + ) as unknown; + update(this); + return returnValue; }, writevBase(this: ImportableVolume, ...args: unknown[]): unknown { // @ts-expect-error private // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const retval = Volume.prototype.writevBase.call(this, ...args) as unknown; - void this.__update__(); - return retval; + const returnValue = Volume.prototype.writevBase.call( + this, + ...args, + ) as unknown; + update(this); + return returnValue; }, linkBase(this: ImportableVolume, ...args: unknown[]): unknown { // @ts-expect-error private // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const retval = Volume.prototype.linkBase.call(this, ...args) as unknown; - void this.__update__(); - return retval; + const returnValue = Volume.prototype.linkBase.call( + this, + ...args, + ) as unknown; + update(this); + return returnValue; }, symlinkBase(this: ImportableVolume, ...args: unknown[]): unknown { // @ts-expect-error private // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - const retval = Volume.prototype.symlinkBase.call(this, ...args) as unknown; - void this.__update__(); - return retval; + const returnValue = Volume.prototype.symlinkBase.call( + this, + ...args, + ) as unknown; + update(this); + return returnValue; }, }); -let sab: SharedArrayBuffer; -let tmp: string; -let uint8: Uint8Array; - -/** - * Registers the loader hook - * - * @param hooksPath Absolute path to hooks file - * @returns A new MessagePort for communication with the hooks worker - */ -function registerLoaderHook() { - const tmpDir = mkdtempSync(`${tmpdir()}/impvol-`); - tmp = path.join(tmpDir, 'impvol.cbor'); - debug('Created temp file at %s', tmp); - sab = new SharedArrayBuffer(1); - uint8 = new Uint8Array(sab); - Atomics.store(uint8, 0, 0); - register(DEFAULT_HOOKS_PATH, { - parentURL: IMPVOL_URL, - data: { - tmp, - sab, - }, - }); -} - const debug = Debug('impvol'); -export const impvol = ImportableVolume.registerHook; +export const impvol = ImportableVolume.create;