@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 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": "^5.2.0",
31
- "@quenty/loader": "^4.1.0",
32
- "@quenty/maid": "^2.3.0",
33
- "@quenty/promise": "^4.2.0",
34
- "@quenty/rx": "^4.2.0",
35
- "@quenty/signal": "^2.2.0",
36
- "@quenty/valuebaseutils": "^4.2.0"
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": "501844bd6c3d3f765fd3032b997d8030bc963a1f"
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 set is an observable set
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
  ]=]
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "ObservableCollectionTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "observablecollection": {
7
+ "$path": ".."
8
+ }
9
+ }
10
+ }
11
+ }