├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demos ├── canvas.html ├── counter.html └── stress.html ├── icaro.js ├── image.jpg ├── package.json ├── rollup.config.js ├── src ├── index.js └── set-immediate.js └── test └── core.specs.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-riot", 3 | "rules": { 4 | "no-bitwise": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Backup files 55 | *.bak 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # useless files 64 | .DS_Store 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | 5 | branches: 6 | only: 7 | - master 8 | - dev 9 | 10 | script: 11 | npm run build && npm test 12 | 13 | sudo: false 14 | 15 | cache: 16 | directories: 17 | - node_modules 18 | 19 | notifications: 20 | email: false 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.1 2 | 3 | - _fixed_: the `.listen` method will accept only functions - https://github.com/GianlucaGuarini/icaro/pull/9 4 | 5 | # 1.2.0 6 | 7 | - _updated_: avoid unecessary array remapping of the `sort` and `reverse` methods 8 | - _updated_: simplified the source code 9 | - _added_: `icaro()` will initialize with a plain empty object if no arguments were passed to it 10 | 11 | # 1.1.3 12 | 13 | - _updated_: avoid unecessary array remapping 14 | 15 | # 1.1.2 16 | 17 | - _fixed_: dispatch the observer events after the changing property is set 18 | 19 | # 1.1.1 20 | 21 | - _fixed_: removed `Array.map` from the dispatcher methods 22 | 23 | # 1.1.0 24 | 25 | - _added_: let the array native methods dispatch events 26 | - _fixed_: issue transforming arrays using the `toJSON` method 27 | 28 | # 1.0.2 29 | 30 | - _fixed_: tag the demo examples in order to uncache them 31 | 32 | 33 | # 1.0.1 34 | 35 | - _fixed_: array cotaining objects should be remapped 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gianluca Guarini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # icaro 2 | A smart and efficient javascript object observer, ideal for batching DOM updates (__~1kb__) 3 | 4 | [![Build Status][travis-image]][travis-url] 5 | [![NPM version][npm-version-image]][npm-url] 6 | [![NPM downloads][npm-downloads-image]][npm-url] 7 | [![MIT License][license-image]][license-url] 8 | 9 | 10 | 11 | # Installation 12 | 13 | Via npm 14 | ```shell 15 | $ npm i icaro -S 16 | ``` 17 | 18 | ## Script import 19 | 20 | Via ` 24 | ``` 25 | 26 | Via ES2015 modules 27 | 28 | ```js 29 | import icaro from 'icaro' 30 | ``` 31 | 32 | Via commonjs 33 | 34 | ```js 35 | const icaro = require('icaro') 36 | ``` 37 | 38 | # Demos 39 | 40 | - [The Canvas](https://cdn.rawgit.com/GianlucaGuarini/icaro/v1.2.1/demos/canvas.html) 41 | - [The Counter](https://cdn.rawgit.com/GianlucaGuarini/icaro/v1.2.1/demos/counter.html) 42 | - [The Stress](https://cdn.rawgit.com/GianlucaGuarini/icaro/v1.2.1/demos/stress.html) 43 | 44 | # Performance 45 | 46 | `icaro` is really fast [compared to the other reactive libs](https://github.com/GianlucaGuarini/reactive-libs-bench) because it smartly throttles all the state changes. 47 | 48 | # Usage 49 | 50 | `icaro` will let you listen to all the changes happening in a javascript object or array, grouping them efficiently, and optimizing the performance of your listeners. 51 | 52 | ```js 53 | 54 | const obj = icaro({}) 55 | 56 | // the variable "changes" here is a Map and the function is async 57 | obj.listen(function(changes) { 58 | console.log(changes.get('foo')) // 'hi' 59 | console.log(changes.get('bar')) // 'there' 60 | console.log(changes.get('baz')) // 'dude' 61 | 62 | // kill all the listeners 63 | obj.unlisten() 64 | }) 65 | 66 | obj.foo = 'hi' 67 | obj.bar = 'there' 68 | obj.baz = 'dude' 69 | 70 | ``` 71 | 72 | `icaro` will also let you listen to nested objects and all the non primitive properties added to an `icaro` object will be automatically converted into `icaro` observable objects. 73 | 74 | ```js 75 | const obj = icaro({}) 76 | 77 | // listen only the changes happening on the root object 78 | obj.listen(function(changes) { 79 | }) 80 | 81 | obj.nested = { 82 | 83 | } 84 | 85 | obj.nested.listen(function(changes) { 86 | // listen only the changes of obj.nested 87 | }) 88 | 89 | obj.nested.someVal = 'hello' 90 | 91 | ``` 92 | 93 | `icaro` is able also to listen changes in arrays. Any change to the items indexes will dispatch events. 94 | 95 | ```js 96 | 97 | // Here a bit of hardcore async stuff 98 | 99 | const arr = icaro([]) 100 | 101 | // here you will get the index of the items added or who changed their position 102 | arr.listen(function(changes) { 103 | console.log(changes.get('0')) // 'foo' 104 | console.log(changes.get('1')) // 'bar' 105 | // kill all the listeners this included 106 | arr.unlisten() 107 | 108 | // add a brand new listener recursively.. why not? 109 | arr.listen(function(changes) { 110 | // the change was triggered by a 'reverse' and all indexes were updated 111 | console.log(changes.get('0')) // 'bar' 112 | console.log(changes.get('1')) // 'foo' 113 | }) 114 | 115 | // update all the indexes 116 | arr.reverse() 117 | }) 118 | 119 | // initial dispatch 120 | arr.push('foo') 121 | arr.push('bar') 122 | 123 | ``` 124 | 125 | You can also avoid unsubscribing ("unlisten") because `icaro` will automatically remove event listeners when the object is about to be garbage collected. 126 | 127 | # API 128 | 129 | Any `icaro` call will return a Proxy with the following api methods 130 | 131 | ## icaro.listen(callback) 132 | 133 | Listen any object or array calling the callback function asynchronously grouping all the contiguous changes via [setImmediate](https://developer.mozilla.org/en/docs/Web/API/Window/setImmediate) 134 | 135 | __@returns self__ 136 | 137 | ## icaro.unlisten(callback|null) 138 | 139 | Unsubscribing a callback previously subscribed to the object, if no callback is provided all the previous subscriptions will be cleared 140 | 141 | __@returns self__ 142 | 143 | ## icaro.toJSON() 144 | 145 | Return all data contained in an `icaro` Proxy as JSON object 146 | 147 | __@returns Object__ 148 | 149 | # Support 150 | 151 | `icaro` uses advanced es6 features like [Proxies](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy), [WeakMaps](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), [Maps](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) and __it targets only modern browsers__ 152 | 153 | All major evergreen browsers (Edge, Chrome, Safari, Firefox) [should be supported](http://caniuse.com/#search=Proxy) 154 | 155 | [travis-image]:https://img.shields.io/travis/GianlucaGuarini/icaro.svg?style=flat-square 156 | [travis-url]:https://travis-ci.org/GianlucaGuarini/icaro 157 | 158 | [license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square 159 | [license-url]:LICENSE.txt 160 | 161 | [npm-version-image]:http://img.shields.io/npm/v/icaro.svg?style=flat-square 162 | [npm-downloads-image]:http://img.shields.io/npm/dm/icaro.svg?style=flat-square 163 | [npm-url]:https://npmjs.org/package/icaro 164 | -------------------------------------------------------------------------------- /demos/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 42 | 43 | -------------------------------------------------------------------------------- /demos/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 23 | 24 | -------------------------------------------------------------------------------- /demos/stress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 50 | 51 | -------------------------------------------------------------------------------- /icaro.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.icaro = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | // fork of https://github.com/YuzuJS/setImmediate 8 | ((function (global) { 9 | if (global.setImmediate) { 10 | return 11 | } 12 | 13 | const tasksByHandle = {}; 14 | 15 | let nextHandle = 1; // Spec says greater than zero 16 | let currentlyRunningATask = false; 17 | let registerImmediate; 18 | 19 | function setImmediate(callback) { 20 | tasksByHandle[nextHandle] = callback; 21 | registerImmediate(nextHandle); 22 | return nextHandle++ 23 | } 24 | 25 | function clearImmediate(handle) { 26 | delete tasksByHandle[handle]; 27 | } 28 | 29 | function runIfPresent(handle) { 30 | // From the spec: "Wait until any invocations of this algorithm started before this one have completed." 31 | // So if we're currently running a task, we'll need to delay this invocation. 32 | if (currentlyRunningATask) { 33 | // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a 34 | // "too much recursion" error. 35 | setTimeout(runIfPresent, 0, handle); 36 | } else { 37 | const task = tasksByHandle[handle]; 38 | if (task) { 39 | currentlyRunningATask = true; 40 | try { 41 | task(); 42 | } finally { 43 | clearImmediate(handle); 44 | currentlyRunningATask = false; 45 | } 46 | } 47 | } 48 | } 49 | 50 | function installNextTickImplementation() { 51 | registerImmediate = handle => { 52 | process.nextTick(() => { runIfPresent(handle); }); 53 | }; 54 | } 55 | 56 | function installPostMessageImplementation() { 57 | // Installs an event handler on `global` for the `message` event: see 58 | // * https://developer.mozilla.org/en/DOM/window.postMessage 59 | // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages 60 | const messagePrefix = `setImmediate$${Math.random()}$`; 61 | const onGlobalMessage = event => { 62 | if (event.source === global && 63 | typeof event.data === 'string' && 64 | event.data.indexOf(messagePrefix) === 0) { 65 | runIfPresent(+event.data.slice(messagePrefix.length)); 66 | } 67 | }; 68 | 69 | global.addEventListener('message', onGlobalMessage, false); 70 | 71 | registerImmediate = handle => { 72 | global.postMessage(messagePrefix + handle, '*'); 73 | }; 74 | } 75 | 76 | // Don't get fooled by e.g. browserify environments. 77 | if ({}.toString.call(global.process) === '[object process]') { 78 | // For Node.js before 0.9 79 | installNextTickImplementation(); 80 | } else { 81 | // For non-IE10 modern browsers 82 | installPostMessageImplementation(); 83 | } 84 | 85 | global.setImmediate = setImmediate; 86 | global.clearImmediate = clearImmediate; 87 | 88 | }))(typeof self === 'undefined' ? typeof global === 'undefined' ? window : global : self); 89 | 90 | const listeners = new WeakMap(); 91 | const dispatch = Symbol(); 92 | const isIcaro = Symbol(); 93 | const timer = Symbol(); 94 | const isArray = Symbol(); 95 | const changes = Symbol(); 96 | 97 | /** 98 | * Public api 99 | * @type {Object} 100 | */ 101 | const API = { 102 | /** 103 | * Set a listener on any object function or array 104 | * @param {Function} fn - callback function associated to the property to listen 105 | * @returns {API} 106 | */ 107 | listen(fn) { 108 | const type = typeof fn; 109 | if(type !== 'function') 110 | throw `The icaro.listen method accepts as argument "typeof 'function'", "${type}" is not allowed` 111 | 112 | if (!listeners.has(this)) listeners.set(this, []); 113 | listeners.get(this).push(fn); 114 | 115 | return this 116 | }, 117 | 118 | /** 119 | * Unsubscribe to a property previously listened or to all of them 120 | * @param {Function} fn - function to unsubscribe 121 | * @returns {API} 122 | */ 123 | unlisten(fn) { 124 | const callbacks = listeners.get(this); 125 | if (!callbacks) return 126 | if (fn) { 127 | const index = callbacks.indexOf(fn); 128 | if (~index) callbacks.splice(index, 1); 129 | } else { 130 | listeners.set(this, []); 131 | } 132 | 133 | return this 134 | }, 135 | 136 | /** 137 | * Convert the icaro object into a valid JSON object 138 | * @returns {Object} - simple json object from a Proxy 139 | */ 140 | toJSON() { 141 | return Object.keys(this).reduce((ret, key) => { 142 | const value = this[key]; 143 | ret[key] = value && value.toJSON ? value.toJSON() : value; 144 | return ret 145 | }, this[isArray] ? [] : {}) 146 | } 147 | }; 148 | 149 | /** 150 | * Icaro proxy handler 151 | * @type {Object} 152 | */ 153 | const ICARO_HANDLER = { 154 | set(target, property, value) { 155 | // filter the values that didn't change 156 | if (target[property] !== value) { 157 | if (value === Object(value) && !value[isIcaro]) { 158 | target[property] = icaro(value); 159 | } else { 160 | target[property] = value; 161 | } 162 | target[dispatch](property, value); 163 | } 164 | 165 | return true 166 | } 167 | }; 168 | 169 | /** 170 | * Define a private property 171 | * @param {*} obj - receiver 172 | * @param {String} key - property name 173 | * @param {*} value - value to set 174 | */ 175 | function define(obj, key, value) { 176 | Object.defineProperty(obj, key, { 177 | value: value, 178 | enumerable: false, 179 | configurable: false, 180 | writable: false 181 | }); 182 | } 183 | 184 | /** 185 | * Enhance the icaro objects adding some hidden props to them and the API methods 186 | * @param {*} obj - anything 187 | * @returns {*} the object received enhanced with some extra properties 188 | */ 189 | function enhance(obj) { 190 | // add some "kinda hidden" properties 191 | Object.assign(obj, { 192 | [changes]: new Map(), 193 | [timer]: null, 194 | [isIcaro]: true, 195 | [dispatch](property, value) { 196 | if (listeners.has(obj)) { 197 | clearImmediate(obj[timer]); 198 | obj[changes].set(property, value); 199 | obj[timer] = setImmediate(function() { 200 | listeners.get(obj).forEach(function(fn) {fn(obj[changes]);}); 201 | obj[changes].clear(); 202 | }); 203 | } 204 | } 205 | }); 206 | 207 | // Add the API methods bound to the original object 208 | Object.keys(API).forEach(function(key) { 209 | define(obj, key, API[key].bind(obj)); 210 | }); 211 | 212 | // remap values and methods 213 | if (Array.isArray(obj)) { 214 | obj[isArray] = true; 215 | // remap the initial array values 216 | obj.forEach(function(item, i) { 217 | obj[i] = null; // force a reset 218 | ICARO_HANDLER.set(obj, i, item); 219 | }); 220 | } 221 | 222 | return obj 223 | } 224 | 225 | /** 226 | * Factory function 227 | * @param {*} obj - anything can be an icaro Proxy 228 | * @returns {Proxy} 229 | */ 230 | function icaro(obj) { 231 | return new Proxy( 232 | enhance(obj || {}), 233 | Object.create(ICARO_HANDLER) 234 | ) 235 | } 236 | 237 | return icaro; 238 | 239 | }))); 240 | -------------------------------------------------------------------------------- /image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GianlucaGuarini/icaro/a67d774666e6b5386fdb31db908d336e3e53cf09/image.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icaro", 3 | "version": "1.2.1", 4 | "description": "Smart and efficient javascript object observer, ideal for batching DOM updates", 5 | "main": "icaro.js", 6 | "module": "src/index.js", 7 | "main:next": "src/index.js", 8 | "scripts": { 9 | "prepublish": "test $CI && exit 0 || npm run build && sed -i .bak 's/v\\([0-9]\\.[0-9]\\.[0-9]\\)/'`git tag | tail -n1`'/' README.md", 10 | "build": "rollup -c rollup.config.js > icaro.js", 11 | "test": "npm run lint && mocha test", 12 | "lint": "eslint src test" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^3.19.0", 16 | "eslint-config-riot": "^1.0.0", 17 | "mocha": "^3.4.2", 18 | "rollup": "^0.41.6" 19 | }, 20 | "keywords": [ 21 | "observable", 22 | "proxy", 23 | "object listener", 24 | "streams", 25 | "object observable", 26 | "object watch" 27 | ], 28 | "files": [ 29 | "src", 30 | "icaro.js" 31 | ], 32 | "author": "Gianluca Guarini (http://gianlucaguarini.com)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | entry: 'src/index.js', 4 | format: 'umd', 5 | moduleName: 'icaro' 6 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './set-immediate' 2 | 3 | const listeners = new WeakMap(), 4 | dispatch = Symbol(), 5 | isIcaro = Symbol(), 6 | timer = Symbol(), 7 | isArray = Symbol(), 8 | changes = Symbol() 9 | 10 | /** 11 | * Public api 12 | * @type {Object} 13 | */ 14 | const API = { 15 | /** 16 | * Set a listener on any object function or array 17 | * @param {Function} fn - callback function associated to the property to listen 18 | * @returns {API} 19 | */ 20 | listen(fn) { 21 | const type = typeof fn 22 | if(type !== 'function') 23 | throw `The icaro.listen method accepts as argument "typeof 'function'", "${type}" is not allowed` 24 | 25 | if (!listeners.has(this)) listeners.set(this, []) 26 | listeners.get(this).push(fn) 27 | 28 | return this 29 | }, 30 | 31 | /** 32 | * Unsubscribe to a property previously listened or to all of them 33 | * @param {Function} fn - function to unsubscribe 34 | * @returns {API} 35 | */ 36 | unlisten(fn) { 37 | const callbacks = listeners.get(this) 38 | if (!callbacks) return 39 | if (fn) { 40 | const index = callbacks.indexOf(fn) 41 | if (~index) callbacks.splice(index, 1) 42 | } else { 43 | listeners.set(this, []) 44 | } 45 | 46 | return this 47 | }, 48 | 49 | /** 50 | * Convert the icaro object into a valid JSON object 51 | * @returns {Object} - simple json object from a Proxy 52 | */ 53 | toJSON() { 54 | return Object.keys(this).reduce((ret, key) => { 55 | const value = this[key] 56 | ret[key] = value && value.toJSON ? value.toJSON() : value 57 | return ret 58 | }, this[isArray] ? [] : {}) 59 | } 60 | } 61 | 62 | /** 63 | * Icaro proxy handler 64 | * @type {Object} 65 | */ 66 | const ICARO_HANDLER = { 67 | set(target, property, value) { 68 | // filter the values that didn't change 69 | if (target[property] !== value) { 70 | if (value === Object(value) && !value[isIcaro]) { 71 | target[property] = icaro(value) 72 | } else { 73 | target[property] = value 74 | } 75 | target[dispatch](property, value) 76 | } 77 | 78 | return true 79 | } 80 | } 81 | 82 | /** 83 | * Define a private property 84 | * @param {*} obj - receiver 85 | * @param {String} key - property name 86 | * @param {*} value - value to set 87 | */ 88 | function define(obj, key, value) { 89 | Object.defineProperty(obj, key, { 90 | value: value, 91 | enumerable: false, 92 | configurable: false, 93 | writable: false 94 | }) 95 | } 96 | 97 | /** 98 | * Enhance the icaro objects adding some hidden props to them and the API methods 99 | * @param {*} obj - anything 100 | * @returns {*} the object received enhanced with some extra properties 101 | */ 102 | function enhance(obj) { 103 | // add some "kinda hidden" properties 104 | Object.assign(obj, { 105 | [changes]: new Map(), 106 | [timer]: null, 107 | [isIcaro]: true, 108 | [dispatch](property, value) { 109 | if (listeners.has(obj)) { 110 | clearImmediate(obj[timer]) 111 | obj[changes].set(property, value) 112 | obj[timer] = setImmediate(function() { 113 | listeners.get(obj).forEach(function(fn) {fn(obj[changes])}) 114 | obj[changes].clear() 115 | }) 116 | } 117 | } 118 | }) 119 | 120 | // Add the API methods bound to the original object 121 | Object.keys(API).forEach(function(key) { 122 | define(obj, key, API[key].bind(obj)) 123 | }) 124 | 125 | // remap values and methods 126 | if (Array.isArray(obj)) { 127 | obj[isArray] = true 128 | // remap the initial array values 129 | obj.forEach(function(item, i) { 130 | obj[i] = null // force a reset 131 | ICARO_HANDLER.set(obj, i, item) 132 | }) 133 | } 134 | 135 | return obj 136 | } 137 | 138 | /** 139 | * Factory function 140 | * @param {*} obj - anything can be an icaro Proxy 141 | * @returns {Proxy} 142 | */ 143 | export default function icaro(obj) { 144 | return new Proxy( 145 | enhance(obj || {}), 146 | Object.create(ICARO_HANDLER) 147 | ) 148 | } 149 | -------------------------------------------------------------------------------- /src/set-immediate.js: -------------------------------------------------------------------------------- 1 | // fork of https://github.com/YuzuJS/setImmediate 2 | ((function (global) { 3 | if (global.setImmediate) { 4 | return 5 | } 6 | 7 | const tasksByHandle = {} 8 | 9 | let nextHandle = 1 // Spec says greater than zero 10 | let currentlyRunningATask = false 11 | let registerImmediate 12 | 13 | function setImmediate(callback) { 14 | tasksByHandle[nextHandle] = callback 15 | registerImmediate(nextHandle) 16 | return nextHandle++ 17 | } 18 | 19 | function clearImmediate(handle) { 20 | delete tasksByHandle[handle] 21 | } 22 | 23 | function runIfPresent(handle) { 24 | // From the spec: "Wait until any invocations of this algorithm started before this one have completed." 25 | // So if we're currently running a task, we'll need to delay this invocation. 26 | if (currentlyRunningATask) { 27 | // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a 28 | // "too much recursion" error. 29 | setTimeout(runIfPresent, 0, handle) 30 | } else { 31 | const task = tasksByHandle[handle] 32 | if (task) { 33 | currentlyRunningATask = true 34 | try { 35 | task() 36 | } finally { 37 | clearImmediate(handle) 38 | currentlyRunningATask = false 39 | } 40 | } 41 | } 42 | } 43 | 44 | function installNextTickImplementation() { 45 | registerImmediate = handle => { 46 | process.nextTick(() => { runIfPresent(handle) }) 47 | } 48 | } 49 | 50 | function installPostMessageImplementation() { 51 | // Installs an event handler on `global` for the `message` event: see 52 | // * https://developer.mozilla.org/en/DOM/window.postMessage 53 | // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages 54 | const messagePrefix = `setImmediate$${Math.random()}$` 55 | const onGlobalMessage = event => { 56 | if (event.source === global && 57 | typeof event.data === 'string' && 58 | event.data.indexOf(messagePrefix) === 0) { 59 | runIfPresent(+event.data.slice(messagePrefix.length)) 60 | } 61 | } 62 | 63 | global.addEventListener('message', onGlobalMessage, false) 64 | 65 | registerImmediate = handle => { 66 | global.postMessage(messagePrefix + handle, '*') 67 | } 68 | } 69 | 70 | // Don't get fooled by e.g. browserify environments. 71 | if ({}.toString.call(global.process) === '[object process]') { 72 | // For Node.js before 0.9 73 | installNextTickImplementation() 74 | } else { 75 | // For non-IE10 modern browsers 76 | installPostMessageImplementation() 77 | } 78 | 79 | global.setImmediate = setImmediate 80 | global.clearImmediate = clearImmediate 81 | 82 | }))(typeof self === 'undefined' ? typeof global === 'undefined' ? window : global : self) 83 | -------------------------------------------------------------------------------- /test/core.specs.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | // require the lib 4 | const icaro = require('../') 5 | 6 | describe('icaro core', () => { 7 | it('it can listen simple object changes', function(done) { 8 | const i = icaro() 9 | 10 | i.listen(function(changes) { 11 | assert.equal(changes.get('foo'), 'bar') 12 | done() 13 | }) 14 | 15 | i.foo = 'bar' 16 | }) 17 | 18 | it('throws error when listener is not a valid function', function() { 19 | const i = icaro() 20 | 21 | assert.throws( 22 | () => { 23 | i.listen({}) 24 | } 25 | ) 26 | }) 27 | 28 | it('it groups multiple changes together', function(done) { 29 | const i = icaro() 30 | 31 | i.listen(function(changes) { 32 | assert.equal(changes.get('foo'), 'bar') 33 | assert.equal(changes.get('baz'), 'bar') 34 | done() 35 | }) 36 | 37 | i.foo = 'bar' 38 | i.baz = 'bar' 39 | }) 40 | 41 | it('it can listen arrays and sub children', function(done) { 42 | const i = icaro() 43 | 44 | i.arr = [] 45 | i.arr.listen(function(changes) { 46 | assert.equal(changes.get('0'), 'one') 47 | assert.equal(changes.get('1'), 'two') 48 | done() 49 | }) 50 | 51 | i.arr.push('one') 52 | i.arr.push('two') 53 | }) 54 | 55 | it('it can listen array changes', function(done) { 56 | const arr = icaro(['one', 'two']) 57 | 58 | // can loop 59 | arr.listen(function() { 60 | arr.unlisten() 61 | arr.listen(function() { 62 | done() 63 | }) 64 | arr.pop() 65 | }) 66 | 67 | arr.shift() 68 | }) 69 | 70 | it('it can loop arrays', function() { 71 | const arr = icaro(['one', 'two']) 72 | const test = [] 73 | // can loop 74 | arr.forEach(function(item) { 75 | assert.ok(typeof item === 'string') 76 | test.push(item) 77 | }) 78 | 79 | assert.ok(test.length === 2) 80 | 81 | for (let item in arr) { 82 | assert.ok(typeof item === 'string') 83 | test.push(item) 84 | } 85 | 86 | assert.ok(test.length === 4) 87 | }) 88 | 89 | it('the toJSON call return properly either an object or an array', function() { 90 | const arr = icaro([1, 2]) 91 | const obj = icaro({ uno: 1, due: 2 }) 92 | 93 | assert.deepEqual(arr.toJSON(), [1, 2]) 94 | assert.deepEqual(obj.toJSON(), { uno: 1, due: 2 }) 95 | }) 96 | 97 | 98 | it('array values should be listenable if objects', function() { 99 | const i = icaro([{ value: 'foo' }]) 100 | assert.ok(i[0].listen) 101 | }) 102 | 103 | it('"Array.reverse" will dispatch changes', function(done) { 104 | const i = icaro(['foo', 'bar']) 105 | i.listen(function(changes) { 106 | assert.equal(changes.get('0'), 'bar') 107 | assert.equal(changes.get('1'), 'foo') 108 | done() 109 | }) 110 | i.reverse() 111 | }) 112 | 113 | it('"Array.sort" will dispatch changes', function(done) { 114 | const i = icaro(['a', 'c', 'b']) 115 | i.listen(function(changes) { 116 | assert.ok(!changes.get('0')) // 'a' was never moved 117 | assert.equal(changes.get('1'), 'b') 118 | assert.equal(changes.get('2'), 'c') 119 | done() 120 | }) 121 | i.sort() 122 | }) 123 | }) 124 | --------------------------------------------------------------------------------