UNPKG

memory-manager-service

Version:

This service can be used to generate an internal private data that gets automatically disposed when it's not used anymore.

470 lines (408 loc) 13.3 kB
/* * Copyright (c) 2020 * Author: Marco Castiello * E-mail: marco.castiello@gmail.com * Project: MemoryManager.js */ import "index-map-class"; import "thread-manager-service"; import GarbageCollector from "./garbage-collector"; /** * Cross reference between stored objects and their ID. * @type {IndexMap} * @private */ const indexReference = new IndexMap(); /** * List of all the data managed. * @type {Map<String, Object>} * @private */ const dataMap = new Map(); /** * List of all the complex objects associated to the managed data. * @type {Map<String, Object>} * @private */ const complexDataMap = new Map(); /** * List of all the callbacks executed when an object is disposed. * @type {Map<String, Function>} * @private */ const disposeCallbackMap = new Map(); /** * List of all the callbacks executed when a property is updated. * @type {Map<String, Function>} * @private */ const updateCallbackMap = new Map(); /** * Reference to the garbage collector. * @type {WebThread} * @private */ let garbageCollector = null; /** * Queue of messages to send to the garbage collector as soon as it's ready. * @type {Array} * @private */ let garbageCollectorQueue = []; /** * Number of milliseconds used by the garbage collector to * decide if it needs to dispose of an object. * @type {Number} * @private */ let collectionTime = 30000; // Start the garbage collector thread. Threads.run(GarbageCollector).then(thread => { garbageCollector = thread; // Add the listener to all the possible events triggered by the garbage collector. garbageCollector.addEventListener("message", event => { const id = event.data.index; switch (event.data.name) { case "updated": dataMap.set(id, event.data.content); break; case "delete": const callbacks = disposeCallbackMap.get(id); for (let callback of callbacks) { callback(id); } indexReference.delete(id); dataMap.delete(id); complexDataMap.delete(id); disposeCallbackMap.delete(id); updateCallbackMap.delete(id); break; } }); // Execute the queue of messages already generated. for (let message of garbageCollectorQueue) { garbageCollector.postMessage(message); } garbageCollectorQueue.length = 0; }); /** * Check if the parameter is a complex object, like an instance of a class. * @returns {Boolean} * @private */ const isComplexObject = (obj) => { return Boolean(obj) && ((typeof obj === "object" && obj.constructor !== Object) || typeof obj === "function") && !Array.isArray(obj); }; /** * Check if the parameter is an object stored in the manager. * @returns {Boolean} * @private */ const isManagedObject = (obj) => { return Boolean(obj) && typeof obj === "object" && indexReference.has(obj); }; /** * Check if the parameter is the index of an objects stored in the manager. * @returns {Boolean} * @private */ const isManagedIndex = (index) => { return typeof index === "string" && indexReference.has(index); }; /** * Get the value stored in a proxy array. * @returns {*} * @private */ const getArrayValue = (index, list, id) => { let value = list[id]; if (!isNaN(id)) { if (isManagedIndex(value)) { value = indexReference.get(value); } else if (/ComplexObject::/.test(value)) { value = complexDataMap.get(index).get(value.replace("ComplexObject::", "")); } } notifyGarbageCollector({ "name": "update", "index": index }); return value; }; /** * Store a value into a proxy array. * @returns {Boolean} * @private */ const setArrayValue = (index, property, list, id, value) => { if (!isNaN(id)) { if (isManagedObject(value)) { value = indexReference.get(value); } else if (isComplexObject(value)) { complexDataMap.get(index).set(property + "::" + id, value); value = "ComplexObject::" + property + "::" + id; } else if (value === null || value === undefined) { complexDataMap.get(index).delete(property + "::" + id); } } list[id] = value; notifyUpdate(index, property, list); return true; }; /** * Generate a proxy element used to query the original array. * @returns {Array} */ const storeArray = (index, property, arr) => { // Object containing all the traps to manage a proxy array of mangaed objects. const arrayTrap = { get: (...params) => getArrayValue(index, ...params), set: (...params) => setArrayValue(index, property, ...params) }; for (let i=0, ii=arr.length; i<ii; i++) { let value = arr[i]; if (isManagedObject(value)) { value = indexReference.get(value); } else if (isComplexObject(value)) { complexDataMap.get(index).set(property + "::" + i, value); value = "ComplexObject::" + property + "::" + i; } arr[i] = value; } complexDataMap.get(index).set(property, new Proxy(arr, arrayTrap)); return arr; }; /** * Notify that a property value has been updated. * Notification is sent to the Garbage Collector and * to the registered 'onUpdate' callback. * @param {String} index * @param {String} property * @param {*} value * @private */ const notifyUpdate = (index, property, value) => { const callbacks = updateCallbackMap.get(index); notifyGarbageCollector({ "name": "update", "index": index, "content": { [property]: value } }); for (let callback of callbacks) { callback(index, property, value); } }; /** * Send a message to the garbage collector or store it into a queue if it's not yet ready. * @param {Object} message * @private */ const notifyGarbageCollector = message => { if (garbageCollector) { garbageCollector.postMessage(message); } else { garbageCollectorQueue.push(message); } }; /** * Manage data generated for a specific object. * @class */ class MemoryManager { /** * Create the object data and initialise it with the provided content. * The data will be automatically provided with an index. * The method will return the generated index. * If the keepAlive is set to true, the object will not be garbage collected * and will require to be disposed manually. * @param {Object} object * @param {Object} [content] * @param {Boolean} [keepAlive] * @returns {String} */ create(object, content, keepAlive) { let id; content = content || {}; if (indexReference.has(object)) { id = indexReference.get(object); this.update(object, content); } else { id = content.id || Threads.generateUUID(); const data = Object.assign({ "id": id }, content); notifyGarbageCollector({ "name": "create", "index": id, "keepAlive": Boolean(keepAlive) }); dataMap.set(id, {}); indexReference.set(id, object); complexDataMap.set(id, new Map()); disposeCallbackMap.set(id, []); updateCallbackMap.set(id, []); this.update(object, data); } return id; } /** * Get the content of a property stored for a specific reference. * @param {String|Object} reference * @param {String} property * @returns {*} */ get(reference, property) { const index = typeof reference === "string" ? reference : indexReference.get(reference); const data = index && dataMap.get(index); if (data) { let value = data[property]; if (isManagedIndex(value) && property !== "id") { value = indexReference.get(value); } else if (value === "ComplexData::" + property || Array.isArray(value)) { value = complexDataMap.get(index).get(property); } notifyGarbageCollector({ "name": "update", "index": index }); return value; } } /** * Set the value of a property for a specific object reference. * @param {String|Object} reference * @param {String} property * @param {*} value */ set(reference, property, value) { const index = typeof reference === "string" ? reference : indexReference.get(reference); const data = index && dataMap.get(index); if (data) { if (isManagedObject(value)) { value = indexReference.get(value); } else if (isComplexObject(value)) { complexDataMap.get(index).set(property, value); value = "ComplexData::" + property; } else if (Array.isArray(value)) { value = storeArray(index, property, value); } else if (value === null || value === undefined) { complexDataMap.get(index).delete(property); } data[property] = value; notifyUpdate(index, property, value); } } /** * Update all the data for a specific reference. * @param {String|Object} reference * @param {Object} content */ update(reference, content) { const index = typeof reference === "string" ? reference : indexReference.get(reference); if (content && index && indexReference.has(index)) { const keys = Object.keys(content); for (let i=0, ii=keys.length; i<ii; i++) { const key = keys[i]; this.set(index, key, content[key]); } } } /** * Check if a reference exists. * @param {String|Object} reference * @returns {Boolean} */ has(reference) { return indexReference.has(reference); } /** * Get the reference for the requested object. * @param {String|Object} reference * @returns {Object} */ reference(reference) { if (typeof reference === "string") { reference = indexReference.get(reference); } if (indexReference.has(reference)) { notifyGarbageCollector({ "name": "update", "index": indexReference.get(reference) }); return reference; } } /** * Dispose a stored object. * @param {String|Object} reference */ dispose(reference) { const index = typeof reference === "string" ? reference : indexReference.get(reference); if (index) { notifyGarbageCollector({ "name": "dispose", "index": index }); } } /** * Store a callback that will be executed when the object is disposed. * @param {String|Object} reference * @param {Function} callback */ onDispose(reference, callback) { const index = typeof reference === "string" ? reference : indexReference.get(reference); const callbacks = disposeCallbackMap.get(index); if (callbacks && typeof callback === "function") { callbacks.push(callback); } } /** * Store a callback that will be executed when the object is updated. * @param {String|Object} reference * @param {Function} callback */ onUpdate(reference, callback) { const index = typeof reference === "string" ? reference : indexReference.get(reference); const callbacks = updateCallbackMap.get(index); if (callbacks && typeof callback === "function") { callbacks.push(callback); } } /** * Flush the memory. */ flush() { notifyGarbageCollector({ "name": "flush" }); } /** * Gets the garbage collection time. * @returns {Number} */ get collectionTime() { return collectionTime; } /** * Sets the garbage collection time. * @param {Number} time */ set collectionTime(time) { time = Number(time); if (!isNaN(time)) { collectionTime = Math.round(time); notifyGarbageCollector({ "name": "time", "value": collectionTime }); } } } // Initialise the service. const manager = new MemoryManager(); manager.collectionTime = collectionTime; export default manager;