import { is } from '@alloc/is' import { emptyArray, flop, getDescriptor, getOwnDescriptor, hasOwn, nope, setHidden, } from './common' import { emitAdd, emitClear, emitDefine, emitRemove, emitReplace, emitSplice, } from './emit' import { globals, observe } from './globals' import { ArrayIterators, MapIterators, SetIterators } from './iterators' import { noto } from './noto' import { $$, $O, SIZE } from './symbols' const ArrayOverrides: any = { concat(...args: any[]) { const self: any[] = this[$$] observe(self, $O) args.forEach(arg => { arg = arg && arg[$$] if (is.array(arg)) { observe(arg, $O) } }) return noto(() => self.concat(...args)) }, copyWithin: flop, fill: flop, pop() { const self: any[] = this[$$] const oldLength = self.length if (oldLength) { const index = oldLength - 1 const exists = index in self const value = self.pop() if (exists) emitRemove(self, index, value) emitReplace(self, SIZE, index, oldLength) return value } }, push(...values: any[]): number { const self: any[] = this[$$] const oldLength = self.length if (values.length) { const length = self.push(...values) emitSplice(self, oldLength, values, emptyArray) emitReplace(self, SIZE, length, oldLength) return length } return oldLength }, reverse() { const self: any[] = this[$$] const oldValue = [...self] self.reverse() emitSplice(self, 0, [...self], oldValue) return this }, shift() { const self: any[] = this[$$] const oldLength = self.length if (oldLength) { const exists = 0 in self const value = self.shift() if (exists) emitRemove(self, 0, value) emitReplace(self, SIZE, oldLength - 1, oldLength) return value } }, sort(compare: (a: any, b: any) => number) { const self: any[] = this[$$] const oldValue = [...self] self.sort(compare) emitSplice(self, 0, [...self], oldValue) return this }, splice(start: number, removeCount = 0, ...values: any[]): any[] { const self: any[] = this[$$] const oldLength = self.length if (start < 0) { start = Math.max(0, start + oldLength) } else if (start > oldLength) { start = oldLength } const removed = self.splice(start, removeCount, ...values) removeCount = removed.length const addCount = values.length if (addCount || removeCount) { const delta = addCount - removeCount emitSplice(self, start, values, [...removed]) if (delta) emitReplace(self, SIZE, oldLength + delta, oldLength) } return removed }, unshift(...values: any[]) { const self: any[] = this[$$] const oldLength = self.length if (values.length) { const length = self.unshift(...values) emitSplice(self, 0, values, emptyArray) emitReplace(self, SIZE, length, oldLength) return length } return oldLength }, } export const ObjectTraps: ProxyHandler = { has: (self, key) => ( // TODO: Avoid observing "replace" events here. observe(self, key), Reflect.has(self, key) ), get(self, key, proxy) { if (key === $$) return self if (key !== $O) { const desc = getDescriptor(self, key) const get = desc && desc.get if (get) { return get.call(proxy) } // Observe unknown keys and own keys only. if (!desc || hasOwn(self, key)) { observe(self, key) } } return self[key] }, set(self, key, value, proxy) { const desc = getDescriptor(self, key) return desc && desc.get ? Reflect.set(self, key, value, proxy) : setProperty(self, key, value) }, defineProperty(self, key, desc) { const prevDesc = getOwnDescriptor(self, key) return Reflect.defineProperty(self, key, desc) && (desc.get || (prevDesc && prevDesc.get)) ? emitDefine(self, key, desc, prevDesc) : prevDesc ? desc.value === prevDesc.value || emitReplace(self, key, desc.value, prevDesc.value) : emitAdd(self, key, desc.value) }, deleteProperty, preventExtensions: nope, } export const ArrayTraps: ProxyHandler = { has: ObjectTraps.has, get(self: any, key: keyof any) { if (key === $$) return self if (key === 'length') { observe(self, SIZE) return self[key] } return ( ArrayOverrides[key] || (globals.observe && ArrayIterators[key]) || self[key] ) }, set(self, key, value) { if (key === 'length') { const oldLength = self.length if (value !== oldLength) { const oldValues = self.slice(value) self.length = value emitReplace(self, SIZE, value, oldLength) if (value < oldLength) { emitSplice(self, value, emptyArray, oldValues) } } } else { const oldLength = self.length setProperty(self, key, value) if (oldLength !== self.length) { emitReplace(self, SIZE, self.length, oldLength) } } return true }, deleteProperty, preventExtensions: nope, } export class ObservableMap extends Map { [$$]!: Map constructor(source: Map) { super() setHidden(this, $$, source) } get [$O]() { return this[$$][$O] } get size() { observe(this[$$], SIZE) return this[$$].size } has(key: K) { // TODO: Avoid observing "replace" events here. observe(this[$$], key) return this[$$].has(key) } get(key: K) { observe(this[$$], key) return this[$$].get(key) } set(key: K, value: V) { const exists = this[$$].has(key) const oldValue = exists ? this[$$].get(key) : void 0 if (!exists || value !== oldValue) { this[$$].set(key, value) if (exists) { emitReplace(this[$$], key, value, oldValue) } else { const oldSize = this[$$].size emitAdd(this[$$], key, value) emitReplace(this[$$], SIZE, oldSize + 1, oldSize) } } return this } delete(key: K) { const oldSize = this[$$].size const oldValue = this[$$].get(key) return ( this[$$].delete(key) && emitRemove(this[$$], key, oldValue) && emitReplace(this[$$], SIZE, oldSize - 1, oldSize) ) } clear() { const oldSize = this[$$].size if (oldSize) { const oldValues = new Map(this[$$]) this[$$].clear() emitClear(this[$$], oldValues) emitReplace(this[$$], SIZE, 0, oldSize) } } } export class ObservableSet extends Set { [$$]!: Set constructor(source: Set) { super() setHidden(this, $$, source) } get [$O]() { return this[$$][$O] } get size() { observe(this, SIZE) return this[$$].size } has(value: T) { observe(this[$$], SIZE) return this[$$].has(value) } add(value: T) { const oldSize = this[$$].size this[$$].add(value) if (oldSize !== this[$$].size) { emitAdd(this[$$], void 0, value) emitReplace(this[$$], SIZE, oldSize + 1, oldSize) } return this } delete(value: T) { const oldSize = this[$$].size return ( this[$$].delete(value) && emitRemove(this[$$], void 0, value) && emitReplace(this[$$], SIZE, oldSize - 1, oldSize) ) } clear() { const oldSize = this[$$].size if (oldSize) { const oldValues = new Set(this[$$]) this[$$].clear() emitClear(this[$$], oldValues) emitReplace(this[$$], SIZE, 0, oldSize) } } } defineMethods(ObservableMap, MapIterators) defineMethods(ObservableSet, SetIterators) function defineMethods(Class: any, overrides: { [key: string]: any }) { Object.defineProperties( Class.prototype, Object.getOwnPropertyDescriptors(overrides) ) } function setProperty(self: object, key: any, value: any) { const exists = hasOwn(self, key) const oldValue = self[key] self[key] = value return exists ? value === oldValue || emitReplace(self, key, value, oldValue) : emitAdd(self, key, value) } function deleteProperty(self: object, key: any) { if (!hasOwn(self, key)) return true const oldValue = self[key] delete self[key] return emitRemove(self, key, oldValue) }