🚀 Add node_redux for state container

This commit is contained in:
s 2021-04-09 22:10:38 +05:30
parent 8e5109d46e
commit 1088b8ea80
6 changed files with 526 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'})

View File

@ -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

View File

@ -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