├── .editorconfig ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── package.json ├── src ├── applicator.ts ├── applicators │ ├── array.ts │ ├── date.ts │ ├── fallback.ts │ ├── index.ts │ ├── map.ts │ └── set.ts ├── consts.ts ├── index.ts ├── is_proxy.ts ├── make_traps.ts ├── packages │ ├── clone.ts │ ├── clone_deep.ts │ ├── diff.ts │ ├── is_equal.ts │ ├── is_native.ts │ └── is_plain_object.ts ├── record.ts ├── target.ts ├── traps.ts ├── traps_helpers.ts ├── types.ts ├── unwatch.ts ├── utils.ts └── watch.ts ├── tasks ├── benchmark.js └── fixtures.js ├── test └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Numerous always-ignore extensions 3 | *.diff 4 | *.err 5 | *.log 6 | *.orig 7 | *.rej 8 | *.swo 9 | *.swp 10 | *.vi 11 | *.zip 12 | *~ 13 | *.sass-cache 14 | *.ruby-version 15 | *.rbenv-version 16 | 17 | # OS or Editor folders 18 | ._* 19 | .cache 20 | .DS_Store 21 | .idea 22 | .project 23 | .settings 24 | .tmproj 25 | *.esproj 26 | *.sublime-project 27 | *.sublime-workspace 28 | nbproject 29 | Thumbs.db 30 | .fseventsd 31 | .DocumentRevisions* 32 | .TemporaryItems 33 | .Trashes 34 | 35 | # Other paths to ignore 36 | bower_components 37 | node_modules 38 | package-lock.json 39 | dist 40 | .nyc_output 41 | coverage 42 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.12.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxy Watcher 2 | 3 | A library that recursively watches an object for mutations via [`Proxys`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and tells you which paths changed. 4 | 5 | The following values are fully supported: [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), [functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions), [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get), [setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set), [Dates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date), [RegExps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp), [Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object), [Arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array), [ArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [TypedArrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), [Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Sets](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). 6 | 7 | Other values are partially supported: [Promises](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise), [WeakMaps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), [WeakSets](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) and custom classes. Basically mutations happening inside them won't be detected, however setting any of these as a value will be detected as a mutation. 8 | 9 | ## Functions 10 | 11 | The following functions are provided. 12 | 13 | - **watch**: starts watching an object for mutations, and returns a proxy object. 14 | - **unwatch**: stops watching an object for mutations, and returns the raw object being proxied. 15 | - **record**: records and returns an array or map of root paths that have been accessed while executing the provided function. 16 | - **target**: returns the raw object being proxied. 17 | - **isProxy**: returns a boolean indicating if the provided object is a proxy object or not. 18 | 19 | ## Limitations 20 | 21 | - Mutations happening at locations that need to be reached via a [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) aren't detected, as a precise string path for them can't be generated (e.g. `{ [Symbol ()]: { unreachableViaStringPath: true }`). 22 | - Referencing the same object under multiple paths at the same time will throw an error. This is not supported because the library can't lazily deeply watch the watched object safely when duplicate objects are used. 23 | - Referencing the watched object within itself will thrown an error. 24 | - A path is a dot-separated string of keys, therefore using only dots as your keys may lead to some weird paths generated that can't be parsed properly (e.g. `foo.....bar`, is that `foo/.../bar` or `foo/././bar`?) 25 | - Proxys will make certain operations even 100x slower on current engines, however those operations are simple things like property accesses which will almost never be your actual bottleneck, even with this kind of performance hit. 26 | - Proxys are un-polyfillable, if you have to support platforms that don't [support them](https://caniuse.com/#search=proxy) you can't use this library. 27 | - It's possible that the callback will be called when nothing actually changed (e.g. it will happen if you to flip a boolean twice synchronously). 28 | 29 | ## Install 30 | 31 | ```sh 32 | npm install --save proxy-watcher 33 | ``` 34 | 35 | ## Usage 36 | 37 | The following interface is provided: 38 | 39 | ```ts 40 | type Disposer = () => void; 41 | 42 | function watch ( object: Object, callback: ( paths: string[] ) => any ): [Proxy, Disposer]; 43 | function unwatch ( proxy: Proxy ): Object; 44 | function record ( proxy: Proxy, fn: ( proxy: Proxy ) => void ): string[]; 45 | function record ( proxies: Proxy[], fn: ( ...proxies: Proxy[] ) => void ): Map; 46 | function target ( proxy: Proxy ): Object; 47 | function isProxy ( object: Object | Proxy ): boolean; 48 | ``` 49 | 50 | Basically you have to pass the `watch` function an object, which will be watched for any changes, and a callback, which will be called with an array of paths changed whenever changes are detected. 51 | 52 | The function will return an array containing a proxy object, always use this object rather than the raw object you pass the `watch` function, and a disposer function, which when called will stop the watching operation and will return back the original unproxied object. 53 | 54 | ```ts 55 | import {watch, unwatch, record, target, isProxy} from 'proxy-watcher'; 56 | 57 | /* WATCH */ 58 | 59 | const [proxy, dispose] = watch ({ 60 | foo: true, 61 | arr: [1, 2, 3] 62 | }, paths => { 63 | console.log ( 'Something changed at these paths:', paths ); 64 | }); 65 | 66 | proxy.foo; // => true 67 | proxy.arr; // => [1, 2, 3] 68 | 69 | proxy.foo = true; // Nothing actually changed, the callback won't be called 70 | proxy.arr[0] = 1; // Nothing actually changed, the callback won't be called 71 | 72 | proxy.foo = false; // Callback called with paths: ['foo'] 73 | proxy.bar = true; // Callback called with paths: ['bar'] 74 | proxy.arr.push ( 4 ) = true; // Callback called with paths: ['arr.3', 'arr'] 75 | 76 | /* RECORD */ // Record root keys accessed 77 | 78 | record ( proxy, () => { 79 | console.log ( proxy.foo ); 80 | }); // => ['foo'] 81 | 82 | /* TARGET */ // Return the raw unproxied object 83 | 84 | target ( proxy ); // => { foo: false, bar = true, arr: [1, 2, 3, 4] } 85 | 86 | /* IS PROXY */ 87 | 88 | isProxy ( proxy ); // => true 89 | isProxy ( target ( proxy ) ); // => false 90 | 91 | /* UNWATCH */ 92 | 93 | dispose (); // Stop watching 94 | unwatch ( proxy ); // Altertnative way to stop watching 95 | ``` 96 | 97 | ## License 98 | 99 | MIT © Fabio Spampinato 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy-watcher", 3 | "description": "A library that recursively watches an object for mutations via Proxies and tells you which paths changed.", 4 | "version": "3.4.4", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "benchmark": "node tasks/benchmark.js", 9 | "bench": "matcha tasks/bench.js", 10 | "clean:dist": "rimraf dist", 11 | "clean:coverage": "rimraf coverage .nyc_output", 12 | "clean": "npm run clean:dist && npm run clean:coverage", 13 | "compile": "tsc --skipLibCheck && tstei", 14 | "compile:watch": "tsc --skipLibCheck --watch", 15 | "test": "ava", 16 | "test:watch": "ava --watch", 17 | "coverage": "nyc --reporter=html ava", 18 | "report": "nyc report", 19 | "report:html": "open coverage/index.html", 20 | "prepublishOnly": "npm run clean && npm run compile && npm run coverage" 21 | }, 22 | "ava": { 23 | "files": [ 24 | "test/index.js" 25 | ] 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/fabiospampinato/proxy-watcher/issues" 29 | }, 30 | "license": "MIT", 31 | "author": { 32 | "name": "Fabio Spampinato", 33 | "email": "spampinabio@gmail.com" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/fabiospampinato/proxy-watcher.git" 38 | }, 39 | "keywords": [ 40 | "proxy", 41 | "watcher", 42 | "watch", 43 | "change" 44 | ], 45 | "dependencies": {}, 46 | "devDependencies": { 47 | "@types/node": "^14.0.14", 48 | "ava": "^2.4.0", 49 | "ava-spec": "^1.1.1", 50 | "benchloop": "^1.3.2", 51 | "jsdom": "^18.0.0", 52 | "jsdom-global": "^3.0.2", 53 | "lodash": "^4.17.20", 54 | "nyc": "^15.1.0", 55 | "rimraf": "^3.0.2", 56 | "typescript": "^3.9.5", 57 | "typescript-transform-export-interop": "^1.0.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/applicator.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Applicators from './applicators'; 5 | import {Applicator} from './types'; 6 | 7 | /* APPLICATOR */ 8 | 9 | // Optimized "apply" trap executor, with custom comparators for expensive mutating method 10 | 11 | const Applicator = { 12 | 13 | get ( constructor: Function, method: Function ): Applicator { 14 | 15 | const applicators = Applicators[constructor.name]; 16 | 17 | return applicators ? applicators[method.name] || Applicators.fallback : Applicators.fallback; 18 | 19 | }, 20 | 21 | execute ( method: Function, thisArg: any, thisArgTarget: any, args: any[] ): [any, boolean] { 22 | 23 | const applicator = Applicator.get ( thisArgTarget.constructor, method ); 24 | 25 | return applicator ( method, thisArg, thisArgTarget, args ); 26 | 27 | } 28 | 29 | }; 30 | 31 | /* EXPORT */ 32 | 33 | export default Applicator; 34 | -------------------------------------------------------------------------------- /src/applicators/array.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Applicator} from '../types'; 5 | 6 | /* DESTRUCTURING */ 7 | 8 | const {apply} = Reflect; 9 | 10 | /* HELPERS */ 11 | 12 | const clamp = ( nr: number, min: number, max: number ): number => Math.max ( min, Math.min ( max, nr ) ), 13 | normalizeIndex = ( index: number, length: number ): number => ( index < 0 ) ? clamp ( length + index, 0, length ) : clamp ( index, 0, length ); 14 | 15 | /* APPLICATORS */ 16 | 17 | const copyWithin: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 18 | 19 | const {length} = thisArgTarget, 20 | target = normalizeIndex ( args[0], length ), 21 | start = ( args.length >= 2 ) ? normalizeIndex ( args[1], length ) : 0, 22 | end = ( args.length >= 3 ) ? normalizeIndex ( args[2], length ) : length, 23 | copyNr = ( end - start ); 24 | 25 | for ( let i = 0, l = copyNr; i < l; i++ ) { 26 | 27 | if ( ( target + i ) >= length ) break; 28 | 29 | if ( thisArgTarget[start + i] !== thisArgTarget[target + i] ) { 30 | 31 | const result = apply ( method, thisArg, args ); 32 | 33 | return [result, true]; 34 | 35 | } 36 | 37 | } 38 | 39 | return [thisArg, false]; 40 | 41 | }; 42 | 43 | const fill: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 44 | 45 | const {length} = thisArgTarget, 46 | value = args[0], 47 | start = ( args.length >= 2 ) ? normalizeIndex ( args[1], length ) : 0, 48 | end = ( args.length >= 3 ) ? normalizeIndex ( args[2], length ) : length; 49 | 50 | for ( let i = start, l = end; i < l; i++ ) { 51 | 52 | if ( thisArgTarget[i] !== value ) { 53 | 54 | const result = apply ( method, thisArg, args ); 55 | 56 | return [result, true]; 57 | 58 | } 59 | 60 | } 61 | 62 | return [thisArg, false]; 63 | 64 | }; 65 | 66 | const pop: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 67 | 68 | const {length} = thisArgTarget; 69 | 70 | if ( !length ) return [undefined, false]; 71 | 72 | const result = apply ( method, thisArg, args ); 73 | 74 | return [result, true]; 75 | 76 | }; 77 | 78 | const push: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 79 | 80 | const {length} = args; 81 | 82 | if ( !length ) return [thisArgTarget.length, false]; 83 | 84 | const result = apply ( method, thisArg, args ); 85 | 86 | return [result, true]; 87 | 88 | }; 89 | 90 | const reverse: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 91 | 92 | const {length} = thisArgTarget, 93 | midpoint = Math.floor ( length / 2 ); 94 | 95 | for ( let i = 0; i <= midpoint; i++ ) { 96 | 97 | if ( thisArgTarget[i] !== thisArgTarget[length - i] ) { 98 | 99 | const result = apply ( method, thisArg, args ); 100 | 101 | return [result, true]; 102 | 103 | } 104 | 105 | } 106 | 107 | return [thisArg, false]; 108 | 109 | }; 110 | 111 | const shift = pop; 112 | 113 | const sort = undefined; // Might not be worth checking if the array is already sorted 114 | 115 | const splice: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 116 | 117 | const {length} = thisArgTarget, 118 | start = normalizeIndex ( args[0], length ), 119 | deleteNr = ( args.length >= 2 ) ? Math.max ( 0, args[1] ) : length - start, 120 | addNr = Math.max ( 0, args.length - 2 ); 121 | 122 | if ( addNr === deleteNr ) { 123 | 124 | if ( !deleteNr ) return [[], false]; 125 | 126 | for ( let i = 0, l = deleteNr; i < l; i++ ) { 127 | 128 | if ( thisArgTarget[start + i] !== args[2 + i] ) { 129 | 130 | const result = apply ( method, thisArg, args ); 131 | 132 | return [result, true]; 133 | 134 | } 135 | 136 | } 137 | 138 | return [args.slice ( 2 ), false]; 139 | 140 | } 141 | 142 | const result = apply ( method, thisArg, args ); 143 | 144 | return [result, true]; 145 | 146 | }; 147 | 148 | const unshift = push; 149 | 150 | /* ARRAY */ 151 | 152 | const Array = { copyWithin, fill, pop, push, reverse, shift, sort, splice, unshift }; 153 | 154 | /* EXPORT */ 155 | 156 | export default Array; 157 | -------------------------------------------------------------------------------- /src/applicators/date.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Applicator} from '../types'; 5 | 6 | /* DESTRUCTURING */ 7 | 8 | const {apply} = Reflect; 9 | 10 | /* APPLICATORS */ 11 | 12 | const setDate: Applicator = ( method, thisArg, thisArgTarget, args ) => { 13 | 14 | const prev = thisArgTarget.getTime (), 15 | result = apply ( method, thisArg, args ), 16 | next = thisArgTarget.getTime (), 17 | changed = ( prev !== next ); 18 | 19 | return [result, changed]; 20 | 21 | }; 22 | 23 | /* DATE */ 24 | 25 | const Date = { setDate }; 26 | 27 | /* EXPORT */ 28 | 29 | export default Date; 30 | 31 | -------------------------------------------------------------------------------- /src/applicators/fallback.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Applicator} from '../types'; 5 | import Utils from '../utils'; 6 | 7 | /* DESTRUCTURING */ 8 | 9 | const {apply} = Reflect, 10 | {clone, isEqual} = Utils; 11 | 12 | /* FALLBACK */ 13 | 14 | // This works with everything, but involves cloning and comparing objects, which can be really expensive 15 | 16 | const fallback: Applicator = ( method, thisArg, thisArgTarget, args ) => { 17 | 18 | const cloned = clone ( thisArgTarget ), 19 | result = apply ( method, thisArg, args ), 20 | changed = !isEqual ( cloned, thisArgTarget ); 21 | 22 | return [result, changed]; 23 | 24 | }; 25 | 26 | /* EXPORT */ 27 | 28 | export default fallback; 29 | -------------------------------------------------------------------------------- /src/applicators/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Array from './array'; 5 | import Date from './date'; 6 | import fallback from './fallback'; 7 | import Map from './map'; 8 | import Set from './set'; 9 | 10 | /* APPLICATORS */ 11 | 12 | //TODO: Add support for TypedArray 13 | 14 | const Applicators = {Array, Date, fallback, Map, Set}; 15 | 16 | /* EXPORT */ 17 | 18 | export default Applicators; 19 | -------------------------------------------------------------------------------- /src/applicators/map.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Applicator} from '../types'; 5 | import Set from './set'; 6 | 7 | /* DESTRUCTURING */ 8 | 9 | const {apply} = Reflect; 10 | 11 | /* APPLICATORS */ 12 | 13 | const set: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 14 | 15 | const [key, value] = args, 16 | prev = thisArgTarget.get ( key ); 17 | 18 | if ( prev === value && ( prev !== undefined || thisArgTarget.has ( key ) ) ) return [thisArg, false]; 19 | 20 | const result = apply ( method, thisArg, args ); 21 | 22 | return [result, true]; 23 | 24 | }; 25 | 26 | const del = Set.delete; 27 | 28 | const clear = Set.clear; 29 | 30 | /* MAP */ 31 | 32 | const Map = { set, delete: del, clear }; 33 | 34 | /* EXPORT */ 35 | 36 | export default Map; 37 | -------------------------------------------------------------------------------- /src/applicators/set.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Applicator} from '../types'; 5 | 6 | /* DESTRUCTURING */ 7 | 8 | const {apply} = Reflect; 9 | 10 | /* APPLICATORS */ 11 | 12 | const add: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 13 | 14 | const prev = thisArgTarget.has ( args[0] ); 15 | 16 | if ( prev ) return [thisArg, false]; 17 | 18 | const result = apply ( method, thisArg, args ); 19 | 20 | return [result, true]; 21 | 22 | }; 23 | 24 | const del: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 25 | 26 | const prev = thisArgTarget.has ( args[0] ); 27 | 28 | if ( !prev ) return [false, false]; 29 | 30 | const result = apply ( method, thisArg, args ); 31 | 32 | return [result, true]; 33 | 34 | }; 35 | 36 | const clear: Applicator> = ( method, thisArg, thisArgTarget, args ) => { 37 | 38 | const {size} = thisArgTarget; 39 | 40 | if ( !size ) return [undefined, false]; 41 | 42 | const result = apply ( method, thisArg, args ); 43 | 44 | return [result, true]; 45 | 46 | }; 47 | 48 | /* SET */ 49 | 50 | const Set = { add, delete: del, clear }; 51 | 52 | /* EXPORT */ 53 | 54 | export default Set; 55 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | 2 | /* CONSTS */ 3 | 4 | const IS_DEVELOPMENT = false; //TODO: temporarily disabled: ( typeof process === 'object' ) && ( process.env.NODE_ENV === 'development' ); 5 | 6 | /* SYMBOLS */ 7 | 8 | const $IS_PROXY = Symbol ( 'Is Proxy' ); 9 | 10 | const $TARGET = Symbol ( 'Proxy -> Target' ); 11 | 12 | const $STOP = Symbol ( 'Stop proxying' ); 13 | 14 | const $GET_RECORD_START = Symbol ( 'Start recording get paths' ); 15 | 16 | const $GET_RECORD_STOP = Symbol ( 'Stop recording get paths' ); 17 | 18 | /* CONSTRUCTORS */ 19 | 20 | const CONSTRUCTORS_ERRORS = new Set ([ 21 | Error, 22 | EvalError, 23 | RangeError, 24 | ReferenceError, 25 | SyntaxError, 26 | TypeError, 27 | URIError 28 | ]); 29 | 30 | const CONSTRUCTORS_IMMUTABLE = new Set ([ 31 | ...CONSTRUCTORS_ERRORS, 32 | ArrayBuffer, 33 | Boolean, 34 | DataView, 35 | Number, 36 | RegExp, 37 | String, 38 | Symbol 39 | ]); 40 | 41 | const CONSTRUCTORS_MUTABLE = new Set ([ // "Array" should be included here, but then some tests will fail //TODO: Add AsyncGeneratorFunction 42 | (function* () {}).constructor, 43 | (async function () {}).constructor, 44 | Date, 45 | Function, 46 | Map, 47 | Object, 48 | Set, 49 | Int8Array, 50 | Uint8Array, 51 | Uint8ClampedArray, 52 | Int16Array, 53 | Uint16Array, 54 | Int32Array, 55 | Uint32Array, 56 | Float32Array, 57 | Float64Array 58 | ]); 59 | 60 | const CONSTRUCTORS_COMPARABLE = new Set ([ 61 | Array, 62 | ArrayBuffer, 63 | RegExp, 64 | Map, 65 | Set, 66 | Int8Array, 67 | Uint8Array, 68 | Uint8ClampedArray, 69 | Int16Array, 70 | Uint16Array, 71 | Int32Array, 72 | Uint32Array, 73 | Float32Array, 74 | Float64Array 75 | ]); 76 | 77 | if ( typeof BigInt === 'function' ) { 78 | 79 | CONSTRUCTORS_IMMUTABLE.add ( BigInt ); 80 | 81 | if ( typeof BigInt64Array === 'function' ) { 82 | CONSTRUCTORS_MUTABLE.add ( BigInt64Array ); 83 | CONSTRUCTORS_COMPARABLE.add ( BigInt64Array ); 84 | } 85 | 86 | if ( typeof BigUint64Array === 'function' ) { 87 | CONSTRUCTORS_MUTABLE.add ( BigUint64Array ); 88 | CONSTRUCTORS_COMPARABLE.add ( BigUint64Array ); 89 | } 90 | 91 | } 92 | 93 | if ( typeof SharedArrayBuffer === 'function' ) { 94 | 95 | CONSTRUCTORS_IMMUTABLE.add ( SharedArrayBuffer ); 96 | 97 | } 98 | 99 | const CONSTRUCTORS_SUPPORTED = new Set ([ 100 | ...CONSTRUCTORS_IMMUTABLE, 101 | ...CONSTRUCTORS_MUTABLE, 102 | ...CONSTRUCTORS_COMPARABLE 103 | ]); 104 | 105 | const CONSTRUCTORS_UNSUPPORTED = new Set ([ 106 | Promise, 107 | WeakMap, 108 | WeakSet 109 | ]); 110 | 111 | /* METHODS */ // We are assuming the following methods don't get messed up with, and custom methods with the same name that are mutating are not defined 112 | 113 | const STRICTLY_IMMUTABLE_METHODS = new Set ([ // These methods don't directly mutate the object and don't return something that may cause a mutation 114 | /* OBJECT */ 115 | 'hasOwnProperty', 116 | 'isPrototypeOf', 117 | 'propertyIsEnumerable', 118 | 'toLocaleString', 119 | 'toString', 120 | 'valueOf', 121 | /* ARRAY */ 122 | 'includes', 123 | 'indexOf', 124 | 'join', 125 | 'lastIndexOf', 126 | 'toLocaleString', 127 | 'toString', 128 | /* MAP & SET */ 129 | 'has', 130 | /* DATE */ 131 | 'getDate', 132 | 'getDay', 133 | 'getFullYear', 134 | 'getHours', 135 | 'getMilliseconds', 136 | 'getMinutes', 137 | 'getMonth', 138 | 'getSeconds', 139 | 'getTime', 140 | 'getTime', 141 | 'getTimezoneOffset', 142 | 'getUTCDate', 143 | 'getUTCDay', 144 | 'getUTCFullYear', 145 | 'getUTCHours', 146 | 'getUTCMilliseconds', 147 | 'getUTCMinutes', 148 | 'getUTCMonth', 149 | 'getUTCSeconds', 150 | 'getYear', 151 | /* REGEX */ 152 | 'exec', 153 | 'test', 154 | /* TYPED ARRAY */ 155 | 'subarray' 156 | ]); 157 | 158 | const LOOSELY_IMMUTABLE_METHODS = { // These methods don't directly mutate the object but could return something that may cause a mutation 159 | array: new Set ([ 160 | 'concat', 161 | 'entries', 162 | 'every', 163 | 'filter', 164 | 'find', 165 | 'findIndex', 166 | 'forEach', 167 | 'keys', 168 | 'map', 169 | 'reduce', 170 | 'reduceRight', 171 | 'slice', 172 | 'some', 173 | 'values' 174 | ]), 175 | others: new Set ([ 176 | /* MAP & SET */ 177 | 'entries', 178 | 'forEach', 179 | 'get', 180 | 'keys', 181 | 'values' 182 | ]) 183 | }; 184 | 185 | /* EXPORT */ 186 | 187 | export {IS_DEVELOPMENT, $IS_PROXY, $TARGET, $STOP, $GET_RECORD_START, $GET_RECORD_STOP, CONSTRUCTORS_IMMUTABLE, CONSTRUCTORS_MUTABLE, CONSTRUCTORS_COMPARABLE, CONSTRUCTORS_SUPPORTED, CONSTRUCTORS_UNSUPPORTED, STRICTLY_IMMUTABLE_METHODS, LOOSELY_IMMUTABLE_METHODS}; 188 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import watch from './watch'; 5 | import unwatch from './unwatch'; 6 | import record from './record'; 7 | import target from './target'; 8 | import isProxy from './is_proxy'; 9 | 10 | /* EXPORT */ 11 | 12 | export {watch, unwatch, record, target, isProxy}; 13 | -------------------------------------------------------------------------------- /src/is_proxy.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* IMPORT */ 4 | 5 | import {$IS_PROXY} from './consts'; 6 | 7 | /* IS PROXY */ 8 | 9 | function isProxy ( object: Object ): boolean { 10 | 11 | return !!object && ( object[$IS_PROXY] === true ); 12 | 13 | } 14 | 15 | /* EXPORT */ 16 | 17 | export default isProxy; 18 | -------------------------------------------------------------------------------- /src/make_traps.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import TrapsHelpers from './traps_helpers'; 5 | import {Callback, Traps} from './types'; 6 | 7 | /* MAKE TRAPS */ 8 | 9 | function makeTraps ( object: Object, callback: Callback ): Traps { 10 | 11 | return new TrapsHelpers ( object, callback ).traps; 12 | 13 | } 14 | 15 | /* EXPORT */ 16 | 17 | export default makeTraps; 18 | -------------------------------------------------------------------------------- /src/packages/clone.ts: -------------------------------------------------------------------------------- 1 | 2 | /* HELPERS */ 3 | 4 | const {assign} = Object; 5 | 6 | const CONSTRUCTORS_CLONABLE = new Set ([ Map, Set, Date, RegExp ]); 7 | 8 | const cloneMap = ( val: Map ): Map => { 9 | const cloned = new Map (); 10 | for ( const [key, value] of val ) { 11 | cloned.set ( clone ( key ), clone ( value ) ); 12 | } 13 | return cloned; 14 | }; 15 | 16 | const cloneRegExp = ( val: RegExp ): RegExp => { 17 | const cloned = new RegExp ( val.source, val.flags ); 18 | cloned.lastIndex = val.lastIndex; 19 | return cloned; 20 | }; 21 | 22 | const cloneSet = ( val: Set ): Set => { 23 | const cloned = new Set (); 24 | for ( const value of val ) { 25 | cloned.add ( clone ( value ) ); 26 | } 27 | return cloned; 28 | }; 29 | 30 | /* CLONE */ 31 | 32 | const cloneNew = ( val: any ): any => { 33 | 34 | if ( typeof val !== 'object' || val === null ) return val; 35 | 36 | if ( typeof val.slice === 'function' ) return val.slice (); 37 | 38 | const {constructor} = val; 39 | 40 | if ( CONSTRUCTORS_CLONABLE.has ( constructor ) ) { 41 | 42 | if ( constructor === Map ) return cloneMap ( val ); 43 | 44 | if ( constructor === Set ) return cloneSet ( val ); 45 | 46 | if ( constructor === Date ) return new Date ( val.getTime () ); 47 | 48 | if ( constructor === RegExp ) return cloneRegExp ( val ); 49 | 50 | } 51 | 52 | return assign ( {}, val ); 53 | 54 | }; 55 | 56 | const clone = ( val: T ): T => { 57 | 58 | return cloneNew ( val ); 59 | 60 | }; 61 | 62 | /* EXPORT */ 63 | 64 | export default clone; 65 | -------------------------------------------------------------------------------- /src/packages/clone_deep.ts: -------------------------------------------------------------------------------- 1 | 2 | /* HELPERS */ 3 | 4 | const {isArray} = Array; 5 | 6 | const CONSTRUCTORS_CLONABLE = new Set ([ Map, Set, Date, RegExp ]); 7 | 8 | const cloneDeepArray = ( val: any[], circularMap: Map ): any[] => { 9 | const {length} = val; 10 | const cloned = new Array ( length ); 11 | circularMap.set ( val, cloned ); 12 | for ( let i = 0; i < length; i++ ) { 13 | cloned[i] = cloneDeep ( val[i], circularMap ); 14 | } 15 | return cloned; 16 | }; 17 | 18 | const cloneDeepMap = ( val: Map, circularMap: Map ): Map => { 19 | const cloned = new Map (); 20 | circularMap.set ( val, cloned ); 21 | for ( const [key, value] of val ) { 22 | cloned.set ( cloneDeep ( key, circularMap ), cloneDeep ( value, circularMap ) ); 23 | } 24 | return cloned; 25 | }; 26 | 27 | const cloneDeepObject = ( val: object, circularMap: Map ): object => { 28 | const cloned = {}; 29 | circularMap.set ( val, cloned ); 30 | for ( const key in val ) { 31 | cloned[key] = cloneDeep ( val[key], circularMap ); 32 | } 33 | return cloned; 34 | }; 35 | 36 | const cloneRegExp = ( val: RegExp ): RegExp => { 37 | const cloned = new RegExp ( val.source, val.flags ); 38 | cloned.lastIndex = val.lastIndex; 39 | return cloned; 40 | }; 41 | 42 | const cloneDeepSet = ( val: Set, circularMap: Map ): Set => { 43 | const cloned = new Set (); 44 | circularMap.set ( val, cloned ); 45 | for ( const value of val ) { 46 | cloned.add ( cloneDeep ( value, circularMap ) ); 47 | } 48 | return cloned; 49 | }; 50 | 51 | /* CLONE DEEP */ 52 | 53 | // This is just a deep version of "clone" 54 | 55 | const cloneDeepNew = ( val: any, circularMap: Map ): any => { 56 | 57 | if ( typeof val !== 'object' || val === null ) return val; 58 | 59 | if ( isArray ( val ) ) return cloneDeepArray ( val, circularMap ); 60 | 61 | if ( typeof val.slice === 'function' ) return val.slice (); 62 | 63 | const {constructor} = val; 64 | 65 | if ( CONSTRUCTORS_CLONABLE.has ( constructor ) ) { 66 | 67 | if ( constructor === Map ) return cloneDeepMap ( val, circularMap ); 68 | 69 | if ( constructor === Set ) return cloneDeepSet ( val, circularMap ); 70 | 71 | if ( constructor === Date ) return new Date ( val.getTime () ); 72 | 73 | if ( constructor === RegExp ) return cloneRegExp ( val ); 74 | 75 | } 76 | 77 | return cloneDeepObject ( val, circularMap ); 78 | 79 | }; 80 | 81 | const cloneDeep = ( val: T, _circularMap?: Map ): T => { 82 | 83 | const circularMap = _circularMap || new Map (), // Storing references to potentially circular objects 84 | circularClone = circularMap.get ( val ); 85 | 86 | if ( circularClone ) return circularClone; 87 | 88 | return cloneDeepNew ( val, circularMap ); 89 | 90 | }; 91 | 92 | /* EXPORT */ 93 | 94 | export default cloneDeep; 95 | -------------------------------------------------------------------------------- /src/packages/diff.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {CONSTRUCTORS_SUPPORTED, CONSTRUCTORS_UNSUPPORTED} from '../consts'; 5 | import isEqual from './is_equal'; 6 | import isNative from './is_native'; 7 | import isPlainObject from './is_plain_object'; 8 | 9 | /* HELPERS */ 10 | 11 | const {assign} = Object; 12 | 13 | /* DIFF */ 14 | 15 | const diff = ( a: any, b: any, prefix: string = '' ) => { 16 | 17 | const added: Record = {}, 18 | deleted: Record = {}, 19 | updated: Record = {}; 20 | 21 | if ( isPlainObject ( a ) && isPlainObject ( b ) ) { 22 | 23 | for ( const key in a ) { 24 | 25 | if ( !b.hasOwnProperty ( key ) ) { 26 | 27 | deleted[`${prefix}${prefix ? '.' : ''}${key}`] = a[key]; 28 | 29 | } else { 30 | 31 | const result = diff ( a[key], b[key], `${prefix}${prefix ? '.' : ''}${key}` ); 32 | 33 | assign ( added, result.added ); 34 | assign ( deleted, result.deleted ); 35 | assign ( updated, result.updated ); 36 | 37 | } 38 | 39 | } 40 | 41 | for ( const key in b ) { 42 | 43 | if ( a.hasOwnProperty ( key ) ) continue; 44 | 45 | added[`${prefix}${prefix ? '.' : ''}${key}`] = b[key]; 46 | 47 | } 48 | 49 | } else if ( prefix ) { 50 | 51 | const constructorA = a && a.constructor, 52 | constructorB = b && a.constructor; 53 | 54 | if ( !( constructorA && !CONSTRUCTORS_SUPPORTED.has ( constructorA ) && ( CONSTRUCTORS_UNSUPPORTED.has ( constructorA ) || !isNative ( constructorA ) ) ) && !( constructorB && !CONSTRUCTORS_SUPPORTED.has ( constructorB ) && ( CONSTRUCTORS_UNSUPPORTED.has ( constructorB ) || !isNative ( constructorB ) ) ) && !isEqual ( a, b ) ) { 55 | 56 | updated[prefix] = { 57 | before: a, 58 | after: b 59 | }; 60 | 61 | } 62 | 63 | } 64 | 65 | return {added, deleted, updated}; 66 | 67 | }; 68 | 69 | /* EXPORT */ 70 | 71 | export default diff; 72 | -------------------------------------------------------------------------------- /src/packages/is_equal.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {CONSTRUCTORS_UNSUPPORTED, CONSTRUCTORS_COMPARABLE} from '../consts'; 5 | 6 | /* IS EQUAL */ 7 | 8 | // This is basically a fork of "fast-deep-equal" but: "Object.is"-based, with support for uncomparable contructors, support for circular structures and slightly faster 9 | 10 | const {is, keys, prototype} = Object, 11 | {hasOwnProperty, toString, valueOf} = prototype, 12 | NodeConstructor = ( typeof Node === 'function' ) ? Node : undefined; 13 | 14 | const isEqual = ( a: any, b: any ): boolean => { 15 | 16 | const compareMap = new Map (); 17 | 18 | const compare = ( a: any, b: any ): boolean => { 19 | 20 | if ( a && b && typeof a === 'object' && typeof b === 'object' ) { 21 | 22 | const {constructor} = a; 23 | 24 | if ( constructor !== b.constructor ) return false; 25 | 26 | if ( compareMap.get ( a ) === b ) return true; 27 | 28 | compareMap.set ( a, b ); 29 | 30 | let length, i, props; 31 | 32 | if ( CONSTRUCTORS_COMPARABLE.has ( constructor ) ) { 33 | 34 | if ( constructor === Array ) { 35 | 36 | length = a.length; 37 | 38 | if ( length !== b.length ) return false; 39 | 40 | for ( i = length; i-- !== 0; ) { 41 | 42 | if ( !compare ( a[i], b[i] ) ) return false; 43 | 44 | } 45 | 46 | return true; 47 | 48 | } else if ( constructor === Map ) { 49 | 50 | if ( a.size !== b.size ) return false; 51 | 52 | for ( i of a.entries () ) { 53 | 54 | if ( !b.has ( i[0] ) ) return false; 55 | 56 | } 57 | 58 | for ( i of a.entries () ) { 59 | 60 | if ( !compare ( i[1], b.get ( i[0] ) ) ) return false; 61 | 62 | } 63 | 64 | return true; 65 | 66 | } else if ( constructor === Set ) { 67 | 68 | if ( a.size !== b.size ) return false; 69 | 70 | for ( i of a.entries () ) { 71 | 72 | if ( !b.has ( i[0] ) ) return false; 73 | 74 | } 75 | 76 | return true; 77 | 78 | } else if ( constructor === RegExp ) { 79 | 80 | return a.flags === b.flags && a.source === b.source; 81 | 82 | } else { 83 | 84 | if ( constructor === ArrayBuffer ) { 85 | 86 | a = new Uint8Array ( a ); 87 | b = new Uint8Array ( b ); 88 | 89 | } 90 | 91 | length = a['length']; 92 | 93 | if ( length !== b['length'] ) return false; 94 | 95 | for ( i = length; i-- !== 0; ) { 96 | 97 | if ( a[i] !== b[i] ) return false; 98 | 99 | } 100 | 101 | return true; 102 | 103 | } 104 | 105 | } 106 | 107 | if ( CONSTRUCTORS_UNSUPPORTED.has ( constructor ) ) return a === b; 108 | if ( a.valueOf !== valueOf ) return a.valueOf () === b.valueOf (); 109 | if ( a.toString !== toString ) return a.toString () === b.toString (); 110 | if ( NodeConstructor && ( a instanceof NodeConstructor ) ) return a === b; 111 | 112 | props = keys ( a ); 113 | length = props.length; 114 | 115 | if ( length !== keys ( b ).length ) return false; 116 | 117 | for ( i = length; i-- !== 0; ) { 118 | 119 | if ( !hasOwnProperty.call ( b, props[i] ) ) return false; 120 | 121 | } 122 | 123 | for ( i = length; i-- !== 0; ) { 124 | 125 | const prop = props[i]; 126 | 127 | if ( !compare ( a[prop], b[prop] ) ) return false; 128 | 129 | } 130 | 131 | return true; 132 | 133 | } 134 | 135 | return is ( a, b ); 136 | 137 | }; 138 | 139 | return compare ( a, b ); 140 | 141 | }; 142 | 143 | /* EXPORT */ 144 | 145 | export default isEqual; 146 | -------------------------------------------------------------------------------- /src/packages/is_native.ts: -------------------------------------------------------------------------------- 1 | 2 | /* VARIABLES */ 3 | 4 | const escapeRe = /[\\^$.*+?()[\]{}|]/g; 5 | const isNativeRe = new RegExp ( `^${Function.prototype.toString.call ( Object.prototype.hasOwnProperty ).replace ( escapeRe, '\\$&' ).replace ( /hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?' )}$` ); 6 | 7 | /* IS NATIVE */ 8 | 9 | const isNative = ( x: any ): boolean => { 10 | 11 | return isNativeRe.test ( x.toString () ); 12 | 13 | }; 14 | 15 | /* EXPORT */ 16 | 17 | export default isNative; 18 | -------------------------------------------------------------------------------- /src/packages/is_plain_object.ts: -------------------------------------------------------------------------------- 1 | 2 | /* HELPERS */ 3 | 4 | const {getPrototypeOf, prototype} = Object; 5 | 6 | /* IS PLAIN OBJECT */ 7 | 8 | const isPlainObject = ( value: any ): value is Record => { 9 | 10 | if ( typeof value !== 'object' || value === null ) return false; 11 | 12 | const proto = getPrototypeOf ( value ); 13 | 14 | return proto === null || proto === prototype; 15 | 16 | }; 17 | 18 | /* EXPORT */ 19 | 20 | export default isPlainObject; 21 | -------------------------------------------------------------------------------- /src/record.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$GET_RECORD_START, $GET_RECORD_STOP} from './consts'; 5 | import isProxy from './is_proxy'; 6 | 7 | /* RECORD */ 8 | 9 | function record ( proxies: [P1, P2, P3, P4, P5, P6, P7, P8, P9], fn: ( ...proxies: [P1, P2, P3, P4, P5, P6, P7, P8, P9] ) => any ): Map; 10 | function record ( proxies: [P1, P2, P3, P4, P5, P6, P7, P8], fn: ( ...proxies: [P1, P2, P3, P4, P5, P6, P7, P8] ) => any ): Map; 11 | function record ( proxies: [P1, P2, P3, P4, P5, P6, P7], fn: ( ...proxies: [P1, P2, P3, P4, P5, P6, P7] ) => any ): Map; 12 | function record ( proxies: [P1, P2, P3, P4, P5, P6], fn: ( ...proxies: [P1, P2, P3, P4, P5, P6] ) => any ): Map; 13 | function record ( proxies: [P1, P2, P3, P4, P5], fn: ( ...proxies: [P1, P2, P3, P4, P5] ) => any ): Map; 14 | function record ( proxies: [P1, P2, P3, P4], fn: ( ...proxies: [P1, P2, P3, P4] ) => any ): Map; 15 | function record ( proxies: [P1, P2, P3], fn: ( ...proxies: [P1, P2, P3] ) => any ): Map; 16 | function record ( proxies: [P1, P2], fn: ( ...proxies: [P1, P2] ) => any ): Map; 17 | function record ( proxy: Object, fn: ( proxy: Object ) => void ): string[]; 18 | function record ( proxy: Object | Object[], fn: (( proxy: Object ) => void) | (( ...proxies: Object[] ) => void) ) { 19 | 20 | if ( Array.isArray ( proxy ) && !isProxy ( proxy ) ) { 21 | 22 | const paths = new Map (), 23 | {length} = proxy; 24 | 25 | for ( let i = 0; i < length; i++ ) { 26 | 27 | proxy[i][$GET_RECORD_START]; 28 | 29 | } 30 | 31 | fn.apply ( undefined, proxy ); 32 | 33 | for ( let i = 0; i < length; i++ ) { 34 | 35 | paths.set ( proxy[i], proxy[i][$GET_RECORD_STOP] ); 36 | 37 | } 38 | 39 | return paths; 40 | 41 | } else { 42 | 43 | proxy[$GET_RECORD_START]; 44 | 45 | fn ( proxy as Object ); //TSC 46 | 47 | return proxy[$GET_RECORD_STOP]; 48 | 49 | } 50 | 51 | } 52 | 53 | /* EXPORT */ 54 | 55 | export default record; 56 | -------------------------------------------------------------------------------- /src/target.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$TARGET} from './consts'; 5 | 6 | /* TARGET */ 7 | 8 | function target ( object: Object ): Object { 9 | 10 | return object && ( object[$TARGET] || object ); 11 | 12 | } 13 | 14 | /* EXPORT */ 15 | 16 | export default target; 17 | -------------------------------------------------------------------------------- /src/traps.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Applicator from './applicator'; 5 | import {IS_DEVELOPMENT, $IS_PROXY, $TARGET, $STOP, $GET_RECORD_START, $GET_RECORD_STOP} from './consts'; 6 | import isProxy from './is_proxy'; 7 | import getTarget from './target'; 8 | import Utils from './utils'; 9 | import {Traps} from './types'; 10 | 11 | /* DESTRUCTURING */ 12 | 13 | const {isArray} = Array, 14 | {getOwnPropertyDescriptor} = Object, 15 | {apply, defineProperty, deleteProperty, get, set} = Reflect, 16 | {execute} = Applicator, 17 | {isEqual, isLooselyImmutableArrayMethod, isStrictlyImmutableMethod, isValueUnproxiable} = Utils; 18 | 19 | /* TRAPS HELPERS */ 20 | 21 | const Traps: Traps = { 22 | 23 | get ( target, property, receiver ) { 24 | 25 | if ( property === 'constructor' ) return target.constructor; 26 | 27 | if ( typeof property === 'symbol' ) { 28 | 29 | if ( property === $TARGET ) return target; 30 | 31 | if ( property === $GET_RECORD_START ) return this.getPathsRecording = true; 32 | 33 | if ( property === $GET_RECORD_STOP ) { 34 | 35 | const paths = this.getPaths; 36 | 37 | this.getPathsRecording = false; 38 | this.getPaths = []; 39 | 40 | return paths; 41 | 42 | } 43 | 44 | if ( property === $IS_PROXY ) return true; 45 | 46 | if ( property === $STOP ) { 47 | 48 | this.stopped = true; 49 | this.changedPaths = undefined as any; //TSC 50 | this.paths = undefined as any; //TSC 51 | 52 | return target; 53 | 54 | } 55 | 56 | const value = get ( target, property, receiver ); 57 | 58 | if ( typeof value === 'function' ) return value.bind ( target ); //FIXME: Binding here prevents the function to be potentially re-bounded later 59 | 60 | return value; 61 | 62 | } 63 | 64 | receiver = receiver[$TARGET] || receiver; 65 | 66 | if ( this.getPathsRecording && !this.getParentPath ( target ) ) this.getPaths.push ( property as string ); // We are only recording root paths, because I don't see a use case for recording deeper paths too //TSC 67 | 68 | const value = get ( target, property, receiver ); 69 | 70 | if ( this.stopped || isValueUnproxiable ( value ) ) return value; 71 | 72 | const descriptor = getOwnPropertyDescriptor ( target, property ); 73 | 74 | if ( descriptor && !descriptor.configurable && !descriptor.writable ) return value; // Preserving invariants 75 | 76 | if ( typeof value === 'function' && isStrictlyImmutableMethod ( value ) ) return value.bind ( target ); //FIXME: Binding here prevents the function to be potentially re-bounded later 77 | 78 | this.setChildPath ( target, value, property ); 79 | 80 | if ( IS_DEVELOPMENT ) this.checkChildIsRoot ( value ); 81 | 82 | const proxyCached = this.proxies.get ( value ); 83 | 84 | if ( proxyCached ) return proxyCached; 85 | 86 | const proxy = new Proxy ( value, this.traps ); 87 | 88 | this.proxies.set ( value, proxy ); 89 | 90 | return proxy; 91 | 92 | }, 93 | 94 | set ( target, property, value, receiver ) { 95 | 96 | value = getTarget ( value ); 97 | 98 | if ( this.stopped || typeof property === 'symbol' ) return set ( target, property, value, receiver ); 99 | 100 | receiver = receiver[$TARGET] || receiver; 101 | 102 | const isValueUndefined = ( value === undefined ), 103 | didPropertyExist = isValueUndefined && property in target, 104 | isValueUndefinedNew = ( isValueUndefined && !didPropertyExist ), 105 | prev = !isValueUndefinedNew && get ( target, property, receiver ), 106 | result = set ( target, property, value, receiver ), 107 | changed = result && ( isValueUndefinedNew || !isEqual ( prev, value ) ); 108 | 109 | return changed ? this.triggerChange ( result, this.getChildPath ( target, property ) ) : result; 110 | 111 | }, 112 | 113 | defineProperty ( target, property, descriptor ) { 114 | 115 | if ( this.stopped || typeof property === 'symbol' ) return defineProperty ( target, property, descriptor ); 116 | 117 | const prev = getOwnPropertyDescriptor ( target, property ), 118 | changed = defineProperty ( target, property, descriptor ); 119 | 120 | if ( changed ) { 121 | 122 | const next = { configurable: false, enumerable: false, writable: false, ...descriptor }; // Accounting for defaults 123 | 124 | if ( isEqual ( prev, next ) ) return true; 125 | 126 | } 127 | 128 | return changed ? this.triggerChange ( changed, this.getChildPath ( target, property ) ) : changed; 129 | 130 | }, 131 | 132 | deleteProperty ( target, property ) { 133 | 134 | if ( !( property in target ) ) return true; 135 | 136 | const changed = deleteProperty ( target, property ); 137 | 138 | if ( this.stopped || typeof property === 'symbol' ) return changed; 139 | 140 | return changed ? this.triggerChange ( changed, this.getChildPath ( target, property ) ) : changed; 141 | 142 | }, 143 | 144 | apply ( target, thisArg, args ) { 145 | 146 | if ( !isProxy ( thisArg ) ) return apply ( target, thisArg, args ); 147 | 148 | const isArrayThis = isArray ( thisArg ); 149 | 150 | if ( this.stopped || ( isArrayThis && isLooselyImmutableArrayMethod ( target ) ) ) return apply ( target, thisArg, args ); 151 | 152 | const thisArgTarget = ( isArrayThis ? thisArg[$TARGET] : getTarget ( thisArg ) ); 153 | 154 | if ( !isArrayThis ) thisArg = thisArgTarget; 155 | 156 | const [result, changed] = execute ( target, thisArg, thisArgTarget, args ); 157 | 158 | return changed ? this.triggerChange ( result, this.getParentPath ( thisArgTarget ) ) : result; 159 | 160 | } 161 | 162 | }; 163 | 164 | /* EXPORT */ 165 | 166 | export default Traps; 167 | -------------------------------------------------------------------------------- /src/traps_helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {IS_DEVELOPMENT} from './consts'; 5 | import TrapsRaw from './traps'; 6 | import {Callback, Trap, Traps} from './types'; 7 | 8 | /* TRAPS HELPERS */ 9 | 10 | class TrapsHelpers { 11 | 12 | /* VARIABLES */ 13 | 14 | object: Object; 15 | callback: Callback; 16 | traps: Traps; 17 | 18 | stopped: boolean = false; 19 | changed: boolean = false; 20 | changedPaths: string[] = []; 21 | getPathsRecording: boolean = false; 22 | getPaths: string[] = []; 23 | paths: WeakMap = new WeakMap (); 24 | proxies: WeakMap = new WeakMap (); 25 | trapDepth: number = 0; 26 | 27 | /* CONSTRUCTOR */ 28 | 29 | constructor ( object: Object, callback: Callback ) { 30 | 31 | this.object = object; 32 | this.callback = callback; 33 | this.traps = this.wrapTraps ( TrapsRaw ); 34 | 35 | } 36 | 37 | /* HELPERS */ 38 | 39 | checkChildIsRoot ( child: any ): void { 40 | 41 | if ( child !== this.object ) return; 42 | 43 | throw new Error ( `A reference to the whole watched object has been found at path "${this.paths.get ( child )}", this is not supported.` ); 44 | 45 | } 46 | 47 | checkChildPathDuplicate ( child: object, childPath: string ): void { 48 | 49 | const childPathPrev = this.paths.get ( child ); 50 | 51 | if ( childPath === childPathPrev ) return; 52 | 53 | if ( !childPathPrev || !Object.is ( child, childPathPrev.split ( '.' ).reduce ( ( acc, key ) => acc[key] || {}, this.object ) ) ) return; 54 | 55 | throw new Error ( `Duplicate object encountered, the same object is being referenced both at path "${childPathPrev}" and at path "${childPath}". Duplicate objects in a watched object are not supported.` ); 56 | 57 | } 58 | 59 | getChildPath ( parent: object, path: string | number ): string { 60 | 61 | const parentPath = this.getParentPath ( parent ), 62 | childPath = parentPath ? `${parentPath}.${path}` : `${path}`; 63 | 64 | return childPath; 65 | 66 | } 67 | 68 | getParentPath ( parent: object ): string { 69 | 70 | return this.paths.get ( parent ) || ''; 71 | 72 | } 73 | 74 | setChildPath ( parent: object, child: object, path: string | number ): void { 75 | 76 | const childPath = this.getChildPath ( parent, path ); 77 | 78 | if ( IS_DEVELOPMENT && Object.getPrototypeOf ( parent )[path] !== child ) this.checkChildPathDuplicate ( child, childPath ); 79 | 80 | this.paths.set ( child, childPath ); 81 | 82 | } 83 | 84 | triggerChange ( result: T, path: string ): T { 85 | 86 | this.changed = true; 87 | this.changedPaths.push ( path ); 88 | 89 | return result; 90 | 91 | } 92 | 93 | wrapTrap ( trap: Trap ): any { //TSC 94 | 95 | const self = this; 96 | 97 | return function trapWrapper () { 98 | 99 | self.trapDepth++; 100 | 101 | const result = trap.apply ( self, arguments ); 102 | 103 | self.trapDepth--; 104 | 105 | if ( !self.trapDepth && self.changed && !self.stopped ) { 106 | 107 | const paths = self.changedPaths; 108 | 109 | self.changed = false; 110 | self.changedPaths = []; 111 | 112 | self.callback ( paths ); 113 | 114 | } 115 | 116 | return result; 117 | 118 | }; 119 | 120 | } 121 | 122 | wrapTraps ( traps: Traps ): Traps { 123 | 124 | return { 125 | get: this.wrapTrap ( traps.get ), 126 | set: this.wrapTrap ( traps.set ), 127 | defineProperty: this.wrapTrap ( traps.defineProperty ), 128 | deleteProperty: this.wrapTrap ( traps.deleteProperty ), 129 | apply: this.wrapTrap ( traps.apply ) 130 | }; 131 | 132 | } 133 | 134 | } 135 | 136 | /* EXPORT */ 137 | 138 | export default TrapsHelpers; 139 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* TYPES */ 3 | 4 | type Applicator = ( method: Function, thisArg: T, thisArgTarget: T, args: any[] ) => [any, boolean]; 5 | 6 | type Callback = ( paths: string[] ) => any; 7 | 8 | type Disposer = () => T; 9 | 10 | type TrapsHelpers = import ( './traps_helpers' ).default; 11 | 12 | type TrapGet = ( this: TrapsHelpers, target: any, property: PropertyKey, receiver: any ) => any; 13 | type TrapSet = ( this: TrapsHelpers, target: any, property: PropertyKey, value: any, receiver: any ) => boolean; 14 | type TrapDefineProperty = ( this: TrapsHelpers, target: any, property: PropertyKey, descriptor: PropertyDescriptor ) => boolean; 15 | type TrapDeleteProperty = ( this: TrapsHelpers, target: any, property: PropertyKey ) => boolean; 16 | type TrapApply = ( this: TrapsHelpers, target: any, thisArg: any, args?: any ) => any; 17 | type Trap = TrapGet | TrapSet | TrapDefineProperty | TrapDeleteProperty | TrapApply; 18 | 19 | type Traps = { 20 | get: TrapGet, 21 | set: TrapSet, 22 | defineProperty: TrapDefineProperty, 23 | deleteProperty: TrapDeleteProperty, 24 | apply: TrapApply 25 | }; 26 | 27 | /* EXPORT */ 28 | 29 | export {Applicator, Callback, Disposer, Trap, Traps}; 30 | -------------------------------------------------------------------------------- /src/unwatch.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$STOP} from './consts'; 5 | 6 | /* UNWATCH */ 7 | 8 | function unwatch ( object: Object ): Object { 9 | 10 | return object && ( object[$STOP] || object ); 11 | 12 | } 13 | 14 | /* EXPORT */ 15 | 16 | export default unwatch; 17 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {CONSTRUCTORS_IMMUTABLE, CONSTRUCTORS_SUPPORTED, CONSTRUCTORS_UNSUPPORTED, STRICTLY_IMMUTABLE_METHODS, LOOSELY_IMMUTABLE_METHODS} from './consts'; 5 | import clone from './packages/clone'; 6 | import cloneDeep from './packages/clone_deep'; 7 | import diff from './packages/diff'; 8 | import isEqual from './packages/is_equal'; 9 | import isNative from './packages/is_native'; 10 | import isPlainObject from './packages/is_plain_object'; 11 | 12 | /* UTILS */ 13 | 14 | const Utils = { 15 | 16 | clone, 17 | 18 | cloneDeep, 19 | 20 | diff, 21 | 22 | isEqual, 23 | 24 | isNative, 25 | 26 | isPlainObject, 27 | 28 | isValueUnproxiable: ( x: any ): boolean => { 29 | 30 | if ( x === null ) return true; 31 | 32 | const type = typeof x; 33 | 34 | if ( type !== 'object' && type !== 'function' ) return true; 35 | 36 | const {constructor} = x; 37 | 38 | return CONSTRUCTORS_IMMUTABLE.has ( constructor ) || ( !CONSTRUCTORS_SUPPORTED.has ( constructor ) && ( CONSTRUCTORS_UNSUPPORTED.has ( constructor ) || !isNative ( constructor ) ) ); 39 | 40 | }, 41 | 42 | isStrictlyImmutableMethod: ( method: Function ): boolean => { //TODO: Maybe perform "instanceof" checks, for correctness 43 | 44 | return STRICTLY_IMMUTABLE_METHODS.has ( method.name ); 45 | 46 | }, 47 | 48 | isLooselyImmutableArrayMethod: ( method: Function ): boolean => { // It assumes `target` is an array 49 | 50 | return LOOSELY_IMMUTABLE_METHODS.array.has ( method.name ); 51 | 52 | } 53 | 54 | }; 55 | 56 | /* EXPORT */ 57 | 58 | export default Utils; 59 | -------------------------------------------------------------------------------- /src/watch.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import makeTraps from './make_traps'; 5 | import unwatch from './unwatch'; 6 | import Utils from './utils'; 7 | import {Callback, Disposer} from './types'; 8 | 9 | /* WATCH */ 10 | 11 | function watch ( object: Object, callback: Callback ): [Object, Disposer] { 12 | 13 | if ( Utils.isValueUnproxiable ( object ) ) return [object, () => object]; 14 | 15 | const proxy = new Proxy ( object, makeTraps ( object, callback ) ), 16 | disposer: Disposer = () => unwatch ( proxy ); 17 | 18 | return [proxy, disposer]; 19 | 20 | } 21 | 22 | /* EXPORT */ 23 | 24 | export default watch; 25 | -------------------------------------------------------------------------------- /tasks/benchmark.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | const {watch, unwatch, record, target, isProxy} = require ( '../dist' ), 5 | {default: Utils} = require ( '../dist/utils' ), 6 | {NOOP, OBJ, OBJ_HUGE, DIFF_A, DIFF_B} = require ( './fixtures' ), 7 | benchmark = require ( 'benchloop' ); 8 | 9 | /* BENCHMARK */ 10 | 11 | benchmark.defaultOptions = Object.assign ( benchmark.defaultOptions, { 12 | iterations: 1000, 13 | log: 'compact' 14 | }); 15 | 16 | benchmark.group ( 'record', () => { 17 | 18 | benchmark ({ 19 | name: 'single', 20 | beforeEach: ctx => { 21 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 22 | }, 23 | fn: ctx => { 24 | record ( ctx.proxy, () => { 25 | ctx.proxy.obj.deep.deeper; 26 | }); 27 | } 28 | }); 29 | 30 | benchmark ({ 31 | name: 'multiple', 32 | beforeEach: ctx => { 33 | ctx.proxy1 = watch ( OBJ (), NOOP )[0]; 34 | ctx.proxy2 = watch ( OBJ (), NOOP )[0]; 35 | }, 36 | fn: ctx => { 37 | record ( [ctx.proxy1, ctx.proxy2], () => { 38 | ctx.proxy1.obj.deep.deeper; 39 | }); 40 | } 41 | }); 42 | 43 | }); 44 | 45 | benchmark ({ 46 | name: 'target', 47 | beforeEach: ctx => { 48 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 49 | }, 50 | fn: ctx => { 51 | target ( ctx.proxy ); 52 | } 53 | }); 54 | 55 | benchmark ({ 56 | name: 'unwatch', 57 | beforeEach: ctx => { 58 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 59 | }, 60 | fn: ctx => { 61 | unwatch ( ctx.proxy ); 62 | } 63 | }); 64 | 65 | benchmark.group ( 'isProxy', () => { 66 | 67 | benchmark ({ 68 | name: 'raw', 69 | fn: () => { 70 | isProxy ( OBJ ); 71 | } 72 | }); 73 | 74 | benchmark ({ 75 | name: 'proxy', 76 | fn: ctx => { 77 | isProxy ( ctx.proxy ); 78 | } 79 | }); 80 | 81 | }); 82 | 83 | benchmark.group ( 'watch', () => { 84 | 85 | benchmark ({ 86 | name: 'primitive', 87 | fn: () => { 88 | watch ( 123, NOOP ); 89 | } 90 | }); 91 | 92 | benchmark ({ 93 | name: 'object', 94 | fn: () => { 95 | watch ( {}, NOOP ); 96 | } 97 | }); 98 | 99 | benchmark ({ 100 | name: 'array', 101 | fn: () => { 102 | watch ( [], NOOP ); 103 | } 104 | }); 105 | 106 | benchmark ({ 107 | name: 'deep', 108 | beforeEach: ctx => { 109 | ctx.obj = OBJ (); 110 | }, 111 | fn: ctx => { 112 | watch ( ctx.obj, NOOP ); 113 | } 114 | }); 115 | 116 | }); 117 | 118 | benchmark.group ( 'dispose', () => { 119 | 120 | benchmark ({ 121 | name: 'primitive', 122 | beforeEach: ctx => { 123 | ctx.dispose = watch ( 123, NOOP )[1]; 124 | }, 125 | fn: ctx => { 126 | ctx.dispose (); 127 | } 128 | }); 129 | 130 | benchmark ({ 131 | name: 'object', 132 | beforeEach: ctx => { 133 | ctx.dispose = watch ( {}, NOOP )[1]; 134 | }, 135 | fn: ctx => { 136 | ctx.dispose (); 137 | } 138 | }); 139 | 140 | benchmark ({ 141 | name: 'array', 142 | beforeEach: ctx => { 143 | ctx.dispose = watch ( [], NOOP )[1]; 144 | }, 145 | fn: ctx => { 146 | ctx.dispose (); 147 | } 148 | }); 149 | 150 | benchmark ({ 151 | name: 'deep', 152 | beforeEach: ctx => { 153 | ctx.dispose = watch ( OBJ (), NOOP )[1]; 154 | }, 155 | fn: ctx => { 156 | ctx.dispose (); 157 | } 158 | }); 159 | 160 | }); 161 | 162 | benchmark.group ( 'get', () => { 163 | 164 | benchmark ({ 165 | name: 'primitive', 166 | beforeEach: ctx => { 167 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 168 | }, 169 | fn: ctx => { 170 | ctx.proxy.str; 171 | ctx.proxy.nr; 172 | ctx.proxy.symbol; 173 | } 174 | }); 175 | 176 | benchmark ({ 177 | name: 'object:shallow', 178 | beforeEach: ctx => { 179 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 180 | }, 181 | fn: ctx => { 182 | ctx.proxy.arr; 183 | ctx.proxy.obj; 184 | } 185 | }); 186 | 187 | benchmark ({ 188 | name: 'object:deep', 189 | beforeEach: ctx => { 190 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 191 | }, 192 | fn: ctx => { 193 | ctx.proxy.arr[3].undefined; 194 | ctx.proxy.obj.deep.deeper; 195 | } 196 | }); 197 | 198 | benchmark ({ 199 | name: 'date', 200 | beforeEach: ctx => { 201 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 202 | }, 203 | fn: ctx => { 204 | ctx.proxy.date.getTime (); 205 | ctx.proxy.date.getDate (); 206 | ctx.proxy.date.getDay (); 207 | ctx.proxy.date.getFullYear (); 208 | ctx.proxy.date.getHours (); 209 | ctx.proxy.date.getMilliseconds (); 210 | ctx.proxy.date.getMinutes (); 211 | ctx.proxy.date.getMonth (); 212 | ctx.proxy.date.getSeconds (); 213 | ctx.proxy.date.getTime (); 214 | ctx.proxy.date.getTimezoneOffset (); 215 | ctx.proxy.date.getUTCDate (); 216 | ctx.proxy.date.getUTCDay (); 217 | ctx.proxy.date.getUTCFullYear (); 218 | ctx.proxy.date.getUTCHours (); 219 | ctx.proxy.date.getUTCMilliseconds (); 220 | ctx.proxy.date.getUTCMinutes (); 221 | ctx.proxy.date.getUTCMonth (); 222 | ctx.proxy.date.getUTCSeconds (); 223 | ctx.proxy.date.getYear (); 224 | } 225 | }); 226 | 227 | benchmark ({ 228 | name: 'regex', 229 | beforeEach: ctx => { 230 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 231 | }, 232 | fn: ctx => { 233 | ctx.proxy.re.source; 234 | ctx.proxy.re.lastIndex; 235 | ctx.proxy.re.exec ( 'foo' ); 236 | } 237 | }); 238 | 239 | benchmark ({ 240 | name: 'function', 241 | beforeEach: ctx => { 242 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 243 | }, 244 | fn: ctx => { 245 | ctx.proxy.fn.length; 246 | ctx.proxy.fn.name; 247 | } 248 | }); 249 | 250 | benchmark ({ 251 | name: 'array', 252 | beforeEach: ctx => { 253 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 254 | }, 255 | fn: ctx => { 256 | ctx.proxy.arr.concat ( 4 ); 257 | ctx.proxy.arr.entries (); 258 | ctx.proxy.arr.every ( NOOP ); 259 | ctx.proxy.arr.filter ( NOOP ); 260 | ctx.proxy.arr.find ( NOOP ); 261 | ctx.proxy.arr.findIndex ( NOOP ); 262 | ctx.proxy.arr.forEach ( () => {} ); 263 | ctx.proxy.arr.includes ( 1 ); 264 | ctx.proxy.arr.indexOf ( 1 ); 265 | ctx.proxy.arr.join (); 266 | ctx.proxy.arr.keys (); 267 | ctx.proxy.arr.lastIndexOf ( 1 ); 268 | ctx.proxy.arr.map ( NOOP ); 269 | ctx.proxy.arr.reduce ( () => ({}) ); 270 | ctx.proxy.arr.reduceRight ( () => ({}) ); 271 | ctx.proxy.arr.slice (); 272 | ctx.proxy.arr.some ( NOOP ); 273 | ctx.proxy.arr.toLocaleString (); 274 | ctx.proxy.arr.toString (); 275 | ctx.proxy.arr.values (); 276 | } 277 | }); 278 | 279 | benchmark ({ 280 | name: 'arrayBuffer', 281 | beforeEach: ctx => { 282 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 283 | }, 284 | fn: ctx => { 285 | ctx.proxy.arrBuf.byteLength; 286 | ctx.proxy.arrBuf.slice ( 0, 8 ); 287 | } 288 | }); 289 | 290 | benchmark ({ 291 | name: 'arrayTyped', 292 | beforeEach: ctx => { 293 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 294 | }, 295 | fn: ctx => { 296 | ctx.proxy.arrTyped.subarray (); 297 | } 298 | }); 299 | 300 | benchmark ({ 301 | name: 'map', 302 | beforeEach: ctx => { 303 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 304 | }, 305 | fn: ctx => { 306 | ctx.proxy.map.size; 307 | ctx.proxy.map.entries (); 308 | ctx.proxy.map.forEach ( NOOP ); 309 | ctx.proxy.map.has ( '1' ); 310 | ctx.proxy.map.keys (); 311 | ctx.proxy.map.values (); 312 | ctx.proxy.map.get ( '1' ); 313 | } 314 | }); 315 | 316 | benchmark ({ 317 | name: 'set', 318 | beforeEach: ctx => { 319 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 320 | }, 321 | fn: ctx => { 322 | ctx.proxy.set.size; 323 | ctx.proxy.set.entries (); 324 | ctx.proxy.set.forEach ( NOOP ); 325 | ctx.proxy.set.has ( 1 ); 326 | ctx.proxy.set.keys (); 327 | ctx.proxy.set.values (); 328 | } 329 | }); 330 | 331 | }); 332 | 333 | benchmark.group ( 'set', () => { 334 | 335 | benchmark.group ( 'no', () => { 336 | 337 | benchmark ({ 338 | name: 'object:shallow', 339 | beforeEach: ctx => { 340 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 341 | }, 342 | fn: ctx => { 343 | ctx.proxy.arr[0] = 1; 344 | } 345 | }); 346 | 347 | benchmark ({ 348 | name: 'object:deep', 349 | beforeEach: ctx => { 350 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 351 | }, 352 | fn: ctx => { 353 | ctx.proxy.obj.deep.deeper = true; 354 | } 355 | }); 356 | 357 | benchmark ({ 358 | name: 'date', 359 | beforeEach: ctx => { 360 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 361 | }, 362 | fn: ctx => { 363 | ctx.proxy.date.setDate ( ctx.proxy.date.getDate () ); 364 | } 365 | }); 366 | 367 | benchmark ({ 368 | name: 'regex', 369 | beforeEach: ctx => { 370 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 371 | }, 372 | fn: ctx => { 373 | ctx.proxy.re.lastIndex = ctx.proxy.re.lastIndex; 374 | } 375 | }); 376 | 377 | benchmark ({ 378 | name: 'array', 379 | beforeEach: ctx => { 380 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 381 | }, 382 | fn: ctx => { 383 | ctx.proxy.arr.copyWithin ( 0, 0, 0 ); 384 | ctx.proxy.arr.push (); 385 | ctx.proxy.arr.splice ( 0, 0 ); 386 | } 387 | }); 388 | 389 | benchmark ({ 390 | name: 'map', 391 | beforeEach: ctx => { 392 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 393 | }, 394 | fn: ctx => { 395 | ctx.proxy.map.delete ( 'none' ); 396 | ctx.proxy.map.set ( '1', 1 ); 397 | } 398 | }); 399 | 400 | benchmark ({ 401 | name: 'set', 402 | beforeEach: ctx => { 403 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 404 | }, 405 | fn: ctx => { 406 | ctx.proxy.set.delete ( 'none' ); 407 | ctx.proxy.set.add ( 1 ); 408 | } 409 | }); 410 | 411 | }); 412 | 413 | benchmark.group ( 'yes', () => { 414 | 415 | benchmark ({ 416 | name: 'object:shallow', 417 | beforeEach: ctx => { 418 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 419 | }, 420 | fn: ctx => { 421 | ctx.proxy.arr[0] = 10; 422 | ctx.proxy.obj.foo = 10; 423 | } 424 | }); 425 | 426 | benchmark ({ 427 | name: 'object:deep', 428 | beforeEach: ctx => { 429 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 430 | }, 431 | fn: ctx => { 432 | ctx.proxy.arr[3].undefined = 10; 433 | ctx.proxy.obj.deep.deeper = 10; 434 | } 435 | }); 436 | 437 | benchmark ({ 438 | name: 'date', 439 | beforeEach: ctx => { 440 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 441 | }, 442 | fn: ctx => { 443 | ctx.proxy.date.setDate ( ctx.proxy.date.getDate () % 2 + 1 ); 444 | } 445 | }); 446 | 447 | benchmark ({ 448 | name: 'regex', 449 | beforeEach: ctx => { 450 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 451 | }, 452 | fn: ctx => { 453 | ctx.proxy.re.lastIndex = -1; 454 | } 455 | }); 456 | 457 | benchmark ({ 458 | name: 'function', 459 | beforeEach: ctx => { 460 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 461 | }, 462 | fn: ctx => { 463 | ctx.proxy.fn.displayName = 'Name'; 464 | } 465 | }); 466 | 467 | benchmark ({ 468 | name: 'array', 469 | beforeEach: ctx => { 470 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 471 | }, 472 | fn: ctx => { 473 | ctx.proxy.arr.copyWithin ( 0, 1, 2 ); 474 | ctx.proxy.arr.fill ( 0 ); 475 | ctx.proxy.arr.pop (); 476 | ctx.proxy.arr.push ( -1, -2, -3 ); 477 | ctx.proxy.arr.reverse (); 478 | ctx.proxy.arr.shift (); 479 | ctx.proxy.arr.sort (); 480 | ctx.proxy.arr.splice ( 0, 1, 2 ); 481 | ctx.proxy.arr.unshift ( 5 ); 482 | } 483 | }); 484 | 485 | benchmark ({ 486 | name: 'map', 487 | beforeEach: ctx => { 488 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 489 | }, 490 | fn: ctx => { 491 | ctx.proxy.map.delete ( '1' ); 492 | ctx.proxy.map.clear (); 493 | ctx.proxy.map.set ( '4', 4 ); 494 | } 495 | }); 496 | 497 | benchmark ({ 498 | name: 'set', 499 | beforeEach: ctx => { 500 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 501 | }, 502 | fn: ctx => { 503 | ctx.proxy.set.add ( 3 ); 504 | ctx.proxy.set.delete ( 1 ); 505 | ctx.proxy.set.clear (); 506 | } 507 | }); 508 | 509 | }); 510 | 511 | benchmark.group.skip ( 'huge', () => { 512 | 513 | benchmark ({ 514 | name: 'date:setDate', 515 | before: ctx => { 516 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 517 | }, 518 | fn: ctx => { 519 | ctx.proxy.date.setDate ( 123 ); 520 | } 521 | }); 522 | 523 | benchmark ({ 524 | name: 'array:copyWithin', 525 | before: ctx => { 526 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 527 | }, 528 | fn: ctx => { 529 | ctx.proxy.arr.copyWithin ( 0, 1, 2 ); 530 | } 531 | }); 532 | 533 | benchmark ({ 534 | name: 'array:fill', 535 | before: ctx => { 536 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 537 | }, 538 | fn: ctx => { 539 | ctx.proxy.arr.fill ( 0 ); 540 | } 541 | }); 542 | 543 | benchmark ({ 544 | name: 'array:pop', 545 | before: ctx => { 546 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 547 | }, 548 | fn: ctx => { 549 | ctx.proxy.arr.pop (); 550 | } 551 | }); 552 | 553 | benchmark ({ 554 | name: 'array:push', 555 | before: ctx => { 556 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 557 | }, 558 | fn: ctx => { 559 | ctx.proxy.arr.push ( -1 ); 560 | } 561 | }); 562 | 563 | benchmark ({ 564 | iterations: 1, 565 | name: 'array:reverse', 566 | before: ctx => { 567 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 568 | }, 569 | fn: ctx => { 570 | ctx.proxy.arr.reverse (); 571 | } 572 | }); 573 | 574 | benchmark ({ 575 | iterations: 1, 576 | name: 'array:shift', 577 | before: ctx => { 578 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 579 | }, 580 | fn: ctx => { 581 | ctx.proxy.arr.shift (); 582 | } 583 | }); 584 | 585 | benchmark ({ 586 | iterations: 1, 587 | name: 'array:sort', 588 | before: ctx => { 589 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 590 | }, 591 | fn: ctx => { 592 | ctx.proxy.arr.sort (); 593 | } 594 | }); 595 | 596 | benchmark ({ 597 | name: 'array:splice', 598 | before: ctx => { 599 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 600 | }, 601 | fn: ctx => { 602 | ctx.proxy.arr.splice ( 0, 1, 2 ); 603 | } 604 | }); 605 | 606 | benchmark ({ 607 | iterations: 1, 608 | name: 'array:unshift', 609 | before: ctx => { 610 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 611 | }, 612 | fn: ctx => { 613 | ctx.proxy.arr.unshift ( 5 ); 614 | } 615 | }); 616 | 617 | benchmark ({ 618 | name: 'map:set', 619 | before: ctx => { 620 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 621 | }, 622 | fn: ctx => { 623 | ctx.proxy.map.set ( 1, 'foo' ); 624 | } 625 | }); 626 | 627 | benchmark ({ 628 | name: 'map:delete', 629 | before: ctx => { 630 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 631 | }, 632 | fn: ctx => { 633 | ctx.proxy.map.delete ( 1 ); 634 | } 635 | }); 636 | 637 | benchmark ({ 638 | name: 'map:clear', 639 | before: ctx => { 640 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 641 | }, 642 | fn: ctx => { 643 | ctx.proxy.map.clear (); 644 | } 645 | }); 646 | 647 | benchmark ({ 648 | name: 'set:add', 649 | before: ctx => { 650 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 651 | }, 652 | fn: ctx => { 653 | ctx.proxy.set.add ( 3 ); 654 | } 655 | }); 656 | 657 | benchmark ({ 658 | name: 'set:delete', 659 | before: ctx => { 660 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 661 | }, 662 | fn: ctx => { 663 | ctx.proxy.set.delete ( 1 ); 664 | } 665 | }); 666 | 667 | benchmark ({ 668 | name: 'set:clear', 669 | before: ctx => { 670 | ctx.proxy = watch ( OBJ_HUGE (), NOOP )[0]; 671 | }, 672 | fn: ctx => { 673 | ctx.proxy.set.clear (); 674 | } 675 | }); 676 | 677 | }); 678 | 679 | }); 680 | 681 | benchmark.group ( 'delete', () => { 682 | 683 | benchmark ({ 684 | name: 'object:shallow', 685 | beforeEach: ctx => { 686 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 687 | }, 688 | fn: ctx => { 689 | delete ctx.proxy.arr; 690 | } 691 | }); 692 | 693 | benchmark ({ 694 | name: 'object:deep', 695 | beforeEach: ctx => { 696 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 697 | }, 698 | fn: ctx => { 699 | delete ctx.proxy.obj.deep.deeper; 700 | } 701 | }); 702 | 703 | benchmark ({ 704 | name: 'map', 705 | beforeEach: ctx => { 706 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 707 | }, 708 | fn: ctx => { 709 | ctx.proxy.map.delete ( '1' ); 710 | } 711 | }); 712 | 713 | benchmark ({ 714 | name: 'set', 715 | beforeEach: ctx => { 716 | ctx.proxy = watch ( OBJ (), NOOP )[0]; 717 | }, 718 | fn: ctx => { 719 | ctx.proxy.set.delete ( 1 ); 720 | } 721 | }); 722 | 723 | }); 724 | 725 | benchmark.group ( 'Utils', () => { 726 | 727 | benchmark ({ 728 | name: 'clone', 729 | fn: () => { 730 | Utils.clone ( OBJ () ); 731 | } 732 | }); 733 | 734 | benchmark ({ 735 | name: 'cloneDeep', 736 | fn: () => { 737 | Utils.cloneDeep ( OBJ () ); 738 | } 739 | }); 740 | 741 | benchmark ({ 742 | name: 'diff', 743 | fn: () => { 744 | Utils.diff ( DIFF_A, DIFF_B ); 745 | } 746 | }); 747 | 748 | benchmark ({ 749 | name: 'isEqual', 750 | fn: () => { 751 | Utils.isEqual ( DIFF_A, DIFF_B ); 752 | } 753 | }); 754 | 755 | }); 756 | 757 | benchmark.summary (); 758 | -------------------------------------------------------------------------------- /tasks/fixtures.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | const _ = require ( 'lodash' ); 5 | 6 | /* FIXTURES */ 7 | 8 | const NOOP = () => {}; 9 | 10 | const OBJ = () => ({ 11 | str: 'string', 12 | null: null, 13 | undefined: undefined, 14 | nr: 123, 15 | bigint: 10n, 16 | symbol: Symbol (), 17 | re: /foo/g, 18 | fn: function () {}, 19 | arr: [1, 2, 3, {}], 20 | arrBuf: new ArrayBuffer ( 12 ), 21 | arrTyped: new Int8Array ( new ArrayBuffer ( 24 ) ), 22 | obj: { 23 | deep: { 24 | deeper: true 25 | } 26 | }, 27 | date: new Date (), 28 | map: new Map ([ ['1', 1], ['2', 2] ]), 29 | set: new Set ([ 1, 2, 3 ]) 30 | }); 31 | 32 | const OBJ_HUGE = () => ({ 33 | arr: _.range ( 0, 100000 ), 34 | date: new Date (), 35 | map: new Map ( _.range ( 0, 100000 ).map ( nr => [`${nr}`, nr] ) ), 36 | set: new Set ( _.range ( 0, 100000 ) ) 37 | }); 38 | 39 | const CLS = new class {}; 40 | 41 | const DIFF_A = () => ({ 42 | foo: { 43 | bar: { 44 | a: ['a', 'b'], 45 | b: 2, 46 | c: ['x', 'y'], 47 | e: 100 48 | } 49 | }, 50 | primitive: 123, 51 | buzz: 'world', 52 | map: new Map ([[ 1, 1 ]]), 53 | map2: new Map ([[ 1, 1 ]]), 54 | cls: CLS, 55 | cls2: new class {} 56 | }); 57 | 58 | const DIFF_B = () => ({ 59 | foo: { 60 | bar: { 61 | a: ['a'], 62 | b: 2, 63 | c: ['x', 'y', 'z'], 64 | d: 'Hello, world!', 65 | f: 123 66 | } 67 | }, 68 | primitive: null, 69 | buzz: 'fizz', 70 | map: new Map ([[ 1, 1 ]]), 71 | map2: new Map ([[ 2, 2 ]]), 72 | cls2: new class {} 73 | }); 74 | 75 | const DIFF_RESULT = () => ({ 76 | added: { 77 | 'foo.bar.d': 'Hello, world!', 78 | 'foo.bar.f': 123, 79 | }, 80 | deleted: { 81 | 'foo.bar.e': 100, 82 | 'cls': CLS 83 | }, 84 | updated: { 85 | 'foo.bar.a': { 86 | before: ['a', 'b'], 87 | after: ['a'] 88 | }, 89 | 'foo.bar.c': { 90 | before: ['x', 'y'], 91 | after: ['x', 'y', 'z'] 92 | }, 93 | 'primitive': { 94 | before: 123, 95 | after: null 96 | }, 97 | 'buzz': { 98 | before: 'world', 99 | after: 'fizz' 100 | }, 101 | 'map2': { 102 | before: new Map ([[ 1, 1 ]]), 103 | after: new Map ([[ 2, 2 ]]) 104 | } 105 | } 106 | }); 107 | 108 | /* EXPORT */ 109 | 110 | module.exports = {NOOP, OBJ, OBJ_HUGE, DIFF_A, DIFF_B, DIFF_RESULT}; 111 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import 'jsdom-global/register'; 5 | import * as _ from 'lodash'; 6 | import {describe} from 'ava-spec'; 7 | import {watch, unwatch, record, target, isProxy} from '../dist'; 8 | import * as Consts from '../dist/consts'; 9 | import Utils from '../dist/utils'; 10 | import Fixtures from '../tasks/fixtures'; 11 | 12 | /* HELPERS */ 13 | 14 | function makeData ( object ) { 15 | 16 | let callsNr = 0, 17 | paths = []; 18 | 19 | const [proxy, dispose] = watch ( object, changedPaths => { 20 | callsNr++; 21 | paths = paths.concat ( changedPaths ); 22 | }); 23 | 24 | return { 25 | object, 26 | proxy, 27 | dispose, 28 | get nr () { 29 | return callsNr; 30 | }, 31 | get paths () { 32 | const result = paths; 33 | paths = []; // Resetting 34 | return result; 35 | } 36 | }; 37 | 38 | } 39 | 40 | /* PROXY WATCHER */ 41 | 42 | describe ( 'Proxy Watcher', () => { 43 | 44 | describe ( 'watch', it => { 45 | 46 | it ( 'get invariants are respected', t => { 47 | 48 | const obj = {}; 49 | 50 | Object.defineProperty ( obj, 'nonWritable', { 51 | configurable: false, 52 | writable: false, 53 | value: { a: true } 54 | }); 55 | 56 | Object.defineProperty ( obj, 'nonReadable', { 57 | configurable: false, 58 | set: () => {} 59 | }); 60 | 61 | const data = makeData ( obj ); 62 | 63 | t.is ( data.proxy.nonWritable, obj.nonWritable ); 64 | t.is ( data.proxy.nonReadable, undefined ); 65 | t.is ( data.nr, 0 ); 66 | 67 | }); 68 | 69 | it ( 'trap errors don\'t break things', t => { 70 | 71 | t.plan ( 5 ); 72 | 73 | const obj = { 74 | foo: true, 75 | frozen: Object.freeze ( {} ) 76 | }; 77 | 78 | Object.defineProperty ( obj, 'nonWritable', { 79 | configurable: false, 80 | writable: false, 81 | value: { a: true } 82 | }); 83 | 84 | const data = makeData ( obj ); 85 | 86 | try { 87 | 88 | data.proxy.nonWritable = 123; 89 | 90 | } catch ( err ) { 91 | 92 | t.true ( err instanceof Error ); 93 | 94 | } 95 | 96 | try { 97 | 98 | delete data.proxy.nonWritable; 99 | 100 | } catch ( err ) { 101 | 102 | t.true ( err instanceof Error ); 103 | 104 | } 105 | 106 | try { 107 | 108 | data.proxy.frozen.foo = {}; 109 | 110 | } catch ( err ) { 111 | 112 | t.true ( err instanceof Error ); 113 | 114 | } 115 | 116 | t.is ( data.nr, 0 ); 117 | 118 | data.proxy.foo = false; 119 | 120 | t.is ( data.nr, 1 ); 121 | 122 | }); 123 | 124 | it ( 'watching immutable ~primitives doesn\'t throw an error', t => { 125 | 126 | const values = [ 127 | null, 128 | undefined, 129 | 123, 130 | 123n, 131 | NaN, 132 | true, 133 | false, 134 | 'string', 135 | Symbol (), 136 | /foo/g, 137 | new ArrayBuffer ( 123 ), 138 | new Number ( 123 ), 139 | new Boolean ( true ), 140 | new String ( 'string' ) 141 | ]; 142 | 143 | values.forEach ( value => t.is ( value, watch ( value )[0] ) ); 144 | 145 | }); 146 | 147 | it ( 'watching mutations nested inside symbols aren\'t detected', t => { 148 | 149 | const symbol = Symbol (), 150 | data = makeData ({ [symbol]: { unreachable: true } }); 151 | 152 | t.true ( data.proxy[symbol].unreachable ); 153 | 154 | data.proxy[symbol].unreachable = false;; 155 | 156 | t.false ( data.proxy[symbol].unreachable ); 157 | 158 | t.is ( data.nr, 0 ); 159 | 160 | }); 161 | 162 | it ( 'assignment are also checked for equality', t => { 163 | 164 | const obj = { 165 | deep: { 166 | deeper: true 167 | } 168 | }; 169 | 170 | const data = makeData ( obj ); 171 | 172 | data.proxy.deep = { deeper: true }; 173 | data.proxy.deep = { deeper: true }; 174 | Object.defineProperty ( data.proxy, 'deep', Object.getOwnPropertyDescriptor ( data.proxy, 'deep' ) ); 175 | Object.defineProperty ( data.proxy, 'deep2', { configurable: true, value: { deeper: true } } ); 176 | Object.defineProperty ( data.proxy, 'deep2', { configurable: true, value: { deeper: true } } ); 177 | 178 | t.is ( data.nr, 1 ); 179 | 180 | }); 181 | 182 | it ( 'throws when duplicate structures are encountered', t => { 183 | 184 | Consts.IS_DEVELOPMENT = true; 185 | 186 | const obj = { bool: true, arr: [{}] }, 187 | root = { foo: obj, bar: obj }, 188 | data = makeData ( root ); 189 | 190 | data.proxy.foo.bool = false; 191 | 192 | t.deepEqual ( data.paths, ['foo.bool'] ); 193 | 194 | t.throws ( () => { 195 | 196 | data.proxy.bar.bool = true; 197 | 198 | }, /duplicate.*"foo".*"bar"/i ); 199 | 200 | data.proxy.foo.arr[0]; 201 | data.proxy.foo.arr.unshift ( true ); 202 | data.proxy.foo.arr[1]; 203 | 204 | t.throws ( () => { 205 | 206 | data.proxy.foo.arr[0] = data.proxy.foo.arr[1]; 207 | data.proxy.foo.arr[0]; 208 | 209 | }, /duplicate.*"foo\.arr\.1".*"foo\.arr\.0"/i ); 210 | 211 | Consts.IS_DEVELOPMENT = false; 212 | 213 | }); 214 | 215 | it ( 'doesn\'t count prototype values as duplicates', t => { 216 | 217 | Consts.IS_DEVELOPMENT = true; 218 | 219 | const cls = class Foo { method () {} }; 220 | 221 | const makeObjs = () => [ 222 | false, 223 | Boolean ( false ), 224 | new Boolean ( false ), 225 | 3, 226 | Number ( 3 ), 227 | new Number ( 3 ), 228 | 3n, 229 | BigInt ( 3n ), 230 | 'foo', 231 | String ( 'foo' ), 232 | new String ( 'foo' ), 233 | Symbol (), 234 | function fn () {}, 235 | new Date (), 236 | /foo/i, 237 | {}, 238 | [], 239 | new ArrayBuffer ( 12 ), 240 | new Int8Array ( new ArrayBuffer ( 24 ) ), 241 | new Uint8Array ( new ArrayBuffer ( 24 ) ), 242 | new Uint8ClampedArray ( new ArrayBuffer ( 24 ) ), 243 | new Int16Array ( new ArrayBuffer ( 24 ) ), 244 | new Uint16Array ( new ArrayBuffer ( 24 ) ), 245 | new Int32Array ( new ArrayBuffer ( 24 ) ), 246 | new Uint32Array ( new ArrayBuffer ( 24 ) ), 247 | new Float32Array ( new ArrayBuffer ( 24 ) ), 248 | new Float64Array ( new ArrayBuffer ( 24 ) ), 249 | new BigInt64Array ( new ArrayBuffer ( 24 ) ), 250 | new BigUint64Array ( new ArrayBuffer ( 24 ) ), 251 | new Map (), 252 | new WeakMap (), 253 | new Set (), 254 | new WeakSet (), 255 | Promise.resolve ( 'foo' ), 256 | new cls () 257 | ]; 258 | 259 | const properties = [ 260 | 'toString', 261 | 'toString', 262 | 'toString', 263 | 'toFixed', 264 | 'toFixed', 265 | 'toFixed', 266 | 'toString', 267 | 'toString', 268 | 'toUpperCase', 269 | 'toUpperCase', 270 | 'toUpperCase', 271 | Symbol.toPrimitive, 272 | 'call', 273 | 'toDateString', 274 | 'test', 275 | 'hasOwnProperty', 276 | 'forEach', 277 | 'slice', 278 | 'reduce', 279 | 'reduce', 280 | 'reduce', 281 | 'reduce', 282 | 'reduce', 283 | 'reduce', 284 | 'reduce', 285 | 'reduce', 286 | 'reduce', 287 | 'reduce', 288 | 'reduce', 289 | 'entries', 290 | 'entries', 291 | 'entries', 292 | 'entries', 293 | 'then', 294 | 'method' 295 | ]; 296 | 297 | const data = makeData ({ 298 | one: makeObjs (), 299 | two: makeObjs () 300 | }); 301 | 302 | properties.forEach ( ( property, index ) => { 303 | 304 | data.proxy.one[index][property]; 305 | data.proxy.two[index][property]; 306 | 307 | }); 308 | 309 | t.pass (); 310 | 311 | Consts.IS_DEVELOPMENT = false; 312 | 313 | }); 314 | 315 | it ( 'throws when referencing the root object', t => { 316 | 317 | Consts.IS_DEVELOPMENT = true; 318 | 319 | const root = {}; 320 | 321 | root.root = root; 322 | root.deep = { root }; 323 | 324 | const data = makeData ( root ); 325 | 326 | t.throws ( () => { 327 | 328 | data.proxy.root; 329 | 330 | }, /reference.*watched object/i ); 331 | 332 | t.throws ( () => { 333 | 334 | data.proxy.deep.root; 335 | 336 | }, /reference.*watched object/i ); 337 | 338 | Consts.IS_DEVELOPMENT = false; 339 | 340 | }); 341 | 342 | it ( 'basic support for circular structures', t => { 343 | 344 | const makeCircular = () => { const root = {}; root.root = {root, slice: {}}; return root; }, 345 | circular1 = makeCircular (), 346 | circular2 = makeCircular (); 347 | 348 | t.true ( Utils.isEqual ( circular1, circular2 ) ); 349 | t.true ( Utils.isEqual ( circular1, Utils.clone ( circular1 ) ) ); 350 | t.true ( Utils.isEqual ( circular1, Utils.cloneDeep ( circular1 ) ) ); 351 | 352 | }); 353 | 354 | it ( 'has a basic diff function', t => { 355 | 356 | const result = Utils.diff ( Fixtures.DIFF_A (), Fixtures.DIFF_B () ); 357 | 358 | t.true ( Utils.isEqual ( result, Fixtures.DIFF_RESULT () ) ); 359 | 360 | }); 361 | 362 | it ( 'returns a disposer', t => { 363 | 364 | const obj = { 365 | deep: { 366 | deeper: true 367 | } 368 | }; 369 | 370 | const data = makeData ( obj ); 371 | 372 | data.proxy.deep.deeper = false; // In order to deeply proxy 373 | 374 | t.is ( data.nr, 1 ); 375 | 376 | const target = data.dispose (); 377 | 378 | t.is ( target, obj ); 379 | 380 | data.proxy.foo = true; 381 | data.proxy.deep.foo = true; 382 | data.proxy.deep.deeper = { foo: true }; 383 | delete data.proxy.deep; 384 | 385 | t.is ( data.nr, 1 ); 386 | 387 | }); 388 | 389 | describe ( 'structures', it => { 390 | 391 | it ( 'basics', t => { 392 | 393 | const data = makeData ({ foo: true }); 394 | 395 | data.proxy.foo; 396 | data.proxy.bar; 397 | data.proxy.foo = true; 398 | 399 | t.is ( data.nr, 0 ); 400 | 401 | data.proxy.bar = undefined; 402 | 403 | t.is ( data.nr, 1 ); 404 | t.deepEqual ( data.paths, ['bar'] ); 405 | 406 | data.proxy.foo = false; 407 | data.proxy.foo = false; 408 | 409 | t.is ( data.nr, 2 ); 410 | t.deepEqual ( data.paths, ['foo'] ); 411 | 412 | data.proxy.bar = { deep: true }; 413 | data.proxy.bar = { deep: true }; 414 | data.proxy.bar = { deep: true }; 415 | 416 | t.is ( data.nr, 3 ); 417 | t.deepEqual ( data.paths, ['bar'] ); 418 | 419 | data.proxy.bar.deep = undefined; 420 | data.proxy.baz = undefined; 421 | delete data.proxy.bar.deep; 422 | delete data.proxy.bar.deep; 423 | delete data.proxy.bar; 424 | 425 | t.is ( data.nr, 7 ); 426 | t.deepEqual ( data.paths, ['bar.deep', 'baz', 'bar.deep', 'bar'] ); 427 | 428 | Object.defineProperty ( data.proxy, 'bar', { value: 2 }); 429 | Object.defineProperty ( data.proxy, 'bar', { value: 2 }); 430 | 431 | t.is ( data.nr, 8 ); 432 | t.deepEqual ( data.paths, ['bar'] ); 433 | 434 | t.true ( data.proxy.hasOwnProperty ( 'foo' ) ); 435 | t.true ( 'foo' in data.proxy ); 436 | t.false ( data.proxy.hasOwnProperty ( 'qux' ) ); 437 | t.false ( 'qux' in data.proxy ); 438 | 439 | t.is ( data.nr, 8 ); 440 | 441 | }); 442 | 443 | it ( 'accessors', t => { 444 | 445 | const obj = {}; 446 | 447 | Object.defineProperty ( obj, 'accessor', { 448 | set ( val ) { 449 | this._accessor = val; 450 | }, 451 | get () { 452 | return this._accessor; 453 | } 454 | }); 455 | 456 | const data = makeData ( obj ); 457 | 458 | data.proxy.accessor = 10; 459 | data.proxy.accessor = 10; 460 | 461 | t.is ( data.proxy.accessor, 10 ); 462 | t.is ( data.nr, 1 ); 463 | t.deepEqual ( data.paths, ['accessor'] ); 464 | 465 | }); 466 | 467 | it ( 'deep', t => { 468 | 469 | const data = makeData ({ 470 | deep: { 471 | arr: [1, 2, { foo: true }, { zzz: true }], 472 | map: new Map ([ ['1', {}], ['2', {}] ]), 473 | set: new Set ([ {}, {} ]) 474 | } 475 | }); 476 | 477 | data.proxy.deep.arr[0] = 1; 478 | data.proxy.deep.arr[1] = 2; 479 | data.proxy.deep.arr[2].foo = true; 480 | 481 | t.is ( data.nr, 0 ); 482 | 483 | data.proxy.deep.arr[0] = -1; 484 | data.proxy.deep.arr[1] = -2; 485 | data.proxy.deep.arr[2].foo = false; 486 | data.proxy.deep.arr[2].bar = 123; 487 | data.proxy.deep.arr[4] = { other: true }; 488 | data.proxy.deep.arr[4] = { other: false }; 489 | data.proxy.deep.arr.forEach ( x => x.zzz && ( x.mod = true ) ); 490 | data.proxy.deep.map.forEach ( x => x.mod = true ); 491 | data.proxy.deep.set.forEach ( x => x.mod = true ); 492 | 493 | _.merge ( data.proxy, { 494 | root: true, 495 | deep: { 496 | deeper: { 497 | bottom: true 498 | } 499 | } 500 | }); 501 | 502 | _.merge ( data.proxy, { 503 | root: false, 504 | deep: { 505 | deeper: { 506 | bottom: false 507 | } 508 | } 509 | }); 510 | 511 | t.is ( data.nr, 13 ); 512 | t.deepEqual ( data.paths, ['deep.arr.0', 'deep.arr.1', 'deep.arr.2.foo', 'deep.arr.2.bar', 'deep.arr.4', 'deep.arr.4', 'deep.arr.3.mod', 'deep.map', 'deep.set', 'root', 'deep.deeper', 'root', 'deep.deeper.bottom'] ); 513 | 514 | }); 515 | 516 | it ( 'primitives - tricky', t => { 517 | 518 | const data = makeData ({ 519 | minInf: -Infinity, 520 | inf: Infinity, 521 | minZero: -0, 522 | zero: 0, 523 | nan: NaN, 524 | bigint: 1n 525 | }); 526 | 527 | data.proxy.minInf = -Infinity; 528 | data.proxy.inf = Infinity; 529 | data.proxy.minZero = -0; 530 | data.proxy.zero = 0; 531 | data.proxy.nan = NaN; 532 | data.proxy.bigint = 1n; 533 | 534 | t.is ( data.nr, 0 ); 535 | 536 | data.proxy.minInf = Infinity; 537 | data.proxy.inf = -Infinity; 538 | data.proxy.minZero = 0; 539 | data.proxy.zero = -0; 540 | data.proxy.nan = 0; 541 | data.proxy.bigint = 2n; 542 | 543 | t.is ( data.nr, 6 ); 544 | t.deepEqual ( data.paths, ['minInf', 'inf', 'minZero', 'zero', 'nan', 'bigint'] ); 545 | 546 | }); 547 | 548 | it ( 'primitives - constructors', t => { 549 | 550 | const data = makeData ({ 551 | fn: { 552 | symbol: Symbol (), 553 | bool: Boolean ( true ), 554 | str: String ( 'string' ), 555 | nr: Number ( 123 ) 556 | }, 557 | new: { 558 | bool: new Boolean ( true ), 559 | str: new String ( 'string' ), 560 | nr: new Number ( 123 ) 561 | } 562 | }); 563 | 564 | data.proxy.fn.symbol; 565 | data.proxy.fn.bool; 566 | data.proxy.fn.str; 567 | data.proxy.fn.nr; 568 | data.proxy.new.bool; 569 | data.proxy.new.str; 570 | data.proxy.new.nr; 571 | 572 | t.is ( data.nr, 0 ); 573 | 574 | data.proxy.fn.bool = true; 575 | data.proxy.fn.str = 'string'; 576 | data.proxy.fn.nr = 123; 577 | 578 | t.is ( data.nr, 0 ); 579 | 580 | data.proxy.fn.symbol = Symbol (); 581 | data.proxy.fn.bool = new Boolean ( true ); 582 | data.proxy.fn.str = new String ( 'string' ); 583 | data.proxy.fn.nr = new Number ( 123 ); 584 | 585 | t.is ( data.nr, 4 ); 586 | t.deepEqual ( data.paths, ['fn.symbol', 'fn.bool', 'fn.str', 'fn.nr'] ); 587 | 588 | data.proxy.new.bool = new Boolean ( true ); 589 | data.proxy.new.str = new String ( 'string' ); 590 | data.proxy.new.nr = new Number ( 123 ); 591 | 592 | t.is ( data.nr, 4 ); 593 | 594 | data.proxy.new.bool = true; 595 | data.proxy.new.str = 'string'; 596 | data.proxy.new.nr = 123; 597 | 598 | t.is ( data.nr, 7 ); 599 | t.deepEqual ( data.paths, ['new.bool', 'new.str', 'new.nr'] ); 600 | 601 | delete data.proxy.fn.bool; 602 | delete data.proxy.fn.str; 603 | delete data.proxy.fn.nr; 604 | delete data.proxy.new.bool; 605 | delete data.proxy.new.str; 606 | delete data.proxy.new.nr; 607 | 608 | t.is ( data.nr, 13 ); 609 | t.deepEqual ( data.paths, ['fn.bool', 'fn.str', 'fn.nr', 'new.bool', 'new.str', 'new.nr'] ); 610 | 611 | }); 612 | 613 | it ( 'date', t => { 614 | 615 | const data = makeData ({ date: new Date () }); 616 | 617 | data.proxy.date.getTime (); 618 | data.proxy.date.getDate (); 619 | data.proxy.date.getDay (); 620 | data.proxy.date.getFullYear (); 621 | data.proxy.date.getHours (); 622 | data.proxy.date.getMilliseconds (); 623 | data.proxy.date.getMinutes (); 624 | data.proxy.date.getMonth (); 625 | data.proxy.date.getSeconds (); 626 | data.proxy.date.getTime (); 627 | data.proxy.date.getTimezoneOffset (); 628 | data.proxy.date.getUTCDate (); 629 | data.proxy.date.getUTCDay (); 630 | data.proxy.date.getUTCFullYear (); 631 | data.proxy.date.getUTCHours (); 632 | data.proxy.date.getUTCMilliseconds (); 633 | data.proxy.date.getUTCMinutes (); 634 | data.proxy.date.getUTCMonth (); 635 | data.proxy.date.getUTCSeconds (); 636 | data.proxy.date.getYear (); 637 | 638 | t.is ( data.nr, 0 ); 639 | 640 | data.proxy.date.toDateString (); 641 | data.proxy.date.toISOString (); 642 | data.proxy.date.toJSON (); 643 | data.proxy.date.toGMTString (); 644 | data.proxy.date.toLocaleDateString (); 645 | data.proxy.date.toLocaleString (); 646 | data.proxy.date.toLocaleTimeString (); 647 | data.proxy.date.toString (); 648 | data.proxy.date.toTimeString (); 649 | data.proxy.date.toUTCString (); 650 | data.proxy.date.valueOf (); 651 | 652 | t.is ( data.nr, 0 ); 653 | 654 | data.proxy.date.setDate ( data.proxy.date.getDate () ); 655 | data.proxy.date.setFullYear ( data.proxy.date.getFullYear () ); 656 | data.proxy.date.setHours ( data.proxy.date.getHours () ); 657 | data.proxy.date.setMilliseconds ( data.proxy.date.getMilliseconds () ); 658 | data.proxy.date.setMinutes ( data.proxy.date.getMinutes () ); 659 | data.proxy.date.setMonth ( data.proxy.date.getMonth () ); 660 | data.proxy.date.setSeconds ( data.proxy.date.getSeconds () ); 661 | data.proxy.date.setTime ( data.proxy.date.getTime () ); 662 | data.proxy.date.setUTCDate ( data.proxy.date.getUTCDate () ); 663 | data.proxy.date.setUTCFullYear ( data.proxy.date.getUTCFullYear () ); 664 | data.proxy.date.setUTCHours ( data.proxy.date.getUTCHours () ); 665 | data.proxy.date.setUTCMilliseconds ( data.proxy.date.getUTCMilliseconds () ); 666 | data.proxy.date.setUTCMinutes ( data.proxy.date.getUTCMinutes () ); 667 | data.proxy.date.setUTCMonth ( data.proxy.date.getUTCMonth () ); 668 | data.proxy.date.setUTCSeconds ( data.proxy.date.getUTCSeconds () ); 669 | 670 | t.is ( data.nr, 0 ); 671 | 672 | const next = x => x % 2 + 1; // Computing an always different valid value 673 | 674 | data.proxy.date.setDate ( next ( data.proxy.date.getDate () ) ); 675 | data.proxy.date.setFullYear ( next ( data.proxy.date.getFullYear () ) ); 676 | data.proxy.date.setHours ( next ( data.proxy.date.getHours () ) ); 677 | data.proxy.date.setMilliseconds ( next ( data.proxy.date.getMilliseconds () ) ); 678 | data.proxy.date.setMinutes ( next ( data.proxy.date.getMinutes () ) ); 679 | data.proxy.date.setMonth ( next ( data.proxy.date.getMonth () ) ); 680 | data.proxy.date.setSeconds ( next ( data.proxy.date.getSeconds () ) ); 681 | data.proxy.date.setTime ( next ( data.proxy.date.getTime () ) ); 682 | data.proxy.date.setUTCDate ( next ( data.proxy.date.getUTCDate () ) ); 683 | data.proxy.date.setUTCFullYear ( next ( data.proxy.date.getUTCFullYear () ) ); 684 | data.proxy.date.setUTCHours ( next ( data.proxy.date.getUTCHours () ) ); 685 | data.proxy.date.setUTCMilliseconds ( next ( data.proxy.date.getUTCMilliseconds () ) ); 686 | data.proxy.date.setUTCMinutes ( next ( data.proxy.date.getUTCMinutes () ) ); 687 | data.proxy.date.setUTCMonth ( next ( data.proxy.date.getUTCMonth () ) ); 688 | data.proxy.date.setUTCSeconds ( next ( data.proxy.date.getUTCSeconds () ) ); 689 | 690 | t.is ( data.nr, 15 ); 691 | t.deepEqual ( data.paths, ['date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date', 'date'] ); 692 | 693 | }); 694 | 695 | it ( 'regex', t => { 696 | 697 | const data = makeData ({ re: /foo/gi }); 698 | 699 | data.proxy.re.lastIndex; 700 | data.proxy.re.source; 701 | 702 | t.is ( data.nr, 0 ); 703 | 704 | data.proxy.re.lastIndex = data.proxy.re.lastIndex; 705 | 706 | t.is ( data.nr, 0 ); 707 | 708 | data.proxy.re.exec ( 'foo' ); 709 | data.proxy.re.test ( 'foo' ); 710 | 'foo'.match ( data.proxy.re ); 711 | 'foo'.matchAll ( data.proxy.re ); 712 | 'foo'.replace ( data.proxy.re, '' ); 713 | 'foo'.search ( data.proxy.re ); 714 | 'foo'.split ( data.proxy.re ); 715 | 716 | t.is ( data.nr, 0 ); 717 | 718 | // data.proxy.re.lastIndex = -10; //FIXME: https://github.com/lodash/lodash/issues/4645 719 | 720 | // t.is ( data.nr, 1 ); 721 | // t.deepEqual ( data.paths, ['re.lastIndex'] ); 722 | 723 | }); 724 | 725 | it ( 'function', t => { 726 | 727 | const data = makeData ({ fn: function () {} }); 728 | 729 | data.proxy.fn (); 730 | 731 | const {fn} = data.proxy; 732 | 733 | fn (); 734 | 735 | data.proxy.fn.length; 736 | data.proxy.fn.name; 737 | data.proxy.fn.displayName; 738 | 739 | t.is ( data.nr, 0 ); 740 | 741 | data.proxy.fn.displayName = 'Name'; 742 | 743 | t.is ( data.nr, 1 ); 744 | t.deepEqual ( data.paths, ['fn.displayName'] ); 745 | 746 | }); 747 | 748 | it ( 'array', t => { 749 | 750 | const data = makeData ({ arr: [2, 1, 3] }); 751 | 752 | data.proxy.arr.constructor; 753 | t.is ( data.proxy.arr.length, 3 ); 754 | 755 | t.is ( data.nr, 0 ); 756 | 757 | data.proxy.arr.concat ( 4 ); 758 | data.proxy.arr.entries (); 759 | data.proxy.arr.every ( () => false ); 760 | data.proxy.arr.filter ( () => false ); 761 | data.proxy.arr.find ( () => false ); 762 | data.proxy.arr.findIndex ( () => false ); 763 | data.proxy.arr.forEach ( () => {} ); 764 | data.proxy.arr.includes ( 1 ); 765 | data.proxy.arr.indexOf ( 1 ); 766 | data.proxy.arr.join (); 767 | data.proxy.arr.keys (); 768 | data.proxy.arr.lastIndexOf ( 1 ); 769 | data.proxy.arr.map ( () => false ); 770 | data.proxy.arr.reduce ( () => ({}) ); 771 | data.proxy.arr.reduceRight ( () => ({}) ); 772 | data.proxy.arr.slice (); 773 | data.proxy.arr.some ( () => false ); 774 | data.proxy.arr.toLocaleString (); 775 | data.proxy.arr.toString (); 776 | data.proxy.arr.values (); 777 | 778 | t.is ( data.nr, 0 ); 779 | 780 | data.proxy.arr.length = 10; 781 | 782 | t.is ( data.nr, 1 ); 783 | t.deepEqual ( data.paths, ['arr.length'] ); 784 | 785 | data.proxy.arr.copyWithin ( 0, 0, 0 ); 786 | data.proxy.arr.push (); 787 | data.proxy.arr.splice ( 0, 0 ); 788 | 789 | t.is ( data.nr, 1 ); 790 | 791 | data.proxy.arr.copyWithin ( 0, 1, 2 ); 792 | data.proxy.arr.fill ( 0 ); 793 | data.proxy.arr.pop (); 794 | data.proxy.arr.push ( -1, -2, -3 ); 795 | data.proxy.arr.reverse (); 796 | data.proxy.arr.shift (); 797 | data.proxy.arr.sort (); 798 | data.proxy.arr.splice ( 0, 1, 2 ); 799 | data.proxy.arr.unshift ( 5 ); 800 | 801 | t.is ( data.nr, 10 ); 802 | t.deepEqual ( data.paths, ['arr.0', 'arr', 'arr.0', 'arr.1', 'arr.2', 'arr.3', 'arr.4', 'arr.5', 'arr.6', 'arr.7', 'arr.8', 'arr.9', 'arr', 'arr.9', 'arr.length', 'arr', 'arr.9', 'arr.10', 'arr.11', 'arr', 'arr.0', 'arr.11', 'arr.1', 'arr.10', 'arr.2', 'arr.9', 'arr', 'arr.0', 'arr.1', 'arr.2', 'arr.11', 'arr.length', 'arr', 'arr.0', 'arr.1', 'arr', 'arr.0', 'arr', 'arr.11', 'arr.2', 'arr.1', 'arr.0', 'arr'] ); 803 | 804 | }); 805 | 806 | it ( 'array buffer', t => { 807 | 808 | const data = makeData ({ arr: new ArrayBuffer ( 12 ) }); 809 | 810 | data.proxy.arr.constructor; 811 | data.proxy.arr.byteLength; 812 | 813 | t.is ( data.nr, 0 ); 814 | 815 | data.proxy.arr.slice ( 0, 8 ); 816 | 817 | t.is ( data.nr, 0 ); 818 | 819 | }); 820 | 821 | it ( 'typed array', t => { 822 | 823 | const Constructors = [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array]; 824 | 825 | Constructors.map ( Constructor => { 826 | 827 | const data = makeData ({ arr: new Constructor ( new ArrayBuffer ( 24 ) ) }); 828 | 829 | data.proxy.arr.constructor; 830 | data.proxy.arr.constructor.name; 831 | data.proxy.arr.BYTES_PER_ELEMENT; 832 | data.proxy.arr.byteLength; 833 | data.proxy.arr.byteOffset; 834 | data.proxy.arr.buffer; 835 | 836 | t.is ( data.nr, 0 ); 837 | 838 | data.proxy.arr.entries (); 839 | data.proxy.arr.every ( () => false ); 840 | data.proxy.arr.filter ( () => false ); 841 | data.proxy.arr.find ( () => false ); 842 | data.proxy.arr.findIndex ( () => false ); 843 | data.proxy.arr.forEach ( () => {} ); 844 | data.proxy.arr.includes ( 1 ); 845 | data.proxy.arr.indexOf ( 1 ); 846 | data.proxy.arr.join (); 847 | data.proxy.arr.keys (); 848 | data.proxy.arr.lastIndexOf ( 1 ); 849 | data.proxy.arr.map ( () => false ); 850 | data.proxy.arr.reduce ( () => ({}) ); 851 | data.proxy.arr.reduceRight ( () => ({}) ); 852 | data.proxy.arr.slice (); 853 | data.proxy.arr.some ( () => false ); 854 | data.proxy.arr.subarray (); 855 | data.proxy.arr.toLocaleString (); 856 | data.proxy.arr.toString (); 857 | data.proxy.arr.values (); 858 | 859 | t.is ( data.nr, 0 ); 860 | 861 | data.proxy.arr.copyWithin ( 0, 0, 0 ); 862 | 863 | t.is ( data.nr, 0 ); 864 | 865 | const sampleDigit = data.proxy.arr.constructor.name.startsWith ( 'Big' ) ? 1n : 1; 866 | 867 | data.proxy.arr.set ([ sampleDigit ]); 868 | data.proxy.arr.copyWithin ( 1, 0, 1 ); 869 | data.proxy.arr.reverse (); 870 | data.proxy.arr.fill ( sampleDigit ); 871 | 872 | t.is ( data.nr, 4 ); 873 | t.deepEqual ( data.paths, ['arr', 'arr', 'arr', 'arr'] ); 874 | 875 | }); 876 | 877 | }); 878 | 879 | it ( 'map', t => { 880 | 881 | const data = makeData ({ map: new Map ([ ['1', 1], ['2', 2] ]) }); 882 | 883 | data.proxy.map.constructor; 884 | data.proxy.map.length; 885 | t.is ( data.proxy.map.size, 2 ); 886 | 887 | t.is ( data.nr, 0 ); 888 | 889 | data.proxy.map.entries (); 890 | data.proxy.map.forEach ( () => {} ); 891 | data.proxy.map.has ( '1' ); 892 | data.proxy.map.keys (); 893 | data.proxy.map.values (); 894 | data.proxy.map.get ( '1' ); 895 | 896 | t.is ( data.nr, 0 ); 897 | 898 | data.proxy.map.delete ( 'none' ); 899 | data.proxy.map.set ( '1', 1 ); 900 | 901 | t.is ( data.nr, 0 ); 902 | 903 | data.proxy.map.delete ( '1' ); 904 | data.proxy.map.clear (); 905 | data.proxy.map.set ( '4', 4 ); 906 | 907 | t.is ( data.nr, 3 ); 908 | t.deepEqual ( data.paths, ['map', 'map', 'map'] ); 909 | 910 | }); 911 | 912 | it ( 'weakmap', t => { 913 | 914 | const data = makeData ({ weakmap: new WeakMap () }); 915 | 916 | t.is ( data.proxy.weakmap.constructor.name, 'WeakMap' ); 917 | 918 | data.proxy.weakmap.has ( 'foo' ); 919 | 920 | t.is ( data.nr, 0 ); 921 | 922 | data.proxy.weakmap = data.proxy.weakmap; 923 | 924 | t.is ( data.nr, 0 ); 925 | 926 | data.proxy.weakmap = new WeakMap (); 927 | 928 | t.is ( data.nr, 1 ); 929 | 930 | }); 931 | 932 | it ( 'set', t => { 933 | 934 | const data = makeData ({ set: new Set ([ 1, 2 ]) }); 935 | 936 | data.proxy.set.constructor; 937 | t.is ( data.proxy.set.size, 2 ); 938 | 939 | t.is ( data.nr, 0 ); 940 | 941 | data.proxy.set.entries (); 942 | data.proxy.set.forEach ( () => {} ); 943 | data.proxy.set.has ( 1 ); 944 | data.proxy.set.keys (); 945 | data.proxy.set.values (); 946 | 947 | t.is ( data.nr, 0 ); 948 | 949 | data.proxy.set.delete ( 'none' ); 950 | data.proxy.set.add ( 1 ); 951 | 952 | t.is ( data.nr, 0 ); 953 | 954 | data.proxy.set.add ( 3 ); 955 | data.proxy.set.delete ( 1 ); 956 | data.proxy.set.clear (); 957 | 958 | t.is ( data.nr, 3 ); 959 | t.deepEqual ( data.paths, ['set', 'set', 'set'] ); 960 | 961 | }); 962 | 963 | it ( 'weakset', t => { 964 | 965 | const data = makeData ({ weakset: new WeakSet () }); 966 | 967 | t.is ( data.proxy.weakset.constructor.name, 'WeakSet' ); 968 | 969 | data.proxy.weakset.has ( 'foo' ); 970 | 971 | t.is ( data.nr, 0 ); 972 | 973 | data.proxy.weakset = data.proxy.weakset; 974 | 975 | t.is ( data.nr, 0 ); 976 | 977 | data.proxy.weakset = new WeakSet (); 978 | 979 | t.is ( data.nr, 1 ); 980 | 981 | }); 982 | 983 | it ( 'promise', async t => { 984 | 985 | const data = makeData ({ 986 | string: Promise.resolve ( 'string' ), 987 | number: Promise.resolve ( 123 ), 988 | arr: Promise.resolve ([ 1, 2, 3 ]), 989 | obj: Promise.resolve ({ foo: true }), 990 | set: Promise.resolve ( new Set ([ 1, 2, 3 ]) ), 991 | deep: Promise.resolve ( Promise.resolve ({ deep: true }) ), 992 | }); 993 | 994 | t.is ( await data.proxy.string, 'string' ); 995 | t.is ( await data.proxy.number, 123 ); 996 | t.deepEqual ( await data.proxy.arr, [1, 2, 3] ); 997 | t.deepEqual ( await data.proxy.obj, { foo: true } ); 998 | t.deepEqual ( await data.proxy.set, new Set ([ 1, 2, 3 ]) ); 999 | t.deepEqual ( await data.proxy.deep, { deep: true } ); 1000 | t.is ( data.nr, 0 ); 1001 | 1002 | data.proxy.string = data.proxy.string; 1003 | data.proxy.number = data.proxy.number; 1004 | data.proxy.arr = data.proxy.arr; 1005 | data.proxy.obj = data.proxy.obj; 1006 | data.proxy.set = data.proxy.set; 1007 | data.proxy.deep = data.proxy.deep; 1008 | t.is ( data.nr, 0 ); 1009 | 1010 | data.proxy.arr.then ( arr => arr[0] = 1 ); 1011 | data.proxy.obj.then ( obj => obj.foo = true ); 1012 | data.proxy.set.then ( set => set.delete ( 4 ) ); 1013 | data.proxy.set.then ( set => set.has ( 4 ) ); 1014 | data.proxy.deep.then ( obj => obj.deep = true ); 1015 | t.is ( data.nr, 0 ); 1016 | 1017 | data.proxy.string = Promise.resolve ( 'string' ); 1018 | data.proxy.number = Promise.resolve ( 123 ); 1019 | data.proxy.arr = Promise.resolve ([ 1, 2, 3 ]); 1020 | data.proxy.obj = Promise.resolve ({ foo: true }); 1021 | data.proxy.set = Promise.resolve ( new Set ([ 1, 2, 3 ]) ); 1022 | data.proxy.deep = Promise.resolve ( Promise.resolve ({ deep: true }) ); 1023 | t.is ( data.nr, 6 ); 1024 | 1025 | // data.proxy.arr.then ( arr => arr[0] = 2 ); 1026 | // data.proxy.arr.then ( arr => arr.push ( 4 ) ); 1027 | // data.proxy.obj.then ( obj => obj.foo = false ); 1028 | // data.proxy.set.then ( set => set.delete ( 1 ) ); 1029 | // data.proxy.deep.then ( obj => obj.deep = false ); 1030 | // t.is ( data.nr, 11 ); //TODO: Detect changes happening inside promises 1031 | 1032 | }); 1033 | 1034 | it ( 'custom class', t => { 1035 | 1036 | class Custom { 1037 | map = new Map ([[ 'one', 1 ]]); 1038 | foo () {} 1039 | } 1040 | 1041 | const data = makeData ({ custom: new Custom () }); 1042 | 1043 | t.is ( data.proxy.custom.constructor.name, 'Custom' ); 1044 | 1045 | data.proxy.custom.map.has ( 'foo' ); 1046 | data.proxy.custom.map.set ( 'two', 2 ); 1047 | 1048 | t.is ( data.nr, 0 ); 1049 | 1050 | data.proxy.custom = data.proxy.custom; 1051 | 1052 | t.is ( data.nr, 0 ); 1053 | 1054 | data.proxy.custom = new Custom (); 1055 | 1056 | t.is ( data.nr, 1 ); 1057 | 1058 | }); 1059 | 1060 | it ( 'dom nodes', t => { 1061 | 1062 | const comment = () => document.createComment ( '' ); 1063 | const div = () => document.createElement ( 'div' ); 1064 | const text = () => document.createTextNode ( '' ); 1065 | 1066 | const data = makeData ({ comment: comment (), div: div (), text: text () }); 1067 | 1068 | data.proxy.comment = data.proxy.comment; 1069 | data.proxy.div = data.proxy.div; 1070 | data.proxy.text = data.proxy.text; 1071 | 1072 | t.is ( data.nr, 0 ); 1073 | 1074 | data.proxy.comment = comment (); 1075 | 1076 | t.is ( data.nr, 1 ); 1077 | 1078 | data.proxy.div = div (); 1079 | 1080 | t.is ( data.nr, 2 ); 1081 | 1082 | data.proxy.text = text (); 1083 | 1084 | t.is ( data.nr, 3 ); 1085 | 1086 | }); 1087 | 1088 | it ( 'symbol functions', t => { 1089 | 1090 | const data = makeData ({ arr: [{}], set: new Set ([ 1, 2, 3 ]) }); 1091 | 1092 | const arrClone = Array.from ( data.proxy.arr ); 1093 | 1094 | t.is ( data.nr, 0 ); 1095 | t.deepEqual ( arrClone, [{}] ); 1096 | 1097 | const setArr = Array.from ( data.proxy.set ); 1098 | 1099 | t.is ( data.nr, 0 ); 1100 | t.deepEqual ( setArr, [1, 2, 3] ); 1101 | 1102 | // for ( const item of data.proxy.arr ) { 1103 | 1104 | // item.foo = true; 1105 | 1106 | // } 1107 | 1108 | // t.is ( data.nr, 1 ); //FIXME 1109 | 1110 | }); 1111 | 1112 | }); 1113 | 1114 | }); 1115 | 1116 | describe ( 'unwatch', it => { 1117 | 1118 | it ( 'stops watching', t => { 1119 | 1120 | const obj = { 1121 | deep: { 1122 | deeper: true 1123 | } 1124 | }; 1125 | 1126 | const data = makeData ( obj ); 1127 | 1128 | data.proxy.deep.deeper = false; // In order to deeply proxy 1129 | 1130 | t.is ( data.nr, 1 ); 1131 | 1132 | const target = unwatch ( data.proxy ); 1133 | 1134 | t.is ( target, obj ); 1135 | 1136 | data.proxy.foo = true; 1137 | data.proxy.deep.foo = true; 1138 | data.proxy.deep.deeper = { foo: true }; 1139 | delete data.proxy.deep; 1140 | 1141 | t.is ( data.nr, 1 ); 1142 | 1143 | }); 1144 | 1145 | it ( 'unwatching immutable ~primitives doesn\'t throw an error', t => { 1146 | 1147 | const values = [ 1148 | null, 1149 | undefined, 1150 | 123, 1151 | 123n, 1152 | NaN, 1153 | true, 1154 | false, 1155 | 'string', 1156 | Symbol (), 1157 | /foo/g, 1158 | new ArrayBuffer ( 123 ), 1159 | new Number ( 123 ), 1160 | new Boolean ( true ), 1161 | new String ( 'string' ) 1162 | ]; 1163 | 1164 | values.forEach ( value => t.is ( value, unwatch ( value ) ) ); 1165 | 1166 | }); 1167 | 1168 | }); 1169 | 1170 | describe ( 'record', it => { 1171 | 1172 | it ( 'can record get root paths of a single proxy', t => { 1173 | 1174 | const data = makeData ({ 1175 | deep: { 1176 | arr: [1, 2, { foo: true }, { zzz: true }] 1177 | } 1178 | }); 1179 | 1180 | const paths = record ( data.proxy, proxy => { 1181 | 1182 | t.is ( data.proxy, proxy ); 1183 | 1184 | data.proxy.deep.arr[0] = 1; 1185 | data.proxy.deep.arr[1] = 2; 1186 | data.proxy.deep.arr[2].foo = true; 1187 | data.proxy.deep.arr[2].bar; 1188 | 1189 | }); 1190 | 1191 | t.deepEqual ( paths, ['deep', 'deep', 'deep', 'deep'] ); 1192 | 1193 | }); 1194 | 1195 | it ( 'can record get root paths of multiple proxies', t => { 1196 | 1197 | const data1 = makeData ({ 1198 | foo: 123 1199 | }); 1200 | 1201 | const data2 = makeData ({ 1202 | deep: { 1203 | arr: [1, 2, { foo: true }, { zzz: true }] 1204 | } 1205 | }); 1206 | 1207 | const data3 = makeData ({ 1208 | bar: true 1209 | }); 1210 | 1211 | const pathsMap = record ( [data1.proxy, data2.proxy, data3.proxy], ( proxy1, proxy2, proxy3 ) => { 1212 | 1213 | t.is ( data1.proxy, proxy1 ); 1214 | t.is ( data2.proxy, proxy2 ); 1215 | t.is ( data3.proxy, proxy3 ); 1216 | 1217 | proxy1.foo; 1218 | 1219 | proxy2.deep.arr[0] = 1; 1220 | proxy2.deep.arr[1] = 2; 1221 | proxy2.deep.arr[2].foo = true; 1222 | proxy2.deep.arr[2].bar; 1223 | 1224 | }); 1225 | 1226 | t.is ( pathsMap.size, 3 ); 1227 | t.deepEqual ( pathsMap.get ( data1.proxy ), ['foo'] ); 1228 | t.deepEqual ( pathsMap.get ( data2.proxy ), ['deep', 'deep', 'deep', 'deep'] ); 1229 | t.deepEqual ( pathsMap.get ( data3.proxy ), [] ); 1230 | 1231 | }); 1232 | 1233 | }); 1234 | 1235 | describe ( 'target', it => { 1236 | 1237 | it ( 'retrieves the row unproxied object', t => { 1238 | 1239 | const obj = { foo: true }, 1240 | data = makeData ( obj ); 1241 | 1242 | t.not ( data.proxy, obj ); 1243 | t.is ( target ( data.proxy ), obj ); 1244 | 1245 | }); 1246 | 1247 | }); 1248 | 1249 | describe ( 'isProxy', it => { 1250 | 1251 | it ( 'checks if the passed object is a proxy', t => { 1252 | 1253 | const obj = { foo: true }, 1254 | data = makeData ( obj ); 1255 | 1256 | t.false ( isProxy ( obj ) ); 1257 | t.false ( isProxy ( target ( data.proxy ) ) ); 1258 | t.true ( isProxy ( data.proxy ) ); 1259 | 1260 | }); 1261 | 1262 | it ( 'using immutable ~primitives doesn\'t throw an error', t => { 1263 | 1264 | const values = [ 1265 | null, 1266 | undefined, 1267 | 123, 1268 | 123n, 1269 | NaN, 1270 | true, 1271 | false, 1272 | 'string', 1273 | Symbol (), 1274 | /foo/g, 1275 | new ArrayBuffer ( 123 ), 1276 | new Number ( 123 ), 1277 | new Boolean ( true ), 1278 | new String ( 'string' ) 1279 | ]; 1280 | 1281 | values.forEach ( value => t.false ( isProxy ( value ) ) ); 1282 | 1283 | }); 1284 | 1285 | }); 1286 | 1287 | }); 1288 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "inlineSourceMap": false, 9 | "jsx": "react", 10 | "lib": ["dom", "scripthost", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020"], 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "newLine": "LF", 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": false, 17 | "outDir": "dist", 18 | "pretty": true, 19 | "strictNullChecks": true, 20 | "target": "es2016" 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------