Comparing version 0.1.0 to 0.2.1
150
lib/slots.js
@@ -9,2 +9,4 @@ "use strict"; | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -14,2 +16,20 @@ | ||
var _debug = require("debug"); | ||
var _debug2 = _interopRequireDefault(_debug); | ||
var _util = require("util"); | ||
var _util2 = _interopRequireDefault(_util); | ||
var d = (0, _debug2["default"])("slt"); | ||
var log = (0, _debug2["default"])("slt:log"); | ||
function insp(value) { | ||
value = value.toJS ? value.toJS() : value; | ||
value = isArray(value) ? value.join(".") : value; | ||
value = isFunction(value.then) ? "__promise__" : value; | ||
return _util2["default"].inspect(value, { colors: typeof window === "undefined", depth: 0 }).replace("\n", ""); | ||
} | ||
function isFunction(v) { | ||
@@ -19,2 +39,10 @@ return Object.prototype.toString.call(v) === "[object Function]"; | ||
function isArray(v) { | ||
return Object.prototype.toString.call(v) === "[object Array]"; | ||
} | ||
function isString(v) { | ||
return Object.prototype.toString.call(v) === "[object String]"; | ||
} | ||
var Slots = (function () { | ||
@@ -55,9 +83,15 @@ function Slots() { | ||
var value = arguments[1] === undefined ? {} : arguments[1]; | ||
var state = arguments[2] === undefined ? null : arguments[2]; | ||
var _this = this; | ||
var optimistic = arguments[2] === undefined ? true : arguments[2]; | ||
var save = arguments[3] === undefined ? true : arguments[3]; | ||
var optimistic = arguments[3] === undefined ? true : arguments[3]; | ||
var save = arguments[4] === undefined ? true : arguments[4]; | ||
path = Slots.path(path); | ||
state = state || this.state; | ||
var reduced = this.reducePathAndValue(path, value); | ||
path = reduced.path; | ||
value = reduced.value; | ||
if (value && isFunction(value.then)) { | ||
@@ -67,2 +101,3 @@ this.promises.push(value); | ||
_this.promises.splice(_this.promises.indexOf(value), 1); | ||
log("RESOLVED %s", insp(path)); | ||
_this.set(path, val); // RECURSION with resolved value | ||
@@ -74,55 +109,42 @@ }).error(function (msg) { | ||
})["catch"](function (msg) {})["finally"](function () {}); | ||
} else { | ||
var _ret = (function () { | ||
var i = path.length; | ||
var v = value; | ||
while (i--) { | ||
var p = path.slice(0, i); | ||
var tmp = {}; | ||
tmp[path.slice(i)] = v; | ||
v = tmp; | ||
if (_this.rules.get(p.join("."))) { | ||
path = p; | ||
value = v; | ||
} | ||
} | ||
var imValue = (0, _immutable.fromJS)(value); | ||
var result = _this.state.mergeDeepIn(path, imValue); | ||
var applyRules = function applyRules() { | ||
var path = arguments[0] === undefined ? new _immutable.List() : arguments[0]; | ||
var value = arguments[1] === undefined ? {} : arguments[1]; | ||
} | ||
log("SET %s TO %s", insp(path), insp(value)); | ||
var imValue = (0, _immutable.fromJS)(value); | ||
var result = imValue.toJS ? state.mergeDeepIn(path, imValue) : state.setIn(path, imValue); | ||
d("Merged \n%s", insp(result)); | ||
var applyRules = function applyRules() { | ||
var path = arguments[0] === undefined ? new _immutable.List() : arguments[0]; | ||
var value = arguments[1] === undefined ? new _immutable.Map() : arguments[1]; | ||
if (!_immutable.Map.isMap(value)) { | ||
return; | ||
} | ||
var rule = _this.rules.get(path.toArray().join(".")); | ||
if (isFunction(rule)) { | ||
result = result.merge(rule(result.getIn(path).toJS(), _this.getContext())); | ||
} | ||
value.flip().toList().map(function (k) { | ||
return applyRules(path.push(k), value.get(k)); | ||
var rule = _this.rules.get(path.toArray().join(".")); | ||
if (isFunction(rule)) { | ||
var p = result.getIn(path); | ||
d("Applying rule on path %s with value %s", insp(path), insp(p)); | ||
result = result.mergeDeep(rule(p && p.toJS && p.toJS() || p, _this.getContext(result)).getState()); | ||
d("Result is %s", insp(result)); | ||
} | ||
if (!_immutable.Map.isMap(value)) { | ||
return; | ||
} | ||
value.flip().toList().map(function (k) { | ||
return applyRules(path.push(k), value.get(k)); | ||
}); | ||
}; | ||
applyRules(new _immutable.List(path), result); | ||
var newState = result; | ||
if (optimistic && !(0, _immutable.is)(this.state, newState)) { | ||
if (save) { | ||
log("SAVE %s", insp(newState)); | ||
this.state = newState; | ||
if (!this.promises.length) { | ||
this.onPromisesAreMadeListeners.forEach(function (f) { | ||
return f(_this.state.toJS()); | ||
}); | ||
}; | ||
applyRules(new _immutable.List(path), result); | ||
var newState = result; | ||
if (optimistic && !(0, _immutable.is)(_this.state, newState)) { | ||
if (save) { | ||
_this.state = newState; | ||
if (!_this.promises.length) { | ||
_this.onPromisesAreMadeListeners.forEach(function (f) { | ||
return f(_this.state.toJS()); | ||
}); | ||
} | ||
_this.onChangeListeners.forEach(function (f) { | ||
return f(_this.state.toJS()); | ||
}); | ||
} | ||
} | ||
return { | ||
v: result | ||
}; | ||
})(); | ||
if (typeof _ret === "object") return _ret.v; | ||
this.onChangeListeners.forEach(function (f) { | ||
return f(_this.state.toJS()); | ||
}); | ||
} | ||
} | ||
return this.getContext(result); | ||
} | ||
@@ -153,3 +175,3 @@ }, { | ||
key: "getContext", | ||
value: function getContext() { | ||
value: function getContext(state) { | ||
var _this2 = this; | ||
@@ -159,3 +181,6 @@ | ||
set: function set(path, value) { | ||
return _this2.set(path, value, false, false); | ||
return _this2.set(path, value, state, false, false); | ||
}, | ||
getState: function getState() { | ||
return state; | ||
} | ||
@@ -165,2 +190,19 @@ }; | ||
}, { | ||
key: "reducePathAndValue", | ||
value: function reducePathAndValue(path, value) { | ||
var i = path.length; | ||
var v = value; | ||
while (i--) { | ||
var p = path.slice(0, i); | ||
var tmp = {}; | ||
tmp[path.slice(i)] = v; | ||
v = tmp; | ||
if (this.rules.get(p.join("."))) { | ||
path = p; | ||
value = v; | ||
} | ||
} | ||
return { path: path, value: value }; | ||
} | ||
}, { | ||
key: "getRule", | ||
@@ -207,3 +249,3 @@ value: function getRule(path) { | ||
} | ||
return Object.prototype.toString.call(_path) === "[object Array]" && _path || Object.prototype.toString.call(_path) === "[object String]" && _path.split(".") || (function () { | ||
return isArray(_path) && _path || isString(_path) && _path.split(".") || (function () { | ||
throw new Error("path should be an array or dot-separated string or null,\n " + Object.prototype.toString.call(_path) + " given"); | ||
@@ -210,0 +252,0 @@ })(); |
{ | ||
"name": "slt", | ||
"version": "0.1.0", | ||
"version": "0.2.1", | ||
"description": "Take care of your state", | ||
@@ -18,2 +18,3 @@ "main": "index.js", | ||
"bluebird": "^2.9.26", | ||
"debug": "^2.2.0", | ||
"immutable": "^3.7.3" | ||
@@ -20,0 +21,0 @@ }, |
116
README.md
@@ -0,2 +1,116 @@ | ||
## DISCLAIMER | ||
I've developed this peace of software during implementation my own project, which was originally started with Flux. I realized that Flux adds a lot of unnecessary (not quite useful) entities and adds a lot of boilerplate to my codebase which is a reason why there is a lot of different implementations. Also I had no time to wait for Facebook Relay (http://facebook.github.io/react/blog/2015/02/20/introducing-relay-and-graphql.html) and I didn't want to bloat my codebase with knowingly out-of-date architecture. Now I glad to introduce my solution to app state managemet. It not dependend/related on any lib except https://facebook.github.io/immutable-js/ which is used for inner state management and not exposed to user (also I have a plan to make pure immutable version w/o any conversion). | ||
Contributers are welcome! | ||
## DESCRIPTION | ||
Slots could be consider as missing part of React. It's like Flux, but better. | ||
Slots could be consider as a missing part of React (not only). It's like Flux, but better. | ||
## TODO | ||
* connections: express middleware, React and others as separate libs | ||
* | ||
* documentation (currently you can learn from tests, it's really self-descriptive) | ||
* add transaction context. Add rollbacks and state history navigation | ||
* add performance tests | ||
* add Promise interface support (e.g. Slots.then ect) | ||
* errors handling | ||
* async tests. More tests for Promises | ||
* rules which are dependent on state conditions (eg Flux waitFor) | ||
* add support for groups of (sub)rules. Could be useful for sharing common rules | ||
* monitoring capabilities (for progress bars, for example) and better debugging | ||
* pure immutable version w/o conversion fromJS and .toJS | ||
* more examples | ||
## USAGE (short man) | ||
###Enable logging | ||
```javascript | ||
window.debug = require("debug"); | ||
window.debug.enable("slt:log"); | ||
``` | ||
###Rules example: | ||
I use https://github.com/AlexeyFrolov/routr-map to parse url. | ||
```javascript | ||
import r from "superagent-bluebird-promise"; | ||
import router from "./router"; | ||
import Slots from "slt"; | ||
const slots = new Slots ({ | ||
"request": (req, context) => { | ||
let route = router.match(req.url); | ||
let session = req.session; | ||
route.url = req.url; | ||
return context.set("route", route) | ||
.set("session", req.session); | ||
}, | ||
"route": (route, context) => { | ||
let {name, params: { id }} = route; | ||
if (name === "login") { | ||
return context; | ||
} | ||
let url = router.url({name, params: {id}}); | ||
return context | ||
.set(url.substr(1).replace("/", "."), r.get("http://example.com/api/" + url) | ||
.then(({body}) => body)) | ||
} | ||
}); | ||
``` | ||
###On the server (express middleware): | ||
```javascript | ||
import slots from "./slots"; // Configured Slots | ||
server.use((req, res, next) => { | ||
slots.onPromisesAreMade((state) => { | ||
state = slots.getStateWithAliases(); | ||
var html = render(state); | ||
res.status(state.response && state.response.status || 200).send(html); | ||
slots.reset(); | ||
}); | ||
let { url, session } = req; | ||
slots.set("request", { url, session }); | ||
}); | ||
``` | ||
###On the client: | ||
```javascript | ||
require("babel/polyfill"); | ||
import React from "react"; | ||
import Application from "./Application.js"; | ||
import slots from "./slots"; // Configured Slots | ||
slots.onChange((state) => { | ||
renderApp(state); | ||
}) | ||
function renderApp(state) { | ||
React.render(<Application state={state} />, document.getElementById("root"), () => { | ||
debug("Application has been mounted"); | ||
}); | ||
} | ||
renderApp(state); | ||
``` | ||
```javascript | ||
slots.set("request", {"url": "/users/555e5c37a5311543fc8890c9"}) | ||
``` | ||
Outputs: | ||
``` | ||
slt:log SET 'request' TO { url: '/users/555e5c37a5311543fc8890c9', session: [object Object] } +0ms | ||
slt:log SET 'route' TO { node: [Object], params: [Object], routePath: [Object], query: {}, name: 'users', domain: '', scheme: '', | ||
url: '/users/555e5c37a5311543fc8890c9' } +4ms | ||
slt:log SET 'users.555e5c37a5311543fc8890c9' TO '__promise__' +5ms | ||
slt:log SET 'session' TO [object Object] +5ms | ||
slt:log SAVE { request: [Object], route: [Object], users: [Object], session: [object Object] } +2ms | ||
giftter Detected locale (from browser) is en +20ms | ||
GET /api//users/555e5c37a5311543fc8890c9 200 23.483 ms - - | ||
slt:log RESOLVED 'users.555e5c37a5311543fc8890c9' +31ms | ||
slt:log SET 'users.555e5c37a5311543fc8890c9' TO { _id: '555e5c37a5311543fc8890c9' } +1ms | ||
slt:log SAVE { request: [Object], route: [Object], users: [Object], session: [object Object] } +76ms | ||
``` | ||
Final state (w/o 'user', I have different rules, which sets it as well) | ||
![Alt text](https://monosnap.com/file/otw3slLjWwRCYqS12jQM4JXTB4kT2J.png) |
124
src/slots.js
import { fromJS, is, Map, List} from "immutable"; | ||
import debug from "debug"; | ||
import util from 'util'; | ||
const d = debug("slt"); | ||
const log = debug("slt:log"); | ||
function insp(value) { | ||
value = value.toJS ? value.toJS() : value; | ||
value = isArray(value) ? value.join(".") : value; | ||
value = isFunction(value.then) ? "__promise__" : value; | ||
return util.inspect(value, {colors: typeof window === "undefined", depth: 0}).replace('\n', ''); | ||
} | ||
function isFunction(v) { | ||
@@ -7,2 +18,10 @@ return Object.prototype.toString.call(v) === "[object Function]"; | ||
function isArray(v) { | ||
return Object.prototype.toString.call(v) === "[object Array]"; | ||
} | ||
function isString(v) { | ||
return Object.prototype.toString.call(v) === "[object String]"; | ||
} | ||
class Slots { | ||
@@ -34,10 +53,16 @@ constructor(rules = {}, state = {}) { | ||
set(path = [], value = {}, optimistic = true, save = true) { | ||
set(path = [], value = {}, state = null, optimistic = true, save = true) { | ||
path = Slots.path(path); | ||
state = state || this.state; | ||
let reduced = this.reducePathAndValue(path, value); | ||
path = reduced.path; | ||
value = reduced.value; | ||
if (value && isFunction(value.then)) { | ||
this.promises.push(value); | ||
value.then((val) => { | ||
this.promises.splice(this.promises.indexOf(value), 1); | ||
this.set(path, val); // RECURSION with resolved value | ||
}) | ||
this.promises.splice(this.promises.indexOf(value), 1); | ||
log("RESOLVED %s", insp(path)); | ||
this.set(path, val); // RECURSION with resolved value | ||
}) | ||
.error((msg) => { | ||
@@ -51,42 +76,35 @@ this.onPromiseErrorListeners.forEach(f => f(msg)); | ||
}); | ||
} else { | ||
let i = path.length; | ||
let v = value; | ||
while (i--) { | ||
let p = path.slice(0, i); | ||
let tmp = {}; | ||
tmp[path.slice(i)] = v; | ||
v = tmp; | ||
if (this.rules.get(p.join("."))) { | ||
path = p; | ||
value = v; | ||
} | ||
} | ||
log("SET %s TO %s", insp(path), insp(value)); | ||
let imValue = fromJS(value); | ||
let result = imValue.toJS ? state.mergeDeepIn(path, imValue) | ||
: state.setIn(path, imValue); | ||
d("Merged \n%s", insp(result)); | ||
const applyRules = (path = new List(), value = new Map()) => { | ||
let rule = this.rules.get(path.toArray().join(".")); | ||
if (isFunction(rule)) { | ||
let p = result.getIn(path); | ||
d("Applying rule on path %s with value %s", insp(path), insp(p)); | ||
result = result.mergeDeep( | ||
rule(p && p.toJS && p.toJS() || p, this.getContext(result)).getState()); | ||
d("Result is %s", insp(result)); | ||
} | ||
let imValue = fromJS(value); | ||
let result = this.state.mergeDeepIn(path, imValue); | ||
const applyRules = (path = new List(), value = {}) => { | ||
if (!Map.isMap(value)) { | ||
return; | ||
if (!Map.isMap(value)) { | ||
return; | ||
} | ||
value.flip().toList().map((k) => applyRules(path.push(k), value.get(k))); | ||
}; | ||
applyRules(new List(path), result); | ||
let newState = result; | ||
if (optimistic && !is(this.state, newState)) { | ||
if (save) { | ||
log("SAVE %s", insp(newState)); | ||
this.state = newState; | ||
if (!this.promises.length) { | ||
this.onPromisesAreMadeListeners.forEach(f => f(this.state.toJS())); | ||
} | ||
let rule = this.rules.get(path.toArray().join(".")); | ||
if (isFunction(rule)) { | ||
result = result.merge( | ||
rule( | ||
result.getIn(path).toJS(), this.getContext())); | ||
} | ||
value.flip().toList().map((k) => applyRules(path.push(k), value.get(k))); | ||
}; | ||
applyRules(new List(path), result); | ||
let newState = result; | ||
if (optimistic && !is(this.state, newState)) { | ||
if (save) { | ||
this.state = newState; | ||
if (!this.promises.length) { | ||
this.onPromisesAreMadeListeners.forEach(f => f(this.state.toJS())); | ||
} | ||
this.onChangeListeners.forEach(f => f(this.state.toJS())); | ||
} | ||
this.onChangeListeners.forEach(f => f(this.state.toJS())); | ||
} | ||
return result; | ||
} | ||
return this.getContext(result); | ||
} | ||
@@ -111,6 +129,9 @@ | ||
getContext() { | ||
getContext(state) { | ||
return { | ||
set: (path, value) => { | ||
return this.set(path, value, false, false); | ||
return this.set(path, value, state, false, false); | ||
}, | ||
getState: () => { | ||
return state; | ||
} | ||
@@ -120,2 +141,18 @@ } | ||
reducePathAndValue(path, value) { | ||
let i = path.length; | ||
let v = value; | ||
while (i--) { | ||
let p = path.slice(0, i); | ||
let tmp = {}; | ||
tmp[path.slice(i)] = v; | ||
v = tmp; | ||
if (this.rules.get(p.join("."))) { | ||
path = p; | ||
value = v; | ||
} | ||
} | ||
return { path, value } | ||
} | ||
getRule(path) { | ||
@@ -154,4 +191,3 @@ path = Slots.path(path); | ||
} | ||
return Object.prototype.toString.call(path) === "[object Array]" && path || | ||
Object.prototype.toString.call(path) === "[object String]" && path.split('.') || | ||
return isArray(path) && path || isString(path) && path.split('.') || | ||
(() => { throw new Error ( | ||
@@ -158,0 +194,0 @@ `path should be an array or dot-separated string or null, |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
66987
38
823
117
1
3
+ Addeddebug@^2.2.0
+ Addeddebug@2.6.9(transitive)
+ Addedms@2.0.0(transitive)