diff --git a/lua_modules/node_redux/README.md b/lua_modules/node_redux/README.md new file mode 100644 index 00000000..24ff7910 --- /dev/null +++ b/lua_modules/node_redux/README.md @@ -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) + + diff --git a/lua_modules/node_redux/actionType_utils.lua b/lua_modules/node_redux/actionType_utils.lua new file mode 100644 index 00000000..266ec74a --- /dev/null +++ b/lua_modules/node_redux/actionType_utils.lua @@ -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 diff --git a/lua_modules/node_redux/combineReducers.lua b/lua_modules/node_redux/combineReducers.lua new file mode 100644 index 00000000..cf3c9bfd --- /dev/null +++ b/lua_modules/node_redux/combineReducers.lua @@ -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 diff --git a/lua_modules/node_redux/createStore_example.lua b/lua_modules/node_redux/createStore_example.lua new file mode 100644 index 00000000..e16c60c7 --- /dev/null +++ b/lua_modules/node_redux/createStore_example.lua @@ -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'}) diff --git a/lua_modules/node_redux/isPlainObject_utils.lua b/lua_modules/node_redux/isPlainObject_utils.lua new file mode 100644 index 00000000..d2a61e7b --- /dev/null +++ b/lua_modules/node_redux/isPlainObject_utils.lua @@ -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 diff --git a/lua_modules/node_redux/node_redux.lua b/lua_modules/node_redux/node_redux.lua new file mode 100644 index 00000000..426c6667 --- /dev/null +++ b/lua_modules/node_redux/node_redux.lua @@ -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