132 lines
3.2 KiB
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
|