├── .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 |
--------------------------------------------------------------------------------