self-reload-json
Version:
Self reloading JSON handler
266 lines (219 loc) • 8.22 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
var _walkthroughtree = require('./walkthroughtree');
Object.keys(_walkthroughtree).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _walkthroughtree[key];
}
});
});
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _events = require('events');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const getOwnPropertyHashes = typeof Reflect === 'object' && Reflect.ownKeys || (Object.getOwnPropertySymbols ? function getOwnPropertyHashes(obj) {
return Array.prototype.concat(Object.getOwnPropertyNames(obj), Object.getOwnPropertySymbols(obj));
} : Object.getOwnPropertyNames);
const privateProps = typeof Symbol === 'function' ? Symbol('internals') : '__internals';
function isObject(o) {
if (!o) return false;
var type = typeof o;
return type === 'object' || type === 'function';
}
function getAllProperties(obj, output = []) {
if (!obj) return output;
Array.prototype.push.apply(output, getOwnPropertyHashes(obj));
return getAllProperties(Object.getPrototypeOf(obj), output);
}
function fileChanged(a, b, deferred) {
const internals = this[privateProps],
{ options } = internals;
if (options.delay) {
if (internals.fileChangeTimeout) {
clearTimeout(internals.fileChangeTimeout);
delete internals.fileChangeTimeout;
}
if (!deferred && !internals.updateFileLock) {
internals.fileChangeTimeout = setTimeout(internals.fileChanged, options.delay, a, b, true);
return;
}
}
if (internals.updateFileLock) return;
try {
if (a instanceof _fs2.default.Stats ? a.mtime === b.mtime : b !== internals.fileName) return;
this.forceUpdate();
} catch (err) {
console.log(err.stack || err);
}
}
function deepPatchLayer(srcObject, srcDescriptor, patchDescriptor, key, depth, options) {
// Pass the patched value if source / patch is not patchable, or already patched.
if (!depth || !srcDescriptor) return Object.defineProperty(srcObject, key, patchDescriptor);
const srcValue = srcDescriptor.value;
const patchValue = patchDescriptor.value;
if (srcValue === patchValue || !isObject(srcValue) || !isObject(patchValue)) return Object.defineProperty(srcObject, key, patchDescriptor);
const { ignoreProperties, keepNonExists } = options;
// Fetch all keys from patcher object.
const patchKeys = getOwnPropertyHashes(patchValue);
for (const key of patchKeys) {
if (ignoreProperties && ignoreProperties.includes(key)) continue;
const srcDescriptor = Object.getOwnPropertyDescriptor(srcValue, key);
if (srcDescriptor && !srcDescriptor.configurable) continue;
const patchDescriptor = Object.getOwnPropertyDescriptor(patchValue, key);
if (!srcDescriptor || !srcDescriptor.get && !patchDescriptor.get) this.next(srcValue, srcDescriptor, patchDescriptor, key, depth - 1, options);
}
// Take away properties that does not available in patcher (Pending to be deleted)
if (!keepNonExists) for (const key of getOwnPropertyHashes(srcValue)) {
if (patchKeys.includes(key) || ignoreProperties && ignoreProperties.includes(key)) continue;
const srcDescriptor = Object.getOwnPropertyDescriptor(srcValue, key);
if (!srcDescriptor.configurable) continue;
delete srcValue[key];
}
return srcObject;
}
class SelfReloadJSON extends _events.EventEmitter {
constructor(options) {
super();
switch (typeof options) {
case 'string':
options = { fileName: options };break;
case 'object':case 'undefined':
break;
default:
throw new Error('Invalid options type.');
}
// Prevent removal of internal handlers
this[privateProps] = null;
this[privateProps] = {
keys: [],
fileName: '',
watcher: null,
content: null,
fileChanged: (a, b, deferred) => fileChanged.call(this, a, b, deferred),
forceUpdate: () => this.forceUpdate(),
omitKeys: getAllProperties(this),
options: Object.assign({
fileName: '',
encoding: 'utf8',
additive: false,
method: 'native',
interval: 5000,
reviver: null,
replacer: null,
delay: 0,
depth: -1
}, options)
};
// Convert all internal values to non-enumerable,
// prevents those values exposed by save function.
for (let key in this) {
const value = this[key];
delete this[key];
Object.defineProperty(this, key, {
value,
enumerable: false,
configurable: true,
writable: true
});
}
this.resume();
}
stop() {
const internals = this[privateProps];
if (!internals.watcher) return;
if (typeof internals.watcher === 'string') _fs2.default.unwatchFile(internals.watcher, internals.fileChanged);else internals.watcher.close();
internals.watcher = null;
}
resume() {
this.stop();
const internals = this[privateProps],
{ options } = internals;
options.fileName = _path2.default.resolve(options.fileName);
internals.fileName = _path2.default.basename(options.fileName);
switch (options.method) {
case 'native':
internals.watcher = _fs2.default.watch(options.fileName, { encoding: options.encoding }, internals.fileChanged);
break;
case 'polling':
internals.watcher = options.fileName;
_fs2.default.watchFile(options.fileName, { interval: options.interval }, internals.fileChanged);
break;
}
this.forceUpdate();
}
save(options) {
const internals = this[privateProps];
options = Object.assign({ space: null }, internals.options, options);
internals.updateFileLock = true;
try {
const json = JSON.stringify(this, options.replacer, options.space);
_fs2.default.writeFileSync(internals.options.fileName, json, options);
internals.raw = json;
} finally {
internals.updateFileLock = false;
}
}
forceUpdate() {
const internals = this[privateProps],
{ options } = internals;
if (internals.updateFileLock) return;
internals.updateFileLock = true;
if (internals.retryTimer) {
clearTimeout(internals.retryTimer);
delete internals.retryTimer;
}
if (internals.fileChangeTimeout) {
clearTimeout(internals.fileChangeTimeout);
delete internals.fileChangeTimeout;
}
try {
const rawContent = _fs2.default.readFileSync(options.fileName, { encoding: options.encoding });
if (internals.raw === rawContent) return;
internals.raw = rawContent;
let newContent = JSON.parse(rawContent, options.reviver);
if (typeof newContent !== 'object') newContent = { value: newContent };
SelfReloadJSON.deepPatch(this, newContent, {
keepNonExists: options.additive,
ignoreProperties: internals.omitKeys,
depth: options.depth
});
internals.newContent = SelfReloadJSON.deepPatch(internals.newContent || {}, newContent, {
keepNonExists: options.additive,
depth: options.depth
});
} catch (err) {
switch (err && err.code) {
case 'EBUSY':
case 'EAGAIN':
internals.retryTimer = setTimeout(internals.forceUpdate, options.delay);
return;
}
this.emit('error', err);
return;
} finally {
internals.updateFileLock = false;
}
this.emit('updated', internals.newContent);
}
static deepPatch(source, patch, options = {}) {
if (!isObject(source) || !isObject(patch)) return patch;
const dummy = { value: source };
return (0, _walkthroughtree.walkThroughTree)(deepPatchLayer, {
firstResult: true
}, dummy, dummy, {
value: patch,
configurable: true,
enumerable: true,
writable: true
}, 'value', 'depth' in options ? options.depth : -1, options).value;
}
}
exports.default = SelfReloadJSON;
module.exports = exports['default'];
;