-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathStore.lua
131 lines (101 loc) · 3.2 KB
/
Store.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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