🚀 Add node_redux for state container
This commit is contained in:
parent
8e5109d46e
commit
1088b8ea80
|
@ -0,0 +1,66 @@
|
||||||
|
# NodeRedux
|
||||||
|
Redux is a predictable state container for lua apps and NodeMCU.
|
||||||
|
|
||||||
|
## Influences
|
||||||
|
|
||||||
|
NodeRedux evolves the ideas of [Redux](https://redux.js.org/),
|
||||||
|
which help to maintain state lua base app basically NodeMCU devices.
|
||||||
|
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
The whole global state of your app is stored in an object tree inside a single _store_.
|
||||||
|
The only way to change the state tree is to create an _action_,
|
||||||
|
an object describing what happened, and _dispatch_ it to the store.
|
||||||
|
To specify how state gets updated in response to an action, you write pure _reducer_
|
||||||
|
functions that calculate a new state based on the old state and the action.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local redux = require('redux') -- optional line for nodemcu, only need for lua app
|
||||||
|
|
||||||
|
-- This is a reducer - a function that takes a current state value and an
|
||||||
|
-- action object describing "what happened", and returns a new state value.
|
||||||
|
-- A reducer's function signature is: (state, action) => newState
|
||||||
|
--
|
||||||
|
-- The NodeRedux state should contain only plain Lua table.
|
||||||
|
-- The root state value is usually an table. It's important that you should
|
||||||
|
-- not mutate the state table, but return a new table if the state changes.
|
||||||
|
--
|
||||||
|
-- You can use any conditional logic you want in a reducer. In this example,
|
||||||
|
-- we use a if statement, but it's not required.
|
||||||
|
|
||||||
|
local function counterReducer(state, action)
|
||||||
|
state = state or { value = 0 }
|
||||||
|
if action.type == 'counter/incremented' then
|
||||||
|
return { value = state.value + 2 }
|
||||||
|
elseif action.type == 'counter/decremented' then
|
||||||
|
return { value = state.value - 1 }
|
||||||
|
else
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redux.createStore(counterReducer)
|
||||||
|
|
||||||
|
local function console(pState, cState)
|
||||||
|
print('Previous State: '.. pState.value, 'Current State: '.. cState.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
redux.store.subscribe(console)
|
||||||
|
|
||||||
|
redux.store.dispatch({type = 'counter/incremented'})
|
||||||
|
-- {value = 1}
|
||||||
|
redux.store.dispatch({type = 'counter/incremented'})
|
||||||
|
-- {value = 2}
|
||||||
|
redux.store.dispatch({type = 'counter/decremented'})
|
||||||
|
-- {value = 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Here](./createStore_example.lua) the example code
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
local ActionType
|
||||||
|
do
|
||||||
|
local upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
local lowerCase = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
local numbers = "0123456789"
|
||||||
|
|
||||||
|
local characterSet = upperCase .. lowerCase .. numbers
|
||||||
|
|
||||||
|
local function randomeSting(length)
|
||||||
|
local output = ""
|
||||||
|
for _ = 1, length do
|
||||||
|
output = math.random(#characterSet) .. output
|
||||||
|
end
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
local intString = randomeSting(36)
|
||||||
|
local INIT = "@redux/INIT" .. intString
|
||||||
|
|
||||||
|
local function PROBE_UNKNOWN_ACTION()
|
||||||
|
return '@@redux/PROBE_UNKNOWN_ACTION' .. randomeSting(36)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
ActionType = {
|
||||||
|
INIT = INIT,
|
||||||
|
PROBE_UNKNOWN_ACTION = PROBE_UNKNOWN_ACTION
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return ActionType
|
|
@ -0,0 +1,172 @@
|
||||||
|
local require, type, error, pairs, table, tostring = require, type, error, pairs, table, tostring
|
||||||
|
|
||||||
|
local ActionTypes = require('actionType_utils')
|
||||||
|
local isPlainObject = require('isPlainObject_utils')
|
||||||
|
|
||||||
|
|
||||||
|
local function _getKeys(object)
|
||||||
|
local keyset={}
|
||||||
|
local n = 0
|
||||||
|
for k, _ in pairs(object) do
|
||||||
|
n = n + 1
|
||||||
|
keyset[n] = k
|
||||||
|
end
|
||||||
|
return keyset
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _getKeysString(object)
|
||||||
|
local keys = ''
|
||||||
|
for k, _ in pairs(object) do
|
||||||
|
keys = tostring(k) .. ', ' .. keys
|
||||||
|
end
|
||||||
|
return string.sub(keys, 1, -3)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache)
|
||||||
|
local reducerKeys = _getKeys(reducers)
|
||||||
|
local argumentName = 'previous state received by the reducer'
|
||||||
|
if(action ~= nil and action.type == ActionTypes.INIT) then
|
||||||
|
argumentName = 'preloadedState argument passed to createStore'
|
||||||
|
end
|
||||||
|
|
||||||
|
if(#reducerKeys == 0) then
|
||||||
|
return 'Store does not have a valid reducer. Make sure the argument passed ' ..
|
||||||
|
'to combineReducers is an object whose values are reducers.'
|
||||||
|
end
|
||||||
|
|
||||||
|
if (not isPlainObject(inputState)) then
|
||||||
|
local keys = _getKeysString(reducers)
|
||||||
|
return 'The' .. argumentName .. 'has unexpected type. ' ..
|
||||||
|
'Expected argument to be an object with the following ' ..
|
||||||
|
'keys "' .. keys .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
local unexpectedKeys = {}
|
||||||
|
for k, _ in pairs(inputState) do
|
||||||
|
if(reducers[k] == nil and not unexpectedKeyCache[k]) then
|
||||||
|
table.insert(unexpectedKeys, k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, v in pairs(unexpectedKeys) do
|
||||||
|
unexpectedKeyCache[v] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if (action ~= nil and action.type == ActionTypes.REPLACE) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if (#unexpectedKeys > 0) then
|
||||||
|
return "Unexpected " .. #unexpectedKeys > 1 and "'keys'" or "'key'" ..
|
||||||
|
'"'.. _getKeysString(unexpectedKeys) ..'" found in ' .. argumentName .. '.' ..
|
||||||
|
'Expected to find one of the known reducer keys instead: "' ..
|
||||||
|
_getKeysString(unexpectedKeys) ..'". Unexpected keys will be ignored.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertReducerShape(reducers)
|
||||||
|
for k, v in pairs(reducers) do
|
||||||
|
local reducer = v
|
||||||
|
local initialState = reducer(nil, { type = ActionTypes.INIT })
|
||||||
|
|
||||||
|
if (type(initialState) == 'nil') then
|
||||||
|
error(
|
||||||
|
'The slice reducer for key "' .. k .. '" returned undefined during initialization. ' ..
|
||||||
|
'If the state passed to the reducer is undefined, you must '..
|
||||||
|
'explicitly return the initial state. The initial state may '..
|
||||||
|
"not be undefined. If you don't want to set a value for this reducer, "..
|
||||||
|
"you can use null instead of undefined.",
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
initialState = reducer(nil, { type = ActionTypes.PROBE_UNKNOWN_ACTION() })
|
||||||
|
|
||||||
|
if (type(initialState) == 'nil') then
|
||||||
|
error(
|
||||||
|
'The slice reducer for key "' .. k .. '" returned undefined when probed with a random type. ' ..
|
||||||
|
"Don't try to handle '" .. ActionTypes.INIT .. "' or other actions in \"redux/*\" " ..
|
||||||
|
'namespace. They are considered private. Instead, you must return the ' ..
|
||||||
|
'current state for any unknown actions, unless it is undefined, ' ..
|
||||||
|
'in which case you must return the initial state, regardless of the ' ..
|
||||||
|
'action type. The initial state may not be undefined, but can be null.',
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function combineReducers(reducers)
|
||||||
|
local reducerKeys = _getKeys(reducers)
|
||||||
|
local finalReducers = {}
|
||||||
|
|
||||||
|
for _, v in pairs(reducerKeys) do
|
||||||
|
if (type(reducers[v]) == 'function' ) then
|
||||||
|
finalReducers[v] = reducers[v]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local finalReducerKeys = _getKeys(finalReducers)
|
||||||
|
|
||||||
|
|
||||||
|
local unexpectedKeyCache = {}
|
||||||
|
local shapeAssertionError
|
||||||
|
local res, val = pcall(assertReducerShape, finalReducers)
|
||||||
|
|
||||||
|
if(not res) then
|
||||||
|
shapeAssertionError = val
|
||||||
|
end
|
||||||
|
|
||||||
|
local function combination(state, action)
|
||||||
|
if (shapeAssertionError) then
|
||||||
|
error (shapeAssertionError, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
state = state or {}
|
||||||
|
|
||||||
|
local warningMessage = getUnexpectedStateShapeWarningMessage(
|
||||||
|
state,
|
||||||
|
finalReducers,
|
||||||
|
action,
|
||||||
|
unexpectedKeyCache
|
||||||
|
)
|
||||||
|
|
||||||
|
if (warningMessage) then
|
||||||
|
print('\27[93mWARNING :: '..warningMessage..'\27[0m')
|
||||||
|
end
|
||||||
|
|
||||||
|
local hasChanged = false
|
||||||
|
local nextState = {}
|
||||||
|
|
||||||
|
|
||||||
|
for _, v in pairs(finalReducerKeys) do
|
||||||
|
local key = v
|
||||||
|
local reducer = finalReducers[key]
|
||||||
|
local previousStateForKey = state[key]
|
||||||
|
local nextStateForKey = reducer(previousStateForKey, action)
|
||||||
|
|
||||||
|
if(type(nextStateForKey) == 'nil') then
|
||||||
|
local actionType = action and action.type
|
||||||
|
error(
|
||||||
|
'When called with an action of type "'.. actionType or '(unknown type)' .. '"' ..
|
||||||
|
'the slice reducer for key "' .. key .. '" returned undefined. ' ..
|
||||||
|
'To ignore an action, you must explicitly return the previous state. ' ..
|
||||||
|
'If you want this reducer to hold no value, you can return null instead of undefined.',
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
nextState[key] = nextStateForKey
|
||||||
|
hasChanged = hasChanged or nextStateForKey ~= previousStateForKey
|
||||||
|
end
|
||||||
|
hasChanged = hasChanged or #finalReducerKeys ~= #_getKeys(state)
|
||||||
|
|
||||||
|
if (hasChanged) then
|
||||||
|
return nextState
|
||||||
|
else
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return combination
|
||||||
|
end
|
||||||
|
|
||||||
|
return combineReducers
|
|
@ -0,0 +1,76 @@
|
||||||
|
--
|
||||||
|
-- Created by IntelliJ IDEA.
|
||||||
|
-- User: shubhams
|
||||||
|
-- Date: 06/04/21
|
||||||
|
-- Time: 1:53 PM
|
||||||
|
-- To change this template use File | Settings | File Templates.
|
||||||
|
--
|
||||||
|
|
||||||
|
local redux = require('node_redux')
|
||||||
|
|
||||||
|
local function dump(o)
|
||||||
|
if type(o) == 'table' then
|
||||||
|
local s = '{ '
|
||||||
|
for k,v in pairs(o) do
|
||||||
|
s = s .. ' ' .. k .. ' = ' .. dump(v) .. ','
|
||||||
|
end
|
||||||
|
s = string.sub(s, 1, -2)
|
||||||
|
return s .. ' }'
|
||||||
|
else
|
||||||
|
return tostring(o)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reducer1(state, action)
|
||||||
|
state = state or {
|
||||||
|
value = {0}
|
||||||
|
}
|
||||||
|
if action.type == 'counter/incremented' then
|
||||||
|
return {
|
||||||
|
value = {state.value[1] + 1}
|
||||||
|
}
|
||||||
|
elseif action.type == 'counter/decremented' then
|
||||||
|
return {
|
||||||
|
value = {state.value[1] - 2}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reducer2(state, action)
|
||||||
|
state = state or {
|
||||||
|
value = 0
|
||||||
|
}
|
||||||
|
if action.type == 'counter/incremented' then
|
||||||
|
return {
|
||||||
|
value = state.value + 2
|
||||||
|
}
|
||||||
|
elseif action.type == 'counter/decremented' then
|
||||||
|
return {
|
||||||
|
value = state.value - 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local newReducer = redux.combineReducers({
|
||||||
|
r1 = reducer1,
|
||||||
|
r2 = reducer2
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
redux.createStore(newReducer)
|
||||||
|
|
||||||
|
|
||||||
|
local function s(pState, cState)
|
||||||
|
print('Previous State: ' .. dump(pState), '\nCurrent State: ' .. dump(cState) .. '\n\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
redux.store.subscribe(s)
|
||||||
|
|
||||||
|
redux.store.dispatch({type = 'counter/incremented'})
|
||||||
|
redux.store.dispatch({type = 'counter/decremented'})
|
||||||
|
redux.store.dispatch({type = 'counter/incremented'})
|
|
@ -0,0 +1,27 @@
|
||||||
|
local pairs, type = pairs, type
|
||||||
|
|
||||||
|
local function _checkObect(obj)
|
||||||
|
local check = true
|
||||||
|
for _, v in pairs(obj) do
|
||||||
|
local obType = type(v)
|
||||||
|
if (obType == 'string' or obType == 'number' or obType == 'nil' or obType == 'boolean') then
|
||||||
|
check = check and true
|
||||||
|
elseif(obType == 'table') then
|
||||||
|
check = check and _checkObect(v)
|
||||||
|
else
|
||||||
|
check = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return check
|
||||||
|
end
|
||||||
|
|
||||||
|
local isPlainObject = function (obj)
|
||||||
|
if(type(obj) ~= 'table' or obj == nil) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return _checkObect(obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return isPlainObject
|
|
@ -0,0 +1,154 @@
|
||||||
|
local require, type, error, ipairs, table, select = require, type, error, ipairs, table, select
|
||||||
|
|
||||||
|
local isPlainObject = require('isPlainObject_utils')
|
||||||
|
local ActionTypes = require('actionType_utils')
|
||||||
|
local combineReducers = require('combineReducers')
|
||||||
|
|
||||||
|
local redux, store
|
||||||
|
do
|
||||||
|
local function createStore(reducer, preloadedState, enhancer, ...)
|
||||||
|
if(type(reducer) ~= 'function') then
|
||||||
|
error("Expected the root reducer to be a function. Instead, received: "..type(reducer), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local arg_4 = select(1, ...)
|
||||||
|
|
||||||
|
if((type(preloadedState) == 'function' and type(enhancer) == 'function')
|
||||||
|
or (type(enhancer) == 'function' and type(arg_4) == 'function') ) then
|
||||||
|
error('It looks like you are passing several store enhancers to ' ..
|
||||||
|
'createStore(). This is not supported. Instead, compose them ' ..
|
||||||
|
'together to a single function.', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if(type(preloadedState) == 'function' and type(enhancer) == 'nil') then
|
||||||
|
enhancer = preloadedState
|
||||||
|
preloadedState = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if(type(enhancer) ~= 'nil') then
|
||||||
|
if(type(enhancer) ~= 'function') then
|
||||||
|
error('Expected the enhancer to be a function. Instead, received: ' .. type(enhancer), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return enhancer(createStore)(reducer, preloadedState)
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentReducer, currentState, currentListeners, nextListeners, isDispatching, countListeners
|
||||||
|
do
|
||||||
|
currentReducer = reducer
|
||||||
|
currentState = preloadedState
|
||||||
|
currentListeners = {}
|
||||||
|
nextListeners = currentListeners
|
||||||
|
isDispatching = false
|
||||||
|
countListeners = 0
|
||||||
|
|
||||||
|
local function copy(array)
|
||||||
|
if type(array) ~= 'table' then return array end
|
||||||
|
local res = {}
|
||||||
|
local index = 0
|
||||||
|
for _, v in ipairs(array) do
|
||||||
|
res[index] = v
|
||||||
|
index = index + 1
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureCanMutateNextListeners()
|
||||||
|
if(nextListeners == currentListeners) then
|
||||||
|
nextListeners = copy(currentListeners)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getState()
|
||||||
|
if(isDispatching) then
|
||||||
|
error('You may not call store.getState() while the reducer is executing. ' ..
|
||||||
|
'The reducer has already received the state as an argument. ' ..
|
||||||
|
'Pass it down from the top reducer instead of reading it from the store.', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return currentState
|
||||||
|
end
|
||||||
|
|
||||||
|
local function subscribe(listener)
|
||||||
|
if(type(listener) ~= 'function') then
|
||||||
|
error('You may not call store.subscribe() while the reducer is executing. ' ..
|
||||||
|
'If you would like to be notified after the store has been updated, subscribe from a ' ..
|
||||||
|
'component and invoke store.getState() in the callback to access the latest state. ', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local isSubscribed = true
|
||||||
|
ensureCanMutateNextListeners()
|
||||||
|
table.insert(nextListeners, listener)
|
||||||
|
|
||||||
|
countListeners = countListeners + 1
|
||||||
|
|
||||||
|
local function unsubscribe()
|
||||||
|
if (not isSubscribed) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if (isDispatching) then
|
||||||
|
error('You may not unsubscribe from a store listener while the reducer is executing.', 2)
|
||||||
|
end
|
||||||
|
isSubscribed = false
|
||||||
|
ensureCanMutateNextListeners()
|
||||||
|
table.remove(nextListeners, countListeners)
|
||||||
|
currentListeners = nil
|
||||||
|
end
|
||||||
|
return unsubscribe
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dispatch(action)
|
||||||
|
if (not isPlainObject(action)) then
|
||||||
|
error("Actions must be plain objects.", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if (type(action.type) == 'nil') then
|
||||||
|
error('Actions may not have an undefined "type" property. ' ..
|
||||||
|
'You may have misspelled an action type string constant.', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if(isDispatching) then
|
||||||
|
error('Reducers may not dispatch actions.')
|
||||||
|
end
|
||||||
|
isDispatching = true
|
||||||
|
local previousState = currentState
|
||||||
|
currentState = currentReducer(currentState, action)
|
||||||
|
isDispatching = false
|
||||||
|
currentListeners = nextListeners
|
||||||
|
local listeners = currentListeners
|
||||||
|
for _, listener in ipairs(listeners) do
|
||||||
|
listener(previousState, currentState)
|
||||||
|
end
|
||||||
|
return action
|
||||||
|
end
|
||||||
|
|
||||||
|
local function replaceReducer(nextReducer)
|
||||||
|
if(type(nextReducer) ~= 'function') then
|
||||||
|
error('Expected the nextReducer to be a function. Instead, received: '..type(nextReducer), 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
currentReducer = nextReducer
|
||||||
|
|
||||||
|
dispatch({ type = ActionTypes.REPLACE })
|
||||||
|
end
|
||||||
|
|
||||||
|
dispatch({ type = ActionTypes.INIT })
|
||||||
|
|
||||||
|
store = {
|
||||||
|
dispatch = dispatch,
|
||||||
|
subscribe = subscribe,
|
||||||
|
getState = getState,
|
||||||
|
replaceReducer = replaceReducer,
|
||||||
|
}
|
||||||
|
redux.store = store
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redux = {
|
||||||
|
createStore = createStore,
|
||||||
|
combineReducers = combineReducers,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return redux
|
Loading…
Reference in New Issue