live-directory
Advanced tools
Comparing version 1.1.0 to 2.0.0
{ | ||
"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, | ||
}; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
2
26350
2
462
173
1
+ Addedchokidar@^3.5.2
+ Addedanymatch@3.1.3(transitive)
+ Addedbinary-extensions@2.3.0(transitive)
+ Addedbraces@3.0.2(transitive)
+ Addedchokidar@3.6.0(transitive)
+ Addedfill-range@7.0.1(transitive)
+ Addedfsevents@2.3.3(transitive)
+ Addedglob-parent@5.1.2(transitive)
+ Addedis-binary-path@2.1.0(transitive)
+ Addedis-extglob@2.1.1(transitive)
+ Addedis-glob@4.0.3(transitive)
+ Addedis-number@7.0.0(transitive)
+ Addednormalize-path@3.0.0(transitive)
+ Addedpicomatch@2.3.1(transitive)
+ Addedreaddirp@3.6.0(transitive)
+ Addedto-regex-range@5.0.1(transitive)