├── .jshintignore ├── .jshintrc ├── LICENSE.md ├── README.md ├── atom.js ├── package.json ├── test.html └── test.js /.jshintignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "passfail" : false, // Stop on first error. 4 | "maxerr" : 100, // Maximum error before stopping. 5 | 6 | 7 | // Predefined globals whom JSHint will ignore. 8 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 9 | 10 | "node" : true, 11 | "rhino" : false, 12 | "couch" : false, 13 | "wsh" : false, // Windows Scripting Host. 14 | 15 | "jquery" : false, 16 | "prototypejs" : false, 17 | "mootools" : false, 18 | "dojo" : false, 19 | 20 | 21 | // Development. 22 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 23 | "devel" : false, // Allow developments statements e.g. `console.log();`. 24 | 25 | 26 | // ECMAScript 5. 27 | "strict" : false, // Require `use strict` pragma in every file. 28 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 29 | 30 | 31 | // The Good Parts. 32 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 33 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 34 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 35 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 36 | "curly" : true, // Require {} for every new block or scope. 37 | "eqeqeq" : true, // Require triple equals i.e. `===`. 38 | "eqnull" : false, // Tolerate use of `== null`. 39 | "evil" : false, // Tolerate use of `eval`. 40 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 41 | "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`. 42 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 43 | "latedef" : true, // Prohibit variable use before definition. 44 | "loopfunc" : false, // Allow functions to be defined within loops. 45 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 46 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 47 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 48 | "scripturl" : true, // Tolerate script-targeted URLs. 49 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 50 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 51 | "undef" : true, // Require all non-global variables be declared before they are used. 52 | 53 | 54 | // Personal styling preferences. 55 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 56 | "noempty" : true, // Prohibit use of empty blocks. 57 | "nonew" : true, // Prohibit use of constructors for side-effects. 58 | "nomen" : true, // Prohibit use of initial or trailing underbars in names. 59 | "onevar" : false, // Allow only one `var` statement per function. 60 | "plusplus" : false, // Prohibit use of `++` & `--`. 61 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 62 | "trailing" : true, // Prohibit trailing whitespaces. 63 | "white" : true, // Check against strict whitespace and indentation rules. 64 | "indent" : 4 // Specify indentation spacing 65 | } 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Zynga Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Please note:** This project is deprecated at Zynga and is no longer maintained. 2 | 3 | --- 4 | 5 | Overview 6 | ======== 7 | 8 | Atom.js is a small, easy to use JavaScript class that provides asynchronous 9 | control flow, event/property listeners, barriers, and more. 10 | 11 | 12 | Features 13 | ======== 14 | 15 | - Small: 3.4kB minified, 1.5kB gzipped. 16 | - No dependencies: works in a browser, or in node. 17 | - Enables programming patterns that reduce the need for deeply nested 18 | callbacks and conditionals. 19 | 20 | 21 | Install 22 | ======= 23 | 24 | npm install atom-js 25 | 26 | 27 | Unit Tests 28 | ========== 29 | 30 | To run from command line using node.js: 31 | 32 | node test.js // brief 33 | node test.js -v // verbose 34 | 35 | To run in a browser, open `test.html`, or go 36 | [here](http://zynga.github.io/atom/test.html). 37 | 38 | 39 | Tutorial 40 | ======== 41 | 42 | This is `a`. 43 | 44 | ```js 45 | var a 46 | ``` 47 | 48 | `a` is an atom. 49 | 50 | ```js 51 | var a = atom(); 52 | ``` 53 | 54 | 55 | ### Properties 56 | 57 | An atom has properties. The `.get()` and `.set()` methods may be employed to 58 | read and write values of any type. 59 | 60 | ```js 61 | a.set('key', 'value'); 62 | console.log('Value of key: ' + a.get('key')); 63 | 64 | a.set({ 65 | pi: 3.141592653, 66 | r: 5, 67 | circumference: function () { 68 | return 2 * a.get('pi') * a.get('r'); 69 | } 70 | }); 71 | console.log('Circumference: ' + a.get('circumference')()); 72 | ``` 73 | 74 | Parameters to the constructor will also be set as properties. 75 | 76 | ```js 77 | a = atom('key', 'value'); 78 | 79 | a = atom({ pi: 3.141592653, r: 5 }); 80 | ``` 81 | 82 | Use `.has()` to query for existence of a property, and `.keys()` to get a list 83 | of all properties that have been set. 84 | 85 | ```js 86 | if (a.has('game')) { 87 | console.log('What "a" brings to the table: ' + a.keys()); 88 | } 89 | ``` 90 | 91 | The `.each()` method lets you execute a function on a series of properties. 92 | 93 | ```js 94 | a.set({ r: 0xBA, g: 0xDA, b: 0x55 }); 95 | a.each(['r', 'g', 'b'], function (key, value) { 96 | console.log(key + ': ' + value); 97 | }); 98 | ``` 99 | 100 | 101 | ### Listeners 102 | 103 | Listeners may be attached to atoms in a variety of ways. 104 | 105 | To be notified as soon as a property is set, use the `.once()` method. The 106 | callback will be called immediately if the property is already set. 107 | 108 | ```js 109 | a.once('userInfo', function (userInfo) { 110 | alert('Welcome, ' + userInfo.name + '!'); 111 | }); 112 | ``` 113 | 114 | Many atom methods can work with more than one property at a time. 115 | 116 | ```js 117 | a.once(['userInfo', 'appInfo'], function (user, app) { 118 | alert('Welcome to ' + app.name + ', ' + user.name + '!'); 119 | }); 120 | ``` 121 | 122 | When you just want to know about the next change, even if the property is 123 | already set, use `.next()`. 124 | 125 | ```js 126 | a.next('click', function (click) { 127 | alert('Are you done clicking on ' + click.button + ' yet?'); 128 | }); 129 | ``` 130 | 131 | To watch for any future changes to a property, use the `.on()` (alias `.bind()`) 132 | method. 133 | 134 | ```js 135 | function myErrorHandler(error) { 136 | console.log('There was a grevious calamity of code in ' + a.get('module')); 137 | console.log(error); 138 | } 139 | a.on('error', myErrorHandler); 140 | ``` 141 | 142 | Note that setting a property with a primitive (string/number/boolean) value will 143 | only trigger listeners if the value is *different*. On the other hand, setting 144 | an array or object value will *always* trigger listeners. 145 | 146 | You can unregister any listener using `.off()` (alias `.unbind()`). 147 | 148 | ```js 149 | a.off(myErrorHandler); 150 | ``` 151 | 152 | If you only want to remove the listener associated with a particular key or 153 | keys, you can specify those too: 154 | 155 | ```js 156 | a.off(['a', 'b'], myErrorHandler); 157 | ``` 158 | 159 | 160 | ### Needs and Providers 161 | 162 | You can register a provider for a property. 163 | 164 | ```js 165 | a.provide('privacyPolicy', function (done) { 166 | httpRequest(baseUrl + '/privacy.txt', function (content) { 167 | done(content); 168 | }); 169 | }); 170 | ``` 171 | 172 | Providers only get invoked if there is a need, and if the property is not 173 | already set. Use the `.need()` method to declare a need for a particular 174 | property. If a corresponding provider is registered, it will be invoked. 175 | Otherwise, `.need()` behaves just like `.once()`. 176 | 177 | ```js 178 | a.on('clickPrivacy', function () { 179 | a.need('privacyPolicy', function (text) { 180 | element.innerText = text; 181 | }); 182 | }); 183 | ``` 184 | 185 | 186 | ### Entanglement 187 | 188 | Properties of two or more atoms can be entangled, using the `.entangle()` 189 | method. When an entangled property gets set on one atom, the value will 190 | instantly propagate to the other. 191 | 192 | ```js 193 | var b = atom(); 194 | a.entangle(b, 'email'); 195 | a.set('email', 'someone@example.com'); 196 | console.log('Entangled email: ' + b.get('email')); 197 | ``` 198 | 199 | `.entangle()` also works when called with a list of properties. 200 | 201 | ```js 202 | a.entangle(b, ['firstname', 'lastname']); 203 | ``` 204 | 205 | If called with a map of property names, then property 'X' on one atom can be 206 | entangled with property 'Y' on the other atom. 207 | 208 | ```js 209 | a.entangle(b, { firstname: 'first', lastname: 'last' }); 210 | a.set('firstname', 'Joe'); 211 | console.log('Welcome, ' + b.get('first')); 212 | ``` 213 | 214 | Note that entangled properties are not actually synchronized until the first 215 | change *after* entanglement. 216 | 217 | 218 | ### Asynchronous Queueing 219 | 220 | String together a series of asynchronous functions using the `.chain()` method. 221 | 222 | ```js 223 | a.chain( 224 | function (nextLink) { 225 | callAjaxMethod('callThisFirst', function (firstResult) { 226 | nextLink(firstResult); 227 | }); 228 | }, 229 | function (nextLink, firstResult) { 230 | callAjaxMethod('callThisSecond', function (secondResult) { 231 | nextLink(secondResult); 232 | }); 233 | } 234 | ); 235 | ``` 236 | 237 | 238 | ### Method Chaining 239 | 240 | Not to be confused with the `.chain()` method specifically, "method chaining" 241 | actually refers to the practice of stringing together multiple method calls in 242 | a single expression. 243 | 244 | ```js 245 | a = atom('start', new Date()) 246 | .once('loaded', function () { 247 | console.log('Finished loading.'); 248 | }) 249 | .once('shutdown', function () { 250 | console.log('Shutting down.'); 251 | }) 252 | .set('loaded', true); 253 | ``` 254 | 255 | The `.chain()`, `.each()`, `.entangle()`, `.mixin()`, `.need()`, `.next()`, 256 | `.off()`, `.on()`, `.once()`, `.provide()` and `.set()` methods are all 257 | chainable. 258 | 259 | 260 | ### Cleanup 261 | 262 | Release references to all data and callback functions with the `.destroy()` 263 | method. 264 | 265 | ```js 266 | a.destroy(); 267 | ``` 268 | 269 | After being destroyed, most of an atom's functions will throw exceptions when 270 | called. 271 | 272 | 273 | Additional Resources 274 | ==================== 275 | 276 | - [Blog post: Barriers with Atom](http://christophercampbell.wordpress.com/2013/01/01/barriers-with-atom/) 277 | - [Blog post: Serial and Parallel Tasks with Atom](http://christophercampbell.wordpress.com/2013/01/01/serial-and-parallel-tasks-with-atom/) 278 | - [Blog post: Providers with Atom](http://christophercampbell.wordpress.com/2013/01/01/providers-with-atom/) 279 | -------------------------------------------------------------------------------- /atom.js: -------------------------------------------------------------------------------- 1 | // 2 | // atom.js 3 | // https://github.com/zynga/atom 4 | // Author: Chris Campbell (@quaelin) 5 | // License: BSD 6 | // 7 | (function (undef) { 8 | 'use strict'; 9 | 10 | var 11 | atom, 12 | name = 'atom', 13 | VERSION = '0.5.6', 14 | 15 | ObjProto = Object.prototype, 16 | hasOwn = ObjProto.hasOwnProperty, 17 | 18 | typeObj = 'object', 19 | typeUndef = 'undefined', 20 | 21 | root = typeof window !== typeUndef ? window : global, 22 | had = hasOwn.call(root, name), 23 | prev = root[name] 24 | ; 25 | 26 | 27 | // Convenience methods 28 | var slice = Array.prototype.slice; 29 | var isArray = Array.isArray || function (obj) { 30 | return ObjProto.toString.call(obj) === '[object Array]'; 31 | }; 32 | function inArray(arr, value) { 33 | for (var i = arr.length; --i >= 0;) { 34 | if (arr[i] === value) { 35 | return true; 36 | } 37 | } 38 | } 39 | function toArray(obj) { 40 | return isArray(obj) ? obj : [obj]; 41 | } 42 | function isEmpty(obj) { 43 | for (var p in obj) { 44 | if (hasOwn.call(obj, p)) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | } 50 | 51 | 52 | // Property getter 53 | function get(nucleus, keyOrList, func) { 54 | var isList = isArray(keyOrList), keys = isList ? keyOrList : [keyOrList], 55 | key, values = [], props = nucleus.props, missing = {}, 56 | result = { values: values }; 57 | for (var i = keys.length; --i >= 0;) { 58 | key = keys[i]; 59 | if (!hasOwn.call(props, key)) { 60 | result.missing = missing; 61 | missing[key] = true; 62 | } 63 | values.unshift(props[key]); 64 | } 65 | return func ? func.apply({}, values) : result; 66 | } 67 | 68 | 69 | // Helper to remove an exausted listener from the listeners array 70 | function removeListener(listeners) { 71 | for (var i = listeners.length; --i >= 0;) { 72 | // There should only be ONE exhausted listener. 73 | if (!listeners[i].calls) { 74 | return listeners.splice(i, 1); 75 | } 76 | } 77 | } 78 | 79 | 80 | // Used to detect listener recursion; a given object may only appear once. 81 | var objStack = []; 82 | 83 | // Property setter 84 | function set(nucleus, key, value) { 85 | var keys, listener, listeners = nucleus.listeners, missing, 86 | listenersCopy = [].concat(listeners), i = listenersCopy.length, 87 | props = nucleus.props, oldValue = props[key], 88 | had = hasOwn.call(props, key), 89 | isObj = value && typeof value === typeObj; 90 | props[key] = value; 91 | if (!had || oldValue !== value || (isObj && !inArray(objStack, value))) { 92 | if (isObj) { 93 | objStack.push(value); 94 | } 95 | while (--i >= 0) { 96 | listener = listenersCopy[i]; 97 | keys = listener.keys; 98 | missing = listener.missing; 99 | if (missing) { 100 | if (hasOwn.call(missing, key)) { 101 | delete missing[key]; 102 | if (isEmpty(missing)) { 103 | listener.cb.apply({}, get(nucleus, keys).values); 104 | listener.calls--; 105 | } 106 | } 107 | } else if (inArray(keys, key)) { 108 | listener.cb.apply({}, get(nucleus, keys).values); 109 | listener.calls--; 110 | } 111 | if (!listener.calls) { 112 | removeListener(listeners); 113 | } 114 | } 115 | delete nucleus.needs[key]; 116 | if (isObj) { 117 | objStack.pop(); 118 | } 119 | } 120 | } 121 | 122 | 123 | // Wrapper to prevent a callback from getting invoked more than once. 124 | function preventMultiCall(callback) { 125 | var ran; 126 | return function () { 127 | if (!ran) { 128 | ran = 1; 129 | callback.apply(this, arguments); 130 | } 131 | }; 132 | } 133 | 134 | 135 | // Helper function for setting up providers. 136 | function provide(nucleus, key, provider) { 137 | provider(preventMultiCall(function (result) { 138 | set(nucleus, key, result); 139 | })); 140 | } 141 | 142 | 143 | // Determine whether two keys (or sets of keys) are equivalent. 144 | function keysMatch(keyOrListA, keyOrListB) { 145 | var a, b; 146 | if (keyOrListA === keyOrListB) { 147 | return true; 148 | } 149 | a = [].concat(toArray(keyOrListA)).sort(); 150 | b = [].concat(toArray(keyOrListB)).sort(); 151 | return a + '' === b + ''; 152 | } 153 | 154 | 155 | // Return an instance. 156 | atom = root[name] = function () { 157 | var 158 | args = slice.call(arguments, 0), 159 | nucleus = {}, 160 | props = nucleus.props = {}, 161 | needs = nucleus.needs = {}, 162 | providers = nucleus.providers = {}, 163 | listeners = nucleus.listeners = [], 164 | q = [] 165 | ; 166 | 167 | // Execute the next function in the async queue. 168 | function doNext() { 169 | if (q) { 170 | q.pending = q.next = (!q.next && q.length) ? 171 | q.shift() : q.next; 172 | q.args = slice.call(arguments, 0); 173 | if (q.pending) { 174 | q.next = 0; 175 | q.pending.apply({}, [preventMultiCall(doNext)].concat(q.args)); 176 | } 177 | } 178 | } 179 | 180 | var me = { 181 | 182 | // Add a function or functions to the async queue. Functions added 183 | // thusly must call their first arg as a callback when done. Any args 184 | // provided to the callback will be passed in to the next function in 185 | // the queue. 186 | chain: function () { 187 | if (q) { 188 | for (var i = 0, len = arguments.length; i < len; i++) { 189 | q.push(arguments[i]); 190 | if (!q.pending) { 191 | doNext.apply({}, q.args || []); 192 | } 193 | } 194 | } 195 | return me; 196 | }, 197 | 198 | // Remove references to all properties and listeners. This releases 199 | // memory, and effective stops the atom from working. 200 | destroy: function () { 201 | delete nucleus.props; 202 | delete nucleus.needs; 203 | delete nucleus.providers; 204 | delete nucleus.listeners; 205 | while (q.length) { 206 | q.pop(); 207 | } 208 | nucleus = props = needs = providers = listeners = 209 | q = q.pending = q.next = q.args = 0; 210 | }, 211 | 212 | // Call `func` on each of the specified keys. The key is provided as 213 | // the first arg, and the value as the second. 214 | each: function (keyOrList, func) { 215 | var keys = toArray(keyOrList), i = -1, len = keys.length, key; 216 | while (++i < len) { 217 | key = keys[i]; 218 | func(key, me.get(key)); 219 | } 220 | return me; 221 | }, 222 | 223 | // Establish two-way binding between a key or list of keys for two 224 | // different atoms, so that changing a property on either atom will 225 | // propagate to the other. If a map is provided for `keyOrListOrMap`, 226 | // properties on this atom may be bound to differently named properties 227 | // on `otherAtom`. Note that entangled properties will not actually be 228 | // synchronized until the first change *after* entanglement. 229 | entangle: function (otherAtom, keyOrListOrMap) { 230 | var 231 | isList = isArray(keyOrListOrMap), 232 | isMap = !isList && typeof keyOrListOrMap === typeObj, 233 | i, key, 234 | keys = isList ? keyOrListOrMap : isMap ? [] : [keyOrListOrMap], 235 | map = isMap ? keyOrListOrMap : {} 236 | ; 237 | if (isMap) { 238 | for (key in map) { 239 | if (hasOwn.call(map, key)) { 240 | keys.push(key); 241 | } 242 | } 243 | } else { 244 | for (i = keys.length; --i >= 0;) { 245 | key = keys[i]; 246 | map[key] = key; 247 | } 248 | } 249 | me.each(keys, function (key) { 250 | var otherKey = map[key]; 251 | me.on(key, function (value) { 252 | otherAtom.set(otherKey, value); 253 | }); 254 | otherAtom.on(otherKey, function (value) { 255 | me.set(key, value); 256 | }); 257 | }); 258 | return me; 259 | }, 260 | 261 | // Get current values for the specified keys. If `func` is provided, 262 | // it will be called with the values as args. 263 | get: function (keyOrList, func) { 264 | var result = get(nucleus, keyOrList, func); 265 | return func ? result : typeof keyOrList === 'string' ? 266 | result.values[0] : result.values; 267 | }, 268 | 269 | // Returns true iff all of the specified keys exist (regardless of 270 | // value). 271 | has: function (keyOrList) { 272 | var keys = toArray(keyOrList); 273 | for (var i = keys.length; --i >= 0;) { 274 | if (!hasOwn.call(props, keys[i])) { 275 | return false; 276 | } 277 | } 278 | return true; 279 | }, 280 | 281 | // Return a list of all keys. 282 | keys: function () { 283 | var keys = []; 284 | for (var key in props) { 285 | if (hasOwn.call(props, key)) { 286 | keys.push(key); 287 | } 288 | } 289 | return keys; 290 | }, 291 | 292 | // Add arbitrary properties to this atom's interface. 293 | mixin: function (obj) { 294 | for (var p in obj) { 295 | if (hasOwn.call(obj, p)) { 296 | me[p] = obj[p]; 297 | } 298 | } 299 | return me; 300 | }, 301 | 302 | // Call `func` as soon as all of the specified keys have been set. If 303 | // they are already set, the function will be called immediately, with 304 | // all the values provided as args. In this, it is identical to 305 | // `once()`. However, calling `need()` will additionally invoke 306 | // providers when possible, in order to try and create the required 307 | // values. 308 | need: function (keyOrList, func) { 309 | var key, keys = toArray(keyOrList), provider; 310 | for (var i = keys.length; --i >= 0;) { 311 | key = keys[i]; 312 | provider = providers[key]; 313 | if (!hasOwn.call(props, key) && provider) { 314 | provide(nucleus, key, provider); 315 | delete providers[key]; 316 | } else { 317 | needs[key] = true; 318 | } 319 | } 320 | if (func) { 321 | me.once(keys, func); 322 | } 323 | return me; 324 | }, 325 | 326 | // Call `func` whenever any of the specified keys is next changed. The 327 | // values of all keys will be provided as args to the function. The 328 | // function will automatically be unbound after being called the first 329 | // time, so it is guaranteed to be called no more than once. 330 | next: function (keyOrList, func) { 331 | listeners.unshift( 332 | { keys: toArray(keyOrList), cb: func, calls: 1 }); 333 | return me; 334 | }, 335 | 336 | // Unregister a listener `func` that was previously registered using 337 | // `on()`, `bind()`, `need()`, `next()` or `once()`. `keyOrList` is 338 | // optional; if provided, it will selectively remove the listener only 339 | // for the specified combination of properties. 340 | off: function (keyOrList, func) { // alias: `unbind` 341 | var i = listeners.length, listener; 342 | if (arguments.length === 1) { 343 | func = keyOrList; 344 | keyOrList = 0; 345 | } 346 | while (--i >= 0) { 347 | listener = listeners[i]; 348 | if (listener.cb === func && 349 | (!keyOrList || keysMatch(listener.keys, keyOrList))) 350 | { 351 | listeners.splice(i, 1); 352 | } 353 | } 354 | return me; 355 | }, 356 | 357 | // Call `func` whenever any of the specified keys change. The values 358 | // of the keys will be provided as args to func. 359 | on: function (keyOrList, func) { // alias: `bind` 360 | listeners.unshift({ keys: toArray(keyOrList), cb: func, 361 | calls: Infinity }); 362 | return me; 363 | }, 364 | 365 | // Call `func` as soon as all of the specified keys have been set. If 366 | // they are already set, the function will be called immediately, with 367 | // all the values provided as args. Guaranteed to be called no more 368 | // than once. 369 | once: function (keyOrList, func) { 370 | var keys = toArray(keyOrList), 371 | results = get(nucleus, keys), 372 | values = results.values, 373 | missing = results.missing; 374 | if (!missing) { 375 | func.apply({}, values); 376 | } else { 377 | listeners.unshift( 378 | { keys: keys, cb: func, missing: missing, calls: 1 }); 379 | } 380 | return me; 381 | }, 382 | 383 | // Register a provider for a particular key. The provider `func` is a 384 | // function that will be called if there is a need to create the key. 385 | // It must call its first arg as a callback, with the value. Provider 386 | // functions will be called at most once. 387 | provide: function (key, func) { 388 | if (needs[key]) { 389 | provide(nucleus, key, func); 390 | } else if (!providers[key]) { 391 | providers[key] = func; 392 | } 393 | return me; 394 | }, 395 | 396 | // Set value for a key, or if `keyOrMap` is an object then set all the 397 | // keys' corresponding values. 398 | set: function (keyOrMap, value) { 399 | if (typeof keyOrMap === typeObj) { 400 | for (var key in keyOrMap) { 401 | if (hasOwn.call(keyOrMap, key)) { 402 | set(nucleus, key, keyOrMap[key]); 403 | } 404 | } 405 | } else { 406 | set(nucleus, keyOrMap, value); 407 | } 408 | return me; 409 | } 410 | }; 411 | me.bind = me.on; 412 | me.unbind = me.off; 413 | 414 | if (args.length) { 415 | me.set.apply(me, args); 416 | } 417 | 418 | return me; 419 | }; 420 | 421 | atom.VERSION = VERSION; 422 | 423 | // For backwards compatibility with < 0.4.0 424 | atom.create = atom; 425 | 426 | atom.noConflict = function () { 427 | if (root[name] === atom) { 428 | root[name] = had ? prev : undef; 429 | if (!had) { 430 | try { 431 | delete root[name]; 432 | } catch (ex) { 433 | } 434 | } 435 | } 436 | return atom; 437 | }; 438 | 439 | if (typeof module !== typeUndef && module.exports) { 440 | module.exports = atom; 441 | } 442 | }()); 443 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-js", 3 | "version": "0.5.6", 4 | "description": "Small JS class that provides async control flow, property listeners, barrier pattern, and more.", 5 | "author": "Chris Campbell (https://github.com/quaelin)", 6 | "homepage": "https://github.com/zynga/atom", 7 | "bugs": { 8 | "url": "https://github.com/zynga/atom/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/zynga/atom.git" 13 | }, 14 | "main": "./atom.js", 15 | "scripts": { 16 | "test": "node test.js" 17 | }, 18 | "license": "BSD" 19 | } 20 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | atom.js tests 6 | 7 | 8 | 57 | 58 | 59 | 60 | 61 |

62 | 
63 | 
64 | 
65 | 


--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
  1 | /*global atom:true, logger:true, process, require*/
  2 | atom = typeof atom === 'undefined' ? require('./atom') : atom;
  3 | logger = (typeof logger !== 'undefined' && logger) || console.log;
  4 | 
  5 | logger('atom ' + atom.VERSION);
  6 | 
  7 | var
  8 | 	inBrowser = typeof document !== 'undefined',
  9 | 	inNode = !inBrowser,
 10 | 	argv = inNode && process.argv,
 11 | 	arg2 = argv && argv.length > 2 && argv[2],
 12 | 	verbose = inBrowser || arg2 === '-v',
 13 | 	a = atom(),
 14 | 	results = [],
 15 | 	totals = { success: 0, fail: 0, total: 0 }
 16 | ;
 17 | 
 18 | function assert(msg, success) {
 19 | 	totals.total++;
 20 | 	if (success) {
 21 | 		totals.success++;
 22 | 		if (verbose) {
 23 | 			logger(msg + '... success.');
 24 | 		}
 25 | 	} else {
 26 | 		totals.fail++;
 27 | 		logger(msg + '... FAIL!');
 28 | 	}
 29 | }
 30 | 
 31 | assert('constructor args treated as set(), single property',
 32 | 	atom('a', 'b').get('a') === 'b');
 33 | 
 34 | assert('constructor args treated as set(), multiple properties',
 35 | 	atom({ a: 'b', c: 'd' }).get(['a', 'c']) + '' === 'b,d');
 36 | 
 37 | a.set('a', 'A');
 38 | assert('get() returns a single value', a.get('a') === 'A');
 39 | 
 40 | a.set('b', 'B');
 41 | assert('get() returns a list of values', a.get(['a', 'b']) + '' === 'A,B');
 42 | 
 43 | a.get('a', function (a) {
 44 | 	results.push(a);
 45 | });
 46 | assert('get() calls back with a single value', results + '' === 'A');
 47 | 
 48 | results = [];
 49 | a.get(['a', 'b'], function (a, b) {
 50 | 	results = results.concat([a, b]);
 51 | });
 52 | assert('get() calls back with a list of values', results + '' === 'A,B');
 53 | 
 54 | a.set('c', 'C');
 55 | assert('set() sets a single value', a.get('c') === 'C');
 56 | 
 57 | a.set({ d: 'D', e: 'E' });
 58 | assert('set() sets a map of values', a.get('d') === 'D' && a.get('e') === 'E');
 59 | 
 60 | assert('has() works', a.has('a') && !a.has('f'));
 61 | 
 62 | assert('keys() works', a.keys() + '' === 'a,b,c,d,e');
 63 | 
 64 | results = [];
 65 | function aListener(a) {
 66 | 	results.push(a);
 67 | }
 68 | a.on('a', aListener);
 69 | a.set('a', 'A1');
 70 | assert('on() works when called with a single key', results + '' === 'A1');
 71 | 
 72 | a.on(['b', 'c'], function (b, c) {
 73 | 	results = results.concat([b, c]);
 74 | });
 75 | a.set('b', 'B1');
 76 | assert('on() works when called with a list', results + '' === 'A1,B1,C');
 77 | 
 78 | a.set('a', 'A2');
 79 | a.off(aListener);
 80 | a.set('a', 'A3');
 81 | assert('off() prevents the function from being called again',
 82 | 	results + '' === 'A1,B1,C,A2');
 83 | 
 84 | results = [];
 85 | a.on('aa', aListener);
 86 | a.once('bb', aListener);
 87 | a.once(['cc', 'dd'], aListener);
 88 | a.off('aa', aListener);
 89 | a.off(['cc', 'dd'], aListener);
 90 | a.set({ aa: 'AA', bb: 'BB', cc: 'CC', dd: 'DD' });
 91 | assert('off() will be selective about unbinding a listener, if keyOrList provided',
 92 | 	results + '' === 'BB');
 93 | 
 94 | results = [];
 95 | a.once('c', function (c) {
 96 | 	results.push(c);
 97 | });
 98 | assert('once() works when called with one condition that is already complete',
 99 | 	results + '' === 'C');
100 | 
101 | results = [];
102 | a.once(['b', 'a'], function (b, a) {
103 | 	results = results.concat([b, a]);
104 | });
105 | assert('once() works when called with two conditions that are already ' +
106 | 	'complete', results + '' === 'B1,A3');
107 | 
108 | results = ['set'];
109 | a.set('done');
110 | a.once('done', function (val) {
111 | 	results = results.concat(['once', val]);
112 | });
113 | assert('once() works when the conditions are set with no value provided',
114 | 	results + '' === 'set,once,');
115 | 
116 | results = [];
117 | a.once('f', function (f) {
118 | 	results.push('once');
119 | 	results.push(f);
120 | });
121 | results.push('set');
122 | a.set('f', 'F');
123 | assert('once() works when called with one condition that is not complete',
124 | 	results + '' === 'set,once,F');
125 | 
126 | results = [];
127 | a.once(['g', 'h'], function (g, h) {
128 | 	results = results.concat(['once', g, h]);
129 | });
130 | results.push('set');
131 | a.set({ g: 'G', h: 'H' });
132 | assert('once() works when called with multiple conditions that are ' +
133 | 	'not complete', results + '' === 'set,once,G,H');
134 | 
135 | results = [];
136 | a.once(['h', 'i'], function (h, i) {
137 | 	results = results.concat(['once', h, i]);
138 | });
139 | results.push('set');
140 | a.set('i', 'I');
141 | assert('once() works when called with a mix of complete and incomplete ' +
142 | 	'conditions', results + '' === 'set,once,H,I');
143 | 
144 | var aCalls = 0;
145 | a.once('a', function () {
146 | 	aCalls++;
147 | });
148 | a.set('a', 'A3');
149 | assert('once() gets called only once, even when conditions are completed ' +
150 | 	'multiple times', aCalls === 1);
151 | 
152 | results = [];
153 | a.next('a', function (a) {
154 | 	results.push(a);
155 | });
156 | a.set('a', 'A4');
157 | assert('next() works when called with a single condition',
158 | 	results + '' === 'A4');
159 | 
160 | results = [];
161 | a.next(['b', 'c'], function (b, c) {
162 | 	results = results.concat(['next', b, c]);
163 | });
164 | results.push('set');
165 | a.set('c', 'C1');
166 | assert('next() works when called with multiple conditions',
167 | 	results + '' === 'set,B1,C1,next,B1,C1');
168 | 
169 | results = [];
170 | a.next('x', function (y) {
171 | 	results.push('x=' + y);
172 | 	a.next('x1', function (y1) {
173 | 		results.push('x1=' + y1);
174 | 	});
175 | });
176 | results.push('setX');
177 | a.set('x', 'y');
178 | results.push('setX1');
179 | a.set('x1', 'y1');
180 | assert('set() takes it in stride when the listener list is synchronously ' +
181 | 	'modified by one of the listeners', results + '' === 'setX,x=y,setX1,x1=y1');
182 | 
183 | results = ['need'];
184 | a.need('d0');
185 | a.provide('d0', function (done) {
186 | 	results = results.concat(['provider']);
187 | 	done(1);
188 | });
189 | assert('need() can be called with no callback, to invoke the provider',
190 | 	results + '' === 'need,provider');
191 | 
192 | results = ['need'];
193 | a.need('d', function (d) {
194 | 	results = results.concat(['satisfy', d]);
195 | });
196 | assert('need() calls back immediately when the (single) need is pre-satisfied',
197 | 	results + '' === 'need,satisfy,D');
198 | 
199 | results = ['need'];
200 | a.need(['e', 'f'], function (e, f) {
201 | 	results = results.concat(['satisfy', e, f]);
202 | });
203 | assert('need() calls back immediately when the needs (plural) are pre-satisfied',
204 | 	results + '' === 'need,satisfy,E,F');
205 | 
206 | results = ['need'];
207 | a.need('j', function (j) {
208 | 	results = results.concat(['satisfy', j]);
209 | });
210 | results.push('set');
211 | a.set('j', 'J');
212 | a.set('j', 'J1');
213 | assert('need() callback gets triggered after the needed value is set()',
214 | 	results + '' === 'need,set,satisfy,J');
215 | 
216 | results = ['need'];
217 | a.need('k', function (k) {
218 | 	results = results.concat(['satisfy', k]);
219 | });
220 | results.push('provide');
221 | a.provide('k', function (done) {
222 | 	results.push('fulfill');
223 | 	done('K');
224 | });
225 | assert('need() registered before provide() works',
226 | 	results + '' === 'need,provide,fulfill,satisfy,K');
227 | 
228 | results = ['provide'];
229 | a.provide('l', function (done) {
230 | 	results.push('fulfill');
231 | 	done('L');
232 | });
233 | results.push('need');
234 | a.need('l', function (l) {
235 | 	results = results.concat(['satisfy', l]);
236 | });
237 | assert('need() registered after provide() works',
238 | 	results + '' === 'provide,need,fulfill,satisfy,L');
239 | 
240 | a.provide('count', function (done) {
241 | 	done(1);
242 | 	done(2);
243 | });
244 | a.need('count');
245 | results = a.get('count');
246 | assert("provide() providers can't provide more than once", results === 1);
247 | 
248 | results = [];
249 | a.chain(function (nextLink) {
250 | 	results.push(1);
251 | 	nextLink(2);
252 | });
253 | a.chain(function (nextLink, lastArg) {
254 | 	results.push(lastArg);
255 | 	results.push(3);
256 | 	nextLink(4, 5);
257 | });
258 | a.chain(
259 | 	function (nextLink, lastArg1, lastArg2) {
260 | 		results.push(lastArg1);
261 | 		results.push(lastArg2);
262 | 		results.push(6);
263 | 		nextLink(7);
264 | 	},
265 | 	function (nextLink, lastArg) {
266 | 		results.push(lastArg);
267 | 		results.push(8);
268 | 		nextLink();
269 | 	}
270 | );
271 | assert('chain() works', results + '' === '1,2,3,4,5,6,7,8');
272 | 
273 | results = [];
274 | a.chain(
275 | 	function (nextLink) {
276 | 		results.push(1);
277 | 		a.once('start-linking', function () {
278 | 			nextLink();
279 | 			nextLink(); // This should be ineffectual.
280 | 		});
281 | 	},
282 | 	function (nextLink) {
283 | 		results.push(2);
284 | 	},
285 | 	function (nextLink) {
286 | 		// We shouldn't get here, since the previous link doesn't finish.
287 | 		results.push(3);
288 | 	}
289 | );
290 | a.set('start-linking');
291 | assert('chain() links can only get called once', results + '' === '1,2');
292 | 
293 | results = [];
294 | a.each(['a', 'b', 'd', 'c'], function (name, val) {
295 | 	results.push(name + '=' + val);
296 | });
297 | assert('each() works', results + '' === 'a=A4,b=B1,d=D,c=C1');
298 | 
299 | results = [];
300 | var otherAtom = atom();
301 | a.entangle(otherAtom, 'e');
302 | a.next('e', function (e) {
303 | 	results = results.concat(['next', e]);
304 | });
305 | results.push('set');
306 | otherAtom.set('e', 'E1');
307 | assert('entangle() works for a single key', results + '' === 'set,next,E1');
308 | 
309 | results = [];
310 | a.entangle(otherAtom, ['f', 'g']);
311 | otherAtom.once(['f', 'g'], function (f, g) {
312 | 	results = results.concat(['once', f, g]);
313 | });
314 | results.push('set');
315 | a.set({ f: 'F1', g: 'G1' });
316 | assert('entangle() works for a list of keys', results + '' === 'set,once,F1,G1');
317 | 
318 | results = [];
319 | a.entangle(otherAtom, { m: 'oM', n: 'oN' });
320 | otherAtom.once(['oM', 'oN'], function (oM, oN) {
321 | 	results = results.concat(['once', oM, oN]);
322 | });
323 | results.push('set');
324 | a.set({ m: 'M1', n: 'N1' });
325 | assert('entangle() works for maps passed to set()',
326 | 	results + '' === 'set,once,M1,N1');
327 | 
328 | results = [];
329 | a.entangle(otherAtom, 'object');
330 | otherAtom.once('object', function (object) {
331 | 	results.push(object.a);
332 | 	results.push(object.b);
333 | });
334 | a.set('object', { a: 'A', b: 'B' });
335 | assert('entangle() works for object values', results + '' === 'A,B');
336 | 
337 | var mc = atom();
338 | assert('method chaining works',
339 | 	mc.need('a', function (a) { mc.set('b', 'c'); })
340 | 	.next('b', function (b) { mc.set('c', 'd'); })
341 | 	.on('c', function (c) { mc.set('d', 'e'); })
342 | 	.once('d', function (d) { mc.set('e', 'f'); })
343 | 	.provide('a', function (done) { done('b'); })
344 | 	.chain(function () {
345 | 		mc.set('f', 'g');
346 | 	})
347 | 	.set('success', mc.get('a,b,c,d,e,f'.split(',')) + '' === 'b,c,d,e,f,g')
348 | 	.get('success') === true);
349 | 
350 | 
351 | logger(totals);
352 | 
353 | setTimeout(function () {
354 | 	var
355 | 		num = 10000,
356 | 		i = num,
357 | 		arr = [],
358 | 		set = a.set,
359 | 		start = new Date()
360 | 	;
361 | 	while (--i >= 0) {
362 | 		arr.push('z' + i);
363 | 	}
364 | 	a.once(arr, function () {
365 | 		logger('Time to set ' + num + ' properties: ' +
366 | 			(new Date() - start) + 'ms');
367 | 	});
368 | 	while (++i < num) {
369 | 		set('z' + i);
370 | 	}
371 | 	logger('END');
372 | 
373 | 	if (totals.fail && inNode) {
374 | 		process.exit(1);
375 | 	}
376 | }, 100);
377 | 


--------------------------------------------------------------------------------