Comparing version 2.0.1 to 2.0.2
@@ -180,3 +180,3 @@ declare namespace onChange { | ||
*/ | ||
target<ObjectType extends {[key: string]: any}>(object: ObjectType): ObjectType; | ||
target<ObjectType extends {[key: string]: any}>(object: ObjectType): ObjectType; // eslint-disable-line @typescript-eslint/method-signature-style | ||
@@ -189,5 +189,5 @@ /** | ||
*/ | ||
unsubscribe<ObjectType extends {[key: string]: any}>(object: ObjectType): ObjectType; | ||
unsubscribe<ObjectType extends {[key: string]: any}>(object: ObjectType): ObjectType; // eslint-disable-line @typescript-eslint/method-signature-style | ||
}; | ||
export = onChange; |
263
index.js
'use strict'; | ||
const {TARGET, UNSUBSCRIBE} = require('./lib/constants'); | ||
const isBuiltin = require('./lib/is-builtin'); | ||
const path = require('./lib/path'); | ||
const isArray = require('./lib/is-array'); | ||
const isSymbol = require('./lib/is-symbol'); | ||
const ignoreProperty = require('./lib/ignore-property'); | ||
const Cache = require('./lib/cache'); | ||
const SmartClone = require('./lib/smart-clone'); | ||
const isPrimitive = value => value === null || (typeof value !== 'object' && typeof value !== 'function'); | ||
const isBuiltinWithoutMutableMethods = value => value instanceof RegExp || value instanceof Number; | ||
const isBuiltinWithMutableMethods = value => value instanceof Date; | ||
const isSameDescriptor = (a, b) => { | ||
return a !== undefined && b !== undefined && | ||
Object.is(a.value, b.value) && | ||
(a.writable || false) === (b.writable || false) && | ||
(a.enumerable || false) === (b.enumerable || false) && | ||
(a.configurable || false) === (b.configurable || false); | ||
}; | ||
const shallowClone = value => { | ||
if (isArray(value)) { | ||
return value.slice(); | ||
} | ||
return {...value}; | ||
}; | ||
const onChange = (object, onChange, options = {}) => { | ||
const proxyTarget = Symbol('ProxyTarget'); | ||
let inApply = false; | ||
let changed = false; | ||
let applyPath; | ||
let applyPrevious; | ||
let isUnsubscribed = false; | ||
const equals = options.equals || Object.is; | ||
let propCache = new WeakMap(); | ||
let pathCache = new WeakMap(); | ||
let proxyCache = new WeakMap(); | ||
const cache = new Cache(equals); | ||
const smartClone = new SmartClone(); | ||
const handleChange = (changePath, property, previous, value) => { | ||
if (isUnsubscribed) { | ||
return; | ||
const handleChangeOnTarget = (target, property, previous, value) => { | ||
if (!ignoreProperty(cache, options, property)) { | ||
handleChange(cache.getPath(target), property, previous, value); | ||
} | ||
}; | ||
if (!inApply) { | ||
const handleChange = (changePath, property, previous, value) => { | ||
if (smartClone.isCloning) { | ||
smartClone.update(changePath, property, previous); | ||
} else { | ||
onChange(path.concat(changePath, property), value, previous); | ||
return; | ||
} | ||
if (inApply && applyPrevious && previous !== undefined && value !== undefined && property !== 'length') { | ||
let item = applyPrevious; | ||
if (changePath !== applyPath) { | ||
changePath = path.after(changePath, applyPath); | ||
path.walk(changePath, key => { | ||
item[key] = shallowClone(item[key]); | ||
item = item[key]; | ||
}); | ||
} | ||
item[property] = previous; | ||
} | ||
changed = true; | ||
}; | ||
const getOwnPropertyDescriptor = (target, property) => { | ||
let props = propCache !== null && propCache.get(target); | ||
if (props) { | ||
props = props.get(property); | ||
} | ||
if (props) { | ||
return props; | ||
} | ||
props = new Map(); | ||
propCache.set(target, props); | ||
let prop = props.get(property); | ||
if (!prop) { | ||
prop = Reflect.getOwnPropertyDescriptor(target, property); | ||
props.set(property, prop); | ||
} | ||
return prop; | ||
}; | ||
const invalidateCachedDescriptor = (target, property) => { | ||
const props = propCache ? propCache.get(target) : undefined; | ||
if (props) { | ||
props.delete(property); | ||
} | ||
}; | ||
const buildProxy = (value, path) => { | ||
if (isUnsubscribed) { | ||
return value; | ||
} | ||
pathCache.set(value, path); | ||
let proxy = proxyCache.get(value); | ||
if (proxy === undefined) { | ||
proxy = new Proxy(value, handler); | ||
proxyCache.set(value, proxy); | ||
} | ||
return proxy; | ||
}; | ||
const unsubscribe = target => { | ||
isUnsubscribed = true; | ||
propCache = null; | ||
pathCache = null; | ||
proxyCache = null; | ||
return target; | ||
}; | ||
const ignoreProperty = property => { | ||
return isUnsubscribed || | ||
(options.ignoreSymbols === true && isSymbol(property)) || | ||
(options.ignoreUnderscores === true && property.charAt(0) === '_') || | ||
(options.ignoreKeys !== undefined && options.ignoreKeys.includes(property)); | ||
}; | ||
const handler = { | ||
get(target, property, receiver) { | ||
if (property === proxyTarget || property === TARGET) { | ||
return target; | ||
} | ||
if (isSymbol(property)) { | ||
if (property === proxyTarget || property === TARGET) { | ||
return target; | ||
} | ||
if (property === UNSUBSCRIBE && | ||
pathCache !== null && | ||
pathCache.get(target) === '') { | ||
return unsubscribe(target); | ||
if ( | ||
property === UNSUBSCRIBE && | ||
!cache.isUnsubscribed && | ||
cache.getPath(target).length === 0 | ||
) { | ||
cache.unsubscribe(); | ||
return target; | ||
} | ||
} | ||
@@ -149,7 +51,7 @@ | ||
if ( | ||
isPrimitive(value) || | ||
isBuiltinWithoutMutableMethods(value) || | ||
isBuiltin.withoutMutableMethods(value) || | ||
property === 'constructor' || | ||
options.isShallow === true || | ||
ignoreProperty(property) | ||
ignoreProperty(cache, options, property) || | ||
cache.isGetInvariant(target, property) | ||
) { | ||
@@ -159,15 +61,3 @@ return value; | ||
// Preserve invariants | ||
const descriptor = getOwnPropertyDescriptor(target, property); | ||
if (descriptor && !descriptor.configurable) { | ||
if (descriptor.set && !descriptor.get) { | ||
return undefined; | ||
} | ||
if (descriptor.writable === false) { | ||
return value; | ||
} | ||
} | ||
return buildProxy(value, path.concat(pathCache.get(target), property)); | ||
return cache.getProxy(value, path.concat(cache.getPath(target), property), handler); | ||
}, | ||
@@ -180,32 +70,27 @@ | ||
const ignore = ignoreProperty(property); | ||
const previous = ignore ? null : Reflect.get(target, property, receiver); | ||
const isChanged = !(property in target) || !equals(previous, value); | ||
let result = true; | ||
const reflectTarget = target[proxyTarget] || target; | ||
const previous = Reflect.get(reflectTarget, property, receiver); | ||
const hasProperty = property in target; | ||
if (isChanged) { | ||
result = Reflect.set(target[proxyTarget] || target, property, value); | ||
if (cache.setProperty(reflectTarget, property, value, receiver, previous)) { | ||
if (!equals(previous, value) || !hasProperty) { | ||
handleChangeOnTarget(target, property, previous, value); | ||
} | ||
if (!ignore && result) { | ||
handleChange(pathCache.get(target), property, previous, value); | ||
} | ||
return true; | ||
} | ||
return result; | ||
return false; | ||
}, | ||
defineProperty(target, property, descriptor) { | ||
let result = true; | ||
if (!cache.isSameDescriptor(descriptor, target, property)) { | ||
if (!cache.defineProperty(target, property, descriptor)) { | ||
return false; | ||
} | ||
if (!isSameDescriptor(descriptor, getOwnPropertyDescriptor(target, property))) { | ||
result = Reflect.defineProperty(target, property, descriptor); | ||
if (result && !ignoreProperty(property) && !isSameDescriptor()) { | ||
invalidateCachedDescriptor(target, property); | ||
handleChange(pathCache.get(target), property, undefined, descriptor.value); | ||
} | ||
handleChangeOnTarget(target, property, undefined, descriptor.value); | ||
} | ||
return result; | ||
return true; | ||
}, | ||
@@ -218,53 +103,49 @@ | ||
const ignore = ignoreProperty(property); | ||
const previous = ignore ? null : Reflect.get(target, property); | ||
const result = Reflect.deleteProperty(target, property); | ||
const previous = Reflect.get(target, property); | ||
if (!ignore && result) { | ||
invalidateCachedDescriptor(target, property); | ||
if (cache.deleteProperty(target, property, previous)) { | ||
handleChangeOnTarget(target, property, previous); | ||
handleChange(pathCache.get(target), property, previous); | ||
return true; | ||
} | ||
return result; | ||
return false; | ||
}, | ||
apply(target, thisArg, argumentsList) { | ||
const compare = isBuiltinWithMutableMethods(thisArg); | ||
const isMutable = isBuiltin.withMutableMethods(thisArg); | ||
if (compare) { | ||
if (isMutable) { | ||
thisArg = thisArg[proxyTarget]; | ||
} | ||
if (!inApply) { | ||
inApply = true; | ||
if (smartClone.isCloning || cache.isUnsubscribed) { | ||
return Reflect.apply(target, thisArg, argumentsList); | ||
} | ||
if (compare) { | ||
applyPrevious = thisArg.valueOf(); | ||
} | ||
const applyPath = path.initial(cache.getPath(target)); | ||
if (isArray(thisArg) || toString.call(thisArg) === '[object Object]') { | ||
applyPrevious = shallowClone(thisArg[proxyTarget]); | ||
} | ||
if ( | ||
isMutable || | ||
isArray(thisArg) || | ||
toString.call(thisArg) === '[object Object]' | ||
) { | ||
smartClone.start(thisArg[proxyTarget] || thisArg, applyPath); | ||
} | ||
applyPath = path.initial(pathCache.get(target)); | ||
const result = Reflect.apply(target, thisArg, argumentsList); | ||
const result = Reflect.apply(target, thisArg, argumentsList); | ||
if (smartClone.isChanged(isMutable, thisArg, equals)) { | ||
const {clone} = smartClone; | ||
smartClone.done(); | ||
handleChange(applyPath, '', clone, thisArg[proxyTarget] || thisArg); | ||
} | ||
inApply = false; | ||
smartClone.done(); | ||
if (changed || (compare && !equals(applyPrevious, thisArg.valueOf()))) { | ||
handleChange(applyPath, '', applyPrevious, thisArg[proxyTarget] || thisArg); | ||
applyPrevious = null; | ||
changed = false; | ||
} | ||
return result; | ||
} | ||
return Reflect.apply(target, thisArg, argumentsList); | ||
return result; | ||
} | ||
}; | ||
const proxy = buildProxy(object, options.pathAsArray === true ? [] : ''); | ||
const proxy = cache.getProxy(object, options.pathAsArray === true ? [] : '', handler); | ||
onChange = onChange.bind(proxy); | ||
@@ -271,0 +152,0 @@ |
@@ -63,3 +63,3 @@ 'use strict'; | ||
if (isArray(path)) { | ||
path.forEach(callback); | ||
path.forEach(key => callback(key)); | ||
} else if (path !== '') { | ||
@@ -66,0 +66,0 @@ let position = 0; |
{ | ||
"name": "on-change", | ||
"version": "2.0.1", | ||
"version": "2.0.2", | ||
"description": "Watch an object or array for changes", | ||
@@ -42,8 +42,9 @@ "license": "MIT", | ||
"devDependencies": { | ||
"ava": "^3.5.0", | ||
"display-value": "^1.6.0", | ||
"karma-webpack-bundle": "^0.1.2", | ||
"tsd": "^0.11.0", | ||
"xo": "^0.28.0" | ||
"ava": "^3.11.1", | ||
"display-value": "^1.7.3", | ||
"karma-webpack-bundle": "^0.5.0", | ||
"powerset": "0.0.1", | ||
"tsd": "^0.13.1", | ||
"xo": "^0.32.1" | ||
} | ||
} |
@@ -225,3 +225,3 @@ # on-change [](https://travis-ci.org/sindresorhus/on-change) | ||
- [negative-array](https://github.com/sindresorhus/negative-array) - Negative array index support `array[-1]` *(Uses `Proxy` too)* | ||
- [statux](https://github.com/franciscop/state) - State manager *(Uses `Proxy` too)* | ||
- [atama](https://github.com/franciscop/atama) - State manager *(Uses `Proxy` too)* | ||
- [introspected](https://github.com/WebReflection/introspected) - Never-ending Proxy with multiple observers *(Uses `Proxy` too)* | ||
@@ -228,0 +228,0 @@ |
21393
13
538
6