Skip to content

NodeRedux state container for ESP devices #3421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions docs/lua-modules/node_redux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# NodeRedux Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2021-04-09 | [Shubham Srivastava](https://github.com/shubham-sri) | [Shubham Srivastava](https://github.com/shubham-sri) | [node_redux.lua](../../lua_modules/node_redux/node_redux.lua) |

<br>
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')

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](../../lua_modules/node_redux/createStore_example.lua) the example code

### Require
```lua
local redux = require("node_redux")
```

## redux.createStore()
Creates a Redux store that holds the complete state tree of device. There should only be a single store in your device.

#### Syntax
`redux.createStore(reducer)`

#### Parameters
- `reducer`(Function): A reducing function that returns the next state tree,
given the current state tree, and an action to handle.
- `preloadedState`(any): The initial state. You may optionally specify it to
hydrate the state from the server in universal apps, or to restore a previously
serialized user session. If you produced reducer with combineReducers, this must
be a plain object with the same shape as the keys passed to it. Otherwise,
you are free to pass anything that your reducer can understand.
- `enhancer` (Function): The store enhancer. You may optionally specify it to
enhance the store with third-party capabilities such as middleware, time travel,
persistence, etc. The only store enhancer that ships with Redux is
applyMiddleware() ***TODO: Support added in near next PR***

#### Returns
- `nil`

## redux.combineReducers()
As your app grows more complex, you'll want to split your reducing function into
separate functions, each managing independent parts of the state.
The combineReducers helper function turns an object whose values are different
reducing functions into a single reducing function you can pass to createStore.

#### Syntax
```lua
redux.combineReducers({
reducer_one = reducer_1,
reducer_two = reducer_2,
...
})
```

#### Parameters
- `table`(Table of Functions values): A reducing functions that returns the next
state tree, given the current state tree, and an action to handle.
#### Returns

- `combined_reducer` (Function) A single combined reducer build by combining
all reducers

#### Example

```lua
local redux = require('redux')

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

local function inverseCounterReducer(state, action)
state = state or { value = 0 }
if action.type == 'counter/incremented' then
return { value = state.value - 1 }
elseif action.type == 'counter/decremented' then
return { value = state.value + 1 }
else
return state
end
end

local reducer = redux.combineReducers({
counterReducer = counterReducer,
inverseCounterReducer = inverseCounterReducer,
})

redux.createStore(reducer)

local function console(pState, cState)
print('Previous State: counterReducer' ..
pState.counterReducer.value,
'Current State: counterReducer' ..
cState.counterReducer.value
)
print('Previous State: inverseCounterReducer' ..
pState.inverseCounterReducer.value,
'Current State: inverseCounterReducer' ..
cState.inverseCounterReducer.value
)
end

redux.store.subscribe(console)

redux.store.dispatch({type = 'counter/incremented'})
-- {counterReducer = { value = 2 }, inverseCounterReducer = { value = -1 } }
redux.store.dispatch({type = 'counter/incremented'})
-- {counterReducer = { value = 4 }, inverseCounterReducer = { value = -2 } }
redux.store.dispatch({type = 'counter/decremented'})
-- {counterReducer = { value = 3 }, inverseCounterReducer = { value = -1 } }
```

## redux.store.getState()
Returns the current state tree of your application. It is equal to the last value
returned by the store's reducer.
#### Syntax
`redux.store.getState()`

#### Parameters
None

#### Returns
The current state tree of your application.

## redux.store.dispatch()
Dispatches an action. This is the only way to trigger a state change.

The store's reducing function will be called with the current getState() result,
and the given action synchronously. Its return value will be considered the next state.
It will be returned from getState() from now on, and the change listeners will
immediately be notified.
#### Syntax
`redux.store.dispatch(action)`

#### Parameters
- `action` (_Table_): A plain object describing the change that makes sense for
your application. Actions must have a type field that indicates the `type`
of action being performed. Types can be defined as constants and imported
from another module. It's better to use strings for `type` than Symbols
because strings are serializable.

#### Returns
(_Table_): The dispatched action

## redux.store.subscribe()
Adds a change listener. It will be called any time an action is dispatched, and some
part of the state tree may potentially have changed. You may then use `1st arg` for
previous state and `2nd arg` for current state.

- The listener should only call dispatch() either in response to user actions or
under specific conditions (e.g. dispatching an action when the store has
a specific field). Calling dispatch() without any conditions is technically
possible, however it leads to an infinite loop as every dispatch() call
usually triggers the listener again.

#### Syntax
`redux.store.subscribe(listener)`

#### Parameters
- `listener` (Function): The callback to be invoked any time an action has been
dispatched, and the state tree might have changed. You may then use `1st arg` for
previous state and `2nd arg` for current state

#### Example - listener
```lua
local function listener(pState, cState)
print('Previous State: counterReducer' ..
pState.counterReducer.value,
'Current State: counterReducer' ..
cState.counterReducer.value
)
print('Previous State: inverseCounterReducer' ..
pState.inverseCounterReducer.value,
'Current State: inverseCounterReducer' ..
cState.inverseCounterReducer.value
)
end
```

## redux.store.replaceReducer()
It is an advanced API. You might need this if your app implements code
splitting, and you want to load some of the reducers dynamically.
You might also need this if you implement a hot reloading mechanism for Redux.

#### Syntax
`redux.store.replaceReducer(nextReducer)`

#### Parameters
- `nextReducer` (Function): The next reducer for the store to use.
3 changes: 3 additions & 0 deletions lua_modules/node_redux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# NodeRedux Module

Documentation for this Lua module is available in the [node_redux.md](../../docs/lua-modules/node_redux.md) file and in the [Official NodeMCU Documentation](https://nodemcu.readthedocs.io/) in `Lua Modules` section.
31 changes: 31 additions & 0 deletions lua_modules/node_redux/actionType_utils.lua
Original file line number Diff line number Diff line change
@@ -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
172 changes: 172 additions & 0 deletions lua_modules/node_redux/combineReducers.lua
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions lua_modules/node_redux/createStore_example.lua
Original file line number Diff line number Diff line change
@@ -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'})
27 changes: 27 additions & 0 deletions lua_modules/node_redux/isPlainObject_utils.lua
Original file line number Diff line number Diff line change
@@ -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
154 changes: 154 additions & 0 deletions lua_modules/node_redux/node_redux.lua
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ pages:
- 'lm92': 'lua-modules/lm92.md'
- 'mcp23008': 'lua-modules/mcp23008.md'
- 'mcp23017': 'lua-modules/mcp23017.md'
- 'node_redux': 'lua-modules/node_redux.md'
- 'redis': 'lua-modules/redis.md'
- 'telnet': 'lua-modules/telnet.md'
- 'yeelink': 'lua-modules/yeelink.md'