@quenty/observablecollection 2.2.0 → 2.2.1-canary.261.5628274.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/package.json +10 -9
- package/src/Shared/ObservableCountingMap.lua +338 -0
- package/src/Shared/ObservableCountingMap.spec.lua +38 -0
- package/src/Shared/ObservableList.lua +400 -0
- package/src/Shared/ObservableList.spec.lua +82 -0
- package/src/Shared/ObservableMap.lua +302 -0
- package/src/Shared/ObservableMap.spec.lua +74 -0
- package/src/Shared/ObservableMapSet.lua +26 -0
- package/src/Shared/ObservableSet.lua +19 -2
- package/test/default.project.json +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.2.1-canary.261.5628274.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@2.2.0...@quenty/observablecollection@2.2.1-canary.261.5628274.0) (2022-05-21)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add more observable collections ([039cea8](https://github.com/Quenty/NevermoreEngine/commit/039cea80f4f21c075f3d857f70dac709c2ce1a64))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [2.2.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@2.1.0...@quenty/observablecollection@2.2.0) (2022-03-27)
|
|
7
18
|
|
|
8
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/observablecollection",
|
|
3
|
-
"version": "2.2.0",
|
|
3
|
+
"version": "2.2.1-canary.261.5628274.0",
|
|
4
4
|
"description": "An observable set",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"Quenty"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@quenty/brio": "
|
|
31
|
-
"@quenty/loader": "
|
|
32
|
-
"@quenty/maid": "
|
|
33
|
-
"@quenty/promise": "
|
|
34
|
-
"@quenty/rx": "
|
|
35
|
-
"@quenty/signal": "
|
|
36
|
-
"@quenty/
|
|
30
|
+
"@quenty/brio": "5.2.1-canary.261.5628274.0",
|
|
31
|
+
"@quenty/loader": "4.1.1-canary.261.5628274.0",
|
|
32
|
+
"@quenty/maid": "2.3.0",
|
|
33
|
+
"@quenty/promise": "4.2.1-canary.261.5628274.0",
|
|
34
|
+
"@quenty/rx": "4.2.1-canary.261.5628274.0",
|
|
35
|
+
"@quenty/signal": "2.2.0",
|
|
36
|
+
"@quenty/symbol": "2.1.0",
|
|
37
|
+
"@quenty/valuebaseutils": "4.2.1-canary.261.5628274.0"
|
|
37
38
|
},
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "5628274461f4fa0135e63b62e99fcd508d4c94f9"
|
|
42
43
|
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
An observable map that counts up/down and removes when the count is zero.
|
|
3
|
+
@class ObservableCountingMap
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Signal = require("Signal")
|
|
9
|
+
local Observable = require("Observable")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Brio = require("Brio")
|
|
12
|
+
local RxValueBaseUtils = require("RxValueBaseUtils")
|
|
13
|
+
|
|
14
|
+
local ObservableCountingMap = {}
|
|
15
|
+
ObservableCountingMap.ClassName = "ObservableCountingMap"
|
|
16
|
+
ObservableCountingMap.__index = ObservableCountingMap
|
|
17
|
+
|
|
18
|
+
--[=[
|
|
19
|
+
Constructs a new ObservableCountingMap
|
|
20
|
+
@return ObservableCountingMap<T>
|
|
21
|
+
]=]
|
|
22
|
+
function ObservableCountingMap.new()
|
|
23
|
+
local self = setmetatable({}, ObservableCountingMap)
|
|
24
|
+
|
|
25
|
+
self._maid = Maid.new()
|
|
26
|
+
self._map = {}
|
|
27
|
+
|
|
28
|
+
self._totalKeyCountValue = Instance.new("IntValue")
|
|
29
|
+
self._totalKeyCountValue.Value = 0
|
|
30
|
+
self._maid:GiveTask(self._totalKeyCountValue)
|
|
31
|
+
|
|
32
|
+
--[=[
|
|
33
|
+
Fires when an key is added
|
|
34
|
+
@readonly
|
|
35
|
+
@prop KeyAdded Signal<T>
|
|
36
|
+
@within ObservableCountingMap
|
|
37
|
+
]=]
|
|
38
|
+
self.KeyAdded = Signal.new()
|
|
39
|
+
self._maid:GiveTask(self.KeyAdded)
|
|
40
|
+
|
|
41
|
+
--[=[
|
|
42
|
+
Fires when an key is removed.
|
|
43
|
+
@readonly
|
|
44
|
+
@prop KeyRemoved Signal<T>
|
|
45
|
+
@within ObservableCountingMap
|
|
46
|
+
]=]
|
|
47
|
+
self.KeyRemoved = Signal.new()
|
|
48
|
+
self._maid:GiveTask(self.KeyRemoved)
|
|
49
|
+
|
|
50
|
+
--[=[
|
|
51
|
+
Fires when an item count changes
|
|
52
|
+
@readonly
|
|
53
|
+
@prop KeyChanged Signal<T>
|
|
54
|
+
@within ObservableCountingMap
|
|
55
|
+
]=]
|
|
56
|
+
self.KeyChanged = Signal.new()
|
|
57
|
+
self._maid:GiveTask(self.KeyChanged)
|
|
58
|
+
|
|
59
|
+
--[=[
|
|
60
|
+
Fires when the total count changes.
|
|
61
|
+
@prop CountChanged RBXScriptSignal
|
|
62
|
+
@within ObservableCountingMap
|
|
63
|
+
]=]
|
|
64
|
+
self.TotalKeyCountChanged = self._totalKeyCountValue.Changed
|
|
65
|
+
|
|
66
|
+
return self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
--[=[
|
|
70
|
+
Returns whether the value is an observable counting map
|
|
71
|
+
@param value any
|
|
72
|
+
@return boolean
|
|
73
|
+
]=]
|
|
74
|
+
function ObservableCountingMap.isObservableMap(value)
|
|
75
|
+
return type(value) == "table" and getmetatable(value) == ObservableCountingMap
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
--[=[
|
|
79
|
+
Observes the current set of active keys
|
|
80
|
+
@return Observable<{ T }>
|
|
81
|
+
]=]
|
|
82
|
+
function ObservableCountingMap:ObserveKeysList()
|
|
83
|
+
return self:_observeDerivedDataStructureFromKeys(function()
|
|
84
|
+
local list = {}
|
|
85
|
+
|
|
86
|
+
for key, _ in pairs(self._map) do
|
|
87
|
+
table.insert(list, key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
return list
|
|
91
|
+
end)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
--[=[
|
|
95
|
+
Observes the current set of active keys
|
|
96
|
+
@return Observable<{ [T]: true }>
|
|
97
|
+
]=]
|
|
98
|
+
function ObservableCountingMap:ObserveKeysSet()
|
|
99
|
+
return self:_observeDerivedDataStructureFromKeys(function()
|
|
100
|
+
local set = {}
|
|
101
|
+
|
|
102
|
+
for key, _ in pairs(self._map) do
|
|
103
|
+
set[key] = true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return set
|
|
107
|
+
end)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
function ObservableCountingMap:_observeDerivedDataStructureFromKeys(gatherValues)
|
|
111
|
+
return Observable.new(function(sub)
|
|
112
|
+
local maid = Maid.new()
|
|
113
|
+
|
|
114
|
+
local function emit()
|
|
115
|
+
sub:Fire(gatherValues())
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
maid:GiveTask(self.KeyAdded:Connect(emit))
|
|
119
|
+
maid:GiveTask(self.KeyRemoved:Connect(emit))
|
|
120
|
+
|
|
121
|
+
emit()
|
|
122
|
+
|
|
123
|
+
self._maid[sub] = maid
|
|
124
|
+
maid:GiveTask(function()
|
|
125
|
+
self._maid[sub] = nil
|
|
126
|
+
sub:Complete()
|
|
127
|
+
end)
|
|
128
|
+
|
|
129
|
+
return maid
|
|
130
|
+
end)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
--[=[
|
|
134
|
+
Observes all keys in the map
|
|
135
|
+
@return Observable<Brio<T>>
|
|
136
|
+
]=]
|
|
137
|
+
function ObservableCountingMap:ObserveKeysBrio()
|
|
138
|
+
return Observable.new(function(sub)
|
|
139
|
+
local maid = Maid.new()
|
|
140
|
+
|
|
141
|
+
local function handleItem(key)
|
|
142
|
+
local brio = Brio.new(key)
|
|
143
|
+
maid[key] = brio
|
|
144
|
+
sub:Fire(brio)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
for key, _ in pairs(self._map) do
|
|
148
|
+
handleItem(key)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
maid:GiveTask(self.KeyAdded:Connect(handleItem))
|
|
152
|
+
maid:GiveTask(self.KeyRemoved:Connect(function(key)
|
|
153
|
+
maid[key] = nil
|
|
154
|
+
end))
|
|
155
|
+
|
|
156
|
+
self._maid[sub] = maid
|
|
157
|
+
maid:GiveTask(function()
|
|
158
|
+
self._maid[sub] = nil
|
|
159
|
+
sub:Complete()
|
|
160
|
+
end)
|
|
161
|
+
|
|
162
|
+
return maid
|
|
163
|
+
end)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
--[=[
|
|
167
|
+
Returns whether the map contains the key
|
|
168
|
+
@param key T
|
|
169
|
+
@return boolean
|
|
170
|
+
]=]
|
|
171
|
+
function ObservableCountingMap:Contains(key)
|
|
172
|
+
assert(key ~= nil, "Bad key")
|
|
173
|
+
|
|
174
|
+
return self._map[key] ~= nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
--[=[
|
|
178
|
+
Returns the count for the key or 0 if there is no key
|
|
179
|
+
@param key T
|
|
180
|
+
@return number
|
|
181
|
+
]=]
|
|
182
|
+
function ObservableCountingMap:Get(key)
|
|
183
|
+
assert(key ~= nil, "Bad key")
|
|
184
|
+
|
|
185
|
+
return self._map[key] or 0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
--[=[
|
|
189
|
+
Gets the count of keys in the map
|
|
190
|
+
@return number
|
|
191
|
+
]=]
|
|
192
|
+
function ObservableCountingMap:GetTotalKeyCount()
|
|
193
|
+
return self._totalKeyCountValue.Value
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
--[=[
|
|
197
|
+
Observes the count of the keys in the map
|
|
198
|
+
@return Observable<number>
|
|
199
|
+
]=]
|
|
200
|
+
function ObservableCountingMap:ObserveTotalKeyCount()
|
|
201
|
+
return RxValueBaseUtils.observeValue(self._totalKeyCountValue)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
--[=[
|
|
205
|
+
Sets the current value
|
|
206
|
+
@param key T
|
|
207
|
+
@param amount number?
|
|
208
|
+
@return callback
|
|
209
|
+
]=]
|
|
210
|
+
function ObservableCountingMap:Set(key, amount)
|
|
211
|
+
local current = self:Get(key)
|
|
212
|
+
if current == amount then
|
|
213
|
+
return
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if current < amount then
|
|
217
|
+
self:Remove(amount - current)
|
|
218
|
+
return
|
|
219
|
+
elseif current == amount then
|
|
220
|
+
return
|
|
221
|
+
else
|
|
222
|
+
self:Add(current - amount)
|
|
223
|
+
return
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
--[=[
|
|
228
|
+
Adds the key to the map if it does not exists.
|
|
229
|
+
@param key T
|
|
230
|
+
@param amount number?
|
|
231
|
+
@return callback
|
|
232
|
+
]=]
|
|
233
|
+
function ObservableCountingMap:Add(key, amount)
|
|
234
|
+
assert(key ~= nil, "Bad key")
|
|
235
|
+
assert(type(amount) == "number" or amount == nil, "Bad amount")
|
|
236
|
+
amount = amount or 1
|
|
237
|
+
|
|
238
|
+
if amount == 0 then
|
|
239
|
+
return
|
|
240
|
+
elseif amount < 0 then
|
|
241
|
+
self:Remove(key, -amount)
|
|
242
|
+
return
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
if not self._map[key] then
|
|
246
|
+
self._map[key] = amount
|
|
247
|
+
self._totalKeyCountValue.Value = self._totalKeyCountValue.Value + 1
|
|
248
|
+
|
|
249
|
+
if self.Destroy then
|
|
250
|
+
self.KeyAdded:Fire(key)
|
|
251
|
+
end
|
|
252
|
+
else
|
|
253
|
+
local newValue = self._map[key] + amount
|
|
254
|
+
self._map[key] = newValue
|
|
255
|
+
self.KeyChanged:Fire(key, newValue)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
local removed = false
|
|
259
|
+
return function()
|
|
260
|
+
if self.Destroy and not removed then
|
|
261
|
+
removed = true
|
|
262
|
+
self:RemoveCount(key, amount)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
--[=[
|
|
268
|
+
Removes the key from the set if it exists.
|
|
269
|
+
@param key T
|
|
270
|
+
@param amount number?
|
|
271
|
+
@return callback
|
|
272
|
+
]=]
|
|
273
|
+
function ObservableCountingMap:RemoveCount(key, amount)
|
|
274
|
+
assert(key ~= nil, "Bad key")
|
|
275
|
+
assert(type(amount) == "number" or amount == nil, "Bad amount")
|
|
276
|
+
amount = amount or 1
|
|
277
|
+
|
|
278
|
+
if amount == 0 then
|
|
279
|
+
return
|
|
280
|
+
elseif amount < 0 then
|
|
281
|
+
self:Add(key, -amount)
|
|
282
|
+
return
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
local current = self._map[key]
|
|
286
|
+
if not current then
|
|
287
|
+
return
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
local newValue = current - amount
|
|
291
|
+
if newValue > 0 then
|
|
292
|
+
self._map[key] = newValue
|
|
293
|
+
self.KeyChanged:Fire(key, newValue)
|
|
294
|
+
else
|
|
295
|
+
self._map[key] = nil
|
|
296
|
+
|
|
297
|
+
self._totalKeyCountValue.Value = self._totalKeyCountValue.Value - 1
|
|
298
|
+
|
|
299
|
+
if self.Destroy then
|
|
300
|
+
self.KeyRemoved:Fire(key)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if self.Destroy then
|
|
304
|
+
self.KeyChanged:Fire(key, 0)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
--[=[
|
|
310
|
+
Gets the first key
|
|
311
|
+
@return T
|
|
312
|
+
]=]
|
|
313
|
+
function ObservableCountingMap:GetFirstKey()
|
|
314
|
+
local value = next(self._map)
|
|
315
|
+
return value
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
--[=[
|
|
319
|
+
Gets a list of all keys.
|
|
320
|
+
@return { T }
|
|
321
|
+
]=]
|
|
322
|
+
function ObservableCountingMap:GetKeysList()
|
|
323
|
+
local list = {}
|
|
324
|
+
for key, _ in pairs(self._map) do
|
|
325
|
+
table.insert(list, key)
|
|
326
|
+
end
|
|
327
|
+
return list
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
--[=[
|
|
331
|
+
Cleans up the ObservableCountingMap and sets the metatable to nil.
|
|
332
|
+
]=]
|
|
333
|
+
function ObservableCountingMap:Destroy()
|
|
334
|
+
self._maid:DoCleaning()
|
|
335
|
+
setmetatable(self, nil)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
return ObservableCountingMap
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ObservableCountingMap.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local ObservableCountingMap = require("ObservableCountingMap")
|
|
8
|
+
|
|
9
|
+
return function()
|
|
10
|
+
describe("ObservableCountingMap.new()", function()
|
|
11
|
+
local observableCountingMap = ObservableCountingMap.new()
|
|
12
|
+
|
|
13
|
+
it("should return 0 for unset values", function()
|
|
14
|
+
expect(observableCountingMap:Get("a")).to.equal(0)
|
|
15
|
+
expect(observableCountingMap:GetTotalKeyCount()).to.equal(0)
|
|
16
|
+
end)
|
|
17
|
+
|
|
18
|
+
it("should allow you to add to a value", function()
|
|
19
|
+
expect(observableCountingMap:Get("a")).to.equal(0)
|
|
20
|
+
expect(observableCountingMap:GetTotalKeyCount()).to.equal(0)
|
|
21
|
+
observableCountingMap:Add("a", 5)
|
|
22
|
+
expect(observableCountingMap:Get("a")).to.equal(5)
|
|
23
|
+
expect(observableCountingMap:GetTotalKeyCount()).to.equal(1)
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
it("should allow you to add to a value that is already defined", function()
|
|
27
|
+
expect(observableCountingMap:Get("a")).to.equal(5)
|
|
28
|
+
expect(observableCountingMap:GetTotalKeyCount()).to.equal(1)
|
|
29
|
+
observableCountingMap:Add("a", 5)
|
|
30
|
+
expect(observableCountingMap:Get("a")).to.equal(10)
|
|
31
|
+
expect(observableCountingMap:GetTotalKeyCount()).to.equal(1)
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("should clean up", function()
|
|
35
|
+
observableCountingMap:Destroy()
|
|
36
|
+
end)
|
|
37
|
+
end)
|
|
38
|
+
end
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
A list that can be observed for blend and other components
|
|
3
|
+
@class ObservableList
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Signal = require("Signal")
|
|
9
|
+
local Observable = require("Observable")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Brio = require("Brio")
|
|
12
|
+
local RxValueBaseUtils = require("RxValueBaseUtils")
|
|
13
|
+
local Symbol = require("Symbol")
|
|
14
|
+
|
|
15
|
+
local ObservableList = {}
|
|
16
|
+
ObservableList.ClassName = "ObservableList"
|
|
17
|
+
ObservableList.__index = ObservableList
|
|
18
|
+
|
|
19
|
+
--[=[
|
|
20
|
+
Constructs a new ObservableList
|
|
21
|
+
@return ObservableList<T>
|
|
22
|
+
]=]
|
|
23
|
+
function ObservableList.new()
|
|
24
|
+
local self = setmetatable({}, ObservableList)
|
|
25
|
+
|
|
26
|
+
self._maid = Maid.new()
|
|
27
|
+
|
|
28
|
+
self._keyList = {} -- { [number]: Symbol }
|
|
29
|
+
self._contents = {} -- { [Symbol]: T }
|
|
30
|
+
self._indexes = {} -- { [Symbol]: number }
|
|
31
|
+
|
|
32
|
+
self._keyObservables = {} -- { [Symbol]: { Subscription } }
|
|
33
|
+
|
|
34
|
+
self._countValue = Instance.new("IntValue")
|
|
35
|
+
self._countValue.Value = 0
|
|
36
|
+
self._maid:GiveTask(self._countValue)
|
|
37
|
+
|
|
38
|
+
--[=[
|
|
39
|
+
Fires when an item is added
|
|
40
|
+
@readonly
|
|
41
|
+
@prop ItemAdded Signal<T, number, Symbol>
|
|
42
|
+
@within ObservableList
|
|
43
|
+
]=]
|
|
44
|
+
self.ItemAdded = Signal.new()
|
|
45
|
+
self._maid:GiveTask(self.ItemAdded)
|
|
46
|
+
|
|
47
|
+
--[=[
|
|
48
|
+
Fires when an item is removed.
|
|
49
|
+
@readonly
|
|
50
|
+
@prop ItemRemoved Signal<T, Symbol>
|
|
51
|
+
@within ObservableList
|
|
52
|
+
]=]
|
|
53
|
+
self.ItemRemoved = Signal.new()
|
|
54
|
+
self._maid:GiveTask(self.ItemRemoved)
|
|
55
|
+
|
|
56
|
+
--[=[
|
|
57
|
+
Fires when the count changes.
|
|
58
|
+
@prop CountChanged RBXScriptSignal
|
|
59
|
+
@within ObservableList
|
|
60
|
+
]=]
|
|
61
|
+
self.CountChanged = self._countValue.Changed
|
|
62
|
+
|
|
63
|
+
return self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
--[=[
|
|
67
|
+
Returns whether the value is an observable list
|
|
68
|
+
@param value any
|
|
69
|
+
@return boolean
|
|
70
|
+
]=]
|
|
71
|
+
function ObservableList.isObservableList(value)
|
|
72
|
+
return type(value) == "table" and getmetatable(value) == ObservableList
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
--[=[
|
|
76
|
+
Observes all items in the list
|
|
77
|
+
@return Observable<Brio<T>>
|
|
78
|
+
]=]
|
|
79
|
+
function ObservableList:ObserveItemsBrio()
|
|
80
|
+
return Observable.new(function(sub)
|
|
81
|
+
local maid = Maid.new()
|
|
82
|
+
|
|
83
|
+
local function handleItem(item, _index, includeKey)
|
|
84
|
+
local brio = Brio.new(item, includeKey)
|
|
85
|
+
maid[includeKey] = brio
|
|
86
|
+
sub:Fire(brio)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
for index, key in pairs(self._keyList) do
|
|
90
|
+
handleItem(self._contents[key], index, key)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
maid:GiveTask(self.ItemAdded:Connect(handleItem))
|
|
94
|
+
maid:GiveTask(self.ItemRemoved:Connect(function(_item, includeKey)
|
|
95
|
+
maid[includeKey] = nil
|
|
96
|
+
end))
|
|
97
|
+
|
|
98
|
+
self._maid[sub] = maid
|
|
99
|
+
maid:GiveTask(function()
|
|
100
|
+
self._maid[sub] = nil
|
|
101
|
+
sub:Complete()
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
return maid
|
|
105
|
+
end)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
--[=[
|
|
109
|
+
Observes the index as it changes, until the entry at the existing
|
|
110
|
+
index is removed.
|
|
111
|
+
|
|
112
|
+
@param indexToObserve number
|
|
113
|
+
@return Observable<number>
|
|
114
|
+
]=]
|
|
115
|
+
function ObservableList:ObserveIndex(indexToObserve)
|
|
116
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
117
|
+
|
|
118
|
+
local key = self._keyList[indexToObserve]
|
|
119
|
+
if not key then
|
|
120
|
+
error(("No entry at index %q, cannot observe changes"):format(indexToObserve))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return self:ObserveIndexByKey(key)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
--[=[
|
|
127
|
+
Observes the index as it changes, until the entry at the existing
|
|
128
|
+
key is removed.
|
|
129
|
+
|
|
130
|
+
@param key Symbol
|
|
131
|
+
@return Observable<number>
|
|
132
|
+
]=]
|
|
133
|
+
function ObservableList:ObserveIndexByKey(key)
|
|
134
|
+
assert(key, "Bad key")
|
|
135
|
+
|
|
136
|
+
return Observable.new(function(sub)
|
|
137
|
+
local currentIndex = self._indexes[key]
|
|
138
|
+
if not currentIndex then
|
|
139
|
+
sub:Complete()
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local maid = Maid.new()
|
|
144
|
+
self._keyObservables[key] = self._keyObservables[key] or {}
|
|
145
|
+
table.insert(self._keyObservables[key], sub)
|
|
146
|
+
|
|
147
|
+
sub:Fire(currentIndex)
|
|
148
|
+
|
|
149
|
+
maid:GiveTask(function()
|
|
150
|
+
local list = self._keyObservables[key]
|
|
151
|
+
if not list then
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
local index = table.find(list, sub)
|
|
156
|
+
if index then
|
|
157
|
+
table.remove(list, index)
|
|
158
|
+
if #list == 0 then
|
|
159
|
+
self._keyObservables[key] = nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end)
|
|
163
|
+
|
|
164
|
+
return maid
|
|
165
|
+
end)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
--[=[
|
|
169
|
+
Gets the count of items in the list
|
|
170
|
+
@return number
|
|
171
|
+
]=]
|
|
172
|
+
function ObservableList:GetCount()
|
|
173
|
+
return self._countValue.Value
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
--[=[
|
|
177
|
+
Observes the count of the list
|
|
178
|
+
@return Observable<number>
|
|
179
|
+
]=]
|
|
180
|
+
function ObservableList:ObserveCount()
|
|
181
|
+
return RxValueBaseUtils.observeValue(self._countValue)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
--[=[
|
|
185
|
+
Adds the item to the list at the specified index
|
|
186
|
+
@param item T
|
|
187
|
+
@return callback -- Call to remove
|
|
188
|
+
]=]
|
|
189
|
+
function ObservableList:Add(item)
|
|
190
|
+
return self:InsertAt(item, #self._keyList + 1)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
--[=[
|
|
194
|
+
Gets the current item at the index, or nil if it is not defined.
|
|
195
|
+
@param index number
|
|
196
|
+
@return T?
|
|
197
|
+
]=]
|
|
198
|
+
function ObservableList:Get(index)
|
|
199
|
+
assert(type(index) == "number", "Bad index")
|
|
200
|
+
|
|
201
|
+
local key = self._keyList[index]
|
|
202
|
+
if not key then
|
|
203
|
+
return nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
return self._contents[key]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
--[=[
|
|
210
|
+
Adds the item to the list at the specified index
|
|
211
|
+
@param item T
|
|
212
|
+
@param index number?
|
|
213
|
+
@return callback -- Call to remove
|
|
214
|
+
]=]
|
|
215
|
+
function ObservableList:InsertAt(item, index)
|
|
216
|
+
assert(item ~= nil, "Bad item")
|
|
217
|
+
assert(type(index) == "number", "Bad index")
|
|
218
|
+
|
|
219
|
+
index = math.clamp(index, 1, #self._keyList + 1)
|
|
220
|
+
|
|
221
|
+
local key = Symbol.named("entryKey")
|
|
222
|
+
|
|
223
|
+
self._contents[key] = item
|
|
224
|
+
self._indexes[key] = index
|
|
225
|
+
|
|
226
|
+
local changed = {}
|
|
227
|
+
|
|
228
|
+
local n = #self._keyList
|
|
229
|
+
for i=n, index, -1 do
|
|
230
|
+
local nextKey = self._keyList[i]
|
|
231
|
+
self._indexes[nextKey] = i + 1
|
|
232
|
+
self._keyList[i + 1] = nextKey
|
|
233
|
+
|
|
234
|
+
local subs = self._keyObservables[nextKey]
|
|
235
|
+
if subs then
|
|
236
|
+
table.insert(changed, {
|
|
237
|
+
key = nextKey;
|
|
238
|
+
newIndex = i + 1;
|
|
239
|
+
subs = subs;
|
|
240
|
+
})
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
self._keyList[index] = key
|
|
245
|
+
|
|
246
|
+
-- Fire off count
|
|
247
|
+
self._countValue.Value = self._countValue.Value + 1
|
|
248
|
+
|
|
249
|
+
-- Fire off add
|
|
250
|
+
self.ItemAdded:Fire(item, index, key)
|
|
251
|
+
|
|
252
|
+
-- Fire off the index change on the value
|
|
253
|
+
do
|
|
254
|
+
local list = self._keyObservables[key]
|
|
255
|
+
if list then
|
|
256
|
+
self._keyObservables[key] = nil
|
|
257
|
+
|
|
258
|
+
for _, sub in pairs(list) do
|
|
259
|
+
if sub:IsPending() then
|
|
260
|
+
sub:Fire(index)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
-- Fire off index change on each key list (if the data isn't stale)
|
|
267
|
+
for _, data in pairs(changed) do
|
|
268
|
+
if self._indexes[data.key] == data.newIndex then
|
|
269
|
+
self:_fireSubs(data.subs, data.newIndex)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
return function()
|
|
274
|
+
if self.Destroy then
|
|
275
|
+
self:RemoveByKey(key)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
--[=[
|
|
281
|
+
Removes the item at the index
|
|
282
|
+
@param index number
|
|
283
|
+
@return T
|
|
284
|
+
]=]
|
|
285
|
+
function ObservableList:RemoveAt(index)
|
|
286
|
+
assert(type(index) == "number", "Bad index")
|
|
287
|
+
|
|
288
|
+
local key = self._keyList[index]
|
|
289
|
+
if not key then
|
|
290
|
+
return nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
return self:RemoveByKey(key)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
--[=[
|
|
297
|
+
Removes the item from the list if it exists.
|
|
298
|
+
@param key Symbol
|
|
299
|
+
@return T
|
|
300
|
+
]=]
|
|
301
|
+
function ObservableList:RemoveByKey(key)
|
|
302
|
+
assert(key ~= nil, "Bad key")
|
|
303
|
+
|
|
304
|
+
local index = self._indexes[key]
|
|
305
|
+
if not index then
|
|
306
|
+
return nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
local item = self._contents[key]
|
|
310
|
+
if not item then
|
|
311
|
+
return nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
local observableSubs = self._keyObservables[key]
|
|
315
|
+
self._keyObservables[key] = nil
|
|
316
|
+
self._indexes[key] = nil
|
|
317
|
+
self._contents[key] = nil
|
|
318
|
+
|
|
319
|
+
local changed = {}
|
|
320
|
+
|
|
321
|
+
-- shift everything down
|
|
322
|
+
local n = #self._keyList
|
|
323
|
+
for i=index, n - 1 do
|
|
324
|
+
local nextKey = self._keyList[i+1]
|
|
325
|
+
self._indexes[nextKey] = i
|
|
326
|
+
self._keyList[i] = nextKey
|
|
327
|
+
|
|
328
|
+
local subs = self._keyObservables[nextKey]
|
|
329
|
+
if subs then
|
|
330
|
+
table.insert(changed, {
|
|
331
|
+
key = nextKey;
|
|
332
|
+
newIndex = i;
|
|
333
|
+
subs = subs;
|
|
334
|
+
})
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
self._keyList[n] = nil
|
|
338
|
+
|
|
339
|
+
-- Fire off that count changed
|
|
340
|
+
self._countValue.Value = self._countValue.Value - 1
|
|
341
|
+
|
|
342
|
+
if self.Destroy then
|
|
343
|
+
self.ItemRemoved:Fire(item, key)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
-- Fire off the index change on the value
|
|
347
|
+
if observableSubs then
|
|
348
|
+
self:_completeSubs(observableSubs)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
-- Fire off index change on each key list (if the data isn't stale)
|
|
352
|
+
for _, data in pairs(changed) do
|
|
353
|
+
if self._indexes[data.key] == data.newIndex then
|
|
354
|
+
self:_fireSubs(data.subs, data.newIndex)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
return item
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
function ObservableList:_fireSubs(list, index)
|
|
362
|
+
for _, sub in pairs(list) do
|
|
363
|
+
if sub:IsPending() then
|
|
364
|
+
task.spawn(function()
|
|
365
|
+
sub:Fire(index)
|
|
366
|
+
end)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
function ObservableList:_completeSubs(list)
|
|
372
|
+
for _, sub in pairs(list) do
|
|
373
|
+
if sub:IsPending() then
|
|
374
|
+
sub:Fire(nil)
|
|
375
|
+
sub:Complete()
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
--[=[
|
|
381
|
+
Gets a list of all entries.
|
|
382
|
+
@return { T }
|
|
383
|
+
]=]
|
|
384
|
+
function ObservableList:GetList()
|
|
385
|
+
local list = {}
|
|
386
|
+
for _, key in pairs(self._keyList) do
|
|
387
|
+
table.insert(list, self._contents[key])
|
|
388
|
+
end
|
|
389
|
+
return list
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
--[=[
|
|
393
|
+
Cleans up the ObservableList and sets the metatable to nil.
|
|
394
|
+
]=]
|
|
395
|
+
function ObservableList:Destroy()
|
|
396
|
+
self._maid:DoCleaning()
|
|
397
|
+
setmetatable(self, nil)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
return ObservableList
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ObservableList.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local ObservableList = require("ObservableList")
|
|
8
|
+
|
|
9
|
+
return function()
|
|
10
|
+
describe("ObservableList.new()", function()
|
|
11
|
+
local observableList = ObservableList.new()
|
|
12
|
+
|
|
13
|
+
it("should return nil for unset values", function()
|
|
14
|
+
expect(observableList:Get(1)).to.equal(nil)
|
|
15
|
+
end)
|
|
16
|
+
|
|
17
|
+
it("should allow inserting an value", function()
|
|
18
|
+
expect(observableList:GetCount()).to.equal(0)
|
|
19
|
+
|
|
20
|
+
observableList:Add("a")
|
|
21
|
+
|
|
22
|
+
expect(observableList:Get(1)).to.equal("a")
|
|
23
|
+
expect(observableList:GetCount()).to.equal(1)
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
it("should allow false as a value", function()
|
|
28
|
+
expect(observableList:Get(2)).to.equal(nil)
|
|
29
|
+
observableList:Add(false)
|
|
30
|
+
expect(observableList:Get(2)).to.equal(false)
|
|
31
|
+
end)
|
|
32
|
+
|
|
33
|
+
it("should fire off events for a specific key", function()
|
|
34
|
+
local seen = {}
|
|
35
|
+
local sub = observableList:ObserveIndex(1):Subscribe(function(value)
|
|
36
|
+
table.insert(seen, value)
|
|
37
|
+
end)
|
|
38
|
+
observableList:InsertAt("c", 1)
|
|
39
|
+
|
|
40
|
+
sub:Destroy()
|
|
41
|
+
|
|
42
|
+
expect(#seen).to.equal(2)
|
|
43
|
+
expect(seen[1]).to.equal(1)
|
|
44
|
+
expect(seen[2]).to.equal(2)
|
|
45
|
+
end)
|
|
46
|
+
|
|
47
|
+
it("should fire off events for all keys", function()
|
|
48
|
+
local seen = {}
|
|
49
|
+
local sub = observableList:ObserveItemsBrio():Subscribe(function(value)
|
|
50
|
+
table.insert(seen, value)
|
|
51
|
+
end)
|
|
52
|
+
observableList:Add("a")
|
|
53
|
+
|
|
54
|
+
expect(#seen).to.equal(4)
|
|
55
|
+
expect(seen[4]:GetValue()).to.equal("a")
|
|
56
|
+
expect(seen[4]:IsDead()).to.equal(false)
|
|
57
|
+
|
|
58
|
+
sub:Destroy()
|
|
59
|
+
|
|
60
|
+
expect(#seen).to.equal(4)
|
|
61
|
+
expect(seen[4]:IsDead()).to.equal(true)
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
it("should fire off events on removal", function()
|
|
65
|
+
local seen = {}
|
|
66
|
+
local sub = observableList:ObserveIndex(2):Subscribe(function(value)
|
|
67
|
+
table.insert(seen, value)
|
|
68
|
+
end)
|
|
69
|
+
observableList:RemoveAt(1)
|
|
70
|
+
|
|
71
|
+
sub:Destroy()
|
|
72
|
+
|
|
73
|
+
expect(#seen).to.equal(2)
|
|
74
|
+
expect(seen[1]).to.equal(2)
|
|
75
|
+
expect(seen[2]).to.equal(1)
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
it("should clean up", function()
|
|
79
|
+
observableList:Destroy()
|
|
80
|
+
end)
|
|
81
|
+
end)
|
|
82
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
A list that can be observed for blend and other components
|
|
3
|
+
@class ObservableMap
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Signal = require("Signal")
|
|
9
|
+
local Observable = require("Observable")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Brio = require("Brio")
|
|
12
|
+
local RxValueBaseUtils = require("RxValueBaseUtils")
|
|
13
|
+
|
|
14
|
+
local ObservableMap = {}
|
|
15
|
+
ObservableMap.ClassName = "ObservableMap"
|
|
16
|
+
ObservableMap.__index = ObservableMap
|
|
17
|
+
|
|
18
|
+
--[=[
|
|
19
|
+
Constructs a new ObservableMap
|
|
20
|
+
@return ObservableMap<TKey, TValue>
|
|
21
|
+
]=]
|
|
22
|
+
function ObservableMap.new()
|
|
23
|
+
local self = setmetatable({}, ObservableMap)
|
|
24
|
+
|
|
25
|
+
self._maid = Maid.new()
|
|
26
|
+
self._map = {}
|
|
27
|
+
|
|
28
|
+
self._keyToSubList = {}
|
|
29
|
+
|
|
30
|
+
self._countValue = Instance.new("IntValue")
|
|
31
|
+
self._countValue.Value = 0
|
|
32
|
+
self._maid:GiveTask(self._countValue)
|
|
33
|
+
|
|
34
|
+
--[=[
|
|
35
|
+
Fires when a key is added
|
|
36
|
+
@readonly
|
|
37
|
+
@prop KeyAdded Signal<TKey>
|
|
38
|
+
@within ObservableMap
|
|
39
|
+
]=]
|
|
40
|
+
self.KeyAdded = Signal.new() -- :Fire(key, value)
|
|
41
|
+
self._maid:GiveTask(self.KeyAdded)
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
Fires when a key is removed
|
|
45
|
+
@readonly
|
|
46
|
+
@prop KeyRemoved Signal<TKey>
|
|
47
|
+
@within ObservableMap
|
|
48
|
+
]=]
|
|
49
|
+
self.KeyRemoved = Signal.new() -- :Fire(key)
|
|
50
|
+
self._maid:GiveTask(self.KeyRemoved)
|
|
51
|
+
|
|
52
|
+
--[=[
|
|
53
|
+
Fires when a key value changes, including add and remove.
|
|
54
|
+
@readonly
|
|
55
|
+
@prop KeyValueChanged Signal<(TKey, TValue, TValue)>
|
|
56
|
+
@within ObservableMap
|
|
57
|
+
]=]
|
|
58
|
+
self.KeyValueChanged = Signal.new() -- :Fire(key, value, oldValue)
|
|
59
|
+
self._maid:GiveTask(self.KeyValueChanged)
|
|
60
|
+
|
|
61
|
+
--[=[
|
|
62
|
+
Fires when the count changes.
|
|
63
|
+
@prop CountChanged RBXScriptSignal
|
|
64
|
+
@within ObservableMap
|
|
65
|
+
]=]
|
|
66
|
+
self.CountChanged = self._countValue.Changed
|
|
67
|
+
|
|
68
|
+
return self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
--[=[
|
|
72
|
+
Returns whether the set is an observable map
|
|
73
|
+
@param value any
|
|
74
|
+
@return boolean
|
|
75
|
+
]=]
|
|
76
|
+
function ObservableMap.isObservableMap(value)
|
|
77
|
+
return type(value) == "table" and getmetatable(value) == ObservableMap
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
--[=[
|
|
81
|
+
Observes all keys in the map
|
|
82
|
+
@return Observable<Brio<TKey>>
|
|
83
|
+
]=]
|
|
84
|
+
function ObservableMap:ObserveKeysBrio()
|
|
85
|
+
return self:_observeKeyValueChanged(function(key, _value)
|
|
86
|
+
return Brio.new(key)
|
|
87
|
+
end)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
--[=[
|
|
91
|
+
Observes all keys in the map
|
|
92
|
+
@return Observable<Brio<TKey>>
|
|
93
|
+
]=]
|
|
94
|
+
function ObservableMap:ObserveValuesBrio()
|
|
95
|
+
return self:_observeKeyValueChanged(function(_key, value)
|
|
96
|
+
return Brio.new(value)
|
|
97
|
+
end)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
--[=[
|
|
101
|
+
Observes all keys in the map
|
|
102
|
+
@return Observable<Brio<(TKey, TValue)>>
|
|
103
|
+
]=]
|
|
104
|
+
function ObservableMap:ObservePairsBrio()
|
|
105
|
+
return self:_observeKeyValueChanged(function(key, value)
|
|
106
|
+
return Brio.new(key, value)
|
|
107
|
+
end)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
function ObservableMap:_observeKeyValueChanged(packValue)
|
|
111
|
+
return Observable.new(function(sub)
|
|
112
|
+
local maid = Maid.new()
|
|
113
|
+
|
|
114
|
+
local function handleValue(key, value)
|
|
115
|
+
if value ~= nil then
|
|
116
|
+
local brio = packValue(key, value)
|
|
117
|
+
maid[key] = brio
|
|
118
|
+
sub:Fire(brio)
|
|
119
|
+
else
|
|
120
|
+
maid[key] = nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
for key, value in pairs(self._map) do
|
|
125
|
+
handleValue(key, value)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
maid:GiveTask(self.KeyValueChanged:Connect(handleValue))
|
|
129
|
+
|
|
130
|
+
self._maid[sub] = maid
|
|
131
|
+
maid:GiveTask(function()
|
|
132
|
+
self._maid[sub] = nil
|
|
133
|
+
sub:Complete()
|
|
134
|
+
end)
|
|
135
|
+
|
|
136
|
+
return maid
|
|
137
|
+
end)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
--[=[
|
|
141
|
+
Returns the value for the given key
|
|
142
|
+
@param key TKey
|
|
143
|
+
@return TValue
|
|
144
|
+
]=]
|
|
145
|
+
function ObservableMap:Get(key)
|
|
146
|
+
assert(key ~= nil, "Bad key")
|
|
147
|
+
|
|
148
|
+
return self._map[key]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
--[=[
|
|
152
|
+
Returns whether the map contains the key
|
|
153
|
+
@param key TKey
|
|
154
|
+
@return boolean
|
|
155
|
+
]=]
|
|
156
|
+
function ObservableMap:ContainsKey(key)
|
|
157
|
+
assert(key ~= nil, "Bad key")
|
|
158
|
+
|
|
159
|
+
return self._map[key] ~= nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
--[=[
|
|
163
|
+
Gets the count of items in the set
|
|
164
|
+
@return number
|
|
165
|
+
]=]
|
|
166
|
+
function ObservableMap:GetCount()
|
|
167
|
+
return self._countValue.Value
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
--[=[
|
|
171
|
+
Observes the count of the set
|
|
172
|
+
@return Observable<number>
|
|
173
|
+
]=]
|
|
174
|
+
function ObservableMap:ObserveCount()
|
|
175
|
+
return RxValueBaseUtils.observeValue(self._countValue)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
--[=[
|
|
179
|
+
Observes the value for the given slot
|
|
180
|
+
@param key TKey
|
|
181
|
+
@return Observable<TValue?>
|
|
182
|
+
]=]
|
|
183
|
+
function ObservableMap:ObserveValueForKey(key)
|
|
184
|
+
assert(key ~= nil, "Bad key")
|
|
185
|
+
|
|
186
|
+
return Observable.new(function(sub)
|
|
187
|
+
local maid = Maid.new()
|
|
188
|
+
|
|
189
|
+
if not self._keyToSubList[key] then
|
|
190
|
+
self._keyToSubList[key] = {}
|
|
191
|
+
end
|
|
192
|
+
table.insert(self._keyToSubList[key], sub)
|
|
193
|
+
|
|
194
|
+
maid:GiveTask(function()
|
|
195
|
+
local subsList = self._keyToSubList[key]
|
|
196
|
+
if subsList then
|
|
197
|
+
local index = table.find(subsList, sub)
|
|
198
|
+
|
|
199
|
+
if index then
|
|
200
|
+
table.remove(subsList, index)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if #subsList == 0 then
|
|
204
|
+
self._keyToSubList[key] = nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end)
|
|
208
|
+
|
|
209
|
+
sub:Fire(self._map[key])
|
|
210
|
+
|
|
211
|
+
return maid
|
|
212
|
+
end)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
--[=[
|
|
216
|
+
Adds the item to the set if it does not exists.
|
|
217
|
+
@param key TKey
|
|
218
|
+
@param value TValue?
|
|
219
|
+
@return callback -- Call to remove the value.
|
|
220
|
+
]=]
|
|
221
|
+
function ObservableMap:Set(key, value)
|
|
222
|
+
assert(key ~= nil, "Bad key")
|
|
223
|
+
|
|
224
|
+
local oldValue = self._map[key]
|
|
225
|
+
if oldValue == value then
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
self._map[key] = value
|
|
230
|
+
|
|
231
|
+
if oldValue == nil then
|
|
232
|
+
self._countValue.Value = self._countValue.Value + 1
|
|
233
|
+
self.KeyAdded:Fire(key, value)
|
|
234
|
+
elseif value == nil then
|
|
235
|
+
self._countValue.Value = self._countValue.Value - 1
|
|
236
|
+
self.KeyRemoved:Fire(key)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
self.KeyValueChanged:Fire(key, value, oldValue)
|
|
240
|
+
|
|
241
|
+
local subList = self._keyToSubList[key]
|
|
242
|
+
if subList then
|
|
243
|
+
for _, sub in pairs(table.clone(subList)) do
|
|
244
|
+
if sub and sub:IsPending() then
|
|
245
|
+
task.spawn(function()
|
|
246
|
+
sub:Fire(value)
|
|
247
|
+
end)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
return function()
|
|
253
|
+
if self.Destroy then
|
|
254
|
+
self:Remove(key)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
--[=[
|
|
260
|
+
Removes the item from the map if it exists.
|
|
261
|
+
@param key TKey
|
|
262
|
+
]=]
|
|
263
|
+
function ObservableMap:Remove(key)
|
|
264
|
+
assert(key ~= nil, "Bad key")
|
|
265
|
+
|
|
266
|
+
self:Set(key, nil)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
--[=[
|
|
270
|
+
Gets a list of all values.
|
|
271
|
+
@return { TValue }
|
|
272
|
+
]=]
|
|
273
|
+
function ObservableMap:GetValueList()
|
|
274
|
+
local list = {}
|
|
275
|
+
for _, value in pairs(self._map) do
|
|
276
|
+
table.insert(list, value)
|
|
277
|
+
end
|
|
278
|
+
return list
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
--[=[
|
|
282
|
+
Gets a list of all keys.
|
|
283
|
+
@return { TKey }
|
|
284
|
+
]=]
|
|
285
|
+
function ObservableMap:GetKeyList()
|
|
286
|
+
local list = {}
|
|
287
|
+
for key, _ in pairs(self._map) do
|
|
288
|
+
table.insert(list, key)
|
|
289
|
+
end
|
|
290
|
+
return list
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
--[=[
|
|
295
|
+
Cleans up the ObservableMap and sets the metatable to nil.
|
|
296
|
+
]=]
|
|
297
|
+
function ObservableMap:Destroy()
|
|
298
|
+
self._maid:DoCleaning()
|
|
299
|
+
setmetatable(self, nil)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
return ObservableMap
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ObservableMap.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local ObservableMap = require("ObservableMap")
|
|
8
|
+
|
|
9
|
+
return function()
|
|
10
|
+
describe("ObservableMap.new()", function()
|
|
11
|
+
local observableMap = ObservableMap.new()
|
|
12
|
+
|
|
13
|
+
it("should return nil for unset values", function()
|
|
14
|
+
expect(observableMap:Get("a")).to.equal(nil)
|
|
15
|
+
end)
|
|
16
|
+
|
|
17
|
+
it("should allow setting a value", function()
|
|
18
|
+
expect(observableMap:GetCount()).to.equal(0)
|
|
19
|
+
|
|
20
|
+
observableMap:Set("a", "Hello World")
|
|
21
|
+
|
|
22
|
+
expect(observableMap:Get("a")).to.equal("Hello World")
|
|
23
|
+
expect(observableMap:GetCount()).to.equal(1)
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
it("should overwrite values", function()
|
|
27
|
+
expect(observableMap:Get("a")).to.equal("Hello World")
|
|
28
|
+
|
|
29
|
+
observableMap:Set("a", "Hello World 2")
|
|
30
|
+
|
|
31
|
+
expect(observableMap:Get("a")).to.equal("Hello World 2")
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("should allow false as a key", function()
|
|
35
|
+
expect(observableMap:Get(false)).to.equal(nil)
|
|
36
|
+
observableMap:Set(false, "Hello")
|
|
37
|
+
expect(observableMap:Get(false)).to.equal("Hello")
|
|
38
|
+
end)
|
|
39
|
+
|
|
40
|
+
it("should fire off events for a specific key", function()
|
|
41
|
+
local seen = {}
|
|
42
|
+
local sub = observableMap:ObserveValueForKey("c"):Subscribe(function(value)
|
|
43
|
+
table.insert(seen, value)
|
|
44
|
+
end)
|
|
45
|
+
observableMap:Set("c", "Hello")
|
|
46
|
+
|
|
47
|
+
sub:Destroy()
|
|
48
|
+
|
|
49
|
+
expect(#seen).to.equal(1)
|
|
50
|
+
expect(seen[1]).to.equal("Hello")
|
|
51
|
+
end)
|
|
52
|
+
|
|
53
|
+
it("should fire off events for all keys", function()
|
|
54
|
+
local seen = {}
|
|
55
|
+
local sub = observableMap:ObserveValuesBrio():Subscribe(function(value)
|
|
56
|
+
table.insert(seen, value)
|
|
57
|
+
end)
|
|
58
|
+
observableMap:Set("d", "Hello")
|
|
59
|
+
|
|
60
|
+
expect(#seen).to.equal(4)
|
|
61
|
+
expect(seen[4]:GetValue()).to.equal("Hello")
|
|
62
|
+
expect(seen[4]:IsDead()).to.equal(false)
|
|
63
|
+
|
|
64
|
+
sub:Destroy()
|
|
65
|
+
|
|
66
|
+
expect(#seen).to.equal(4)
|
|
67
|
+
expect(seen[4]:IsDead()).to.equal(true)
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
it("should clean up", function()
|
|
71
|
+
observableMap:Destroy()
|
|
72
|
+
end)
|
|
73
|
+
end)
|
|
74
|
+
end
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
--[=[
|
|
2
|
+
Holds a map of sets. That is, for a given key, a set of all valid entries. This is great
|
|
3
|
+
for looking up something that may have duplicate keys, like configurations or other things.
|
|
4
|
+
|
|
2
5
|
@class ObservableMapSet
|
|
3
6
|
]=]
|
|
4
7
|
|
|
@@ -13,21 +16,44 @@ local ObservableMapSet = {}
|
|
|
13
16
|
ObservableMapSet.ClassName = "ObservableMapSet"
|
|
14
17
|
ObservableMapSet.__index = ObservableMapSet
|
|
15
18
|
|
|
19
|
+
--[=[
|
|
20
|
+
Constructs a new ObservableMapSet
|
|
21
|
+
@return ObservableMapSet<TKey, TValue>
|
|
22
|
+
]=]
|
|
16
23
|
function ObservableMapSet.new()
|
|
17
24
|
local self = setmetatable({}, ObservableMapSet)
|
|
18
25
|
|
|
19
26
|
self._maid = Maid.new()
|
|
20
27
|
self._observableSetMap = {} -- [key] = ObservableSet<TEntry>
|
|
21
28
|
|
|
29
|
+
--[=[
|
|
30
|
+
Fires when an item is added
|
|
31
|
+
@readonly
|
|
32
|
+
@prop SetAdded Signal<TKey>
|
|
33
|
+
@within ObservableMapSet
|
|
34
|
+
]=]
|
|
22
35
|
self.SetAdded = Signal.new() -- :Fire(key, set)
|
|
23
36
|
self._maid:GiveTask(self.SetAdded)
|
|
24
37
|
|
|
38
|
+
--[=[
|
|
39
|
+
Fires when an item is removed
|
|
40
|
+
@readonly
|
|
41
|
+
@prop SetRemoved Signal<TKey>
|
|
42
|
+
@within ObservableMapSet
|
|
43
|
+
]=]
|
|
25
44
|
self.SetRemoved = Signal.new() -- :Fire(key)
|
|
26
45
|
self._maid:GiveTask(self.SetRemoved)
|
|
27
46
|
|
|
28
47
|
return self
|
|
29
48
|
end
|
|
30
49
|
|
|
50
|
+
--[=[
|
|
51
|
+
Adds an entry with a dynamic key. This is great for caching things
|
|
52
|
+
that need to be looked up by key.
|
|
53
|
+
|
|
54
|
+
@param entry TValue
|
|
55
|
+
@param observeKey Observable<Brio<TKey>>
|
|
56
|
+
]=]
|
|
31
57
|
function ObservableMapSet:Add(entry, observeKey)
|
|
32
58
|
local maid = Maid.new()
|
|
33
59
|
|
|
@@ -17,7 +17,7 @@ ObservableSet.__index = ObservableSet
|
|
|
17
17
|
|
|
18
18
|
--[=[
|
|
19
19
|
Constructs a new ObservableSet
|
|
20
|
-
@return ObservableSet
|
|
20
|
+
@return ObservableSet<T>
|
|
21
21
|
]=]
|
|
22
22
|
function ObservableSet.new()
|
|
23
23
|
local self = setmetatable({}, ObservableSet)
|
|
@@ -58,7 +58,7 @@ function ObservableSet.new()
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
--[=[
|
|
61
|
-
Returns whether the
|
|
61
|
+
Returns whether the value is an observable set
|
|
62
62
|
@param value any
|
|
63
63
|
@return boolean
|
|
64
64
|
]=]
|
|
@@ -129,6 +129,7 @@ end
|
|
|
129
129
|
--[=[
|
|
130
130
|
Adds the item to the set if it does not exists.
|
|
131
131
|
@param item T
|
|
132
|
+
@return callback -- Call to remove
|
|
132
133
|
]=]
|
|
133
134
|
function ObservableSet:Add(item)
|
|
134
135
|
assert(item ~= nil, "Bad item")
|
|
@@ -163,6 +164,10 @@ function ObservableSet:Remove(item)
|
|
|
163
164
|
end
|
|
164
165
|
end
|
|
165
166
|
|
|
167
|
+
--[=[
|
|
168
|
+
Gets an arbitrary item in the set (not guaranteed to be ordered)
|
|
169
|
+
@return T
|
|
170
|
+
]=]
|
|
166
171
|
function ObservableSet:GetFirstItem()
|
|
167
172
|
local value = next(self._set)
|
|
168
173
|
return value
|
|
@@ -180,6 +185,18 @@ function ObservableSet:GetList()
|
|
|
180
185
|
return list
|
|
181
186
|
end
|
|
182
187
|
|
|
188
|
+
--[=[
|
|
189
|
+
Gets a copy of the set
|
|
190
|
+
@return { [T]: true }
|
|
191
|
+
]=]
|
|
192
|
+
function ObservableSet:GetSetCopy()
|
|
193
|
+
local set = {}
|
|
194
|
+
for item, _ in pairs(self._set) do
|
|
195
|
+
set[item] = true
|
|
196
|
+
end
|
|
197
|
+
return set
|
|
198
|
+
end
|
|
199
|
+
|
|
183
200
|
--[=[
|
|
184
201
|
Cleans up the ObservableSet and sets the metatable to nil.
|
|
185
202
|
]=]
|