├── .eslintignore ├── .eslintrc ├── .travis.yml ├── src ├── index.js ├── utils │ ├── getFunctionName.js │ ├── __tests__ │ │ ├── getFunctionNameTest.js │ │ └── updateIdsMapTest.js │ ├── animationFrame.js │ ├── Dep.js │ ├── Promised.js │ └── updateIdsMap.js ├── asserts.js ├── __tests__ │ ├── settersGettersTest.js │ ├── cacheTest.js │ ├── eventsTest.js │ ├── stateChangesTest.js │ └── containerTest.js ├── cursors │ ├── abstract.js │ └── native.js ├── define.js └── container.js ├── test ├── mocha.opts └── tests.js ├── .flowconfig ├── .babelrc ├── conf └── git-hooks │ └── pre-commit ├── .editorconfig ├── .gitignore ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | /cursors 3 | /utils 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "airplus"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "4.0.0" 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Container from './container' 2 | export default Container 3 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 2 | --compilers js:babel/register 3 | test/tests.js 4 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | glob.sync(__dirname + '/../src/**/__tests__/*.js').forEach(file => require(file)) 3 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ./dist 3 | 4 | [include] 5 | ./src 6 | 7 | [libs] 8 | 9 | [options] 10 | module.system=node 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": [ 4 | "runtime", 5 | "es7.decorators", 6 | "utility.inlineEnvironmentVariables" 7 | ], 8 | "plugins": [ 9 | "babel-plugin-espower" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /conf/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check if any .js file changed 4 | git diff --cached --name-only --diff-filter=ACM | grep '.js$' >/dev/null 2>&1 5 | 6 | if [[ $? == 0 ]]; then 7 | npm run pre-commit 8 | fi 9 | 10 | exit $? 11 | -------------------------------------------------------------------------------- /src/utils/getFunctionName.js: -------------------------------------------------------------------------------- 1 | const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg 2 | const FN_MAGIC = 'function' 3 | 4 | export default function getFunctionName(func) { 5 | const fnStr = func.toString().replace(STRIP_COMMENTS, '') 6 | return fnStr.slice(fnStr.indexOf(FN_MAGIC) + FN_MAGIC.length + 1, fnStr.indexOf('(')) 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines with a newline ending every file 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | 6 | # Matches multiple files with brace expansion notation 7 | # Set default charset 8 | [*.{js}] 9 | charset = utf-8 10 | 11 | # Tab indentation (no size specified) 12 | [*.js] 13 | indent_style = space 14 | 15 | [*.jsx] 16 | indent_style = space 17 | 18 | # Matches the exact files either package.json or .travis.yml 19 | [{package.json,.travis.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.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 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | flow 31 | build 32 | dist 33 | .tags* 34 | *.map 35 | /*.js 36 | /*.sh 37 | /__tests__ 38 | /cursors 39 | /utils 40 | -------------------------------------------------------------------------------- /src/utils/__tests__/getFunctionNameTest.js: -------------------------------------------------------------------------------- 1 | import getFunctionName from '../getFunctionName' 2 | import assert from 'power-assert' 3 | 4 | describe('getFunctionName', () => { 5 | it('should return valid function name', () => { 6 | function test() { 7 | } 8 | 9 | assert(getFunctionName(test) === 'test') 10 | }) 11 | 12 | it('should return empty function name for anonymous functions', () => { 13 | function test() { 14 | } 15 | 16 | assert(getFunctionName(() => 0) === '') 17 | }) 18 | 19 | it('should throw error if undefined argument', () => { 20 | assert.throws(() => getFunctionName()) 21 | assert.throws(() => getFunctionName(null)) 22 | }) 23 | 24 | it('should return empty name if empty argument', () => { 25 | assert.equal(getFunctionName(0), '') 26 | assert.equal(getFunctionName(false), '') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stefan 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 | -------------------------------------------------------------------------------- /src/asserts.js: -------------------------------------------------------------------------------- 1 | import getFunctionName from './utils/getFunctionName' 2 | 3 | export function IDep(dep) { 4 | if (process.env.NODE_ENV === 'development') { 5 | if (typeof dep === 'function') { 6 | const name = dep.displayName || getFunctionName(dep) 7 | if (!dep.__di) { 8 | throw new TypeError('Dep ' + name + ' has no __di static property') 9 | } 10 | if (!dep.__di.id) { 11 | throw new TypeError('Dep ' + name + ' has no id') 12 | } 13 | } else if (!Array.isArray(dep)) { 14 | throw new TypeError('Dep ' + dep + ' is not a function or array') 15 | } 16 | } 17 | } 18 | 19 | export function IDeps(deps) { 20 | if (process.env.NODE_ENV === 'development') { 21 | if (Array.isArray(deps)) { 22 | deps.forEach(dep => IDep(dep)) 23 | } else { 24 | if (typeof deps !== 'object') { 25 | throw new Error('stateMap is not an object') 26 | } 27 | Object.keys(deps).forEach(k => IDep(deps[k])) 28 | } 29 | } 30 | } 31 | 32 | export function IPath(path) { 33 | if (process.env.NODE_ENV === 'development') { 34 | if (!Array.isArray(path)) { 35 | throw new TypeError('path is not an array') 36 | } 37 | path.forEach(p => { 38 | if (typeof p !== 'string') { 39 | throw new TypeError('Path element is not a string in array: ' + path) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/animationFrame.js: -------------------------------------------------------------------------------- 1 | const cancelNames = [ 2 | 'cancelAnimationFrame', 3 | 'webkitCancelAnimationFrame', 4 | 'webkitCancelRequestAnimationFrame', 5 | 'mozCancelRequestAnimationFrame', 6 | 'oCancelRequestAnimationFrame', 7 | 'msCancelRequestAnimationFrame' 8 | ] 9 | 10 | const requestNames = [ 11 | 'requestAnimationFrame', 12 | 'webkitRequestAnimationFrame', 13 | 'mozRequestAnimationFrame', 14 | 'oRequestAnimationFrame', 15 | 'msRequestAnimationFrame' 16 | ] 17 | 18 | function getBrowserFn(names) { 19 | let result 20 | if (typeof window !== 'undefined') { 21 | const filteredNames = names.filter(name => !!window[name]) 22 | result = filteredNames.length ? window[filteredNames[0]] : undefined 23 | } 24 | 25 | return result ? result.bind(window) : undefined 26 | } 27 | 28 | function fallbackRequestAnimationFrame(cb) { 29 | return setTimeout(cb, 0) 30 | } 31 | 32 | function fallbackCancelAnimationFrame(handle) { 33 | return clearTimeout(handle) 34 | } 35 | 36 | const _browserCancelAnimationFrame = getBrowserFn(cancelNames) 37 | const browserRequestAnimationFrame = getBrowserFn(requestNames) 38 | 39 | function browserCancelAnimationFrame(handle) { 40 | return handle ? _browserCancelAnimationFrame(handle.value) : null 41 | } 42 | 43 | export const cancelAnimationFrame = _browserCancelAnimationFrame 44 | ? browserCancelAnimationFrame 45 | : fallbackCancelAnimationFrame 46 | 47 | export const requestAnimationFrame = browserRequestAnimationFrame 48 | ? browserRequestAnimationFrame 49 | : fallbackRequestAnimationFrame 50 | -------------------------------------------------------------------------------- /src/utils/Dep.js: -------------------------------------------------------------------------------- 1 | import getFunctionName from './getFunctionName' 2 | import {IDeps} from '../asserts' 3 | 4 | export type IDependency = (v: any) => any 5 | 6 | function normalizeDeps(deps, pathMapper) { 7 | const resultDeps = [] 8 | const isArray = Array.isArray(deps) 9 | const names = isArray ? [] : Object.keys(deps) 10 | const len = isArray ? deps.length : names.length 11 | for (let i = 0; i < len; i++) { 12 | const name = names.length ? names[i] : undefined 13 | const dep = deps[name || i] 14 | resultDeps.push({ 15 | name, 16 | definition: Array.isArray(dep) ? pathMapper(dep) : dep 17 | }) 18 | } 19 | 20 | return resultDeps 21 | } 22 | 23 | 24 | let lastId = 1 25 | export function getId() { 26 | // http://jsperf.com/number-vs-string-object-keys-access 27 | return 'p' + lastId++ 28 | } 29 | 30 | export default function Dep({ 31 | deps, 32 | displayName, 33 | id, 34 | isCachedTemporary, 35 | isClass, 36 | isSetter, 37 | path, 38 | pathMapper 39 | }) { 40 | const _deps = deps || [] 41 | IDeps(_deps) 42 | return function dep(Service) { 43 | id = id || (Service.__di ? Service.__di.id : getId()) 44 | const dn = displayName || Service.displayName || getFunctionName(Service) || id 45 | const newDeps = normalizeDeps(_deps, pathMapper) 46 | Service.displayName = displayName 47 | Service.__di = { 48 | deps: newDeps, 49 | displayName: dn, 50 | id: id, 51 | isSetter: isSetter, 52 | isCachedTemporary: !!isCachedTemporary, 53 | isClass: !!isClass, 54 | isOptions: !Array.isArray(deps), 55 | path 56 | } 57 | 58 | return Service 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/settersGettersTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import assert from 'power-assert' 3 | import Container from '../container' 4 | import NativeCursor from '../cursors/native' 5 | import {Factory, Getter, Setter} from '../define' 6 | import sinon from 'sinon' 7 | 8 | describe('settersGettersTest', () => { 9 | it('should get state in run-time', () => { 10 | const cursor = new NativeCursor({ 11 | a: { 12 | b: 123 13 | } 14 | }) 15 | const container = new Container(cursor) 16 | const fn = sinon.spy(v => v) 17 | const MyDep = Factory([Getter(['a', 'b'])])(fn) 18 | 19 | container.get(MyDep) 20 | assert(container.get(MyDep)() === 123) 21 | cursor.select(['a', 'b']).set(321).commit() 22 | assert(container.get(MyDep)() === 321) 23 | }) 24 | 25 | it('should set state in run-time', () => { 26 | const cursor = new NativeCursor({ 27 | a: { 28 | b: 123 29 | } 30 | }) 31 | const container = new Container(cursor) 32 | const fn = sinon.spy(setId => { 33 | return v => setId(v).commit() 34 | }) 35 | const MyDep = Factory([Setter(['a', 'b'])])(fn) 36 | 37 | assert(cursor.select(['a', 'b']).get() === 123) 38 | container.get(MyDep)(321) 39 | assert(cursor.select(['a', 'b']).get() === 321) 40 | }) 41 | 42 | it('select should return instance of Cursor', () => { 43 | const cursor = new NativeCursor({ 44 | a: { 45 | b: 123 46 | } 47 | }) 48 | assert(cursor.select(['a', 'b']) instanceof NativeCursor) 49 | }) 50 | 51 | it('should throw error if node does not exists in the middle of path', () => { 52 | const cursor = new NativeCursor({ 53 | a: { 54 | b: 123 55 | } 56 | }) 57 | assert.throws(() => cursor.select(['d', 'b', 'a']).get(), /path/) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/utils/Promised.js: -------------------------------------------------------------------------------- 1 | function mapArrayToObject(arr, keys) { 2 | const result = {} 3 | for (let i = 0; i < keys.length; i++) { 4 | result[keys[i]] = arr[i] 5 | } 6 | 7 | return result 8 | } 9 | 10 | export function spread(obj) { 11 | const keys = Object.keys(obj) 12 | const promises = keys.map(k => obj[k]) 13 | function map(values) { 14 | return mapArrayToObject(values, keys) 15 | } 16 | 17 | return Promise.all(promises).then(map) 18 | } 19 | 20 | export function assign(cb) { 21 | return function resolveAssign(obj) { 22 | return spread({ 23 | ...obj, 24 | ...cb(obj) 25 | }) 26 | } 27 | } 28 | 29 | export function ifError(Err, cb) { 30 | return function _ifError(e) { 31 | if (e instanceof Err) { 32 | cb(e) 33 | } else { 34 | throw e 35 | } 36 | } 37 | } 38 | 39 | export function Semaphore() { 40 | const locks = {} 41 | 42 | return function semaphore(map) { 43 | const promises = [] 44 | const keys = Object.keys(map) 45 | for (let i = 0; i < keys.length; i++) { 46 | const k = keys[i] 47 | const [needLoad, cb] = map[k] 48 | promises.push((locks[k] || !needLoad) ? undefined : cb()) 49 | locks[k] = true 50 | } 51 | 52 | return Promise.all(promises) 53 | .then(d => { 54 | const result = {} 55 | for (let i = 0; i < keys.length; i++) { 56 | const k = keys[i] 57 | const data = d[i] 58 | const set = map[k][2] 59 | result[k] = data 60 | if (set && data) { 61 | set(data) 62 | } 63 | delete locks[k] 64 | } 65 | return result 66 | }) 67 | .catch(err => { 68 | for (let i = 0; i < keys.length; i++) { 69 | delete locks[keys[i]] 70 | } 71 | throw err 72 | }) 73 | } 74 | } 75 | 76 | export function ignore(data) { 77 | return function _ignore() { 78 | return data 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/cursors/abstract.js: -------------------------------------------------------------------------------- 1 | import Dep from '../utils/Dep' 2 | 3 | export type PathType = Array 4 | 5 | function pass() { 6 | } 7 | 8 | // Dep used instead of define/Class to prevent cross-dependencies 9 | @Dep({isClass: true}) 10 | export default class AbstractCursor { 11 | __notify: (path: string, isSynced: ?bool) => void = null 12 | _prefix: PathType 13 | 14 | constructor( 15 | state: object, 16 | prefix: ?PathType, 17 | notify: ?(path: string, isSynced: ?bool) => void 18 | ) { 19 | this._state = state || {} 20 | this._prefix = prefix || [] 21 | this.setNotify(notify) 22 | 23 | this.commit = ::this.commit 24 | this.get = ::this.get 25 | this.set = ::this.set 26 | this.select = ::this.select 27 | this.apply = ::this.apply 28 | this.assign = ::this.assign 29 | this.toJSON = ::this.toJSON 30 | this.diff = ::this.diff 31 | this.patch = ::this.patch 32 | } 33 | 34 | setNotify(notify: (path: string, isSynced: ?bool) => void) { 35 | this.__notify = notify 36 | } 37 | 38 | _update(isSynced) { 39 | this.__notify(this._prefix, isSynced) 40 | } 41 | 42 | commit() { 43 | this._update(true) 44 | return this 45 | } 46 | 47 | select(path: PathType = []): AbstractCursor { 48 | return new this.constructor( 49 | this._state, 50 | this._prefix.concat(path), 51 | this.__notify 52 | ) 53 | } 54 | 55 | /* eslint-disable no-unused-vars */ 56 | toJSON(): string { 57 | throw new Error('implement') 58 | } 59 | 60 | snap(): object { 61 | throw new Error('implement') 62 | } 63 | 64 | diff(prevState: object): object { 65 | throw new Error('implement') 66 | } 67 | 68 | patch(patches: Array) { 69 | throw new Error('implement') 70 | } 71 | 72 | get(): State { 73 | throw new Error('implement') 74 | } 75 | 76 | set(newState: State) { 77 | throw new Error('implement') 78 | } 79 | 80 | apply(fn: (v: State) => State) { 81 | throw new Error('implement') 82 | } 83 | 84 | assign(newState: State) { 85 | throw new Error('implement') 86 | } 87 | /* eslint-enable no-unused-vars */ 88 | } 89 | -------------------------------------------------------------------------------- /src/__tests__/cacheTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import assert from 'power-assert' 3 | import Container from '../container' 4 | import NativeCursor from '../cursors/native' 5 | import {Factory} from '../define' 6 | import sinon from 'sinon' 7 | 8 | describe('cache', () => { 9 | it('should hit, if no changes', () => { 10 | const cursor = new NativeCursor({ 11 | a: { 12 | b: 123 13 | } 14 | }) 15 | const container = new Container(cursor) 16 | const fn = sinon.spy(v => v) 17 | const MyDep = Factory([['a', 'b']])(fn) 18 | container.get(MyDep) 19 | container.get(MyDep) 20 | assert(fn.calledOnce) 21 | }) 22 | 23 | it('should not hit, if a.b changed', () => { 24 | const cursor = new NativeCursor({ 25 | a: { 26 | b: 123 27 | } 28 | }) 29 | const container = new Container(cursor) 30 | const fn = sinon.spy(v => v) 31 | const MyDep = Factory([['a', 'b']])(fn) 32 | container.get(MyDep) 33 | cursor.select(['a', 'b']).set(321).commit() 34 | container.get(MyDep) 35 | assert(fn.calledTwice) 36 | assert(fn.firstCall.calledWith(123)) 37 | assert(fn.secondCall.calledWith(321)) 38 | }) 39 | 40 | it('should hit, if a.c changed', () => { 41 | const cursor = new NativeCursor({ 42 | a: { 43 | b: 123, 44 | c: 'test' 45 | } 46 | }) 47 | const container = new Container(cursor) 48 | const fn = sinon.spy(v => v) 49 | const MyDep = Factory([['a', 'b']])(fn) 50 | container.get(MyDep) 51 | cursor.select(['a', 'c']).set('test2').commit() 52 | container.get(MyDep) 53 | assert(fn.calledOnce) 54 | }) 55 | 56 | it('should not hit, if a changed', () => { 57 | const cursor = new NativeCursor({ 58 | a: { 59 | b: 123, 60 | c: 'test' 61 | } 62 | }) 63 | const container = new Container(cursor) 64 | const fn = sinon.spy(v => v) 65 | const MyDep = Factory([['a', 'b']])(fn) 66 | container.get(MyDep) 67 | cursor.select(['a']).set({ 68 | b: 123, 69 | c: 'test2' 70 | }).commit() 71 | container.get(MyDep) 72 | assert(fn.calledTwice) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable-di", 3 | "version": "1.3.34", 4 | "description": "Simple, promise-based dependency injection container with some state handling functions (for facebook flux-like state-management)", 5 | "publishConfig": { 6 | "registry": "https://registry.npmjs.org" 7 | }, 8 | "scripts": { 9 | "postinstall": "cp -f conf/git-hooks/* .git/hooks || exit 0", 10 | "prepublish": "npm run lint && npm run test && npm run build", 11 | "pre-commit": "npm run lint", 12 | "update": "ncu -ua && npm install", 13 | "pull": "git checkout master && git pull origin master", 14 | "push": "git push origin master --tags && npm publish", 15 | "release-patch": "npm run pull && npm version patch && npm run push", 16 | "release-minor": "npm run pull && npm version minor && npm run push", 17 | "release-major": "npm run pull && npm version major && npm run push", 18 | "clean": "rm -rf cursors utils __tests__ *.js *.map build", 19 | "build": "npm run clean && babel src --source-maps --out-dir .", 20 | "deploy": "npm run clean && babel src --source-maps --watch", 21 | "prod": "npm run build -- --production", 22 | "dev": "npm run build -- --watch", 23 | "lint": "exit 0 && eslint src", 24 | "test": "mocha", 25 | "test.dev": "npm run test -- --growl --watch", 26 | "test.cov": "babel-istanbul cover --report text --report html node_modules/mocha/bin/_mocha" 27 | }, 28 | "author": "Stefan Zerkalica ", 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "http://github.com/zerkalica/immutable-di.git" 33 | }, 34 | "keywords": [ 35 | "immutable-di", 36 | "dependency injection", 37 | "di", 38 | "modular", 39 | "state", 40 | "functional", 41 | "immutable", 42 | "hot", 43 | "live", 44 | "replay", 45 | "flux", 46 | "elm" 47 | ], 48 | "files": [ 49 | "*.map", 50 | "*.js", 51 | "README.md", 52 | "LICENSE", 53 | "cursors", 54 | "define", 55 | "react", 56 | "utils" 57 | ], 58 | "dependencies": { 59 | "babel-runtime": "^5.8.24" 60 | }, 61 | "devDependencies": { 62 | "babel": "^5.8.23", 63 | "babel-core": "^5.8.24", 64 | "babel-eslint": "^4.1.2", 65 | "babel-plugin-espower": "^1.0.0", 66 | "eslint": "^1.4.3", 67 | "eslint-config-airbnb": "0.0.8", 68 | "eslint-config-airplus": "^1.0.4", 69 | "eslint-plugin-react": "^3.3.2", 70 | "glob": "^5.0.14", 71 | "mocha": "^2.3.2", 72 | "power-assert": "^1.0.1", 73 | "sinon": "^1.16.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/updateIdsMap.js: -------------------------------------------------------------------------------- 1 | type IPathToIdsMap = {[path: string]: Array} 2 | type IIdToPathsMap = {[id: string]: Array} 3 | 4 | class PathMapUpdater { 5 | _pathsSets: {[id: string]: {[path: string]: bool}} = {} 6 | _ids: Array = [] 7 | 8 | constructor(pathToIdsMap: IPathToIdsMap, idToPathsMap: IIdToPathsMap) { 9 | this._pathToIdsMap = pathToIdsMap 10 | this._idToPathsMap = idToPathsMap 11 | } 12 | 13 | isAffected(id: string) { 14 | const pth: ?Array = this._idToPathsMap[id] 15 | if (pth) { 16 | for (let i = 0; i < pth.length; i++) { 17 | this.addPath(pth[i]) 18 | } 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | 25 | begin(id: string) { 26 | this._ids.push(id) 27 | this._pathsSets[id] = {} 28 | } 29 | 30 | addPath(pathKey: string) { 31 | const ids = this._ids 32 | const pathsSets = this._pathsSets 33 | for (let i = 0; i < ids.length; i++) { 34 | pathsSets[ids[i]][pathKey] = true 35 | } 36 | } 37 | 38 | end(id: string) { 39 | const paths = Object.keys(this._pathsSets[id]) 40 | const pathToIdsMap = this._pathToIdsMap 41 | this._idToPathsMap[id] = paths 42 | for (let i = 0; i < paths.length; i++) { 43 | const k = paths[i] 44 | if (!pathToIdsMap[k]) { 45 | pathToIdsMap[k] = [] 46 | } 47 | pathToIdsMap[k].push(id) 48 | } 49 | delete this._pathsSets[id] 50 | this._ids.pop() 51 | } 52 | } 53 | 54 | function dependencyScanner(definition, acc: PathMapUpdater) { 55 | const {id, deps} = definition.__di 56 | if (!acc.isAffected(id)) { 57 | acc.begin(id) 58 | for (let i = 0; i < deps.length; i++) { 59 | const dep = deps[i].definition 60 | const {path} = dep.__di 61 | if (path) { 62 | let key = '' 63 | const p = path.concat('*') 64 | for (let j = 0, l = p.length; j < l; j++) { 65 | key = key + '.' + p[j] 66 | acc.addPath(key) 67 | } 68 | } else { 69 | dependencyScanner(dep, acc) 70 | } 71 | } 72 | acc.end(id) 73 | } 74 | } 75 | 76 | export default function updateIdsMap( 77 | definition, 78 | pathToIdsMap: IPathToIdsMap, 79 | idToPathsMap: IIdToPathsMap 80 | ) { 81 | dependencyScanner(definition, new PathMapUpdater(pathToIdsMap, idToPathsMap)) 82 | } 83 | -------------------------------------------------------------------------------- /src/__tests__/eventsTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import {Factory} from '../define' 3 | import assert from 'power-assert' 4 | import Container from '../container' 5 | import NativeCursor from '../cursors/native' 6 | import sinon from 'sinon' 7 | 8 | describe('events', () => { 9 | it('should update mounted listener', () => { 10 | const cursor = new NativeCursor({ 11 | a: { 12 | b: 123 13 | } 14 | }) 15 | const container = new Container(cursor) 16 | const fn = sinon.spy(v => v) 17 | const MyDep = Factory([['a', 'b']])(fn) 18 | container.mount(MyDep) 19 | cursor.select(['a', 'b']).set(321).commit() 20 | cursor.select(['a', 'b']).set(333).commit() 21 | assert(fn.calledTwice) 22 | assert(MyDep.firstCall.calledWith(321)) 23 | assert(MyDep.secondCall.calledWith(333)) 24 | }) 25 | 26 | it('should not update listener, if changed another path', () => { 27 | const cursor = new NativeCursor({ 28 | a: { 29 | b: 123, 30 | c: 111 31 | } 32 | }) 33 | const container = new Container(cursor) 34 | const fn = sinon.spy(v => v) 35 | const MyDep = Factory([['a', 'b']])(fn) 36 | container.mount(MyDep) 37 | cursor.select(['a', 'c']).set(321).commit() 38 | assert(fn.notCalled) 39 | }) 40 | 41 | it('should call listener once', () => { 42 | const cursor = new NativeCursor({ 43 | a: { 44 | b: 123, 45 | c: 111 46 | } 47 | }) 48 | const container = new Container(cursor) 49 | const fn = sinon.spy(v => v) 50 | const MyDep = Factory([['a', 'b']])(fn) 51 | container.once([['a', 'b']], MyDep) 52 | cursor.select(['a', 'b']).set(321).commit() 53 | cursor.select(['a', 'b']).set(432).commit() 54 | assert(fn.calledOnce) 55 | assert(fn.calledWith(321)) 56 | }) 57 | 58 | it('should not update unmounted listener', () => { 59 | const cursor = new NativeCursor({ 60 | a: { 61 | b: 123 62 | } 63 | }) 64 | const container = new Container(cursor) 65 | const fn = sinon.spy(v => v) 66 | const MyDep = Factory([['a', 'b']])(fn) 67 | 68 | container.mount(MyDep) 69 | cursor.select(['a', 'b']).set(321).commit() 70 | container.unmount(MyDep) 71 | cursor.select(['a', 'b']).set(333).commit() 72 | 73 | assert(fn.calledOnce) 74 | assert(fn.calledWith(321)) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /src/__tests__/stateChangesTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import assert from 'power-assert' 3 | import Container from '../container' 4 | import NativeCursor from '../cursors/native' 5 | import {Factory} from '../define' 6 | import sinon from 'sinon' 7 | 8 | describe('state changes', () => { 9 | it('should handle a.b, if a changed', () => { 10 | const cursor = new NativeCursor({ 11 | a: { 12 | b: 123 13 | } 14 | }) 15 | const container = new Container(cursor) 16 | const MyDep = Factory([['a', 'b']])(v => v) 17 | container.get(MyDep) 18 | cursor.select(['a']).set({b: 321}).commit() 19 | assert.equal(container.get(MyDep), 321) 20 | }) 21 | 22 | it('should handle a, if a.b changed', () => { 23 | const cursor = new NativeCursor({ 24 | a: { 25 | b: 123 26 | } 27 | }) 28 | const container = new Container(cursor) 29 | const MyDep = Factory([['a']])(v => v) 30 | container.get(MyDep) 31 | cursor.select(['a', 'b']).set(321).commit() 32 | assert.deepEqual(container.get(MyDep), { 33 | b: 321 34 | }) 35 | }) 36 | 37 | it('should not handle a.c, if a.b changed', () => { 38 | const cursor = new NativeCursor({ 39 | a: { 40 | b: 123, 41 | c: 'test' 42 | } 43 | }) 44 | const container = new Container(cursor) 45 | const MyDep = Factory([['a', 'c']])(v => v) 46 | container.get(MyDep) 47 | cursor.select(['a', 'b']).set(321).commit() 48 | assert(container.get(MyDep) === 'test') 49 | }) 50 | 51 | it('should handle a.b, if a.b changed', () => { 52 | const cursor = new NativeCursor({ 53 | a: { 54 | b: 123 55 | } 56 | }) 57 | const container = new Container(cursor) 58 | const MyDep = Factory([['a', 'b']])(v => v) 59 | container.get(MyDep) 60 | cursor.select(['a', 'b']).set(321).commit() 61 | assert(container.get(MyDep) === 321) 62 | }) 63 | 64 | it('should update state on next timer tick', done => { 65 | const cursor = new NativeCursor({ 66 | a: { 67 | b: 123 68 | } 69 | }) 70 | const container = new Container(cursor) 71 | const fn = sinon.spy(v => v) 72 | const MyDep = Factory([['a', 'b']])(fn) 73 | container.get(MyDep) 74 | cursor.select(['a', 'b']).set(321) 75 | setTimeout(() => { 76 | container.get(MyDep) 77 | assert(MyDep.calledTwice) 78 | assert(MyDep.firstCall.calledWith(123)) 79 | assert(MyDep.secondCall.calledWith(321)) 80 | done() 81 | }, 0) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /src/cursors/native.js: -------------------------------------------------------------------------------- 1 | import AbstractCursor from './abstract' 2 | 3 | export default class NativeCursor extends AbstractCursor { 4 | _selector = null 5 | _cnName = null 6 | constructor( 7 | state: object, 8 | prefix: ?PathType, 9 | notify: ?(path: string, isSynced: ?bool) => void 10 | ) { 11 | super(state, prefix, notify) 12 | if (this._prefix.length) { 13 | this._cnName = prefix[prefix.length - 1] 14 | /* eslint-disable no-new-func */ 15 | this._selector = new Function('s', 'return ' + ['s'] 16 | .concat(this._prefix) 17 | .slice(0, -1) 18 | .join('.') 19 | ) 20 | try { 21 | if (this._selector(this._state) === undefined) { 22 | throw new Error('undefined value ' + this._prefix[this.prefix.length - 1]) 23 | } 24 | } catch(e) { 25 | e.message = e.message + ', path: ' + this._prefix.join('.') 26 | throw e 27 | } 28 | /* eslint-enable no-new-func */ 29 | } else { 30 | this._cnName = '_state' 31 | // trick to obtain this._state in get/set/apply/assign 32 | this._selector = function __selector() { 33 | return this 34 | } 35 | } 36 | } 37 | 38 | toJSON(): string { 39 | return JSON.stringify(this._state) 40 | } 41 | 42 | snap(): object { 43 | return JSON.parse(JSON.stringify(this._state)) 44 | } 45 | 46 | diff(prevState: object): object { 47 | return {} 48 | } 49 | 50 | patch(patches: Array) { 51 | throw new Error('implement') 52 | } 53 | 54 | get() { 55 | return this._selector(this._state)[this._cnName] 56 | } 57 | 58 | set(newState) { 59 | const node = this._selector(this._state) 60 | if (newState !== node[this._cnName]) { 61 | let isUpdated = false 62 | if (node === this) { 63 | Object.keys(newState).forEach(k => { 64 | if (newState[k] !== this._state[k]) { 65 | isUpdated = true 66 | this._state[k] = newState[k] 67 | } 68 | }) 69 | } else if (typeof newState === 'object') { 70 | isUpdated = JSON.stringify(newState) !== JSON.stringify(node[this._cnName]) 71 | } else { 72 | isUpdated = true 73 | } 74 | 75 | if (isUpdated) { 76 | node[this._cnName] = newState 77 | this._update() 78 | } 79 | } 80 | 81 | return this 82 | } 83 | 84 | apply(fn: (v: State) => State) { 85 | this.set(fn(this.get())) 86 | return this 87 | } 88 | 89 | assign(newState) { 90 | const node = this._selector(this._state)[this._cnName] 91 | let isUpdated = false 92 | const keys = Object.keys(newState) 93 | for (let i = 0, j = keys.length; i < j; i++) { 94 | const k = keys[i] 95 | if (node[k] !== newState[k]) { 96 | node[k] = newState[k] 97 | isUpdated = true 98 | } 99 | } 100 | if (isUpdated) { 101 | this._update() 102 | } 103 | 104 | return this 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/define.js: -------------------------------------------------------------------------------- 1 | import AbstractCursor from './cursors/abstract' 2 | import Dep, {getId} from './utils/Dep' 3 | import {IPath} from './asserts' 4 | 5 | const ids = {} 6 | 7 | function pass(p) { 8 | return p 9 | } 10 | 11 | function convertId(dn) { 12 | if (!ids[dn]) { 13 | ids[dn] = getId() 14 | } 15 | 16 | return ids[dn] 17 | } 18 | 19 | export function Getter(path) { 20 | IPath(path) 21 | const key = path.join('.') 22 | const displayName = 'get_' + key 23 | function getter(cursor) { 24 | return cursor.select(path).get 25 | } 26 | return Getter.extend(Dep({ 27 | deps: [AbstractCursor], 28 | displayName, 29 | id: convertId(displayName) 30 | }))(getter) 31 | } 32 | Getter.extend = pass 33 | 34 | export function Path(path) { 35 | IPath(path) 36 | const key = path.join('.') 37 | const displayName = 'path_' + key 38 | function getData(get) { 39 | return get() 40 | } 41 | 42 | return Path.extend(Dep({ 43 | deps: [Getter(path)], 44 | displayName, 45 | id: convertId(displayName), 46 | isCachedTemporary: true, 47 | path 48 | }))(getData) 49 | } 50 | Path.extend = pass 51 | 52 | export function Assign(path) { 53 | IPath(path) 54 | const key = path.join('.') 55 | const displayName = 'assign_' + key 56 | function assigner(cursor) { 57 | return cursor.select(path).assign 58 | } 59 | 60 | return Assign.extend(Dep({ 61 | deps: [AbstractCursor], 62 | displayName, 63 | id: convertId(displayName), 64 | isSetter: true 65 | }))(assigner) 66 | } 67 | Assign.extend = pass 68 | 69 | export function Setter(path) { 70 | IPath(path) 71 | const key = path.join('.') 72 | const displayName = 'setter_' + key 73 | function setter(cursor) { 74 | return cursor.select(path).set 75 | } 76 | 77 | return Setter.extend(Dep({ 78 | deps: [AbstractCursor], 79 | displayName, 80 | id: convertId(displayName), 81 | isSetter: true 82 | }))(setter) 83 | } 84 | Setter.extend = pass 85 | 86 | export function Apply(path) { 87 | IPath(path) 88 | const key = path.join('.') 89 | const displayName = 'apply_' + key 90 | function setter(cursor) { 91 | return cursor.select(path).apply 92 | } 93 | 94 | return Apply.extend(Dep({ 95 | deps: [AbstractCursor], 96 | displayName, 97 | id: convertId(displayName), 98 | isSetter: true 99 | }))(setter) 100 | } 101 | Apply.extend = pass 102 | 103 | export function Def(data) { 104 | const displayName = 'def_' + JSON.stringify(data) 105 | function def() { 106 | return data 107 | } 108 | 109 | return Def.extend(Dep({ 110 | displayName, 111 | id: convertId(displayName) 112 | }))(def) 113 | } 114 | Def.extend = pass 115 | 116 | export function Class(deps, displayName) { 117 | return Class.extend(Dep({ 118 | deps, 119 | displayName, 120 | isClass: true, 121 | pathMapper: Path 122 | })) 123 | } 124 | Class.extend = pass 125 | 126 | export function Facet(deps, displayName) { 127 | return Facet.extend(Dep({ 128 | deps, 129 | displayName, 130 | isCachedTemporary: true, 131 | pathMapper: Path 132 | })) 133 | } 134 | Facet.extend = pass 135 | 136 | export function Factory(deps, displayName) { 137 | return Factory.extend(Dep({ 138 | deps, 139 | displayName, 140 | pathMapper: Path 141 | })) 142 | } 143 | Factory.extend = pass 144 | -------------------------------------------------------------------------------- /src/__tests__/containerTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import assert from 'power-assert' 3 | import Container from '../container' 4 | import NativeCursor from '../cursors/native' 5 | import {Factory, Class} from '../define' 6 | import sinon from 'sinon' 7 | 8 | describe('container', () => { 9 | describe('basics', () => { 10 | it('should throws exception if incorrect data passed to constructor', () => { 11 | assert.throws(() => new Container(), /instance/) 12 | }) 13 | }) 14 | 15 | describe('get', () => { 16 | it('should throws exception if no arguments passed', () => { 17 | const cursor = new NativeCursor({}) 18 | const container = new Container(cursor) 19 | assert.throws(() => container.get()) 20 | }) 21 | 22 | it('should throws exception if no decorated function passed', () => { 23 | const cursor = new NativeCursor({}) 24 | const container = new Container(cursor) 25 | function WrongDep() { 26 | } 27 | 28 | assert.throws(() => container.get(WrongDep)) 29 | }) 30 | 31 | /* eslint-disable padded-blocks */ 32 | it('should return class instance', () => { 33 | const cursor = new NativeCursor({}) 34 | const container = new Container(cursor) 35 | @Class() 36 | class Test { 37 | } 38 | const instance = container.get(Test) 39 | assert(instance instanceof Test) 40 | }) 41 | /* eslint-enable padded-blocks */ 42 | 43 | /* eslint-disable padded-blocks */ 44 | it('should cache class instance', () => { 45 | const cursor = new NativeCursor({}) 46 | const container = new Container(cursor) 47 | @Class() 48 | class TestBase { 49 | 50 | } 51 | const Test = sinon.spy(TestBase) 52 | 53 | 54 | const instance1 = container.get(Test) 55 | const instance2 = container.get(Test) 56 | 57 | assert.strictEqual(instance1, instance2) 58 | assert(Test.calledOnce) 59 | }) 60 | /* eslint-enable padded-blocks */ 61 | 62 | it('should cache factory return value', () => { 63 | const cursor = new NativeCursor({}) 64 | const container = new Container(cursor) 65 | const MyDep = Factory()(function _MyDep() { 66 | return 123 67 | }) 68 | 69 | const instance1 = container.get(MyDep) 70 | assert.strictEqual(instance1, 123) 71 | }) 72 | 73 | it('should handle simple deps from array definition', () => { 74 | const cursor = new NativeCursor({}) 75 | const container = new Container(cursor) 76 | const MyDep = Factory()(function _MyDep() { 77 | return 123 78 | }) 79 | 80 | @Class([MyDep]) 81 | class Test { 82 | } 83 | const TestFake = sinon.spy(Test) 84 | container.get(TestFake) 85 | assert(TestFake.calledWith(123)) 86 | }) 87 | 88 | it('should handle simple deps from object definition', () => { 89 | const cursor = new NativeCursor({}) 90 | const container = new Container(cursor) 91 | const MyDep = Factory()(function _MyDep() { 92 | return 123 93 | }) 94 | 95 | @Class({fac: MyDep}) 96 | class Test { 97 | } 98 | const TestFake = sinon.spy(Test) 99 | container.get(TestFake) 100 | assert(TestFake.calledWith({fac: 123})) 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /src/utils/__tests__/updateIdsMapTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import assert from 'power-assert' 3 | import {Factory} from '../../define' 4 | import updateIdsMap from '../updateIdsMap' 5 | 6 | function cmpArr(a1, a2) { 7 | return JSON.stringify(a1.sort()) === JSON.stringify(a2.sort()) 8 | } 9 | 10 | describe('updateIdsMap', () => { 11 | it('B->a, A->a, B->A: to a: [A, B]', () => { 12 | const pathToIdsMap = {} 13 | const idToPathsMap = {} 14 | const DepA = Factory([['a']])(v => v) 15 | const DepB = Factory([DepA, ['a']])(v => v) 16 | DepA.__di.id = 'A' 17 | DepB.__di.id = 'B' 18 | 19 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 20 | assert.deepEqual(pathToIdsMap, { 21 | '.a': [ 'A', 'B' ], 22 | '.a.*': [ 'A', 'B' ] 23 | }) 24 | }) 25 | 26 | it('A->C, A->B, B->C, C->a, B->b to a: [A, B, C], b: [A, B]', () => { 27 | const pathToIdsMap = {} 28 | const idToPathsMap = {} 29 | const DepC = Factory([['a']])(v => v) 30 | const DepB = Factory([DepC, ['b']])(v => v) 31 | const DepA = Factory([DepC, DepB])(v => v) 32 | DepA.__di.id = 'A' 33 | DepB.__di.id = 'B' 34 | DepC.__di.id = 'C' 35 | 36 | updateIdsMap(DepA, pathToIdsMap, idToPathsMap) 37 | assert.deepEqual(pathToIdsMap, { 38 | '.a': [ 'C', 'B', 'A' ], 39 | '.a.*': [ 'C', 'B', 'A' ], 40 | '.b': [ 'B', 'A' ], 41 | '.b.*': [ 'B', 'A' ] 42 | }) 43 | }) 44 | 45 | it('A->C, A->B, B->C, C->a, B->b, then add D->B to a: [A, B, C, D], b: [A, B, D]', () => { 46 | const pathToIdsMap = {} 47 | const idToPathsMap = {} 48 | const DepC = Factory([['a']])(v => v) 49 | const DepB = Factory([DepC, ['b']])(v => v) 50 | const DepA = Factory([DepC, DepB])(v => v) 51 | const DepD = Factory([DepB])(v => v) 52 | DepA.__di.id = 'A' 53 | DepB.__di.id = 'B' 54 | DepC.__di.id = 'C' 55 | DepD.__di.id = 'D' 56 | 57 | updateIdsMap(DepA, pathToIdsMap, idToPathsMap) 58 | updateIdsMap(DepD, pathToIdsMap, idToPathsMap) 59 | 60 | assert.deepEqual(pathToIdsMap, { 61 | '.a': [ 'C', 'B', 'A', 'D' ], 62 | '.a.*': [ 'C', 'B', 'A', 'D' ], 63 | '.b': [ 'B', 'A', 'D' ], 64 | '.b.*': [ 'B', 'A', 'D' ] 65 | }) 66 | }) 67 | 68 | it('B->A, C->A, A->a to a: [A, B, C]', () => { 69 | const pathToIdsMap = {} 70 | const idToPathsMap = {} 71 | const DepA = Factory([['a']])(v => v) 72 | const DepB = Factory([DepA])(v => v) 73 | const DepC = Factory([DepA])(v => v) 74 | DepA.__di.id = 'A' 75 | DepB.__di.id = 'B' 76 | DepC.__di.id = 'C' 77 | 78 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 79 | updateIdsMap(DepC, pathToIdsMap, idToPathsMap) 80 | 81 | assert.deepEqual(pathToIdsMap, { 82 | '.a': [ 'A', 'B', 'C' ], 83 | '.a.*': [ 'A', 'B', 'C' ] 84 | }) 85 | }) 86 | 87 | it('B->A, C->B, A->a to a: [A, B, C]', () => { 88 | const pathToIdsMap = {} 89 | const idToPathsMap = {} 90 | const DepA = Factory([['a']])(v => v) 91 | const DepB = Factory([DepA])(v => v) 92 | const DepC = Factory([DepB])(v => v) 93 | DepA.__di.id = 'A' 94 | DepB.__di.id = 'B' 95 | DepC.__di.id = 'C' 96 | 97 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 98 | updateIdsMap(DepC, pathToIdsMap, idToPathsMap) 99 | 100 | assert.deepEqual(pathToIdsMap, { 101 | '.a': [ 'A', 'B', 'C' ], 102 | '.a.*': [ 'A', 'B', 'C' ] 103 | }) 104 | }) 105 | 106 | it('B->A, A->a.b, B->a to a: [A, B], a.b: [A, B]', () => { 107 | const pathToIdsMap = {} 108 | const idToPathsMap = {} 109 | const DepA = Factory([['a', 'b']])(v => v) 110 | const DepB = Factory([DepA, ['a']])(v => v) 111 | DepA.__di.id = 'A' 112 | DepB.__di.id = 'B' 113 | 114 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 115 | 116 | assert.deepEqual(pathToIdsMap, { 117 | '.a': [ 'A', 'B' ], 118 | '.a.b': [ 'A', 'B' ], 119 | '.a.b.*': [ 'A', 'B' ], 120 | '.a.*': [ 'B' ] 121 | }) 122 | }) 123 | 124 | it('B->a, A->a.b, C->a.c to a: [A, B, C], a.b: [A], a.c: [C]', () => { 125 | const pathToIdsMap = {} 126 | const idToPathsMap = {} 127 | const DepA = Factory([['a', 'b']])(v => v) 128 | const DepB = Factory([['a']])(v => v) 129 | const DepC = Factory([['a', 'c']])(v => v) 130 | DepA.__di.id = 'A' 131 | DepB.__di.id = 'B' 132 | DepC.__di.id = 'C' 133 | 134 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 135 | updateIdsMap(DepA, pathToIdsMap, idToPathsMap) 136 | updateIdsMap(DepC, pathToIdsMap, idToPathsMap) 137 | assert.deepEqual(pathToIdsMap, { 138 | '.a': [ 'B', 'A', 'C' ], 139 | '.a.*': [ 'B' ], 140 | '.a.b': [ 'A' ], 141 | '.a.b.*': [ 'A' ], 142 | '.a.c': [ 'C' ], 143 | '.a.c.*': [ 'C' ] 144 | }) 145 | }) 146 | 147 | it('B->A, A->a, B->b to a: [A, B], b: [B]', () => { 148 | const pathToIdsMap = {} 149 | const idToPathsMap = {} 150 | const DepA = Factory([['a']])(v => v) 151 | const DepB = Factory([DepA, ['b']])(v => v) 152 | DepA.__di.id = 'A' 153 | DepB.__di.id = 'B' 154 | 155 | updateIdsMap(DepB, pathToIdsMap, idToPathsMap) 156 | assert.deepEqual(pathToIdsMap, { 157 | '.a': [ 'A', 'B' ], 158 | '.a.*': [ 'A', 'B' ], 159 | '.b': [ 'B' ], 160 | '.b.*': [ 'B' ] 161 | }) 162 | }) 163 | }) 164 | /* 165 | A - a.b.c, B - a, C - a.b.d, D - a.b 166 | 167 | a -> ABCD 168 | a.b -> ABCD 169 | a.b.c -> ABD 170 | a.b.d -> BCD 171 | 172 | 173 | */ 174 | -------------------------------------------------------------------------------- /src/container.js: -------------------------------------------------------------------------------- 1 | import {cancelAnimationFrame, requestAnimationFrame} from './utils/animationFrame' 2 | import {Facet} from './define' 3 | import {IDep} from './asserts' 4 | import AbstractCursor from './cursors/abstract' 5 | import getFunctionName from './utils/getFunctionName' 6 | import type {IDependency} from './utils/Dep' 7 | import updateIdsMap from './utils/updateIdsMap' 8 | 9 | export default class Container { 10 | _cache: Map = {} 11 | _listeners: Array = [] 12 | _affectedPaths = [] 13 | _timerId = null 14 | _definitionMap = {} 15 | 16 | __pathToIdsMap = {} 17 | __idToPathsMap = {} 18 | _isSynced = false 19 | 20 | constructor(state: AbstractCursor, options: ?{isSynced: bool}) { 21 | if (!(state instanceof AbstractCursor)) { 22 | throw new TypeError('state is not an instance of AbstractCursor: ' + state) 23 | } 24 | this.get = ::this.get 25 | this.once = ::this.once 26 | this.mount = ::this.mount 27 | this.unmount = ::this.unmount 28 | this.notify = ::this.notify 29 | this.__notify = ::this.__notify 30 | // Store instance of AbstractCursor, our decorators uses them for Setter/Getter factories 31 | this._cache[AbstractCursor.__di.id] = state 32 | state.setNotify(this.notify) 33 | if (options) { 34 | this._isSynced = options.isSynced 35 | } 36 | } 37 | 38 | override(fromDefinition: IDependency, toDefinition: IDependency) { 39 | IDep(fromDefinition) 40 | IDep(toDefinition) 41 | this._definitionMap[fromDefinition.__di.id] = toDefinition 42 | } 43 | 44 | _updatePathMap(definition) { 45 | if (!this.__idToPathsMap[definition.__di.id]) { 46 | updateIdsMap(definition, this.__pathToIdsMap, this.__idToPathsMap) 47 | } 48 | } 49 | 50 | _clear(path: string[]) { 51 | let key = '' 52 | if (path.length === 0) { 53 | const cursorId = AbstractCursor.__di.id 54 | const cursorInstance = this._cache[cursorId] 55 | this._cache = { 56 | [cursorId]: cursorInstance 57 | } 58 | return 59 | } 60 | 61 | for (let j = 0, l = path.length - 1; j <= l; j++) { 62 | key = key + '.' + path[j] 63 | const k = key + (j === l ? '' : '.*') 64 | const idsMap = this.__pathToIdsMap[k] || [] 65 | for (let i = 0, m = idsMap.length; i < m; i++) { 66 | delete this._cache[idsMap[i]] 67 | } 68 | } 69 | } 70 | 71 | __notify() { 72 | const paths = this._affectedPaths 73 | for (let i = 0; i < paths.length; i++) { 74 | this._clear(paths[i]) 75 | } 76 | 77 | const listeners = this._listeners 78 | for (let i = 0, j = listeners.length; i < j; i++) { 79 | this.get(listeners[i]) 80 | } 81 | cancelAnimationFrame(this._timerId) 82 | this._affectedPaths = [] 83 | this._timerId = null 84 | } 85 | 86 | notify(path: string[], isSynced: ?bool) { 87 | this._affectedPaths.push(path) 88 | if (isSynced === undefined ? this._isSynced : isSynced) { 89 | this.__notify() 90 | } else if (!this._timerId) { 91 | this._timerId = requestAnimationFrame(this.__notify) 92 | } 93 | } 94 | 95 | mount(definition: IDependency) { 96 | IDep(definition) 97 | // do not call listener on another state change 98 | this._cache[definition.__di.id] = null 99 | this._updatePathMap(definition) 100 | this._listeners.push(definition) 101 | } 102 | 103 | unmount(listenerDef: IDependency) { 104 | IDep(listenerDef) 105 | this._cache[listenerDef.__di.id] = null 106 | this._listeners = this._listeners.filter(d => listenerDef !== d) 107 | } 108 | 109 | once(stateMap: object, listener: (v: any) => any, displayName: ?string) { 110 | const definition = Facet(stateMap, displayName || getFunctionName(listener))((...args) => { 111 | this.unmount(definition) 112 | return listener(...args) 113 | }) 114 | this.mount(definition) 115 | } 116 | 117 | _get(definition: IDependency, tempCache: object, debugCtx: Array): any { 118 | if (!definition || !definition.__di) { 119 | throw new Error('Property .__id not exist in ' + debugCtx) 120 | } 121 | const {id, isCachedTemporary} = definition.__di 122 | const cache = isCachedTemporary ? tempCache : this._cache 123 | let result = cache[id] 124 | if (result === undefined) { 125 | const fn = this._definitionMap[id] || definition 126 | this._updatePathMap(fn) 127 | const {displayName, deps, isClass, isOptions} = fn.__di 128 | const args = {} 129 | const defArgs = isOptions ? [args] : [] 130 | for (let i = 0, j = deps.length; i < j; i++) { 131 | const dep = deps[i] 132 | const value = this._get( 133 | dep.definition, 134 | tempCache, 135 | debugCtx.concat([displayName, i]) 136 | ) 137 | if (isOptions) { 138 | args[dep.name] = value 139 | } else { 140 | defArgs.push(value) 141 | } 142 | } 143 | /* eslint-disable new-cap */ 144 | result = isClass 145 | ? new fn(...defArgs) 146 | : fn(...defArgs) 147 | /* eslint-enable new-cap */ 148 | if (result === undefined) { 149 | result = null 150 | } 151 | cache[id] = result 152 | } 153 | 154 | return result 155 | } 156 | 157 | get(definition: IDependency): any { 158 | IDep(definition) 159 | return this._get(definition, {}, []) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | immutable-di [![Build Status](https://secure.travis-ci.org/zerkalica/immutable-di.png)](http://travis-ci.org/zerkalica/immutable-di) 2 | ==================================================================================================================================== 3 | 4 | DEPRECATED, use [reactive-di](https://github.com/zerkalica/reactive-di) 5 | 6 | [![NPM](https://nodei.co/npm/immutable-di.png?downloads=true&stars=true)](https://nodei.co/npm/immutable-di/) 7 | 8 | Simple, dependency injection container with some state handling functions. 9 | 10 | General 11 | ------- 12 | 13 | - Install: `npm install --save immutable-di immutable-di-react` 14 | - Tests: `npm test` 15 | - Examples: `npm run dev.examples` 16 | 17 | Why not \*-flux? 18 | ---------------- 19 | 20 | Our main focus make Flux-like API as less and simple as possible. Which with less words you can express more. The ideas behind similar to the [redux](https://github.com/gaearon/redux), [baobab](https://github.com/Yomguithereal/baobab), [nuclear-js](https://github.com/optimizely/nuclear-js), but implementation based on dependency injection. And of course you can use dependency injection as standalone. 21 | 22 | Usecases 23 | -------- 24 | 25 | React all-in example 26 | -------------------- 27 | 28 | ```js 29 | import {default as React, Component} from 'react'; 30 | import {Getter, Facet, Factory, Setter} from 'immutable-di/define' 31 | import root from 'immutable-di-react/root' 32 | import statefull from 'immutable-di-react/statefull' 33 | import Container from 'immutable-di' 34 | import NativeCursor from 'immutable-di/cursors/native' 35 | 36 | const cursor = new NativeCursor({ 37 | tis: { 38 | a: 1, 39 | b: 2 40 | } 41 | }) 42 | 43 | const container = new Container(cursor) 44 | 45 | var abbaFacet = Facet({ 46 | a: ['tis', 'a'] 47 | })(function bloomyFacet({a}) { 48 | return a + 10; 49 | }) 50 | 51 | 52 | var ChangeAction = Factory({ 53 | setA: Setter(['tis', 'a']), 54 | setIsLoading: Setter(['tis', 'isLoading']) 55 | })(function ({setA, setIsLoading}) { 56 | return function (num) { 57 | // Here will be all mutate state logic. for example server side request 58 | // communication with API layer and etc. 59 | setIsLoading(true); 60 | $.get('/server/route').then((data) => { 61 | setIsLoading(false); 62 | var a = num + data.a; 63 | setA(a); 64 | }) 65 | 66 | } 67 | }); 68 | 69 | @root() 70 | @statefull({ 71 | abba: abbaFacet, 72 | changeAction: ChangeAction 73 | }) 74 | class Application extends React.Component { 75 | handleClick () { 76 | this.props.changeAction(100); 77 | } 78 | render () { 79 | return
Bloom: {this.props.abba}
80 | } 81 | } 82 | 83 | 84 | export default function () { 85 | React.render(, document.querySelector('.app')); 86 | } 87 | ``` 88 | 89 | Define dependency 90 | ----------------- 91 | 92 | ```js 93 | import {Facet, Factory, Class} from 'immutable-di/define' 94 | // A, B - functions or classes with di definitions 95 | 96 | // For functions: 97 | Factory([A, B])(C) // resolve functions A, B and pass them as arguments to C 98 | Factory({a: A, b: B})(C) // resolve functions A, B and pass them as object {a, b} to C 99 | 100 | //Facet - same as Factory, but do not cache factory return value 101 | 102 | // For classes: 103 | @Class([A, B]) 104 | class C { 105 | constructor(a, b) { 106 | 107 | } 108 | } 109 | 110 | // or 111 | class C { 112 | constructor(a, b) { 113 | 114 | } 115 | } 116 | 117 | export default Class([A, B])(C) 118 | 119 | // or 120 | @Class({ 121 | a: A, 122 | b: B 123 | }) 124 | class C { 125 | constructor({a, b}) { 126 | } 127 | } 128 | 129 | // for State 130 | @Class([ 131 | A, 132 | B, 133 | options: ['config', 'c'] 134 | ]) 135 | class C { 136 | constructor(a, b, options) { 137 | 138 | } 139 | } 140 | ``` 141 | 142 | Working with state 143 | ------------------ 144 | 145 | ```js 146 | import Container from 'immutable-di' 147 | import NativeCursor from 'immutable-di/cursors/native' 148 | 149 | const cursor = new NativeCursor({ 150 | config: { 151 | logger: { 152 | opt1: 'test1' 153 | }, 154 | mod2: { 155 | opt1: 'test2' 156 | } 157 | } 158 | }) 159 | 160 | // define di container with state: 161 | const container = new Container(cursor) 162 | 163 | // dep 1: 164 | function MyFaset(state) { 165 | return 'data' 166 | } 167 | Factory()(MyFaset) 168 | 169 | // dep 2: 170 | function myHandler({state, faset}) { 171 | console.log(state, faset) 172 | } 173 | 174 | // bind listener: 175 | const listener = container.on({ 176 | state: ['config', 'logger'], 177 | faset: MyFaset 178 | }, myHandler) 179 | 180 | // trigger my hander 181 | cursor.select(['config', 'logger', 'opt1']).set('test') 182 | 183 | // path config.logger not affected, myHandler is not triggered 184 | cursor.select(['config', 'mod2', 'opt1']).set('1') 185 | 186 | // unbind listener: 187 | container.off(listener) 188 | ``` 189 | 190 | Di factory example 191 | ------------------ 192 | 193 | ```js 194 | import Container from 'immutable-di' 195 | import {Factory, Class} from 'immutable-di/define' 196 | import NativeCursor from 'immutable-di/cursors/native' 197 | 198 | const cursor = new NativeCursor({ 199 | config: { 200 | logger: { 201 | opt1: 'test1' 202 | } 203 | } 204 | }) 205 | const container = new Container(cursor) 206 | 207 | function ConsoleOutputDriver() { 208 | return function consoleOutputDriver(str) { 209 | console.log(str) 210 | } 211 | } 212 | Factory()(ConsoleOutputDriver) 213 | 214 | @Class([ 215 | ConsoleOutputDriver, 216 | ['config', 'logger'] 217 | ]) 218 | class Logger { 219 | constructor(outputDriver, dep, config) { 220 | this._outputDriver = outputDriver 221 | this._config = config 222 | } 223 | log(val) { 224 | this._outputDriver('val:' + val + ', opt:' + this._config.opt1) 225 | } 226 | } 227 | 228 | function SomeDep() { 229 | return 'dep' 230 | } 231 | Factory()(SomeDep) 232 | 233 | function App({logger, someDep}) { 234 | return function app(val) { 235 | logger.log(val + someDep) 236 | } 237 | } 238 | Factory({ 239 | logger: logger, 240 | someDep: SomeDep 241 | })(App) 242 | 243 | container.get(App)('test') // outputs: val: testdep, opt: test1 244 | ``` 245 | 246 | Cache example 247 | ------------- 248 | 249 | ```js 250 | import Container from 'immutable-di' 251 | import {Factory, Class} from 'immutable-di/define' 252 | import NativeCursor from 'immutable-di/cursors/native' 253 | 254 | const cursor = new NativeCursor({ 255 | config: { 256 | myModule: { 257 | opt1: 'test1' 258 | } 259 | } 260 | }) 261 | const container = new Container(cursor) 262 | 263 | function MyModule({opt1}) { 264 | console.log('init', opt1) 265 | 266 | return function myModule(val) { 267 | console.log('out', opt1, ', val', val) 268 | } 269 | } 270 | Factory([ 271 | ['config', 'myModule'] 272 | ])(MyModule) 273 | 274 | container.get(MyModule) // outputs init test1 275 | container.get(MyModule) // no outputs, return from cache 276 | const cursor = cursor.select(['config', 'myModule', 'opt1']) 277 | 278 | cursor.set('test2') // outputs test2 279 | container.get(MyModule) // no outputs: return from cache 280 | 281 | container.get(MyModule)('test3') // outputs out test2, val test3 282 | ``` 283 | 284 | React example 285 | ------------- 286 | 287 | ```js 288 | // my-faset.js 289 | function myFaset(todos) { 290 | return todos.map(todo => todo.id + '-mapped') 291 | } 292 | 293 | export default Factory([ 294 | ['todoApp', 'todos'] 295 | ])(myFaset) 296 | ``` 297 | 298 | ```js 299 | // todo-list.js 300 | import statefull from 'immutable-di-react/statefull' 301 | import root from 'immutable-di-react/root' 302 | import TodoListItem from './todo-list-item' 303 | import myFaset from './my-faset' 304 | import TodoActions from './todo-actions' 305 | 306 | // set container from props to context: 307 | @root() 308 | // bind to setState: 309 | @statefull({ 310 | todos: ['todoApp', 'todos'], // state path 311 | query: ['todoApp', 'query'], // state path 312 | mapped: myFaset, // faset 313 | actions: TodoActions // class with actions 314 | }) 315 | export default class TodoList extends React.Component { 316 | render({todos, mapped, actions}) { 317 | return ( 318 |
319 |
320 |

Mapped todo ids:

321 | {mapped.toString()} 322 |
323 |
    324 | {todos.map(todo => ( 325 | 326 | ))} 327 |
328 |
329 | ) 330 | 331 | } 332 | } 333 | ``` 334 | 335 | ```js 336 | // todo-list-item.js 337 | import React from 'react' 338 | import widget from 'immutable-di-react/widget' 339 | import di from 'immutable-di-react/di' 340 | import TodoActions from './todo-actions' 341 | 342 | function TodoListItem({todo, editMode, actions}) { 343 | return ( 344 |
  • 345 | {todo.title} 346 |
  • 347 | ) 348 | } 349 | 350 | export default Di({ 351 | actions: TodoActions 352 | })(widget(TodoListItem)) 353 | ``` 354 | 355 | ```js 356 | // todo-actions.js 357 | import Container from 'immutable-di' 358 | import {Class} from 'immutable-di/define' 359 | 360 | @Class([Container]) 361 | export default class TodoActions { 362 | constructor(container) { 363 | this._cursor = cursor.select(['todoApp']) 364 | } 365 | 366 | addTodo(todo) { 367 | this._cursor.apply(['todos'] => todos.concat(todo)) 368 | } 369 | } 370 | ``` 371 | 372 | ```js 373 | // index.js 374 | import React from 'react' 375 | import Container from 'immutable-di' 376 | import NativeCursor from 'immutable-di/cursors/native' 377 | import TodoList from './todo-list' 378 | 379 | // define di container with state: 380 | const cursor = new NativeCursor({ 381 | todoApp: { 382 | todos: [], 383 | query: { 384 | 385 | } 386 | } 387 | }) 388 | const container = new Container(cursor) 389 | 390 | const initialProps = cursor.select(['todoApp']).get() 391 | React.render(, document.querySelector('body')) 392 | ``` 393 | 394 | Initial debug support 395 | --------------------- 396 | 397 | ```js 398 | import {Factory, Setter} from 'immutable-di/define' 399 | import Container from 'immutable-di' 400 | import NativeCursor from 'immutable-di/cursors/native' 401 | import MonitorFactory from 'immutable-di/history/MonitorFactory' 402 | 403 | Factory.extend = MonitorFactory 404 | 405 | function showChanges(history) { 406 | console.log(history) 407 | } 408 | 409 | const action = Factory([ 410 | Setter(['tis', 'a']) 411 | ])(function MyAction(setA) { 412 | return function myAction(value) { 413 | setA(value) 414 | } 415 | }) 416 | 417 | const cursor = new NativeCursor({ 418 | tis: { 419 | a: 1, 420 | b: 2 421 | } 422 | }) 423 | const container = new Container(cursor) 424 | const listener = container.on([ 425 | ['__history'] 426 | ], showChanges) 427 | 428 | container.get(action)(123) 429 | // Will produce: 430 | /* 431 | [ 432 | { "displayName": "MyAction", "id": 11, "args": [ 123 ], "diff": {} } 433 | ] 434 | */ 435 | container.off(listener) 436 | ``` 437 | 438 | NativeCursor:diff used for diff generation, this dummy, but NativeCursor can be extended. 439 | --------------------------------------------------------------------------------