UNPKG

@chensational/matrixmap

Version:

extension of Array with automatic Map index by unique key

378 lines (338 loc) 11.5 kB
// MatrixMap.js /** * Creates a MatrixMap—a plain Array decorated with key–indexing capabilities. * * @param {Array} [initialElements=[]] - Initial elements for the MatrixMap. * @param {Object} [options={}] - Options for configuration. * @param {string} [options.keyField='_id'] - The property name used as the key. * @returns {Array} A plain array with additional MatrixMap functionality. */ function createMatrixMap(initialElements = [], options = {}) { const keyField = options.keyField || '_id'; // Create a plain array from the initial elements. const arr = new Proxy([], { defineProperty(target, prop, desc) { // Override property descriptor for numeric indices if (!isNaN(parseInt(prop))) { desc.configurable = true; desc.writable = true; } return Reflect.defineProperty(target, prop, desc); } }); // Copy elements with proper descriptors initialElements.forEach(element => arr.push(element)); // Attach a hidden property for the key map. Object.defineProperty(arr, 'keyMap', { value: new Map(), writable: true, enumerable: false, configurable: false, }); // Attach a hidden property for the index map. Object.defineProperty(arr, 'indexMap', { value: new Map(), writable: true, enumerable: false, configurable: false, }); // Attach a hidden property for the key field. Object.defineProperty(arr, 'keyField', { value: keyField, writable: false, enumerable: false, configurable: false, }); // Internal helper to rebuild both maps from scratch. Object.defineProperty(arr, 'rebuildKeyMaps', { value: function (idx = 0) { this.keyMap.clear(); this.indexMap.clear(); for (let i = idx; i < this.length; i++) { const item = this[i]; if (item?.[this?.keyField] !== undefined) { const key = item[this.keyField]; this.keyMap.set(key, item); this.indexMap.set(key, idx); idx++; } } }, enumerable: false, }); // Build the initial key map and index map.] let idx = 0; arr.forEach((item, index) => { if ((!arr?.indexMap?.has(item?.[arr?.keyField])) && item[arr?.keyField] !== undefined) { arr.keyMap.set(item[arr?.keyField], item); arr.indexMap.set(item[arr?.keyField], idx); idx++; } }); // Define array methods Object.defineProperties(arr, { /** * Returns the element corresponding to the given key. * * @param {*} key - The key value. * @returns {*} The matching element or undefined. */ getByKey: { value: function(key) { return this.keyMap.get(key); }, enumerable: false, }, /** * Updates the element with the given key. * * @param {*} key - The key of the element to update. * @param {*} newValue - The new value to set. * @returns {boolean} True if updated; false if no element with the key exists. */ updateByKey: { value: function(key, newValue) { if (!newValue?.[this?.keyField]) { return false; } const index = this.indexMap.get(key); if (!index) { arr.push(newValue); this.indexMap.set(item[arr?.keyField], arr?.length - 1); return true; } arr[index] = newValue; this.keyMap.set(key, newValue); this.indexMap.set(key, index); return true; }, enumerable: false, }, /** * Override push so that new items are added to the key map and index map. */ push: { value: function(...items) { const startIndex = this.length; let idx = startIndex; items.forEach((item, i) => { const key = item?.[this.keyField]; const index = this.indexMap.get(key); if (!index) { idx++ Object.defineProperty(this, startIndex + idx, { value: item, writable: true, enumerable: true, configurable: true }); arr[idx] = item; this.keyMap.set(key, item); this.indexMap.set(key, startIndex + idx); } else { arr[index] = item; this.keyMap.set(key, item); this.indexMap.set(key, index); } }); return this.length; }, enumerable: false, }, /** * Override pop so that removed items are deleted from the key map and index map. */ pop: { value: function() { const item = Array.prototype.pop.call(this); const key = item?.[this.keyField]; this.keyMap.delete(key); this.indexMap.delete(key); return item; }, enumerable: false, }, /** * Override shift so that the key map and index map are updated. */ shift: { value: function() { // does this need to rebuild indexMap??? const item = Array.prototype.shift.call(this); this.rebuildKeyMaps(); return item; }, enumerable: false, }, /** * Override unshift so that new items are added to the key map and index map. */ unshift: { value: function(...items) { const result = Array.prototype.unshift.apply(this, items); this.rebuildKeyMaps(); return result; }, enumerable: false, }, /** * Override splice so that both removal and insertion update the key map and index map. */ splice: { value: function(start, deleteCount, ...items) { const removed = Array.prototype.splice.apply(this, [start, deleteCount, ...items]); // Remove keys for removed items. removed.forEach(item => { const key = item?.[this?.keyField]; this.keyMap?.delete(key); this.indexMap?.delete(key); }); // Add keys for inserted items. this.rebuildKeyMaps(start); return removed; }, enumerable: false, }, /** * Override fill so that affected indices update the key map and index map. */ fill: { value: function(value, start = 0, end = this.length) { const len = this.length; start = (start < 0) ? Math.max(len + start, 0) : Math.min(start, len); end = start+1 for (let i = start; i < end; i++) { const key = item?.[this?.keyField]; const oldItem = this[i]; this.keyMap?.delete(oldItem?.[key]); this.indexMap?.delete(oldItem?.[key]); arr[i] = value; this.keyMap?.set(key, value); this.indexMap?.set(key, i); } return this; }, enumerable: false, }, /** * Override copyWithin so that the key map and index map are rebuilt after the operation. */ copyWithin: { value: function(target, start, end) { const len = this.length; let to = target < 0 ? Math.max(len + target, 0) : Math.min(target, len); let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len); let final = end < 0 ? Math.max(len + end, 0) : Math.min(end, len); const count = Math.min(final - from, len - to); // Delete keys for items that will be overwritten for (let i = to; i < to + count; i++) { const key = item?.[this?.keyField]; const item = this[i]; this.keyMap?.delete(key); this.indexMap?.delete(key); } // Perform copyWithin in bulk Array.prototype.copyWithin.call(this, target, start, end); // Update keyMap and indexMap with new items for (let i = to; i < to + count; i++) { const key = item?.[this?.keyField]; const newItem = this[i]; this.keyMap?.set(key, newItem); this.indexMap?.set(key, i); } return this; }, enumerable: false, }, /** * Override sort so that the key map and index map are rebuilt after sorting. */ sort: { value: function(compareFn) { Array.prototype.sort.call(this, compareFn); this.rebuildKeyMaps(); return this; }, enumerable: false, }, /** * Override reverse so that the key map and index map remain consistent. */ reverse: { value: function() { Array.prototype.reverse.call(this); this.rebuildKeyMaps(); return this; }, enumerable: false, }, }); // Wrap the array in a Proxy to intercept assignments return new Proxy(arr, { set: (target, property, value, receiver) => { if (property === 'length') { const newLength = value; const oldLength = target.length; if (newLength < oldLength) { for (let i = newLength; i < oldLength; i++) { const key = item?.[target?.keyField]; target.keyMap?.delete(key); const item = target[i]; target.indexMap?.delete(item?.[key]); } } return Reflect.set(target, property, value, target); } // Convert property to numeric index if possible const index = (typeof property === 'string') ? Number(property) : property; // Handle array index assignments (both string and number types) if ((typeof index === 'number') && (index >= 0) && ((index | 0) === index)) { // Try to set directly on the target array first const success = Reflect.set(target, property, value, target); // If direct set fails, try setting the array value directly if (!success) { target[property] = value; } // Get the updated value to ensure it was set const currentValue = target[index]; // Update keyMap and indexMap if the array was successfully updated if (currentValue?.[target.keyField] === value?.[target.keyField]) { // Handle the case where new value is undefined/null (deletion) const oldValue = target[index]; if (!value) { target.keyMap.delete(oldValue?.[target.keyField]); target.indexMap.delete(oldValue?.[target.keyField]); return true; } const newKey = value[target.keyField]; // Only proceed if the new value has a valid key if (newKey !== undefined) { // Remove old value from keyMap and indexMap target.keyMap.delete(oldValue?.[target.keyField]); target.indexMap.delete(oldValue?.[target.keyField]); target.keyMap.set(newKey, value); target.indexMap.set(newKey, index); } return true; } return false; } // For non-index properties, just set the value return Reflect.set(target, property, value, receiver); }, deleteProperty: (target, property) => { if (typeof property === 'string') { const index = Number(property); if (index >= 0 && ((index | 0) === index)) { const item = target[index]; if (item && item[target.keyField] !== undefined) { target.keyMap.delete(item[target.keyField]); target.indexMap.delete(item[target.keyField]); } } } return Reflect.deleteProperty(target, property); } }); } module.exports = { createMatrixMap };