Clients/Client2018/content/LuaPackages/RoduxImpl/Store.lua

132 lines
3.2 KiB
Lua

local RunService = game:GetService("RunService")
local Signal = require(script.Parent.Signal)
local NoYield = require(script.Parent.NoYield)
local Store = {}
-- This value is exposed as a private value so that the test code can stay in
-- sync with what event we listen to for dispatching the Changed event.
-- It may not be Heartbeat in the future.
Store._flushEvent = RunService.Heartbeat
Store.__index = Store
--[[
Create a new Store whose state is transformed by the given reducer function.
Each time an action is dispatched to the store, the new state of the store
is given by:
state = reducer(state, action)
Reducers do not mutate the state object, so the original state is still
valid.
]]
function Store.new(reducer, initialState, middlewares)
assert(typeof(reducer) == "function", "Bad argument #1 to Store.new, expected function.")
assert(middlewares == nil or typeof(middlewares) == "table", "Bad argument #3 to Store.new, expected nil or table.")
local self = {}
self._reducer = reducer
self._state = reducer(initialState, {
type = "@@INIT",
})
self._lastState = self._state
self._mutatedSinceFlush = false
self._connections = {}
self.changed = Signal.new()
setmetatable(self, Store)
local connection = self._flushEvent:Connect(function()
self:flush()
end)
table.insert(self._connections, connection)
if middlewares then
local unboundDispatch = self.dispatch
local dispatch = function(...)
return unboundDispatch(self, ...)
end
for i = #middlewares, 1, -1 do
local middleware = middlewares[i]
dispatch = middleware(dispatch, self)
end
self.dispatch = function(self, ...)
return dispatch(...)
end
end
return self
end
--[[
Get the current state of the Store. Do not mutate this!
]]
function Store:getState()
return self._state
end
--[[
Dispatch an action to the store. This allows the store's reducer to mutate
the state of the application by creating a new copy of the state.
Listeners on the changed event of the store are notified when the state
changes, but not necessarily on every Dispatch.
]]
function Store:dispatch(action)
if typeof(action) == "table" then
if action.type == nil then
error("action does not have a type field", 2)
end
self._state = self._reducer(self._state, action)
self._mutatedSinceFlush = true
else
error(("actions of type %q are not permitted"):format(typeof(action)), 2)
end
end
--[[
Marks the store as deleted, disconnecting any outstanding connections.
]]
function Store:destruct()
for _, connection in ipairs(self._connections) do
connection:Disconnect()
end
self._connections = nil
end
--[[
Flush all pending actions since the last change event was dispatched.
]]
function Store:flush()
if not self._mutatedSinceFlush then
return
end
self._mutatedSinceFlush = false
-- On self.changed:fire(), further actions may be immediately dispatched, in
-- which case self._lastState will be set to the most recent self._state,
-- unless we cache this value first
local state = self._state
-- If a changed listener yields, *very* surprising bugs can ensue.
-- Because of that, changed listeners cannot yield.
NoYield(function()
self.changed:fire(state, self._lastState)
end)
self._lastState = state
end
return Store