├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ └── atom-test.js ├── circle.yml ├── gulpfile.js ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Webstorm/IntelliJ project files 30 | .idea/ 31 | 32 | # Compiled ES6 33 | dist/ 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "esnext": true, 4 | "predef": [ "jest", "require", "describe", "it", "expect" ] 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __tests__ 3 | src 4 | gulpfile.js 5 | npm-debug.log 6 | .git 7 | .gitignore 8 | .jshintrc 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robin Ricard 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atomstore 2 | 3 | [![Circle CI](https://circleci.com/gh/rricard/atomstore/tree/master.svg?style=svg)](https://circleci.com/gh/rricard/atomstore/tree/master) 4 | 5 | The missing piece for an immutable flux application architecture 6 | 7 | ## Introduction 8 | 9 | This piece of architecture is intended to be used with the [Facebook Flux's](https://github.com/facebook/flux) [own dispatcher](https://github.com/facebook/flux/blob/master/src/Dispatcher.js). It solves the problem of having a mutable store when using [Immutable.js](https://github.com/facebook/immutable-js). 10 | 11 | `Atom` is a class behaving like [Clojure's Atoms](http://clojure.org/atoms) (but it's not thread-safe for now on threaded js environments for example: web workers). Its goal is to ensure that you can mutate safely your store and dereference immutable objects from it. 12 | 13 | ## Install 14 | 15 | For now, you have to use browserify in order to import atomstore 16 | 17 | ```shell 18 | $ npm install --save atomstore 19 | ``` 20 | 21 | ## Usage 22 | 23 | Let's take [this simple example from Facebook](https://facebook.github.io/flux/docs/dispatcher.html). Here's how to use atoms with it: 24 | 25 | ```js 26 | var Dispatcher = require('flux').Dispatcher; 27 | var Atom = require('atomstore').Atom; 28 | var Immutable = require('immutable'); 29 | 30 | // Init dispatcher & stores 31 | var flightDispatcher = new Dispatcher(); 32 | var CountryStore = new Atom({country: null}); 33 | var CityStore = new Atom({city: null}); 34 | var FlightPriceStore = new Atom({price: null}); 35 | 36 | // Define the swap function. In this example: a set key function for immutable maps. 37 | // Keep in mind that you get an immutable object in the `this` 38 | // and you have to return an another immutable 39 | function assoc(k, v) { 40 | return this.set(k, v); 41 | } 42 | 43 | // Digest a country-update 44 | // flightDispatcher.dispatch(Immutable.Map({ 45 | // actionType: 'country-update', 46 | // selectedCountry: 'australia' 47 | // })); 48 | CountryStore.dispatchToken = flightDispatcher.register(function(payload) { 49 | if (payload.get('actionType') === 'country-update') { 50 | CountryStore.swap(assoc, "city", payload.get('selectedCountry')); 51 | } 52 | }); 53 | 54 | // Digest a city-update 55 | // flightDispatcher.dispatch(Immutable.Map({ 56 | // actionType: 'city-update', 57 | // selectedCity: 'paris' 58 | // })); 59 | CityStore.dispatchToken = flightDispatcher.register(function(payload) { 60 | if (payload.get('actionType') === 'city-update') { 61 | flightDispatcher.waitFor([CountryStore.dispatchToken]); 62 | CityStore.swap(assoc, "city", payload.get('selectedCity')); 63 | } 64 | }); 65 | 66 | // Digest a city-update or country-update in order to upgrade the price 67 | FlightPriceStore.dispatchToken = flightDispatcher.register(function(payload) { 68 | switch (payload.get('actionType')) { 69 | case 'country-update': 70 | case 'city-update': 71 | flightDispatcher.waitFor([CityStore.dispatchToken]); 72 | var newPrice = getFlightPriceStore(CountryStore.deref().get('country'), 73 | CityStore.deref().get('city')); 74 | FlightPriceStore.swap(assoc, "price", newPrice); 75 | break; 76 | } 77 | }); 78 | ``` 79 | 80 | It may seem more verbose at first but in the end you are able to ensure that your store is updated atomically as well as keeping your application completely immutable. 81 | 82 | ## Test & contribute 83 | 84 | ```shell 85 | $ npm install 86 | # Test the code, will auto-compile the es6 87 | $ npm test 88 | # Compile the es6 manually 89 | $ gulp es6 90 | ``` 91 | 92 | PRs are welcome. You just need to provide tests with your code and make the CI pass ! 93 | 94 | ## Author 95 | 96 | [Robin Ricard](http://www.rricard.me) 97 | 98 | ## Licence 99 | 100 | MIT 101 | -------------------------------------------------------------------------------- /__tests__/atom-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | jest.dontMock('../dist/atomstore'); 4 | jest.dontMock('immutable'); 5 | 6 | var Atom = require('../dist/atomstore').Atom; 7 | var Immutable = require('immutable'); 8 | 9 | describe("Atom", function() { 10 | it("should convert non-immutable constructions", function() { 11 | var map = {hello: "world"}; 12 | var atom = new Atom(map); 13 | expect(atom.deref()).toEqual(Immutable.Map(map)); 14 | }); 15 | 16 | it("should keep immutable constructions", function() { 17 | var map = Immutable.Map({hello: "world"}); 18 | var atom = new Atom(map); 19 | expect(atom.deref()).toBe(map); 20 | }); 21 | 22 | it("should compare and set when the value matches", function() { 23 | var atom = new Atom({a: "b"}); 24 | atom.compareAndSet(Immutable.Map({a: "b"}), {a: "c"}); 25 | atom.compareAndSet(Immutable.Map({a: "c"}), {a: "d"}); 26 | expect(atom.deref()).toEqual(Immutable.Map({a: "d"})); 27 | }); 28 | 29 | it("should compare and set when the value does not match", function() { 30 | var atom = new Atom({a: "b"}); 31 | atom.compareAndSet(Immutable.Map({a: "b"}), {a: "c"}); 32 | atom.compareAndSet(Immutable.Map({a: "b"}), {a: "d"}); 33 | expect(atom.deref()).toEqual(Immutable.Map({a: "c"})); 34 | }); 35 | 36 | it("should swap as soon as possible", function() { 37 | var assoc = function(k, v) { 38 | return this.set(k, v); 39 | }; 40 | var atom = new Atom({a: "b"}); 41 | atom.swap(assoc, "b", "c"); 42 | atom.swap(assoc, "c", "d"); 43 | // Need to directly target entries in order to avoid issues with the owner id 44 | expect(atom.deref()._root.entries) 45 | .toEqual(Immutable.Map({a: "b", b: "c", c: "d"})._root.entries); 46 | }); 47 | }); -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - npm test 4 | - npm run-script lint -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var gulp = require('gulp'); 4 | var sourcemaps = require('gulp-sourcemaps'); 5 | var babel = require('gulp-babel'); 6 | var concat = require('gulp-concat'); 7 | 8 | gulp.task('default', ['es6']); 9 | 10 | gulp.task('es6', function() { 11 | return gulp.src("src/index.js") 12 | .pipe(sourcemaps.init()) 13 | .pipe(babel()) 14 | .pipe(concat('atomstore.js')) 15 | .pipe(sourcemaps.write('.')) 16 | .pipe(gulp.dest('dist')); 17 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomstore", 3 | "version": "0.1.3", 4 | "description": "The missing piece for an immutable flux application architecture", 5 | "main": "dist/atomstore.js", 6 | "scripts": { 7 | "lint": "jshint src/ __tests__/", 8 | "pretest": "gulp", 9 | "prepublish": "gulp", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/rricard/atomstore.git" 15 | }, 16 | "keywords": [ 17 | "flux", 18 | "atom", 19 | "immutable" 20 | ], 21 | "author": "Robin Ricard ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/rricard/atomstore/issues" 25 | }, 26 | "homepage": "https://github.com/rricard/atomstore", 27 | "dependencies": { 28 | "flux": "^2.0.1", 29 | "immutable": "^3.7.1" 30 | }, 31 | "devDependencies": { 32 | "gulp": "^3.8.11", 33 | "gulp-babel": "^5.0.0", 34 | "gulp-concat": "^2.5.2", 35 | "gulp-sourcemaps": "^1.5.1", 36 | "jest-cli": "^0.4.0", 37 | "jshint": "^2.6.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Immutable from "immutable"; 4 | 5 | export class Atom { 6 | /** 7 | * @constructor 8 | * Constructs a new Atom from an immutable object. 9 | * If the object is mutable, it will be converted automatically to an Immutable.js object. 10 | */ 11 | constructor(immutableObject) { 12 | if(immutableObject instanceof Immutable.Iterable) { 13 | this._ref = immutableObject; 14 | } else { 15 | this._ref = Immutable.fromJS(immutableObject); 16 | } 17 | this._lock = false; 18 | this._listeners = Immutable.List([]); 19 | } 20 | 21 | /** 22 | * @return The referenced immutable structure 23 | */ 24 | deref() { 25 | return this._ref; 26 | } 27 | 28 | /** 29 | * Adds a new change listener to the store 30 | * @param listener A callback function 31 | */ 32 | addChangeListener(listener) { 33 | this._listeners = this._listeners.push(listener); 34 | } 35 | 36 | /** 37 | * Atomically sets the value of atom to newval if and only if the current value of the atom is identical to oldval. 38 | * @param oldval The expected current value of the atom 39 | * @param newval The targeted value of the atom 40 | * @return The success of the operation 41 | */ 42 | compareAndSet(oldval, newval) { 43 | // Really bad mutex implementation (multithreaded node.js won't work, but this may not happen) 44 | // TODO: do something better 45 | while(this._lock) {} 46 | this._lock = true; 47 | 48 | var updated = false; 49 | if(this.deref().equals(oldval)) { 50 | if(newval instanceof Immutable.Iterable) { 51 | this._ref = newval; 52 | } else { 53 | this._ref = Immutable.fromJS(newval); 54 | } 55 | updated = true; 56 | } 57 | this._lock = false; 58 | this._listeners.forEach((listener) => { 59 | listener(this._ref); 60 | }); 61 | return updated; 62 | } 63 | 64 | /* 65 | * Atomically swaps the value of atom to be: f.apply(currentValueOfAtom, args) 66 | * Note that f may be called multiple times, and thus should be free of side effects. 67 | * @param f The pure function to apply on the swap 68 | * @param *args The other expected args to apply on the swap 69 | * @return The swapped new value 70 | */ 71 | swap(f) { 72 | var args = Array.prototype.slice.call(arguments, 1); 73 | // TODO: Make it better too 74 | while(!this.compareAndSet(this._ref, f.apply(this._ref, args))) {} 75 | return this._ref; 76 | } 77 | } 78 | --------------------------------------------------------------------------------