This commit is contained in:
walkpan
2024-08-18 23:38:55 +08:00
parent e8dbb9bab3
commit 8f57f57c1d
1004 changed files with 234067 additions and 16087 deletions

View File

@@ -0,0 +1,42 @@
/* IMPORT */
import {NOOP} from '../consts';
import {Exception, FN} from '../types';
/* ATTEMPTIFY */
//TODO: Maybe publish this as a standalone package
//FIXME: The type castings here aren't exactly correct
const attemptifyAsync = <T extends FN> ( fn: T, onError: FN<[Exception]> = NOOP ): T => {
return function () {
return fn.apply ( undefined, arguments ).catch ( onError );
} as T;
};
const attemptifySync = <T extends FN> ( fn: T, onError: FN<[Exception]> = NOOP ): T => {
return function () {
try {
return fn.apply ( undefined, arguments );
} catch ( error ) {
return onError ( error );
}
} as T;
};
/* EXPORT */
export {attemptifyAsync, attemptifySync};

View File

@@ -0,0 +1,51 @@
/* IMPORT */
import * as fs from 'fs';
import {promisify} from 'util';
import {attemptifyAsync, attemptifySync} from './attemptify';
import Handlers from './fs_handlers';
import {retryifyAsync, retryifySync} from './retryify';
/* FS */
const FS = {
chmodAttempt: attemptifyAsync ( promisify ( fs.chmod ), Handlers.onChangeError ),
chownAttempt: attemptifyAsync ( promisify ( fs.chown ), Handlers.onChangeError ),
closeAttempt: attemptifyAsync ( promisify ( fs.close ) ),
fsyncAttempt: attemptifyAsync ( promisify ( fs.fsync ) ),
mkdirAttempt: attemptifyAsync ( promisify ( fs.mkdir ) ),
realpathAttempt: attemptifyAsync ( promisify ( fs.realpath ) ),
statAttempt: attemptifyAsync ( promisify ( fs.stat ) ),
unlinkAttempt: attemptifyAsync ( promisify ( fs.unlink ) ),
closeRetry: retryifyAsync ( promisify ( fs.close ), Handlers.isRetriableError ),
fsyncRetry: retryifyAsync ( promisify ( fs.fsync ), Handlers.isRetriableError ),
openRetry: retryifyAsync ( promisify ( fs.open ), Handlers.isRetriableError ),
readFileRetry: retryifyAsync ( promisify ( fs.readFile ), Handlers.isRetriableError ),
renameRetry: retryifyAsync ( promisify ( fs.rename ), Handlers.isRetriableError ),
statRetry: retryifyAsync ( promisify ( fs.stat ), Handlers.isRetriableError ),
writeRetry: retryifyAsync ( promisify ( fs.write ), Handlers.isRetriableError ),
chmodSyncAttempt: attemptifySync ( fs.chmodSync, Handlers.onChangeError ),
chownSyncAttempt: attemptifySync ( fs.chownSync, Handlers.onChangeError ),
closeSyncAttempt: attemptifySync ( fs.closeSync ),
mkdirSyncAttempt: attemptifySync ( fs.mkdirSync ),
realpathSyncAttempt: attemptifySync ( fs.realpathSync ),
statSyncAttempt: attemptifySync ( fs.statSync ),
unlinkSyncAttempt: attemptifySync ( fs.unlinkSync ),
closeSyncRetry: retryifySync ( fs.closeSync, Handlers.isRetriableError ),
fsyncSyncRetry: retryifySync ( fs.fsyncSync, Handlers.isRetriableError ),
openSyncRetry: retryifySync ( fs.openSync, Handlers.isRetriableError ),
readFileSyncRetry: retryifySync ( fs.readFileSync, Handlers.isRetriableError ),
renameSyncRetry: retryifySync ( fs.renameSync, Handlers.isRetriableError ),
statSyncRetry: retryifySync ( fs.statSync, Handlers.isRetriableError ),
writeSyncRetry: retryifySync ( fs.writeSync, Handlers.isRetriableError )
};
/* EXPORT */
export default FS;

View File

@@ -0,0 +1,45 @@
/* IMPORT */
import {IS_USER_ROOT} from '../consts';
import {Exception} from '../types';
/* FS HANDLERS */
const Handlers = {
isChangeErrorOk: ( error: Exception ): boolean => { //URL: https://github.com/isaacs/node-graceful-fs/blob/master/polyfills.js#L315-L342
const {code} = error;
if ( code === 'ENOSYS' ) return true;
if ( !IS_USER_ROOT && ( code === 'EINVAL' || code === 'EPERM' ) ) return true;
return false;
},
isRetriableError: ( error: Exception ): boolean => {
const {code} = error;
if ( code === 'EMFILE' || code === 'ENFILE' || code === 'EAGAIN' || code === 'EBUSY' || code === 'EACCESS' || code === 'EACCS' || code === 'EPERM' ) return true;
return false;
},
onChangeError: ( error: Exception ): void => {
if ( Handlers.isChangeErrorOk ( error ) ) return;
throw error;
}
};
/* EXPORT */
export default Handlers;

View File

@@ -0,0 +1,28 @@
/* LANG */
const Lang = {
isFunction: ( x: any ): x is Function => {
return typeof x === 'function';
},
isString: ( x: any ): x is string => {
return typeof x === 'string';
},
isUndefined: ( x: any ): x is undefined => {
return typeof x === 'undefined';
}
};
/* EXPORT */
export default Lang;

View File

@@ -0,0 +1,78 @@
/* IMPORT */
import {Exception, FN} from '../types';
import RetryfyQueue from './retryify_queue';
/* RETRYIFY */
const retryifyAsync = <T extends FN> ( fn: T, isRetriableError: FN<[Exception], boolean | void> ): FN<[number], T> => {
return function ( timestamp: number ) {
return function attempt () {
return RetryfyQueue.schedule ().then ( cleanup => {
return fn.apply ( undefined, arguments ).then ( result => {
cleanup ();
return result;
}, error => {
cleanup ();
if ( Date.now () >= timestamp ) throw error;
if ( isRetriableError ( error ) ) {
const delay = Math.round ( 100 + ( 400 * Math.random () ) ),
delayPromise = new Promise ( resolve => setTimeout ( resolve, delay ) );
return delayPromise.then ( () => attempt.apply ( undefined, arguments ) );
}
throw error;
});
});
} as T;
};
};
const retryifySync = <T extends FN> ( fn: T, isRetriableError: FN<[Exception], boolean | void> ): FN<[number], T> => {
return function ( timestamp: number ) {
return function attempt () {
try {
return fn.apply ( undefined, arguments );
} catch ( error ) {
if ( Date.now () > timestamp ) throw error;
if ( isRetriableError ( error ) ) return attempt.apply ( undefined, arguments );
throw error;
}
} as T;
};
};
/* EXPORT */
export {retryifyAsync, retryifySync};

View File

@@ -0,0 +1,95 @@
/* IMPORT */
import {LIMIT_FILES_DESCRIPTORS} from '../consts';
/* RETRYIFY QUEUE */
const RetryfyQueue = {
interval: 25,
intervalId: <NodeJS.Timeout | undefined> undefined,
limit: LIMIT_FILES_DESCRIPTORS,
queueActive: new Set<Function> (),
queueWaiting: new Set<Function> (),
init: (): void => {
if ( RetryfyQueue.intervalId ) return;
RetryfyQueue.intervalId = setInterval ( RetryfyQueue.tick, RetryfyQueue.interval );
},
reset: (): void => {
if ( !RetryfyQueue.intervalId ) return;
clearInterval ( RetryfyQueue.intervalId );
delete RetryfyQueue.intervalId;
},
add: ( fn: Function ): void => {
RetryfyQueue.queueWaiting.add ( fn );
if ( RetryfyQueue.queueActive.size < ( RetryfyQueue.limit / 2 ) ) { // Active queue not under preassure, executing immediately
RetryfyQueue.tick ();
} else {
RetryfyQueue.init ();
}
},
remove: ( fn: Function ): void => {
RetryfyQueue.queueWaiting.delete ( fn );
RetryfyQueue.queueActive.delete ( fn );
},
schedule: (): Promise<Function> => {
return new Promise ( resolve => {
const cleanup = () => RetryfyQueue.remove ( resolver );
const resolver = () => resolve ( cleanup );
RetryfyQueue.add ( resolver );
});
},
tick: (): void => {
if ( RetryfyQueue.queueActive.size >= RetryfyQueue.limit ) return;
if ( !RetryfyQueue.queueWaiting.size ) return RetryfyQueue.reset ();
for ( const fn of RetryfyQueue.queueWaiting ) {
if ( RetryfyQueue.queueActive.size >= RetryfyQueue.limit ) break;
RetryfyQueue.queueWaiting.delete ( fn );
RetryfyQueue.queueActive.add ( fn );
fn ();
}
}
};
/* EXPORT */
export default RetryfyQueue;

View File

@@ -0,0 +1,60 @@
/* IMPORT */
import {Disposer} from '../types';
/* VARIABLES */
const Queues: Record<string, Function[] | undefined> = {};
/* SCHEDULER */
//TODO: Maybe publish this as a standalone package
const Scheduler = {
next: ( id: string ): void => {
const queue = Queues[id];
if ( !queue ) return;
queue.shift ();
const job = queue[0];
if ( job ) {
job ( () => Scheduler.next ( id ) );
} else {
delete Queues[id];
}
},
schedule: ( id: string ): Promise<Disposer> => {
return new Promise ( resolve => {
let queue = Queues[id];
if ( !queue ) queue = Queues[id] = [];
queue.push ( resolve );
if ( queue.length > 1 ) return;
resolve ( () => Scheduler.next ( id ) );
});
}
};
/* EXPORT */
export default Scheduler;

View File

@@ -0,0 +1,97 @@
/* IMPORT */
import * as path from 'path';
import {LIMIT_BASENAME_LENGTH} from '../consts';
import {Disposer} from '../types';
import FS from './fs';
/* TEMP */
//TODO: Maybe publish this as a standalone package
const Temp = {
store: <Record<string, boolean>> {}, // filePath => purge
create: ( filePath: string ): string => {
const randomness = `000000${Math.floor ( Math.random () * 16777215 ).toString ( 16 )}`.slice ( -6 ), // 6 random-enough hex characters
timestamp = Date.now ().toString ().slice ( -10 ), // 10 precise timestamp digits
prefix = 'tmp-',
suffix = `.${prefix}${timestamp}${randomness}`,
tempPath = `${filePath}${suffix}`;
return tempPath;
},
get: ( filePath: string, creator: ( filePath: string ) => string, purge: boolean = true ): [string, Disposer] => {
const tempPath = Temp.truncate ( creator ( filePath ) );
if ( tempPath in Temp.store ) return Temp.get ( filePath, creator, purge ); // Collision found, try again
Temp.store[tempPath] = purge;
const disposer = () => delete Temp.store[tempPath];
return [tempPath, disposer];
},
purge: ( filePath: string ): void => {
if ( !Temp.store[filePath] ) return;
delete Temp.store[filePath];
FS.unlinkAttempt ( filePath );
},
purgeSync: ( filePath: string ): void => {
if ( !Temp.store[filePath] ) return;
delete Temp.store[filePath];
FS.unlinkSyncAttempt ( filePath );
},
purgeSyncAll: (): void => {
for ( const filePath in Temp.store ) {
Temp.purgeSync ( filePath );
}
},
truncate: ( filePath: string ): string => { // Truncating paths to avoid getting an "ENAMETOOLONG" error //FIXME: This doesn't really always work, the actual filesystem limits must be detected for this to be implemented correctly
const basename = path.basename ( filePath );
if ( basename.length <= LIMIT_BASENAME_LENGTH ) return filePath; //FIXME: Rough and quick attempt at detecting ok lengths
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec ( basename );
if ( !truncable ) return filePath; //FIXME: No truncable part detected, can't really do much without also changing the parent path, which is unsafe, hoping for the best here
const truncationLength = basename.length - LIMIT_BASENAME_LENGTH;
return `${filePath.slice ( 0, - basename.length )}${truncable[1]}${truncable[2].slice ( 0, - truncationLength )}${truncable[3]}`; //FIXME: The truncable part might be shorter than needed here
}
};
/* INIT */
process.on ( 'exit', Temp.purgeSyncAll ); // Ensuring purgeable temp files are purged on exit
/* EXPORT */
export default Temp;