@quenty/observablecollection 5.24.0 → 5.25.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 +5 -4
- package/src/Shared/ObservableCountingMap.lua +1 -1
- package/src/Shared/ObservableList.lua +86 -88
- package/src/Shared/ObservableList.spec.lua +48 -0
- package/src/Shared/ObservableMap.lua +32 -2
- package/src/Shared/ObservableMapList.lua +303 -0
- package/src/Shared/ObservableMapList.spec.lua +24 -0
- package/src/Shared/ObservableMapSet.lua +42 -5
- package/src/Shared/ObservableSortedList.lua +3 -13
- package/src/Shared/ObservableSortedList.spec.lua +8 -1
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
|
+
# [5.25.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@5.24.0...@quenty/observablecollection@5.25.0) (2023-09-04)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add ObservableMapList.new() and fix unit tests ([84e16ed](https://github.com/Quenty/NevermoreEngine/commit/84e16ed86af9bbc1ad19486f0298a9f7f3484b3e))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [5.24.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@5.23.0...@quenty/observablecollection@5.24.0) (2023-08-23)
|
|
7
18
|
|
|
8
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/observablecollection",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.25.0",
|
|
4
4
|
"description": "A set of observable collections, such as sets, maps, sorted lists, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -28,17 +28,18 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@quenty/baseobject": "^6.3.0",
|
|
31
|
-
"@quenty/brio": "^8.
|
|
31
|
+
"@quenty/brio": "^8.18.0",
|
|
32
32
|
"@quenty/loader": "^6.3.0",
|
|
33
33
|
"@quenty/maid": "^2.6.0",
|
|
34
34
|
"@quenty/promise": "^6.8.0",
|
|
35
35
|
"@quenty/rx": "^7.15.0",
|
|
36
36
|
"@quenty/signal": "^2.4.0",
|
|
37
|
+
"@quenty/steputils": "^3.3.0",
|
|
37
38
|
"@quenty/symbol": "^2.2.0",
|
|
38
|
-
"@quenty/valueobject": "^7.
|
|
39
|
+
"@quenty/valueobject": "^7.23.0"
|
|
39
40
|
},
|
|
40
41
|
"publishConfig": {
|
|
41
42
|
"access": "public"
|
|
42
43
|
},
|
|
43
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "fa0826fa54fabecb242d7c0753e2e7644ba02e11"
|
|
44
45
|
}
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
local require = require(script.Parent.loader).load(script)
|
|
7
7
|
|
|
8
|
-
local Signal = require("Signal")
|
|
9
|
-
local Observable = require("Observable")
|
|
10
|
-
local Maid = require("Maid")
|
|
11
8
|
local Brio = require("Brio")
|
|
9
|
+
local Maid = require("Maid")
|
|
10
|
+
local Observable = require("Observable")
|
|
11
|
+
local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
|
|
12
|
+
local Rx = require("Rx")
|
|
13
|
+
local RxBrioUtils = require("RxBrioUtils")
|
|
14
|
+
local Signal = require("Signal")
|
|
12
15
|
local Symbol = require("Symbol")
|
|
13
16
|
local ValueObject = require("ValueObject")
|
|
14
|
-
local Rx = require("Rx")
|
|
15
17
|
|
|
16
18
|
local ObservableList = {}
|
|
17
19
|
ObservableList.ClassName = "ObservableList"
|
|
@@ -30,7 +32,11 @@ function ObservableList.new()
|
|
|
30
32
|
self._contents = {} -- { [Symbol]: T }
|
|
31
33
|
self._indexes = {} -- { [Symbol]: number }
|
|
32
34
|
|
|
33
|
-
self.
|
|
35
|
+
self._indexObservers = ObservableSubscriptionTable.new()
|
|
36
|
+
self._maid:GiveTask(self._indexObservers)
|
|
37
|
+
|
|
38
|
+
self._keyIndexObservables = ObservableSubscriptionTable.new()
|
|
39
|
+
self._maid:GiveTask(self._keyIndexObservables)
|
|
34
40
|
|
|
35
41
|
self._countValue = ValueObject.new(0, "number")
|
|
36
42
|
self._maid:GiveTask(self._countValue)
|
|
@@ -134,12 +140,43 @@ function ObservableList:ObserveIndex(indexToObserve)
|
|
|
134
140
|
|
|
135
141
|
local key = self._keyList[indexToObserve]
|
|
136
142
|
if not key then
|
|
137
|
-
error(("No entry at index %q, cannot observe changes"
|
|
143
|
+
error(string.format("No entry at index %q, cannot observe changes", indexToObserve))
|
|
138
144
|
end
|
|
139
145
|
|
|
140
146
|
return self:ObserveIndexByKey(key)
|
|
141
147
|
end
|
|
142
148
|
|
|
149
|
+
--[=[
|
|
150
|
+
Observes the current value at a given index. This can be useful for observing
|
|
151
|
+
the first entry, or matching stuff up to a given slot.
|
|
152
|
+
|
|
153
|
+
@param indexToObserve number
|
|
154
|
+
@return Observable<T?>
|
|
155
|
+
]=]
|
|
156
|
+
function ObservableList:ObserveAtIndex(indexToObserve)
|
|
157
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
158
|
+
|
|
159
|
+
return self._indexObservers:Observe(indexToObserve, function(sub)
|
|
160
|
+
sub:Fire(self:Get(indexToObserve))
|
|
161
|
+
end)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
--[=[
|
|
165
|
+
Observes the current value at a given index. This can be useful for observing
|
|
166
|
+
the first entry, or matching stuff up to a given slot.
|
|
167
|
+
|
|
168
|
+
@param indexToObserve number
|
|
169
|
+
@return Observable<Brio<T>>
|
|
170
|
+
]=]
|
|
171
|
+
function ObservableList:ObserveAtIndexBrio(indexToObserve)
|
|
172
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
173
|
+
|
|
174
|
+
return self:ObserveAtIndex(indexToObserve):Pipe({
|
|
175
|
+
RxBrioUtils.toBrio();
|
|
176
|
+
RxBrioUtils.onlyLastBrioSurvives();
|
|
177
|
+
})
|
|
178
|
+
end
|
|
179
|
+
|
|
143
180
|
--[=[
|
|
144
181
|
Removes the first instance found in contents
|
|
145
182
|
|
|
@@ -176,35 +213,8 @@ end
|
|
|
176
213
|
function ObservableList:ObserveIndexByKey(key)
|
|
177
214
|
assert(type(key) == "userdata", "Bad key")
|
|
178
215
|
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
if not currentIndex then
|
|
182
|
-
sub:Complete()
|
|
183
|
-
return
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
local maid = Maid.new()
|
|
187
|
-
self._keyObservables[key] = self._keyObservables[key] or {}
|
|
188
|
-
table.insert(self._keyObservables[key], sub)
|
|
189
|
-
|
|
190
|
-
sub:Fire(currentIndex)
|
|
191
|
-
|
|
192
|
-
maid:GiveTask(function()
|
|
193
|
-
local list = self._keyObservables[key]
|
|
194
|
-
if not list then
|
|
195
|
-
return
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
local index = table.find(list, sub)
|
|
199
|
-
if index then
|
|
200
|
-
table.remove(list, index)
|
|
201
|
-
if #list == 0 then
|
|
202
|
-
self._keyObservables[key] = nil
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end)
|
|
206
|
-
|
|
207
|
-
return maid
|
|
216
|
+
return self._keyIndexObservables:Observe(key, function(sub)
|
|
217
|
+
sub:Fire(self:GetIndexByKey(key))
|
|
208
218
|
end)
|
|
209
219
|
end
|
|
210
220
|
|
|
@@ -256,6 +266,8 @@ end
|
|
|
256
266
|
function ObservableList:Get(index)
|
|
257
267
|
assert(type(index) == "number", "Bad index")
|
|
258
268
|
|
|
269
|
+
index = self:_toPositiveIndex(index)
|
|
270
|
+
|
|
259
271
|
local key = self._keyList[index]
|
|
260
272
|
if not key then
|
|
261
273
|
return nil
|
|
@@ -289,17 +301,14 @@ function ObservableList:InsertAt(item, index)
|
|
|
289
301
|
self._indexes[nextKey] = i + 1
|
|
290
302
|
self._keyList[i + 1] = nextKey
|
|
291
303
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
newIndex = i + 1;
|
|
297
|
-
subs = subs;
|
|
298
|
-
})
|
|
299
|
-
end
|
|
304
|
+
table.insert(changed, {
|
|
305
|
+
key = nextKey;
|
|
306
|
+
newIndex = i + 1;
|
|
307
|
+
})
|
|
300
308
|
end
|
|
301
309
|
|
|
302
310
|
self._keyList[index] = key
|
|
311
|
+
local listLength = #self._keyList
|
|
303
312
|
|
|
304
313
|
-- Fire off count
|
|
305
314
|
self._countValue.Value = self._countValue.Value + 1
|
|
@@ -307,23 +316,17 @@ function ObservableList:InsertAt(item, index)
|
|
|
307
316
|
-- Fire off add
|
|
308
317
|
self.ItemAdded:Fire(item, index, key)
|
|
309
318
|
|
|
310
|
-
-- Fire off the index change on the value
|
|
311
|
-
do
|
|
312
|
-
local subs = self._keyObservables[key]
|
|
313
|
-
if subs then
|
|
314
|
-
table.insert(changed, {
|
|
315
|
-
key = key;
|
|
316
|
-
newIndex = index;
|
|
317
|
-
subs = subs;
|
|
318
|
-
})
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
319
|
|
|
320
|
+
-- Fire off the index change on the value
|
|
321
|
+
self._keyIndexObservables:Fire(key, index)
|
|
322
|
+
self._indexObservers:Fire(index, item)
|
|
323
|
+
self._indexObservers:Fire(self:_toNegativeIndex(listLength, index), item)
|
|
322
324
|
|
|
323
|
-
-- Fire off index change on each key list (if the data isn't stale)
|
|
324
325
|
for _, data in pairs(changed) do
|
|
325
326
|
if self._indexes[data.key] == data.newIndex then
|
|
326
|
-
self:
|
|
327
|
+
self._indexObservers:Fire(data.newIndex, self._contents[data.key])
|
|
328
|
+
self._indexObservers:Fire(self:_toNegativeIndex(listLength, index), self._contents[data.key])
|
|
329
|
+
self._keyIndexObservables:Fire(data.key, data.newIndex)
|
|
327
330
|
end
|
|
328
331
|
end
|
|
329
332
|
|
|
@@ -368,8 +371,6 @@ function ObservableList:RemoveByKey(key)
|
|
|
368
371
|
return nil
|
|
369
372
|
end
|
|
370
373
|
|
|
371
|
-
local observableSubs = self._keyObservables[key]
|
|
372
|
-
self._keyObservables[key] = nil
|
|
373
374
|
self._indexes[key] = nil
|
|
374
375
|
self._contents[key] = nil
|
|
375
376
|
|
|
@@ -382,16 +383,13 @@ function ObservableList:RemoveByKey(key)
|
|
|
382
383
|
self._indexes[nextKey] = i
|
|
383
384
|
self._keyList[i] = nextKey
|
|
384
385
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
newIndex = i;
|
|
390
|
-
subs = subs;
|
|
391
|
-
})
|
|
392
|
-
end
|
|
386
|
+
table.insert(changed, {
|
|
387
|
+
key = nextKey;
|
|
388
|
+
newIndex = i;
|
|
389
|
+
})
|
|
393
390
|
end
|
|
394
391
|
self._keyList[n] = nil
|
|
392
|
+
local listLength = #self._keyList
|
|
395
393
|
|
|
396
394
|
-- Fire off that count changed
|
|
397
395
|
self._countValue.Value = self._countValue.Value - 1
|
|
@@ -401,39 +399,25 @@ function ObservableList:RemoveByKey(key)
|
|
|
401
399
|
end
|
|
402
400
|
|
|
403
401
|
-- Fire off the index change on the value
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
self._keyIndexObservables:Complete(key)
|
|
403
|
+
self._indexObservers:Fire(listLength, nil)
|
|
404
|
+
|
|
405
|
+
if listLength == 0 then
|
|
406
|
+
self._indexObservers:Fire(-1, nil)
|
|
406
407
|
end
|
|
407
408
|
|
|
408
409
|
-- Fire off index change on each key list (if the data isn't stale)
|
|
409
410
|
for _, data in pairs(changed) do
|
|
410
411
|
if self._indexes[data.key] == data.newIndex then
|
|
411
|
-
self:
|
|
412
|
+
self._indexObservers:Fire(data.newIndex, self._contents[data.key])
|
|
413
|
+
self._indexObservers:Fire(self:_toNegativeIndex(listLength, index), self._contents[data.key])
|
|
414
|
+
self._keyIndexObservables:Fire(data.key, data.newIndex)
|
|
412
415
|
end
|
|
413
416
|
end
|
|
414
417
|
|
|
415
418
|
return item
|
|
416
419
|
end
|
|
417
420
|
|
|
418
|
-
function ObservableList:_fireSubs(list, index)
|
|
419
|
-
for _, sub in pairs(list) do
|
|
420
|
-
if sub:IsPending() then
|
|
421
|
-
task.spawn(function()
|
|
422
|
-
sub:Fire(index)
|
|
423
|
-
end)
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
function ObservableList:_completeSubs(list)
|
|
429
|
-
for _, sub in pairs(list) do
|
|
430
|
-
if sub:IsPending() then
|
|
431
|
-
sub:Fire(nil)
|
|
432
|
-
sub:Complete()
|
|
433
|
-
end
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
|
|
437
421
|
--[=[
|
|
438
422
|
Gets a list of all entries.
|
|
439
423
|
@return { T }
|
|
@@ -446,6 +430,20 @@ function ObservableList:GetList()
|
|
|
446
430
|
return list
|
|
447
431
|
end
|
|
448
432
|
|
|
433
|
+
function ObservableList:_toPositiveIndex(index)
|
|
434
|
+
if index > 0 then
|
|
435
|
+
return index
|
|
436
|
+
elseif index < 0 then
|
|
437
|
+
return #self._keyList + index + 1
|
|
438
|
+
else
|
|
439
|
+
error(string.format("[ObservableList._toPositiveIndex] - Bad index %d", index))
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
function ObservableList:_toNegativeIndex(listLength, index)
|
|
444
|
+
return -listLength + index - 1
|
|
445
|
+
end
|
|
446
|
+
|
|
449
447
|
--[=[
|
|
450
448
|
Cleans up the ObservableList and sets the metatable to nil.
|
|
451
449
|
]=]
|
|
@@ -23,6 +23,10 @@ return function()
|
|
|
23
23
|
expect(observableList:GetCount()).to.equal(1)
|
|
24
24
|
end)
|
|
25
25
|
|
|
26
|
+
it("should allow negative queries", function()
|
|
27
|
+
expect(observableList:Get(-1)).to.equal("a")
|
|
28
|
+
expect(observableList:Get(-2)).to.equal(nil)
|
|
29
|
+
end)
|
|
26
30
|
|
|
27
31
|
it("should allow false as a value", function()
|
|
28
32
|
expect(observableList:Get(2)).to.equal(nil)
|
|
@@ -30,6 +34,14 @@ return function()
|
|
|
30
34
|
expect(observableList:Get(2)).to.equal(false)
|
|
31
35
|
end)
|
|
32
36
|
|
|
37
|
+
it("should allow negative queries after false", function()
|
|
38
|
+
expect(observableList:Get(1)).to.equal("a")
|
|
39
|
+
expect(observableList:Get(2)).to.equal(false)
|
|
40
|
+
|
|
41
|
+
expect(observableList:Get(-1)).to.equal(false)
|
|
42
|
+
expect(observableList:Get(-2)).to.equal("a")
|
|
43
|
+
end)
|
|
44
|
+
|
|
33
45
|
it("should fire off events for a specific key", function()
|
|
34
46
|
local seen = {}
|
|
35
47
|
local sub = observableList:ObserveIndex(1):Subscribe(function(value)
|
|
@@ -61,6 +73,42 @@ return function()
|
|
|
61
73
|
expect(seen[4]:IsDead()).to.equal(true)
|
|
62
74
|
end)
|
|
63
75
|
|
|
76
|
+
it("it should be able to observe a specific key", function()
|
|
77
|
+
local seen = {}
|
|
78
|
+
local sub = observableList:ObserveAtIndex(1):Subscribe(function(value)
|
|
79
|
+
table.insert(seen, value)
|
|
80
|
+
end)
|
|
81
|
+
|
|
82
|
+
local originalList = observableList:GetList()
|
|
83
|
+
expect(originalList[1]).to.equal("c")
|
|
84
|
+
|
|
85
|
+
observableList:InsertAt("dragon", 1)
|
|
86
|
+
|
|
87
|
+
sub:Destroy()
|
|
88
|
+
|
|
89
|
+
expect(#seen).to.equal(2)
|
|
90
|
+
expect(seen[1]).to.equal("c")
|
|
91
|
+
expect(seen[2]).to.equal("dragon")
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
it("it should be able to observe a specific negative key", function()
|
|
95
|
+
local seen = {}
|
|
96
|
+
local sub = observableList:ObserveAtIndex(-1):Subscribe(function(value)
|
|
97
|
+
table.insert(seen, value)
|
|
98
|
+
end)
|
|
99
|
+
|
|
100
|
+
local originalList = observableList:GetList()
|
|
101
|
+
expect(originalList[#originalList]).to.equal("a")
|
|
102
|
+
|
|
103
|
+
observableList:Add("fire")
|
|
104
|
+
|
|
105
|
+
sub:Destroy()
|
|
106
|
+
|
|
107
|
+
expect(#seen).to.equal(2)
|
|
108
|
+
expect(seen[1]).to.equal("a")
|
|
109
|
+
expect(seen[2]).to.equal("fire")
|
|
110
|
+
end)
|
|
111
|
+
|
|
64
112
|
it("should fire off events on removal", function()
|
|
65
113
|
local seen = {}
|
|
66
114
|
local sub = observableList:ObserveIndex(2):Subscribe(function(value)
|
|
@@ -11,6 +11,7 @@ local Observable = require("Observable")
|
|
|
11
11
|
local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
|
|
12
12
|
local Signal = require("Signal")
|
|
13
13
|
local ValueObject = require("ValueObject")
|
|
14
|
+
local RxBrioUtils = require("RxBrioUtils")
|
|
14
15
|
|
|
15
16
|
local ObservableMap = {}
|
|
16
17
|
ObservableMap.ClassName = "ObservableMap"
|
|
@@ -170,6 +171,7 @@ end
|
|
|
170
171
|
|
|
171
172
|
--[=[
|
|
172
173
|
Observes the count of the set
|
|
174
|
+
|
|
173
175
|
@return Observable<number>
|
|
174
176
|
]=]
|
|
175
177
|
function ObservableMap:ObserveCount()
|
|
@@ -177,11 +179,29 @@ function ObservableMap:ObserveCount()
|
|
|
177
179
|
end
|
|
178
180
|
|
|
179
181
|
--[=[
|
|
180
|
-
Observes the value for the given
|
|
182
|
+
Observes the value for the given key.
|
|
183
|
+
|
|
184
|
+
@param key TKey
|
|
185
|
+
@return Observable<Brio<TValue>>
|
|
186
|
+
]=]
|
|
187
|
+
function ObservableMap:ObserveAtKeyBrio(key)
|
|
188
|
+
assert(key ~= nil, "Bad key")
|
|
189
|
+
|
|
190
|
+
return self:ObserveAtKey(key):Pipe({
|
|
191
|
+
RxBrioUtils.toBrio();
|
|
192
|
+
RxBrioUtils.where(function(value)
|
|
193
|
+
return value ~= nil
|
|
194
|
+
end)
|
|
195
|
+
})
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
--[=[
|
|
199
|
+
Observes the value for the given key.
|
|
200
|
+
|
|
181
201
|
@param key TKey
|
|
182
202
|
@return Observable<TValue?>
|
|
183
203
|
]=]
|
|
184
|
-
function ObservableMap:
|
|
204
|
+
function ObservableMap:ObserveAtKey(key)
|
|
185
205
|
assert(key ~= nil, "Bad key")
|
|
186
206
|
|
|
187
207
|
return self._keySubTable:Observe(key, function(sub)
|
|
@@ -189,6 +209,16 @@ function ObservableMap:ObserveValueForKey(key)
|
|
|
189
209
|
end)
|
|
190
210
|
end
|
|
191
211
|
|
|
212
|
+
--[=[
|
|
213
|
+
Observes the value for the given key. Alias for [ObservableMap.ObserveAtKey].
|
|
214
|
+
|
|
215
|
+
@function ObserveValueForKey
|
|
216
|
+
@param key TKey
|
|
217
|
+
@return Observable<TValue?>
|
|
218
|
+
@within ObservableMap
|
|
219
|
+
]=]
|
|
220
|
+
ObservableMap.ObserveValueForKey = ObservableMap.ObserveAtKey
|
|
221
|
+
|
|
192
222
|
--[=[
|
|
193
223
|
Adds the item to the set if it does not exists.
|
|
194
224
|
@param key TKey
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
Holds a map of lists. This is good for list-based
|
|
3
|
+
|
|
4
|
+
@class ObservableMapList
|
|
5
|
+
]=]
|
|
6
|
+
|
|
7
|
+
local require = require(script.Parent.loader).load(script)
|
|
8
|
+
|
|
9
|
+
local Maid = require("Maid")
|
|
10
|
+
local Observable = require("Observable")
|
|
11
|
+
local ObservableList = require("ObservableList")
|
|
12
|
+
local ObservableMap = require("ObservableMap")
|
|
13
|
+
local Rx = require("Rx")
|
|
14
|
+
local RxBrioUtils = require("RxBrioUtils")
|
|
15
|
+
|
|
16
|
+
local ObservableMapList = {}
|
|
17
|
+
ObservableMapList.ClassName = "ObservableMapList"
|
|
18
|
+
ObservableMapList.__index = ObservableMapList
|
|
19
|
+
|
|
20
|
+
--[=[
|
|
21
|
+
Constructs a new ObservableMapList
|
|
22
|
+
@return ObservableMapList<TKey, TValue>
|
|
23
|
+
]=]
|
|
24
|
+
function ObservableMapList.new()
|
|
25
|
+
local self = setmetatable({}, ObservableMapList)
|
|
26
|
+
|
|
27
|
+
self._maid = Maid.new()
|
|
28
|
+
self._observableMapOfLists = self._maid:Add(ObservableMap.new())
|
|
29
|
+
|
|
30
|
+
return self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
--[=[
|
|
34
|
+
Adds an entry with a dynamic key. This is great for caching things
|
|
35
|
+
that need to be looked up by key.
|
|
36
|
+
|
|
37
|
+
:::tip
|
|
38
|
+
If `observeKey` emits nil then the value will be excluded from the list.
|
|
39
|
+
:::
|
|
40
|
+
|
|
41
|
+
@param entry TValue
|
|
42
|
+
@param observeKey Observable<TKey>
|
|
43
|
+
@return MaidTask -- Cleanup object that will remove the entry
|
|
44
|
+
]=]
|
|
45
|
+
function ObservableMapList:Push(observeKey, entry)
|
|
46
|
+
assert(observeKey ~= nil, "Bad observeKey")
|
|
47
|
+
assert(entry ~= nil, "Bad entry")
|
|
48
|
+
|
|
49
|
+
if not Observable.isObservable(observeKey) then
|
|
50
|
+
observeKey = Rx.of(observeKey)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
local maid = Maid.new()
|
|
54
|
+
|
|
55
|
+
local lastKey = nil
|
|
56
|
+
local function removeLastEntry()
|
|
57
|
+
if lastKey ~= nil then
|
|
58
|
+
self:_removeFromList(lastKey, entry)
|
|
59
|
+
end
|
|
60
|
+
lastKey = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
maid:GiveTask(observeKey:Subscribe(function(key)
|
|
64
|
+
removeLastEntry()
|
|
65
|
+
|
|
66
|
+
if key ~= nil then
|
|
67
|
+
self:_addToList(key, entry)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
lastKey = key
|
|
71
|
+
end))
|
|
72
|
+
|
|
73
|
+
maid:GiveTask(removeLastEntry)
|
|
74
|
+
|
|
75
|
+
-- Ensure self-cleanup when map cleans up
|
|
76
|
+
self._maid[maid] = maid
|
|
77
|
+
maid:GiveTask(function()
|
|
78
|
+
self._maid[maid] = nil
|
|
79
|
+
end)
|
|
80
|
+
|
|
81
|
+
return maid
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
--[=[
|
|
85
|
+
Gets how many lists exist
|
|
86
|
+
@return number
|
|
87
|
+
]=]
|
|
88
|
+
function ObservableMapList:GetListCount()
|
|
89
|
+
return self._observableMapOfLists:GetCount()
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
--[=[
|
|
93
|
+
Observes how many lists exist
|
|
94
|
+
@return Observable<number>
|
|
95
|
+
]=]
|
|
96
|
+
function ObservableMapList:ObserveListCount()
|
|
97
|
+
return self._observableMapOfLists:ObserveCount()
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
--[=[
|
|
101
|
+
Gets the current value at the list index
|
|
102
|
+
|
|
103
|
+
@param key TKey
|
|
104
|
+
@param index number
|
|
105
|
+
@return Observable<TValue?>
|
|
106
|
+
]=]
|
|
107
|
+
function ObservableMapList:GetAtListIndex(key, index)
|
|
108
|
+
assert(key ~= nil, "Bad key")
|
|
109
|
+
assert(type(index) == "number", "Bad index")
|
|
110
|
+
|
|
111
|
+
local list = self._observableMapOfLists:Get(key)
|
|
112
|
+
if list then
|
|
113
|
+
return list:Get(index)
|
|
114
|
+
else
|
|
115
|
+
return nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
--[=[
|
|
120
|
+
Observes the current value at the index
|
|
121
|
+
|
|
122
|
+
@param key TKey
|
|
123
|
+
@param index number
|
|
124
|
+
@return Observable<TValue?>
|
|
125
|
+
]=]
|
|
126
|
+
function ObservableMapList:ObserveAtListIndex(key, index)
|
|
127
|
+
assert(key ~= nil, "Bad key")
|
|
128
|
+
assert(type(index) == "number", "Bad index")
|
|
129
|
+
|
|
130
|
+
return self._observableMapOfLists:ObserveAtKey(key):Pipe({
|
|
131
|
+
Rx.switchMap(function(list)
|
|
132
|
+
if list then
|
|
133
|
+
return list:ObserveAtIndex(index)
|
|
134
|
+
else
|
|
135
|
+
return Rx.of(nil)
|
|
136
|
+
end
|
|
137
|
+
end);
|
|
138
|
+
})
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
--[=[
|
|
142
|
+
Gets a list of all keys
|
|
143
|
+
|
|
144
|
+
@return { TKey }
|
|
145
|
+
]=]
|
|
146
|
+
function ObservableMapList:GetKeyList()
|
|
147
|
+
return self._observableMapOfLists:GetKeyList()
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
--[=[
|
|
152
|
+
Observes all keys in the map
|
|
153
|
+
@return Observable<Brio<TKey>>
|
|
154
|
+
]=]
|
|
155
|
+
function ObservableMapList:ObserveKeysBrio()
|
|
156
|
+
return self._observableMapOfLists:ObserveKeysBrio()
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
--[=[
|
|
160
|
+
Observes the current value at the index
|
|
161
|
+
|
|
162
|
+
@param key TKey
|
|
163
|
+
@param index number
|
|
164
|
+
@return Observable<Brio<TValue>>
|
|
165
|
+
]=]
|
|
166
|
+
function ObservableMapList:ObserveAtListIndexBrio(key, index)
|
|
167
|
+
assert(key ~= nil, "Bad key")
|
|
168
|
+
assert(type(index) == "number", "Bad index")
|
|
169
|
+
|
|
170
|
+
return self._observableMapOfLists:ObserveAtKeyBrio(key):Pipe({
|
|
171
|
+
RxBrioUtils.switchMapBrio(function(list)
|
|
172
|
+
return list:ObserveAtIndexBrio(-1)
|
|
173
|
+
end);
|
|
174
|
+
RxBrioUtils.toBrio();
|
|
175
|
+
RxBrioUtils.where(function(value)
|
|
176
|
+
return value ~= nil
|
|
177
|
+
end)
|
|
178
|
+
})
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
--[=[
|
|
182
|
+
Observes all items at the given key
|
|
183
|
+
|
|
184
|
+
@param key TKey
|
|
185
|
+
@return Observable<Brio<TValue>>
|
|
186
|
+
]=]
|
|
187
|
+
function ObservableMapList:ObserveItemsForKeyBrio(key)
|
|
188
|
+
assert(key ~= nil, "Bad key")
|
|
189
|
+
|
|
190
|
+
return self._observableMapOfLists:ObserveAtKeyBrio(key):Pipe({
|
|
191
|
+
RxBrioUtils.switchMapBrio(function(list)
|
|
192
|
+
if list then
|
|
193
|
+
return list:ObserveItemsBrio()
|
|
194
|
+
else
|
|
195
|
+
return Rx.EMPTY
|
|
196
|
+
end
|
|
197
|
+
end);
|
|
198
|
+
})
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
--[=[
|
|
202
|
+
Gets a list for a given key.
|
|
203
|
+
|
|
204
|
+
@param key TKey
|
|
205
|
+
@return { TValue }
|
|
206
|
+
]=]
|
|
207
|
+
function ObservableMapList:GetListForKey(key)
|
|
208
|
+
assert(key ~= nil, "Bad key")
|
|
209
|
+
|
|
210
|
+
local observableList = self:GetListForKey(key)
|
|
211
|
+
if not observableList then
|
|
212
|
+
return {}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
return observableList:GetList()
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
--[=[
|
|
219
|
+
Gets the observable list for the given key
|
|
220
|
+
@param key TKey
|
|
221
|
+
@return ObservableList<TValue>
|
|
222
|
+
]=]
|
|
223
|
+
function ObservableMapList:GetListForKey(key)
|
|
224
|
+
assert(key ~= nil, "Bad key")
|
|
225
|
+
|
|
226
|
+
return self._observableMapOfLists:Get(key)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
function ObservableMapList:ObserveListBrio(key)
|
|
230
|
+
assert(key ~= nil, "Bad key")
|
|
231
|
+
|
|
232
|
+
return self._observableMapOfLists:ObserveAtKey(key)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
function ObservableMapList:ObserveCountForKey(key)
|
|
236
|
+
assert(key ~= nil, "Bad key")
|
|
237
|
+
|
|
238
|
+
return self:ObserveListBrio(key):Pipe({
|
|
239
|
+
RxBrioUtils.switchMapBrio(function(observableList)
|
|
240
|
+
return observableList:ObserveCount()
|
|
241
|
+
end);
|
|
242
|
+
RxBrioUtils.emitOnDeath(0);
|
|
243
|
+
})
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
function ObservableMapList:_addToList(key, entry)
|
|
247
|
+
local list = self:_getOrCreateList(key)
|
|
248
|
+
list:Add(entry)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
function ObservableMapList:_removeFromList(key, entry)
|
|
252
|
+
local list = self._observableMapOfLists:Get(key)
|
|
253
|
+
if not list then
|
|
254
|
+
return
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
-- This happens when we're cleaning up sometimes
|
|
258
|
+
if not list.Destroy then
|
|
259
|
+
return
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if list:Contains(entry) then
|
|
263
|
+
list:Remove(entry)
|
|
264
|
+
|
|
265
|
+
if list:GetCount() == 0 then
|
|
266
|
+
self:_removeList(key)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
function ObservableMapList:_removeList(key)
|
|
272
|
+
local list = self._observableLists[key]
|
|
273
|
+
if list then
|
|
274
|
+
self._observableLists[key] = nil
|
|
275
|
+
self._maid[list] = nil
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
function ObservableMapList:_getOrCreateList(key)
|
|
280
|
+
local existing = self._observableMapOfLists:Get(key)
|
|
281
|
+
if existing then
|
|
282
|
+
return existing
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
local maid = Maid.new()
|
|
286
|
+
local list = maid:Add(ObservableList.new(nil))
|
|
287
|
+
|
|
288
|
+
self._observableMapOfLists:Set(key, list)
|
|
289
|
+
self._maid[list] = maid
|
|
290
|
+
|
|
291
|
+
return list
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
--[=[
|
|
295
|
+
Cleans up the ObservableMapList and sets the metatable to nil.
|
|
296
|
+
]=]
|
|
297
|
+
function ObservableMapList:Destroy()
|
|
298
|
+
self._maid:DoCleaning()
|
|
299
|
+
setmetatable(self, nil)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
return ObservableMapList
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ObservableMapList.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local ObservableMapList = require("ObservableMapList")
|
|
8
|
+
|
|
9
|
+
return function()
|
|
10
|
+
describe("ObservableMapList.new()", function()
|
|
11
|
+
local observableMapList = ObservableMapList.new()
|
|
12
|
+
|
|
13
|
+
it("should return nil for unset values", function()
|
|
14
|
+
expect(observableMapList:GetAtListIndex("dragon", 1)).to.equal(nil)
|
|
15
|
+
end)
|
|
16
|
+
|
|
17
|
+
it("should allow additions", function()
|
|
18
|
+
observableMapList:Add("hello", "dragon")
|
|
19
|
+
expect(observableMapList:GetAtListIndex("dragon", 1)).to.equal("hello")
|
|
20
|
+
expect(observableMapList:GetAtListIndex("dragon", -1)).to.equal("hello")
|
|
21
|
+
expect(observableMapList:GetAtListIndex("fire", 1)).to.equal(nil)
|
|
22
|
+
end)
|
|
23
|
+
end)
|
|
24
|
+
end
|
|
@@ -14,6 +14,7 @@ local Signal = require("Signal")
|
|
|
14
14
|
local Brio = require("Brio")
|
|
15
15
|
local RxBrioUtils = require("RxBrioUtils")
|
|
16
16
|
local ValueObject = require("ValueObject")
|
|
17
|
+
local Rx = require("Rx")
|
|
17
18
|
|
|
18
19
|
local ObservableMapSet = {}
|
|
19
20
|
ObservableMapSet.ClassName = "ObservableMapSet"
|
|
@@ -57,15 +58,28 @@ end
|
|
|
57
58
|
Adds an entry with a dynamic key. This is great for caching things
|
|
58
59
|
that need to be looked up by key.
|
|
59
60
|
|
|
61
|
+
:::tip
|
|
62
|
+
If `observeKey` emits nil then the value will be excluded from the map.
|
|
63
|
+
:::
|
|
64
|
+
|
|
60
65
|
@param entry TValue
|
|
61
|
-
@param observeKey Observable<TKey>
|
|
66
|
+
@param observeKey Observable<TKey> | TKey
|
|
67
|
+
@return MaidTask -- Cleanup object that will remove the entry
|
|
62
68
|
]=]
|
|
63
|
-
|
|
69
|
+
|
|
70
|
+
function ObservableMapSet:Push(observeKey, entry)
|
|
71
|
+
assert(observeKey ~= nil, "Bad observeKey")
|
|
72
|
+
assert(entry ~= nil, "Bad entry")
|
|
73
|
+
|
|
74
|
+
if not Observable.isObservable(observeKey) then
|
|
75
|
+
observeKey = Rx.of(observeKey)
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
local maid = Maid.new()
|
|
65
79
|
|
|
66
80
|
local lastKey = nil
|
|
67
81
|
local function removeLastEntry()
|
|
68
|
-
if lastKey then
|
|
82
|
+
if lastKey ~= nil then
|
|
69
83
|
self:_removeFromObservableSet(lastKey, entry)
|
|
70
84
|
end
|
|
71
85
|
lastKey = nil
|
|
@@ -74,14 +88,13 @@ function ObservableMapSet:Add(entry, observeKey)
|
|
|
74
88
|
maid:GiveTask(observeKey:Subscribe(function(key)
|
|
75
89
|
removeLastEntry()
|
|
76
90
|
|
|
77
|
-
if key then
|
|
91
|
+
if key ~= nil then
|
|
78
92
|
self:_addToObservableSet(key, entry)
|
|
79
93
|
end
|
|
80
94
|
|
|
81
95
|
lastKey = key
|
|
82
96
|
end))
|
|
83
97
|
|
|
84
|
-
|
|
85
98
|
maid:GiveTask(removeLastEntry)
|
|
86
99
|
|
|
87
100
|
-- Ensure self-cleanup when map cleans up
|
|
@@ -93,6 +106,30 @@ function ObservableMapSet:Add(entry, observeKey)
|
|
|
93
106
|
return maid
|
|
94
107
|
end
|
|
95
108
|
|
|
109
|
+
--[=[
|
|
110
|
+
Adds an entry with a dynamic key. This is great for caching things
|
|
111
|
+
that need to be looked up by key.
|
|
112
|
+
|
|
113
|
+
This code is legacy code since our argument order isn't intuitive
|
|
114
|
+
|
|
115
|
+
:::tip
|
|
116
|
+
If `observeKey` emits nil then the value will be excluded from the map.
|
|
117
|
+
:::
|
|
118
|
+
|
|
119
|
+
@param entry TValue
|
|
120
|
+
@param observeKey Observable<TKey> | TKey
|
|
121
|
+
@return MaidTask -- Cleanup object that will remove the entry
|
|
122
|
+
]=]
|
|
123
|
+
function ObservableMapSet:Add(entry, observeKey)
|
|
124
|
+
assert(Observable.isObservable(observeKey), "Bad observeKey")
|
|
125
|
+
assert(entry ~= nil, "Bad entry")
|
|
126
|
+
|
|
127
|
+
warn(string.format("[ObservableMapSet.Add] - This API call will swap observable key order eventually. Use ObservableMapSet.Push for now to suppress this warning.\n%s",
|
|
128
|
+
debug.traceback()))
|
|
129
|
+
|
|
130
|
+
return self:Push(observeKey, entry)
|
|
131
|
+
end
|
|
132
|
+
|
|
96
133
|
--[=[
|
|
97
134
|
Gets how many sets exist
|
|
98
135
|
@return number
|
|
@@ -199,19 +199,9 @@ function ObservableSortedList:ObserveAtIndex(indexToObserve)
|
|
|
199
199
|
|
|
200
200
|
return self._indexObservers:Observe(indexToObserve)
|
|
201
201
|
:Pipe({
|
|
202
|
-
function(
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if key then
|
|
207
|
-
sub:Fire(self._contents[key]) -- Look up the content
|
|
208
|
-
else
|
|
209
|
-
sub:Fire(nil)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
return source:Subscribe(sub:GetFireFailComplete())
|
|
213
|
-
end)
|
|
214
|
-
end
|
|
202
|
+
Rx.start(function()
|
|
203
|
+
return self:Get(indexToObserve)
|
|
204
|
+
end);
|
|
215
205
|
})
|
|
216
206
|
end
|
|
217
207
|
|
|
@@ -6,6 +6,7 @@ local require = require(game:GetService("ServerScriptService"):FindFirstChild("L
|
|
|
6
6
|
|
|
7
7
|
local ObservableSortedList = require("ObservableSortedList")
|
|
8
8
|
local Rx = require("Rx")
|
|
9
|
+
local StepUtils = require("StepUtils")
|
|
9
10
|
|
|
10
11
|
return function()
|
|
11
12
|
describe("ObservableSortedList.new()", function()
|
|
@@ -20,6 +21,8 @@ return function()
|
|
|
20
21
|
|
|
21
22
|
observableSortedList:Add("b", Rx.of("b"))
|
|
22
23
|
|
|
24
|
+
StepUtils.deferWait()
|
|
25
|
+
|
|
23
26
|
expect(observableSortedList:Get(1)).to.equal("b")
|
|
24
27
|
expect(observableSortedList:GetCount()).to.equal(1)
|
|
25
28
|
end)
|
|
@@ -29,6 +32,8 @@ return function()
|
|
|
29
32
|
|
|
30
33
|
observableSortedList:Add("a", Rx.of("a"))
|
|
31
34
|
|
|
35
|
+
StepUtils.deferWait()
|
|
36
|
+
|
|
32
37
|
expect(observableSortedList:Get(1)).to.equal("a")
|
|
33
38
|
expect(observableSortedList:Get(2)).to.equal("b")
|
|
34
39
|
expect(observableSortedList:GetCount()).to.equal(2)
|
|
@@ -40,10 +45,12 @@ return function()
|
|
|
40
45
|
observableSortedList:Add("b", Rx.of(0))
|
|
41
46
|
observableSortedList:Add("c", Rx.of(0))
|
|
42
47
|
|
|
48
|
+
StepUtils.deferWait()
|
|
49
|
+
|
|
43
50
|
expect(observableSortedList:Get(1)).to.equal("a")
|
|
44
51
|
expect(observableSortedList:Get(2)).to.equal("b")
|
|
45
52
|
expect(observableSortedList:Get(3)).to.equal("c")
|
|
46
|
-
expect(observableSortedList:GetCount()).to.equal(
|
|
53
|
+
expect(observableSortedList:GetCount()).to.equal(3)
|
|
47
54
|
end)
|
|
48
55
|
end)
|
|
49
56
|
end
|