├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist └── index.js ├── package.json └── src ├── dispatcher.js ├── index.js ├── store.js └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "quotes": [2, "single"], 9 | "eol-last": [0], 10 | "no-underscore-dangle": [0], 11 | "no-cond-assign" : 0, 12 | "indent": [2, 2], 13 | "semi": [2, "never"], 14 | "comma-style": [2, "first"], 15 | "no-unused-expressions": 0, 16 | "strict": 0, 17 | "camelcase": 0, 18 | "new-cap": 0 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################## 2 | # node.js / npm 3 | ######################## 4 | lib-cov 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.gz 12 | 13 | pids 14 | logs 15 | results 16 | 17 | node_modules/* 18 | bower_components 19 | 20 | npm-debug.log 21 | 22 | 23 | ######################## 24 | # misc / editors 25 | ######################## 26 | *~ 27 | *# 28 | .DS_STORE 29 | .netbeans 30 | nbproject 31 | .idea 32 | 33 | lib 34 | 35 | 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ######################## 2 | # node.js / npm 3 | ######################## 4 | .eslintrc 5 | .gitignore 6 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Naman Goel 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 | # flexy - WARNING: END OF LIFE 2 | 3 | ## Flexy was made months before redux. Redux is superior in almost every way. If you use flexy, you should find a way to transition over to redux. Most of the code should portable using `redux-thunk` and using immtable in your redux store. 4 | 5 | Friendly flux based on channels and immutable data. 6 | 7 | ## Another Flux Library? 8 | 9 | The more the merrier right? 10 | 11 | There are many great implementations of Flux out there, but I think Flexy is different enough from the competition to justify it's existence. 12 | 13 | Think of Flex is something that tries to be the best of both worlds. Flexy has few moving parts, just a dispatcher and store, nothing else. At the same time, it tries to make it possible to build something complicated and powerful. It does this by focussing on the store. 14 | 15 | Using Immutable data, Flexy stores make it possible to easily accomplish optimistic updates. Unlike other stores, you never directly mutate the data in the store. You pass functions that take the old state of the data and return the new state. Further, every change can be applied optimistically and later committed or rejected. 16 | 17 | ## WaitFor? 18 | 19 | Inspired by the talk 'Full Stack Flux', Flexy gets rid of the waitFor metophor altother. Instead, every Store, apart from emitting change events, also pipes through all the events from the Dispatcher, after it handles them. Instead of waitFor, you just listen to another store to accomplish the same. 20 | 21 | ## Observe API 22 | 23 | Flexy is based on channels, but it plays well with the upcoming Observe API in React. You can call the `getObservable` method on a store with a filter function. This will give you something that resembles an observable. You can then subscribe to this to be sotified of changes. When you subscribe, you will get an object with a `.dispose()` method, just like a real observable. 24 | 25 | ## CSP Mults 26 | 27 | In case you want to use channels, that is easy too. You can just call the `.tap` and `.untap` methods to directly subscribe to all changes. In this case you will have to filter through the data yourself. I hope to fix this in the near future. Read about mults in js-csp to understand how they work. 28 | 29 | ## API 30 | 31 | ### Stores 32 | 33 | Flexy is based on definining classes. (This will be useful for isomorphic apps, which is coming in the future) You can define a Store class by calling the 34 | `.defineStore` method with "Transformers", "consumers". 35 | 36 | ```js 37 | 38 | var StoreClass = Flexy.defineStore({ 39 | primaryKey: 'id', 40 | transformers: [...], 41 | consumers: [...], 42 | ... 43 | }) 44 | 45 | ``` 46 | 47 | #### primaryKey *: String* 48 | This is just a simple string 49 | 50 | #### transformers *: Array* 51 | Transformers are simple functions the current data, and return new data. The functions will also receive the payload sent with the action if any 52 | 53 | e.g.: 54 | ```js 55 | 'ACTION_NAME': function(data, payload){ 56 | return data.merge(payload) 57 | } 58 | ``` 59 | 60 | #### consumers *: Array* 61 | Consumers are for times when you need finer grain control over the updates of your stores. It's best to show with an example: 62 | 63 | ```js 64 | 'ACTION_NAME': function({apply, commit, reject}, {payload, promise}){ 65 | apply(function(data){ 66 | return data.setIn([payload.id, 'count'], data.get(payload.id).get(count) + 1) 67 | }) 68 | 69 | setTimeout(commit, 1000) 70 | } 71 | 72 | 'ACTION_NAME': function({apply, commit, reject}, {payload}){ 73 | apply(function(data){ 74 | return data.setIn([payload.id, 'count'], data.get(payload.id).get(count) + 1) 75 | }) 76 | 77 | someAjaxCallPromise 78 | .then(apply) 79 | .catch(reject) 80 | 81 | setTimeout(commit, 1000) 82 | } 83 | ``` 84 | 85 | Here your function gets the controls to your data as the first argument, and the action as the second argument. 86 | You should call apply with a simple function that takes the old data and returns new data. This change will be applied immediately. 87 | After this you can do any async calls that you may need to and only after that, call commit on success. 88 | 89 | If something goes wrong, just call reject, and your change will be rolled back. Special care is taken to ensure that only this particular change is rolled back and not the changes caused by actions that may have occurred since. 90 | 91 | #### Other properties *: Any 92 | Other than primaryKey, consumers and transformers, you can pass in arbitrary properties while defining your store, or while instantiating it. All these properties would be made available on the context. This is a simple and powerful way to pass in various dependencies. 93 | 94 | 95 | ```js 96 | var StoreClass = Flexy.defineStore({ 97 | primaryKey: 'id', 98 | transformers: [...], 99 | consumers: [...], 100 | a: function(){...} 101 | }) 102 | 103 | var store = new StoreClass(initialData, {b: somethingElse}) 104 | 105 | ``` 106 | 107 | In this case, the transformers and consumers will all have access to `this.a` and `this.b` 108 | 109 | ### Dispatchers 110 | Dispatchers should usually not need much configuration. But for bigger projects, you may need something like action creators. Luckily, the dispatchers in Flexy can actually play the role of action creators as well. 111 | 112 | A simple way to think of Dispatchers is that it is a simple stream of events, that lets you pass in map functions to handle particular events and transform them. 113 | 114 | Dispatchers only take a single property, called transformers: 115 | 116 | #### transformers 117 | These are simple functions that have the same name as the action and can then accept the payload and return the new payload. This is usually only good for sanity checking and for firing ajax requests and passing promises along. Flexy is unopinioated about how you handle ajax/async calls in your actions. You can do that in your transformers, or in your stores (or a bit of both). 118 | 119 | 120 | ## Conclusion 121 | I have been using Flexy in production of upclose.me. New patches and features are being added on a regular basis. I will soon switch to it for scribbler.co as well. 122 | 123 | So far, I really like how simple but powerful it is. I have found many flux implementation to be too complex, and as a roadbloack while trying to adopt the flux architecture. 124 | 125 | If you have any questions, suggestions, and pull requests (would be awesome) feel free to contact me. I'm trying to slowly add more documentation and examples. (help there would be amazing!) 126 | 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flexy", 3 | "version": "2.4.1", 4 | "description": "A Flux library based on Channels and reducer functions", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "watch": "npm run watchFolder & npm run watchFile", 9 | "watchFolder": "babel src --out-dir lib --watch --optional es7.objectRestSpread", 10 | "watchFile": "watchify --debug -t [ babelify --optional es7.objectRestSpread ] src/index.js -o dist/index.js -v", 11 | "build": "babel src --out-dir lib --optional es7.objectRestSpread" 12 | }, 13 | "keywords": [ 14 | "Flux", 15 | "React", 16 | "Channels", 17 | "CSP", 18 | "Transducers" 19 | ], 20 | "author": "Naman Goel", 21 | "license": "MIT", 22 | "peerDependencies": { 23 | "js-csp": "^0.4.1", 24 | "immutable": "^3.7.2" 25 | }, 26 | "dependencies": { 27 | "transducers.js": "^0.3.2" 28 | }, 29 | "devDependencies": { 30 | "babel": "^5.2.17", 31 | "babelify": "^6.0.2", 32 | "browserify": "^10.1.3", 33 | "watchify": "^3.2.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import {chan, go, operations, buffers, take, putAsync, CLOSED} from 'js-csp' 2 | import {compose, filter} from 'transducers.js' 3 | import I from 'immutable' 4 | 5 | export function defineDispatcher({transformers}){ 6 | return class { 7 | 8 | constructor(){ 9 | this.outCh = chan(buffers.fixed(10), compose(filter(value => !!value))) 10 | this.outMult = operations.mult(this.outCh) 11 | this._subscribers = I.Set() 12 | } 13 | 14 | listen(source){ 15 | 16 | if(source.outMult && source.throughMult){ 17 | source = source.throughMult 18 | } else if(source.outMult){ 19 | source = source.outMult 20 | } 21 | 22 | const ch = chan() 23 | const that = this 24 | operations.mult.tap(source, ch) 25 | 26 | go(function*(){ 27 | var value = yield take(ch) 28 | while (value !== CLOSED) { 29 | that.trigger(value) 30 | value = yield take(ch) 31 | } 32 | }) 33 | 34 | return this 35 | } 36 | 37 | broadcast(value){ 38 | this._subscribers 39 | .map(list => list.toJS()) 40 | .forEach(([subscriber, ctx]) => subscriber.call(ctx, value)) 41 | } 42 | 43 | subscribe(fn, ctx = null){ 44 | const list = I.List([fn, ctx]) 45 | this._subscribers = this._subscribers.add( list ) 46 | return list 47 | } 48 | 49 | unsubscribe(fn, ctx = null){ 50 | const list = I.List([fn, ctx]) 51 | this._subscribers = this._subscribers.delete( list ) 52 | return list 53 | } 54 | 55 | emit(...args){ 56 | this.trigger(...args) 57 | } 58 | 59 | trigger(name, payload, promise){ 60 | if(typeof name === 'string'){ 61 | let obj = 62 | transformers[name] ? transformers[name]({name, payload, promise}) 63 | : {name, payload, promise} 64 | putAsync(this.outCh, obj) 65 | this.broadcast(obj) 66 | } else if(typeof name === 'object' && typeof name.name === 'object'){ 67 | let obj = 68 | transformers[name] ? transformers[name](name) 69 | : name 70 | putAsync(this.outCh, obj) 71 | this.broadcast(obj) 72 | } else { 73 | console.warn('dispatched event without a name', name) 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from './store' 2 | import {defineDispatcher} from './dispatcher' 3 | 4 | export { defineStore, defineDispatcher } -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import I from 'immutable' 2 | import {chan, go, operations, buffers, take, put, takeAsync, putAsync, CLOSED} from 'js-csp' 3 | import {compose, map, filter, dedupe, merge} from 'transducers.js' 4 | 5 | 6 | export function defineStore({primaryKey = 'id', consumers, transformers, ...ctx}){ 7 | return class { 8 | constructor(initialData, ctx2){ 9 | this.data = 10 | !initialData ? 11 | I.Map() 12 | : Array.isArray(initialData) && primaryKey ? 13 | I.fromJS( initialData.reduce((collection, obj) => Object.assign(collection, {[obj[primaryKey]]: obj}), {}) ) 14 | : typeof initialData === 'object' ? 15 | I.fromJS( initialData ) 16 | : I.Map() 17 | 18 | this.context = merge({}, ctx, ctx2) 19 | 20 | this.reducers = I.OrderedMap() 21 | 22 | this.inCh = chan() 23 | this.outCh = chan( buffers.fixed(100), compose(dedupe()) ) 24 | this.outMult = operations.mult(this.outCh) 25 | this.sources = I.Set() 26 | this.throughCh = chan(buffers.fixed(100)) 27 | this.throughMult = operations.mult(this.throughCh) 28 | } 29 | 30 | toJSON(){ 31 | return JSON.stringify(this.data) 32 | } 33 | 34 | subscribeTo(dispatcher){ 35 | dispatcher.subscribe(this.handleAction, this) 36 | return this 37 | } 38 | 39 | unsubscribeFrom(dispatcher){ 40 | dispatcher.unsubscribe(this.handleAction, this) 41 | return this 42 | } 43 | 44 | listen(source){ 45 | 46 | if(source.outMult && source.throughMult){ 47 | source = source.throughMult 48 | } else if(source.outMult){ 49 | source = source.outMult 50 | } 51 | 52 | const ch = chan() 53 | const that = this 54 | operations.mult.tap(source, ch) 55 | 56 | go(function*(){ 57 | var value = yield take(ch) 58 | while (value !== CLOSED) { 59 | that.handleAction(value) 60 | value = yield take(ch) 61 | } 62 | }) 63 | 64 | return this 65 | } 66 | 67 | trigger(){ 68 | // const oldData = this.data 69 | const fnsToApply = this.reducers.takeWhile(v => v > 0) 70 | this.reducers = this.reducers.skipWhile(v => v > 0).filter(v => v >= 0) 71 | this.data = fnsToApply.reduce((value, val, fn) => fn(value), this.data) 72 | 73 | const dataToSend = this.reducers.reduce((value, val, fn) => fn(value), this.data) 74 | 75 | putAsync(this.outCh, dataToSend) 76 | } 77 | 78 | getObservable(transformer, onUndefined){ 79 | 80 | transformer = transformer || function(a){return a} 81 | const that = this 82 | 83 | return { 84 | subscribe(onNext, onError, onCompleted){ 85 | 86 | const initialData = 87 | transformer( 88 | that.reducers 89 | .filter(v => v >= 0) 90 | .reduce( (value, val, fn) => fn(value), that.data) 91 | ) 92 | 93 | if(initialData === undefined){ 94 | typeof onUndefined === 'function' && onUndefined() 95 | onNext(undefined) 96 | } else { 97 | onNext(initialData) 98 | } 99 | 100 | const tempCh = chan() 101 | operations.mult.tap(that.outMult, tempCh) 102 | let completed = false 103 | 104 | go(function* (){ 105 | try { 106 | let value = yield take(tempCh) 107 | while(value !== CLOSED){ 108 | onNext(transformer(value)) 109 | value = yield take(tempCh) 110 | } 111 | if(completed){ 112 | onCompleted() 113 | } 114 | } catch (e){ 115 | onError(e) 116 | } 117 | }) 118 | 119 | return { 120 | dispose(){ 121 | operations.mult.untap(that.outMult, tempCh) 122 | tempCh.close() 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | subscribe(onNext, onError, onCompleted){ 130 | 131 | const tempCh = chan() 132 | operations.mult.tap(this.outMult, tempCh) 133 | let completed = false 134 | 135 | go(function* (){ 136 | try { 137 | let value = yield take(tempCh) 138 | while(value !== CLOSED){ 139 | onNext(value) 140 | value = yield take(tempCh) 141 | } 142 | if(completed){ 143 | onCompleted() 144 | } 145 | } catch (e){ 146 | onError(e) 147 | } 148 | }) 149 | 150 | return { 151 | dispose(){ 152 | operations.mult.untap(this.outMult, tempCh) 153 | tempCh.close() 154 | } 155 | } 156 | } 157 | 158 | tap(channel){ 159 | operations.mult.tap(this.outMult, channel) 160 | return this 161 | } 162 | 163 | untap(channel){ 164 | operations.mult.untap(this.outMult, channel) 165 | return this 166 | } 167 | 168 | handleAction(action){ 169 | const that = this 170 | const {name, payload, promise} = action 171 | 172 | // in case of full consumer. We provide, (controls, action) 173 | // controls is an object of three functions — apply, commit, and reject 174 | // in case of sync operations, the consumer is expected to call apply with a reducer function and then immediately call commit 175 | // in case of async ops, the consumer should call apply with a reducer function. Then if the async op is successful call commit, 176 | // if the async operation fails, reject should be called. This will roll back the change. 177 | if(consumers[name]){ 178 | let cached = null 179 | consumers[name] 180 | .call( this.context 181 | , { apply(fn){ 182 | if(cached){ 183 | that.reducers = that.reducers.set(cached, -1) 184 | } 185 | cached = fn 186 | that.reducers = that.reducers.set(fn, 0) 187 | that.trigger() 188 | } 189 | , commit(){ 190 | if(!cached){ 191 | return false 192 | } 193 | that.reducers = that.reducers.set(cached, 1) 194 | that.trigger() 195 | cached = null 196 | putAsync(that.throughCh, action) 197 | return true 198 | } 199 | , reject(){ 200 | if(!cached){ 201 | return false 202 | } 203 | that.reducers = that.reducers.set(cached, -1) 204 | that.trigger() 205 | cached = null 206 | putAsync(that.throughCh, action) 207 | return true 208 | } 209 | } 210 | , { payload, promise} 211 | ) 212 | } else if(transformers[name]){ 213 | let cached = (data) => transformers[name].call(this.context, data, payload) 214 | that.reducers = that.reducers.set(cached, 0) 215 | if(promise){ 216 | that.trigger() 217 | promise 218 | .then(() => { 219 | that.reducers = that.reducers.set(cached, 1) 220 | that.trigger() 221 | putAsync(that.throughCh, action) 222 | }) 223 | .catch((err) => { 224 | that.reducers = that.reducers.set(cached, -1) 225 | that.trigger() 226 | console.error(err) 227 | putAsync(that.throughCh, action) 228 | }) 229 | } else { 230 | that.reducers = that.reducers.set(cached, 1) 231 | that.trigger() 232 | putAsync(that.throughCh, action) 233 | } 234 | } else { 235 | putAsync(that.throughCh, action) 236 | } 237 | } 238 | } 239 | } 240 | 241 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'flexy' 2 | 3 | var BroadcastStore = defineStore( 4 | { primaryKey: 'id' 5 | , transformers: 6 | { 'FOLLOW_USER' : function({payload}, data){ 7 | const id = payload.id 8 | return data.set(id, data.get(id).merge(payload)) 9 | } 10 | } 11 | , consumers: 12 | { 'DELETE_IMAGE': function({payload}, {apply, commit, reject}){ 13 | apply(data => data.delete(payload.id)) 14 | request.del('/images').send({id: payload.id}).promise() 15 | .then(commit) 16 | .catch(reject) 17 | } 18 | } 19 | } 20 | ) --------------------------------------------------------------------------------