@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 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.24.0",
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.17.0",
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.22.0"
39
+ "@quenty/valueobject": "^7.23.0"
39
40
  },
40
41
  "publishConfig": {
41
42
  "access": "public"
42
43
  },
43
- "gitHead": "3b7e47e2180964b6b0b156d07814810e063ef7ed"
44
+ "gitHead": "fa0826fa54fabecb242d7c0753e2e7644ba02e11"
44
45
  }
@@ -311,7 +311,7 @@ end
311
311
  Gets a list of all keys.
312
312
  @return { T }
313
313
  ]=]
314
- function ObservableCountingMap:GetKeysList()
314
+ function ObservableCountingMap:GetKeyList()
315
315
  local list = {}
316
316
  for key, _ in pairs(self._map) do
317
317
  table.insert(list, key)
@@ -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._keyObservables = {} -- { [Symbol]: { Subscription } }
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"):format(indexToObserve))
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 Observable.new(function(sub)
180
- local currentIndex = self._indexes[key]
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
- local subs = self._keyObservables[nextKey]
293
- if subs then
294
- table.insert(changed, {
295
- key = nextKey;
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:_fireSubs(data.subs, data.newIndex)
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
- local subs = self._keyObservables[nextKey]
386
- if subs then
387
- table.insert(changed, {
388
- key = nextKey;
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
- if observableSubs then
405
- self:_completeSubs(observableSubs)
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:_fireSubs(data.subs, data.newIndex)
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 slot
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:ObserveValueForKey(key)
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
- function ObservableMapSet:Add(entry, observeKey)
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(source)
203
- return Observable.new(function(sub)
204
- local key = self._keyList[indexToObserve]
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(1)
53
+ expect(observableSortedList:GetCount()).to.equal(3)
47
54
  end)
48
55
  end)
49
56
  end