Socket
Socket
Sign inDemoInstall

live-directory

Package Overview
Dependencies
16
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.0 to 2.0.0

src/components/DirectoryTree.js

5

package.json
{
"name": "live-directory",
"version": "1.1.0",
"description": "A Simple-To-Use Dynamic Template Content Manager For Webservers",
"version": "2.0.0",
"description": "A Simple-To-Use Dynamic File Content Manager For Webservers",
"main": "index.js",

@@ -26,4 +26,5 @@ "scripts": {

"dependencies": {
"chokidar": "^3.5.2",
"etag": "^1.8.1"
}
}

107

README.md

@@ -15,3 +15,3 @@ # LiveDirectory: Dynamic File Content Manager

## Motivation
Implementing your own template management system which consistently reads/updates template content can be tedious. LiveDirectory aims to solve that by acting as an automated file content store making a directory truly come alive. Built solely on the Node.js FileWatcher API with no external dependencies, LiveDirectory can be an efficient solution for fast and iterative web development.
Implementing your own template/file management system which consistently reads/updates file content can be tedious. LiveDirectory aims to solve that by acting as an automated file content store making a directory truly come alive. Powered by the efficient file watching library chokidar, LiveDirectory can be an efficient solution for fast and iterative web development.

@@ -22,3 +22,2 @@ ## Features

- Asynchronous By Nature
- Custom Renderer Support
- Instantaneous Hot Reloading

@@ -41,3 +40,3 @@ - Memory Efficient

- [Examples](#examples)
- [Customized Dashboard User Page](#customized-dashboard-user-page)
- [Serving a basic HTML page](#serving-a-basic-html-page)
- [Customized Dashboard User Page With Compiled Renderer](#customized-dashboard-user-page-with-compiled-renderer)

@@ -56,5 +55,4 @@ - [LiveDirectory](#livedirectory)

#### Customized Dashboard User Page
#### Serving a basic HTML page
```javascript
const MicroMustache = require('micromustache');
const LiveDirectory = require('live-directory');

@@ -67,7 +65,2 @@

// Set default renderer which will render files using MicroMustache.render method
live_templates.set_default_renderer((path, content, options) => {
return MicroMustache.render(content, options);
});
// Create server route for dashboard user page

@@ -81,7 +74,4 @@ some_server.get('/dashboard/user', (request, response) => {

// Generate rendered template code
let html = template.render(user_options);
// Send rendered html code in response
return response.send(html);
return response.send(template.content);
});

@@ -100,20 +90,10 @@ ```

// Store compiled micromustache template instances in this object
const compiled_templates = {};
// Handle 'reload' event from LiveDirectory so we can re-generate a new compiled micromustache instance on each file content update
live_templates.handle('reload', (file) => {
// Generate a compiled micromustache template instance
let compiled = MicroMustache.compile(file.content);
// Store compiled micromustache template instance in compiled_templates identified by file path
compiled_templates[file.path] = compiled;
live_templates.on('file_reload', (file) => {
// We can attach our own properties to the LiveFile object
// Using this, we can recompile a micromustache renderer and attach onto LiveFile
const compiled = MicroMustache.compile(file.content);
compiled_templates[file.path].render = compiled.render;
});
// Set default renderer which will render files using compiled micromustache instance
live_templates.set_default_renderer((path, content, options) => {
// use || operator as a fallback in the scenario compiled is not available for whatever reason
return (compiled_templates[path] || MicroMustache).render(options);
});
// Create server route for dashboard user page

@@ -139,21 +119,11 @@ some_server.get('/dashboard/user', (request, response) => {

#### Constructor Options
* `root_path` [`String`]: Path to the directory.
* `path` [`String`]: Path to the directory.
* **Example**: `./templates/`
* **Required** for a LiveDirectory Instance.
* `file_extensions` [`Array`]: Which file extensions to load.
* **Example**: `['.html', '.css', '.js']`
* **Default**: `[]`
* **Note**: Setting this parameter to `[]` will enable all files with any extension.
* `ignore_files` [`Array`]: Specific file names to ignore.
* **Example**: `['secret.js']`
* **Default**: `[]`
* `ignore_directories` [`Array`]: Specific directory names to ignore.
* **Example**: `['.git', 'private']`
* **Default**: `[]`
* `watcher_delay` [`Number`]: Specify delay between processing new FileWatcher events in **milliseconds**.
* **Default**: `250`
* `read_delay` [`Number`]: Specify delay amount before reading new file content in **milliseconds**.
* **Default**: `250`
* `read_retries` [`Number`]: Number of times to retry reading new file content on empty reads.
* **Default**: `2`
* `ignore` [`Function`]: Ignore/Filter function for deciding which files to load.
* **Example**: `(String: path) => path.includes('node_modules')`
* **Usage**: Return `true` through the function when ignoring a file and vice versa.
* `retry` [`Object`]: File content reading retry policy.
* `every` [`Number`]: Delay between retries in **milliseconds**.
* `max` [`Number`]: Maximum number of retries.

@@ -163,21 +133,31 @@ #### LiveDirectory Properties

| :-------- | :------- | :------------------------- |
| `files` | `Object` | Currently loaded `LiveFile` instances. |
| `tree` | `Object` | Underlying root directory hierarchy tree. |
| `path_prefix` | `String` | Path prefix for path property key in hierarchy tree. |
| `path` | `String` | Root directory path. |
| `watcher` | `FS.Watcher` | Underlying Chokidar watcher instance. |
| `tree` | `Object` | Directory tree with heirarchy. |
| `files` | `Object` | All loaded files with their relative paths. |
#### LiveDirectory Methods
* `get(String: relative_path)`: Returns [`LiveFile`](#livefile) instance for file at specified relative path.
* `ready()`: Returns a `Promise` which is then resolved once instance is fully ready.
* `get(String: path)`: Returns [`LiveFile`](#livefile) instance for file at specified path.
* **Returns** a [`LiveFile`](#livefile) instance or `undefined`
* **Note** a relative path must start with `/` as the root which is then translated automatically into the raw system path.
* `set_default_renderer(Function: renderer)`: Sets default renderer method for all files in current instance.
* **Handler Example**: `(String: path, String: content, Object: options) => {}`
* `path`: System path of file being rendered.
* `content`: File content as a string type.
* `options`: Parameter options from `render(options)` method.
* `handle(String: type, Function: handler)`: Binds a handler for `LiveDirectory` events.
* Event `'error'`: Reports framework errors.
* `handler`: `(String: path, Error: error) => {}`
* Event `'reload'`: Reports file content reloads and can be useful for doing post processing on new file content.
* **Supported Formats**: When root path is `/root/var/www/webserver/templates`.
* **System Path**: `/root/var/www/webserver/templates/dashboard/index.html`
* **Relative Path**: `/dashboard/index.html`
* **Simple Path**: `dashboard/index.html`
* `on(String: type, Function: handler)`: Binds a handler for `LiveDirectory` events.
* Event `'directory_create'`: Reports newly created directories.
* `handler`: `(String: path) => {}`
* Event `'directory_destroy'`: Reports when a directory is deleted.
* `handler`: `(String: path) => {}`
* Event `'file_reload'`: Reports when a file is created/is reloaded.
* `handler`: `(LiveFile: file) => {}`
* See [`LiveFile`](#livefile) documentation for available properties and methods.
* Event `'file_destroy'`: Reports when a file is destroyed.
* `handler`: `(LiveFile: file) => {}`
* See [`LiveFile`](#livefile) documentation for available properties and methods.
* Event `'file_error'`: Reports FileSystem errors for a file.
* `handler`: `(LiveFile: file, Error: error) => {}`
* See [`LiveFile`](#livefile) documentation for available properties and methods.
* Event `'error'`: Reports `LiveDirectory` instance errors.
* `handler`: `(Error: error) => {}`

@@ -198,9 +178,6 @@ ## LiveFile

#### LiveFile Methods
* `set_content(String: content)`: Overwrites/Sets file content. Useful for writing processed file content from `reload` events.
* `set_renderer(Function: renderer)`: Sets renderer method. Useful for setting custom renderer method from compiled template render instances.
* **Renderer Example**: `(String: path, String: content, Object: options) => {}`
* `render(Object: options)`: Renders file content by calling renderer with provided options parameter.
* **Default**: `{}`
* `ready()`: Returns a `Promise` which is resolved once file is ready with initial content.
* `reload()`: Returns a `Promise` which is resolved once the File's content is reloaded.
## License
[MIT](./LICENSE)

@@ -1,194 +0,247 @@

const FileSystem = require('fs');
const DirectoryWatcher = require('./DirectoryWatcher.js');
const EventEmitter = require('events');
const DirectoryTree = require('./DirectoryTree.js');
const chokidar = require('chokidar');
const {
resolve_path,
forward_slashes,
wrap_object,
is_accessible_path,
} = require('../shared/operators.js');
const LiveFile = require('./LiveFile.js');
class LiveDirectory {
#root_watcher;
#files_tree = {};
#default_renderer = (path, content, options) => content;
#handlers = {
error: (path, error) => {},
reload: (file) => {},
class LiveDirectory extends EventEmitter {
#watcher;
#tree;
#options = {
path: '',
ignore: undefined,
retry: {
every: 250,
max: 2,
},
};
constructor({
root_path,
file_extensions = [],
ignore_files = [],
ignore_directories = [],
watcher_delay = 250,
}) {
// Verify provided constructor parameter types
this._verify_types({
root_path: root_path,
file_extensions: file_extensions,
ignore_files: ignore_files,
ignore_directories: ignore_directories,
watcher_delay: watcher_delay,
});
/**
* LiveDirectory constructor options
*
* @param {Object} options
* @param {String} options.path Path of the desired directory
* @param {function(string):Boolean} options.ignore Ignore function that prevents a file from being loaded when returned true.
*/
constructor(options = this.#options) {
super();
// Ensure root_path has a trailing slash for parsing purposes
root_path = DirectoryWatcher._ensure_trailing_slash(root_path);
// Enforce object only options type
if (options == null || typeof options !== 'object')
throw new Error('LiveDirectory options must be an object.');
// Verify provided directory actually exists and throw error on problem
let reference = this;
FileSystem.access(root_path, (error) => {
if (error) throw error;
// Resolve user provided path to absolute path and wrap local options object
options.path = resolve_path(options.path);
wrap_object(this.#options, options);
// Create root directory watcher
reference.#root_watcher = new DirectoryWatcher({
path: root_path,
extensions: file_extensions,
ignore_files: ignore_files,
ignore_directories: ignore_directories,
delay: watcher_delay,
});
// Create a empty directory tree for root path
this.#tree = new DirectoryTree();
// Bind root methods for powering file tree
reference._bind_root_handlers();
});
// Initiate watcher
this._initiate_watcher();
}
/**
* Returns LiveFile instance for specified relative path if one exists
*
* @param {String} relative_path
* @returns {LiveFile} LiveFile
* @private
* Initiates chokidar watcher instance for root library.
*/
get(relative_path) {
return this.#files_tree[relative_path];
}
async _initiate_watcher() {
const { path, ignore } = this.#options;
/**
* Binds handler for specified type event.
*
* @param {String} type
* @param {Function} handler
*/
handle(type, handler) {
if (this.#handlers[type] == undefined)
throw new Error(`${type} event is not supported on LiveDirectory.`);
// Ensure provided root path by user is accessible
if (!(await is_accessible_path(path)))
throw new Error(
'LiveDirectory.path is inaccessible or invalid. Please provide a valid path to a directory that exists.'
);
this.#handlers[type] = handler;
}
// Initiate chokidar watcher instance for root path
this.#watcher = chokidar.watch(path + '/', {
ignored: ignore,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 500,
},
});
/**
* This method can be used to set a default renderer for all files.
*
* @param {Function} renderer
*/
set_default_renderer(renderer) {
this.#default_renderer = renderer;
// Bind watch handlers for chokidar instance
this._bind_watch_handlers();
}
/**
* INTERNAL METHOD!
* This method converts absolute path into a relative path by converting root path into a '/'
* Returns relative path based on root path with forward slashes.
*
* @private
* @param {String} path
* @returns {String} String
*/
_get_relative_path(path) {
return path.replace(this.#root_watcher.root, '/');
_relative_path(path) {
return forward_slashes(path).replace(this.#options.path, '');
}
/**
* INTERNAL METHOD!
* This method verifies provided constructor types.
*
* @param {Object} data
* @private
* Binds watch handlers for chokidar watch instance.
*/
_verify_types({ root_path, file_extensions, ignore_files, ignore_directories, watcher_delay }) {
// Verify root_path
if (typeof root_path !== 'string')
throw new Error('LiveDirectory: constructor.options.root_path must be a String.');
_bind_watch_handlers() {
const reference = this;
// Verify watcher_delay
if (typeof watcher_delay !== 'number')
throw new Error('LiveDirectory: constructor.options.watcher_delay must be a Number.');
// Bind 'addDir' for when a directory is created
this.#watcher.on('addDir', (path) => {
// Add directory to tree and emit directory_create event
const relative = reference._relative_path(path);
reference.#tree.add(relative, {});
reference.emit('directory_create', relative);
});
// Verify file_extensions
if (!Array.isArray(file_extensions))
throw new Error('LiveDirectory: constructor.options.file_extensions must be an Array.');
// Bind 'unlinkDir' for when a directory is deleted
this.#watcher.on('unlinkDir', (path) => {
// Remove directory from tree and emit directory_create event
const relative = reference._relative_path(path);
reference.#tree.remove(relative);
reference.emit('directory_destroy', relative);
});
// Verify ignore_files
if (!Array.isArray(ignore_files))
throw new Error('LiveDirectory: constructor.options.ignore_files must be an Array.');
// Bind 'add' for when a file is created
this.#watcher.on('add', async (path) => {
// Create new LiveFile instance
const relative = reference._relative_path(path);
const file = new LiveFile({
path: forward_slashes(path),
retry: reference.#options.retry,
});
// Verify ignore_directories
if (!Array.isArray(ignore_directories))
throw new Error(
'LiveDirectory: constructor.options.ignore_directories must be an Array.'
);
}
// Add file to directory tree
reference.#tree.add(relative, file);
/**
* INTERNAL METHOD!
* Binds appropriate file handlers to root directory watcher.
*/
_bind_root_handlers() {
// Bind file_add event handler
this.#root_watcher.handle('file_add', (path) => this._add_file(path));
// Perform initial reload for file content readiness
try {
await file.reload();
} catch (error) {
reference.emit('file_error', file, error);
}
// Bind file_remove event handler
this.#root_watcher.handle('file_remove', (path) => this._remove_file(path));
// Emit file_reload event for user processing
reference.emit('file_reload', file);
});
// Bind 'change' for when a file is changed
this.#watcher.on('change', async (path) => {
const relative = reference._relative_path(path);
const file = reference.#tree.files[relative];
if (file) {
// Reload file content since file has changed
try {
await file.reload();
} catch (error) {
reference.emit('file_error', file, error);
}
// Emit file_reload event for user processing
reference.emit('file_reload', file);
}
});
// Bind 'unlink' for when a file is deleted
this.#watcher.on('unlink', (path) => {
const relative = reference._relative_path(path);
const file = reference.#tree.files[relative];
if (file) {
// Remove file from tree and emit file_destroy event
reference.#tree.remove(relative);
reference.emit('file_destroy', file);
}
});
// Bind 'ready' for when all files have been loaded
this.#watcher.once('ready', async () => {
// Wait for all pending reload operations to finish
try {
const files = reference.#tree.files;
const promises = [];
Object.keys(files).forEach((path) => {
// If no buffer exists for file then it is still being read
const current = files[path];
if (current.buffer == undefined) promises.push(current.ready());
});
// Wait for all pending files to have their content read
await Promise.all(promises);
} catch (error) {
reference.emit('error', error);
}
// Resolve pending promise if one exists
if (typeof reference.#ready_resolve == 'function') {
reference.#ready_resolve();
reference.#ready_resolve = null;
}
});
}
#ready_promise;
#ready_resolve;
/**
* INTERNAL METHOD!
* Creates/Adds LiveFile instance for specified path
* Returns a promise which resolves to true once LiveDirectory instance is ready with all files/directories loaded.
*
* @param {String} path
* @returns {Promise}
*/
_add_file(path) {
let relative_path = this._get_relative_path(path);
if (this.#files_tree[relative_path] == undefined) {
this.#files_tree[relative_path] = new LiveFile({
path: path,
watcher_delay: this.#root_watcher.watcher_delay,
renderer: this.#default_renderer,
});
ready() {
// Resolve with true if ready is not a promise
if (this.#ready_promise === true) return Promise.resolve(true);
// Bind Error Handler
this.#files_tree[relative_path]._handle('error', (error) =>
this.#handlers.error(path, error)
);
// Create a promise if one does not exist for ready event
if (this.#ready_promise === undefined)
this.#ready_promise = new Promise((resolve) => (this.#ready_resolve = resolve));
// Bind Reload Handler
const reference = this;
this.#files_tree[relative_path]._handle('reload', () => {
const live_file = reference.#files_tree[relative_path];
if (typeof live_file.content == 'string')
reference.#handlers.reload(reference.#files_tree[relative_path]);
});
}
return this.#ready_promise;
}
/**
* INTERNAL METHOD!
* Destroys/Removes LiveFile instance for specified path
* Resolves a LiveFile based on absolute, relative, or url based path string.
* ASSUME Root is: /var/www/webserver/template
* Path can be: /var/www/webserver/template/dashboard.html
* Path can be: /dashboard.html
* Path can be: dashboard.html
*
* @param {String} path
* @returns {LiveFile|undefined}
*/
_remove_file(path) {
if (this.#files_tree[path]) {
this.#files_tree[path]._destroy();
delete this.#files_tree[path];
}
get(path) {
// Ensure path is a string
if (typeof path !== 'string')
throw new Error('LiveDirectory.get(path) -> path must be a String.');
// Strip root from path if exists
const root = this.#options.path;
if (path.startsWith(root)) path = path.replace(root, '');
// Add a leading slash if one does not exist
if (!path.startsWith('/')) path = '/' + path;
return this.#tree.files[path];
}
/* LiveDirectory Getters */
get files() {
return this.#files_tree;
get path() {
return this.#options.path;
}
get path_prefix() {
return this.#root_watcher.path_prefix;
get watcher() {
return this.#watcher;
}
get tree() {
return this.#root_watcher.tree;
return this.#tree;
}
get files() {
return this.#tree.files;
}
}
module.exports = LiveDirectory;
const etag = require('etag');
const FileSystem = require('fs');
const { async_wait, wrap_object } = require('../shared/operators');
class LiveFile {
#path;
#etag;

@@ -10,135 +10,105 @@ #extension;

#content;
#watcher;
#watcher_delay;
#read_delay;
#read_retries;
#last_update;
#renderer;
#handlers = {
reload: (content) => {},
error: (error) => {},
#options = {
path: '',
retry: {
every: 300,
max: 3,
},
};
constructor({ path, watcher_delay, read_delay = 250, read_retries = 2, renderer }) {
this.#path = path;
const path_chunks = this.#path.split('.');
constructor(options = this.#options) {
// Wrap options object with provided object
wrap_object(this.#options, options);
this.#extension = path_chunks[path_chunks.length - 1];
this.#watcher_delay = watcher_delay;
this.#read_delay = read_delay;
this.#read_retries = read_retries;
this.#last_update = Date.now() - watcher_delay;
this.#renderer = renderer;
this._init_watcher();
this._reload_content(read_delay, read_retries);
// Determine the extension of the file
this.#extension = this.#options.path.split('.');
this.#extension = this.#extension[this.#extension.length - 1];
}
/**
* This method can be used to set/update the render function for current live file.
*
* @param {Function} renderer
*/
set_renderer(renderer) {
if (typeof renderer !== 'function')
throw new Error(
'set_renderer(renderer) -> renderer must be a Function -> (content, options) => {}'
);
#reload_promise;
#reload_resolve;
#reload_reject;
this.#renderer = renderer;
}
/**
* This method can be used to render content by passing in appropriate options to the renderer.
* Reloads buffer/content for file asynchronously with retry policy.
*
* @param {Object} options
* @returns {String} String - Rendered Content
* @param {Boolean} fresh
* @param {Number} count
* @returns {Promise}
*/
render(options = {}) {
return this.#renderer(this.#path, this.#content, options);
}
reload(fresh = true, count = 0) {
const reference = this;
if (fresh) {
// Reuse promise if there if one pending
if (this.#reload_promise instanceof Promise) return this.#reload_promise;
/**
* INTERNAL METHOD
* Binds handler for specified type event.
*
* @param {String} type
* @param {Function} handler
*/
_handle(type, handler) {
if (this.#handlers[type] == undefined)
throw new Error(`${type} event is not supported on LiveFile.`);
// Create a new promise for fresh lookups
this.#reload_promise = new Promise((resolve, reject) => {
reference.#reload_resolve = resolve;
reference.#reload_reject = reject;
});
}
this.#handlers[type] = handler;
}
// Perform filesystem lookup query
FileSystem.readFile(this.#options.path, async (error, buffer) => {
// Pipe filesystem error through promise
if (error) {
reference._flush_ready();
return reference.#reload_reject(error);
}
/**
* INTERNAL METHOD!
* This method performs a check against last_update timestamp
* to ensure sufficient time has passed since last watcher update.
*
* @param {Boolean} touch
* @returns {Boolean} Boolean
*/
_delay_check(touch = true) {
let last_update = this.#last_update;
let watcher_delay = this.#watcher_delay;
let result = Date.now() - last_update > watcher_delay;
if (result && touch) this.#last_update = Date.now();
return result;
}
// Perform retries in accordance with retry policy
// This is to prevent empty reads on atomicity based modifications from third-party programs
const { every, max } = reference.#options.retry;
if (buffer.length == 0 && count < max) {
await async_wait(every);
return reference.reload(false, count + 1);
}
/**
* INTERNAL METHOD!
* This method initiates the FileWatcher used for current live file.
*/
_init_watcher() {
let reference = this;
// Update instance buffer/content/etag/last_update variables
reference.#buffer = buffer;
reference.#content = buffer.toString();
reference.#etag = etag(buffer);
reference.#last_update = Date.now();
// Create FileWatcher For File
this.#watcher = FileSystem.watch(this.#path, (event, file_name) => {
if (reference._delay_check())
reference._reload_content(reference.#read_delay, reference.#read_retries);
// Cleanup reload promises and methods
reference.#reload_resolve();
reference._flush_ready();
reference.#reload_resolve = null;
reference.#reload_reject = null;
reference.#reload_promise = null;
});
// Bind FSWatcher Error Handler To Prevent Execution Halt
this.#watcher.on('error', (error) => this.#handlers.error(error));
return this.#reload_promise;
}
#ready_promise;
#ready_resolve;
/**
* INTERNAL METHOD!
* This method reads/updates content for current live file.
* Flushes pending ready promise.
* @private
*/
_reload_content(delay = 0, retries = 0) {
setTimeout(
(reference, del, ret) =>
FileSystem.readFile(this.#path, (error, buffer) => {
// Report error through error handler
if (error) return reference.#handlers.error(error);
// Retry if no content was read and retries have been defined
if (buffer.length == 0 && ret > 0)
return reference._reload_content(del, ret - 1);
// Update content and trigger reload event
reference.#buffer = buffer;
reference.#content = buffer.toString();
reference.#etag = etag(reference.#buffer);
reference.#handlers.reload(reference.#content);
}),
delay,
this,
delay,
retries
);
_flush_ready() {
if (typeof this.#ready_resolve == 'function') {
this.#ready_resolve();
this.#ready_resolve = null;
}
}
/**
* INTERNAL METHOD!
* This method can be used to destroy current live file and its watcher.
* Returns a promise which resolves once first reload is complete.
*
* @returns {Promise}
*/
_destroy() {
this.#watcher.close();
this.#content = '';
this.#buffer = Buffer.from('');
ready() {
// Return true if no ready promise exists
if (this.#ready_promise === true) return Promise.resolve();
// Create a Promise if one does not exist for ready event
if (this.#ready_promise === undefined)
this.#ready_promise = new Promise((resolve) => (this.#ready_resolve = resolve));
return this.#ready_promise;
}

@@ -148,3 +118,3 @@

get path() {
return this.#path;
return this.#options.path;
}

@@ -151,0 +121,0 @@

@@ -0,67 +1,76 @@

const Path = require('path');
const FileSystem = require('fs');
/**
* INTERNAL METHOD!
* This method acts as an asynchronous forEach loop.
* @param {Array} items
* @param {Function} handler
* @returns {Promise} Promise
* Returns a promise which is resolved after provided delay in milliseconds.
*
* @param {Number} delay
* @returns {Promise}
*/
function async_for_each(items, handler, cursor = 0, final) {
// Return a top level promise to be resolved after cursor hits last item
if (final == undefined)
return new Promise((res, _) => async_for_each(items, handler, 0, res));
function async_wait(delay) {
return new Promise((resolve, reject) => setTimeout((res) => res(), delay, resolve));
}
// Iterate through each item and call each handler with iteration onto next with next callback
if (cursor < items.length)
return handler(items[cursor], () =>
async_for_each(items, handler, cursor + 1, final)
);
/**
* Returns provided path into absolute system path with forward slashes.
*
* @param {String} path
* @returns {String}
*/
function resolve_path(path) {
return forward_slashes(Path.resolve(path));
}
// Resolve master promise at end of exeuction
return final();
/**
* Returns provided path with forward slashes.
*
* @param {String} path
* @returns {String}
*/
function forward_slashes(path) {
return path.split('\\').join('/');
}
function throttled_for_each(
items,
per_eloop = 300,
handler,
cursor = 0,
final
) {
// Return top level promise which is resolved after execution
if (final == undefined)
return new Promise((resolve, reject) => {
if (items.length == 0) return resolve();
return throttled_for_each(
items,
per_eloop,
handler,
cursor,
resolve
);
/**
* Writes values from focus object onto base object.
*
* @param {Object} obj1 Base Object
* @param {Object} obj2 Focus Object
*/
function wrap_object(original, target) {
Object.keys(target).forEach((key) => {
if (typeof target[key] == 'object') {
if (original[key] === null || typeof original[key] !== 'object') original[key] = {};
wrap_object(original[key], target[key]);
} else {
original[key] = target[key];
}
});
}
/**
* Determines whether a path is accessible or not by FileSystem package.
*
* @param {String} path
* @returns {Promise}
*/
function is_accessible_path(path) {
return new Promise((resolve, reject) => {
// Destructure constants for determine read & write codes
const CONSTANTS = FileSystem.constants;
const IS_VALID = CONSTANTS.F_OK;
const HAS_PERMISSION = CONSTANTS.W_OK;
FileSystem.access(path, IS_VALID | HAS_PERMISSION, (error) => {
if (error) return resolve(false);
resolve(true);
});
// Calculuate upper bound and perform synchronous for loop
let upper_bound =
cursor + per_eloop >= items.length ? items.length : cursor + per_eloop;
for (let i = cursor; i < upper_bound; i++) handler(items[i]);
// Offset cursor and queue next iteration with a timeout
cursor = upper_bound;
if (cursor < items.length)
return setTimeout(
(a, b, c, d, e) => throttled_for_each(a, b, c, d, e),
0,
items,
per_eloop,
handler,
cursor,
final
);
return final();
});
}
module.exports = {
async_for_each: async_for_each,
throttled_for_each: throttled_for_each,
async_wait,
resolve_path,
forward_slashes,
wrap_object,
is_accessible_path,
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc