import EventEmitter from 'events'; const SymbolEvents = Symbol('events'); const SymbolArray = Symbol('array'); function emitSet(target, value, prevValue) { const emitter = target[SymbolEvents]; if (!emitter.paused) { const updates = { added: [], removed: [] }; if (prevValue) { emitter.emit('remove', prevValue, target); updates.removed.push(prevValue); } if (value) { emitter.emit('add', value, target); updates.added.push(value); } emitter.emit('change', updates, target); } } function emitRemoved(target, removed) { const emitter = target[SymbolEvents]; if (!emitter.paused) { for (const item of removed) { emitter.emit('remove', item, target); } emitter.emit('change', { added: [], removed }, target); } } function checkType(target, value) { const modelClass = target.constructor.model; if (!modelClass) { throw new Error(`Model type not defined for ${target.constructor.name}`); } if (!(value instanceof modelClass)) { const valueType = value && value.constructor ? value.constructor.name : typeof value; throw new Error(`Attempt to write ${valueType} into ${target.constructor.name}`); } } const ProxyDef = { set(target, property, value) { const numProp = parseInt(property); if (isNaN(numProp)) { target[property] = value; return true; } checkType(target, value); const array = target[SymbolArray]; const prevValue = array[property]; if (prevValue !== value) { array[property] = value; emitSet(target, value, prevValue); } return true; }, get(target, property) { if (typeof property !== 'string') { return target[property]; } const numProp = parseInt(property); if (isNaN(numProp)) { return target[property]; } return target[SymbolArray][property]; } }; class Collection { constructor(items) { const emitter = new EventEmitter(); emitter.setMaxListeners(100); const properties = { [SymbolEvents]: { value: emitter }, [SymbolArray]: { value: [] } }; Object.defineProperties(this, properties); if (items) { this.push(...items); } return new Proxy(this, ProxyDef); } get length() { return this[SymbolArray].length; } set length(value) { const array = this[SymbolArray]; let removed; if (value < array.length) { removed = array.slice(value); } array.length = value; if (removed) { emitRemoved(this, removed); } } push(...items) { if (items.length) { for (const item of items) { checkType(this, item); } this[SymbolEvents].paused = true; this[SymbolArray].push(...items); this[SymbolEvents].paused = false; for (const item of items) { this[SymbolEvents].emit('add', item, this); } this[SymbolEvents].emit('change', { added: items, removed: [] }, this); } } pop() { this[SymbolEvents].paused = true; const item = this[SymbolArray].pop(); this[SymbolEvents].paused = false; if (item) { this[SymbolEvents].emit('remove', item, this); this[SymbolEvents].emit('change', { added: [], removed: [item] }, this); } return item; } shift() { this[SymbolEvents].paused = true; const item = this[SymbolArray].shift(); this[SymbolEvents].paused = false; if (item) { this[SymbolEvents].emit('remove', item, this); this[SymbolEvents].emit('change', { added: [], removed: [item] }, this); } return item; } unshift(...items) { if (items.length) { for (const item of items) { checkType(this, item); } this[SymbolEvents].paused = true; this[SymbolArray].unshift(...items); this[SymbolEvents].paused = false; for (const item of items) { this[SymbolEvents].emit('add', item, this); } this[SymbolEvents].emit('change', { added: items, removed: [] }, this); } } splice(start, deleteCount, ...items) { for (const item of items) { checkType(this, item); } this[SymbolEvents].paused = true; const removed = this[SymbolArray].splice(start, deleteCount, ...items); this[SymbolEvents].paused = false; for (const item of removed) { this[SymbolEvents].emit('remove', item, this); } for (const item of items) { this[SymbolEvents].emit('add', item, this); } if (removed.length || items.length) { this[SymbolEvents].emit('change', { added: items, removed }, this); } } on(eventName, listener) { this[SymbolEvents].on(eventName, listener); } once(eventName, listener) { this[SymbolEvents].once(eventName, listener); } off(eventName, listener) { this[SymbolEvents].off(eventName, listener); } get(id) { return this.find(model => model.id === id); } remove(idOrModel) { for (let i = 0; i < this.length; i++) { while (i < this.length && (this[i].id === idOrModel || this[i] === idOrModel)) { this.splice(i, 1); } } } sort() { return this[SymbolArray].sort(this.comparator); } fill() { throw new Error('Not implemented'); } copyWithin() { throw new Error('Not implemented'); } toJSON() { return this[SymbolArray].concat(); } } const ProxiedArrayMethods = [ Symbol.iterator, 'concat', 'entries', 'every', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'reverse', 'slice', 'some', 'values' ]; for (const method of ProxiedArrayMethods) { Object.defineProperty(Collection.prototype, method, { value: function proxyMethod(...args) { return this[SymbolArray][method](...args); } }); } export { Collection };