🚀 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