Skip to content

Commit

Permalink
fix: no more singletons
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed Jul 28, 2024
1 parent d35e7d2 commit 2e78dcb
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 66 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"package-lock.json"
],
"words": [
"cbor",
"filesystems",
"impvol",
"knip",
Expand Down
181 changes: 115 additions & 66 deletions src/impvol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @packageDocumentation
*/
import Debug from 'debug';
import {type File, type Link, type Node} from 'memfs/lib/node.js';
import {
fromBinarySnapshotSync,
toBinarySnapshotSync,
Expand All @@ -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<ImpVolInitData>(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<ImpVolInitData>(DEFAULT_HOOKS_PATH, {
parentURL: IMPVOL_URL,
data: {
tmp,
sab,
},
});
}

const debug = Debug('impvol');

export const impvol = ImportableVolume.registerHook;
export const impvol = ImportableVolume.create;

0 comments on commit 2e78dcb

Please sign in to comment.