@quenty/observablecollection 3.2.0 → 3.3.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 +16 -0
- package/package.json +7 -7
- package/src/Shared/ObservableList.lua +10 -11
- package/src/Shared/ObservableSortedList.lua +526 -0
- package/src/Shared/ObservableSortedList.spec.lua +37 -0
- package/test/default.project.json +10 -1
- package/test/scripts/Server/ServerMain.server.lua +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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
|
+
# [3.3.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@3.2.0...@quenty/observablecollection@3.3.0) (2022-07-31)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Prevent observables from being removed during insertion of observable ([27a3594](https://github.com/Quenty/NevermoreEngine/commit/27a35941f457f63a5f1dc84169448a750308fdc2))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Add ObservableSortedList ([cca209f](https://github.com/Quenty/NevermoreEngine/commit/cca209fd8a6c2cfeb1ee6e39d2aabce0202b3072))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# [3.2.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@3.1.0...@quenty/observablecollection@3.2.0) (2022-07-02)
|
|
7
23
|
|
|
8
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/observablecollection",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "An observable set",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
"Quenty"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@quenty/brio": "^6.
|
|
30
|
+
"@quenty/brio": "^6.2.0",
|
|
31
31
|
"@quenty/loader": "^5.0.0",
|
|
32
|
-
"@quenty/maid": "^2.
|
|
33
|
-
"@quenty/promise": "^5.
|
|
34
|
-
"@quenty/rx": "^5.
|
|
32
|
+
"@quenty/maid": "^2.4.0",
|
|
33
|
+
"@quenty/promise": "^5.1.0",
|
|
34
|
+
"@quenty/rx": "^5.2.0",
|
|
35
35
|
"@quenty/signal": "^2.2.0",
|
|
36
36
|
"@quenty/symbol": "^2.1.0",
|
|
37
|
-
"@quenty/valuebaseutils": "^5.
|
|
37
|
+
"@quenty/valuebaseutils": "^5.2.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "e31b3a35aa475bb5699a24898a8639e107165b36"
|
|
43
43
|
}
|
|
@@ -131,7 +131,7 @@ end
|
|
|
131
131
|
@return Observable<number>
|
|
132
132
|
]=]
|
|
133
133
|
function ObservableList:ObserveIndexByKey(key)
|
|
134
|
-
assert(key, "Bad key")
|
|
134
|
+
assert(type(key) == "userdata", "Bad key")
|
|
135
135
|
|
|
136
136
|
return Observable.new(function(sub)
|
|
137
137
|
local currentIndex = self._indexes[key]
|
|
@@ -266,18 +266,17 @@ function ObservableList:InsertAt(item, index)
|
|
|
266
266
|
|
|
267
267
|
-- Fire off the index change on the value
|
|
268
268
|
do
|
|
269
|
-
local
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
end
|
|
277
|
-
end
|
|
269
|
+
local subs = self._keyObservables[key]
|
|
270
|
+
if subs then
|
|
271
|
+
table.insert(changed, {
|
|
272
|
+
key = key;
|
|
273
|
+
newIndex = index;
|
|
274
|
+
subs = subs;
|
|
275
|
+
})
|
|
278
276
|
end
|
|
279
277
|
end
|
|
280
278
|
|
|
279
|
+
|
|
281
280
|
-- Fire off index change on each key list (if the data isn't stale)
|
|
282
281
|
for _, data in pairs(changed) do
|
|
283
282
|
if self._indexes[data.key] == data.newIndex then
|
|
@@ -322,7 +321,7 @@ function ObservableList:RemoveByKey(key)
|
|
|
322
321
|
end
|
|
323
322
|
|
|
324
323
|
local item = self._contents[key]
|
|
325
|
-
if
|
|
324
|
+
if item == nil then
|
|
326
325
|
return nil
|
|
327
326
|
end
|
|
328
327
|
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
A list that can be observed for blend and other components and maintains sorting order.
|
|
3
|
+
|
|
4
|
+
This class is very expensive to use as it enforces maintaining order on the object. Each entries produces
|
|
5
|
+
what is most likely 4-5 tables, and changing can result in O(n) table construction and deltas.
|
|
6
|
+
|
|
7
|
+
However, for small lists that don't change frequently, such as a global leaderboard, this can be
|
|
8
|
+
a nice small interactive class.
|
|
9
|
+
|
|
10
|
+
For performance reasons this class defers firing events until the next defer() event frame.
|
|
11
|
+
|
|
12
|
+
@class ObservableSortedList
|
|
13
|
+
]=]
|
|
14
|
+
|
|
15
|
+
local require = require(script.Parent.loader).load(script)
|
|
16
|
+
|
|
17
|
+
local Signal = require("Signal")
|
|
18
|
+
local Observable = require("Observable")
|
|
19
|
+
local Maid = require("Maid")
|
|
20
|
+
local Brio = require("Brio")
|
|
21
|
+
local RxValueBaseUtils = require("RxValueBaseUtils")
|
|
22
|
+
local Symbol = require("Symbol")
|
|
23
|
+
|
|
24
|
+
local function defaultCompare(a, b)
|
|
25
|
+
return a < b
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
local ObservableSortedList = {}
|
|
29
|
+
ObservableSortedList.ClassName = "ObservableSortedList"
|
|
30
|
+
ObservableSortedList.__index = ObservableSortedList
|
|
31
|
+
|
|
32
|
+
--[=[
|
|
33
|
+
Constructs a new ObservableSortedList
|
|
34
|
+
@param compare callback?
|
|
35
|
+
@return ObservableSortedList<T>
|
|
36
|
+
]=]
|
|
37
|
+
function ObservableSortedList.new(compare)
|
|
38
|
+
local self = setmetatable({}, ObservableSortedList)
|
|
39
|
+
|
|
40
|
+
self._maid = Maid.new()
|
|
41
|
+
|
|
42
|
+
self._keyList = {} -- { [number]: Symbol } -- immutable
|
|
43
|
+
|
|
44
|
+
self._sortValue = {} -- { [Symbol]: number }
|
|
45
|
+
self._contents = {} -- { [Symbol]: T }
|
|
46
|
+
self._indexes = {} -- { [Symbol]: number }
|
|
47
|
+
|
|
48
|
+
self._keyObservables = {} -- { [Symbol]: { Subscription } }
|
|
49
|
+
|
|
50
|
+
self._compare = compare or defaultCompare
|
|
51
|
+
self._countValue = Instance.new("IntValue")
|
|
52
|
+
self._countValue.Value = 0
|
|
53
|
+
self._maid:GiveTask(self._countValue)
|
|
54
|
+
|
|
55
|
+
--[=[
|
|
56
|
+
Fires when an item is added
|
|
57
|
+
@readonly
|
|
58
|
+
@prop ItemAdded Signal<T, number, Symbol>
|
|
59
|
+
@within ObservableSortedList
|
|
60
|
+
]=]
|
|
61
|
+
self.ItemAdded = Signal.new()
|
|
62
|
+
self._maid:GiveTask(self.ItemAdded)
|
|
63
|
+
|
|
64
|
+
--[=[
|
|
65
|
+
Fires when an item is removed.
|
|
66
|
+
@readonly
|
|
67
|
+
@prop ItemRemoved Signal<T, Symbol>
|
|
68
|
+
@within ObservableSortedList
|
|
69
|
+
]=]
|
|
70
|
+
self.ItemRemoved = Signal.new()
|
|
71
|
+
self._maid:GiveTask(self.ItemRemoved)
|
|
72
|
+
|
|
73
|
+
--[=[
|
|
74
|
+
Fires when the count changes.
|
|
75
|
+
@prop CountChanged RBXScriptSignal
|
|
76
|
+
@within ObservableSortedList
|
|
77
|
+
]=]
|
|
78
|
+
self.CountChanged = self._countValue.Changed
|
|
79
|
+
|
|
80
|
+
return self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
--[=[
|
|
84
|
+
Returns whether the value is an observable list
|
|
85
|
+
@param value any
|
|
86
|
+
@return boolean
|
|
87
|
+
]=]
|
|
88
|
+
function ObservableSortedList.isObservableSortedList(value)
|
|
89
|
+
return type(value) == "table" and getmetatable(value) == ObservableSortedList
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
--[=[
|
|
93
|
+
Observes all items in the list
|
|
94
|
+
@return Observable<Brio<T>>
|
|
95
|
+
]=]
|
|
96
|
+
function ObservableSortedList:ObserveItemsBrio()
|
|
97
|
+
return Observable.new(function(sub)
|
|
98
|
+
local maid = Maid.new()
|
|
99
|
+
|
|
100
|
+
local function handleItem(item, _index, includeKey)
|
|
101
|
+
local brio = Brio.new(item, includeKey)
|
|
102
|
+
maid[includeKey] = brio
|
|
103
|
+
sub:Fire(brio)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
for index, key in pairs(self._keyList) do
|
|
107
|
+
handleItem(self._contents[key], index, key)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
maid:GiveTask(self.ItemAdded:Connect(handleItem))
|
|
111
|
+
maid:GiveTask(self.ItemRemoved:Connect(function(_item, includeKey)
|
|
112
|
+
maid[includeKey] = nil
|
|
113
|
+
end))
|
|
114
|
+
|
|
115
|
+
self._maid[sub] = maid
|
|
116
|
+
maid:GiveTask(function()
|
|
117
|
+
self._maid[sub] = nil
|
|
118
|
+
sub:Complete()
|
|
119
|
+
end)
|
|
120
|
+
|
|
121
|
+
return maid
|
|
122
|
+
end)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
--[=[
|
|
126
|
+
Observes the index as it changes, until the entry at the existing
|
|
127
|
+
index is removed.
|
|
128
|
+
|
|
129
|
+
@param indexToObserve number
|
|
130
|
+
@return Observable<number>
|
|
131
|
+
]=]
|
|
132
|
+
function ObservableSortedList:ObserveIndex(indexToObserve)
|
|
133
|
+
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
134
|
+
|
|
135
|
+
local key = self._keyList[indexToObserve]
|
|
136
|
+
if not key then
|
|
137
|
+
error(("No entry at index %q, cannot observe changes"):format(indexToObserve))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return self:ObserveIndexByKey(key)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
--[=[
|
|
144
|
+
Observes the index as it changes, until the entry at the existing
|
|
145
|
+
key is removed.
|
|
146
|
+
|
|
147
|
+
@param key Symbol
|
|
148
|
+
@return Observable<number>
|
|
149
|
+
]=]
|
|
150
|
+
function ObservableSortedList:ObserveIndexByKey(key)
|
|
151
|
+
assert(type(key) == "userdata", "Bad key")
|
|
152
|
+
|
|
153
|
+
return Observable.new(function(sub)
|
|
154
|
+
local maid = Maid.new()
|
|
155
|
+
self._keyObservables[key] = self._keyObservables[key] or {}
|
|
156
|
+
table.insert(self._keyObservables[key], sub)
|
|
157
|
+
|
|
158
|
+
local currentIndex = self._indexes[key]
|
|
159
|
+
if currentIndex then
|
|
160
|
+
sub:Fire(currentIndex)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
maid:GiveTask(function()
|
|
164
|
+
local list = self._keyObservables[key]
|
|
165
|
+
if not list then
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
local index = table.find(list, sub)
|
|
170
|
+
if index then
|
|
171
|
+
table.remove(list, index)
|
|
172
|
+
if #list == 0 then
|
|
173
|
+
self._keyObservables[key] = nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end)
|
|
177
|
+
|
|
178
|
+
return maid
|
|
179
|
+
end)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
--[=[
|
|
183
|
+
Gets the current index from the key
|
|
184
|
+
|
|
185
|
+
@param key Symbol
|
|
186
|
+
@return number
|
|
187
|
+
]=]
|
|
188
|
+
function ObservableSortedList:GetIndexByKey(key)
|
|
189
|
+
local currentIndex = self._indexes[key]
|
|
190
|
+
if currentIndex then
|
|
191
|
+
return currentIndex
|
|
192
|
+
else
|
|
193
|
+
return nil
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
--[=[
|
|
198
|
+
Gets the count of items in the list
|
|
199
|
+
@return number
|
|
200
|
+
]=]
|
|
201
|
+
function ObservableSortedList:GetCount()
|
|
202
|
+
return self._countValue.Value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
--[=[
|
|
206
|
+
Gets a list of all entries.
|
|
207
|
+
@return { T }
|
|
208
|
+
]=]
|
|
209
|
+
function ObservableSortedList:GetList()
|
|
210
|
+
local list = {}
|
|
211
|
+
for _, key in pairs(self._keyList) do
|
|
212
|
+
table.insert(list, self._contents[key])
|
|
213
|
+
end
|
|
214
|
+
return list
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
--[=[
|
|
218
|
+
Observes the count of the list
|
|
219
|
+
@return Observable<number>
|
|
220
|
+
]=]
|
|
221
|
+
function ObservableSortedList:ObserveCount()
|
|
222
|
+
return RxValueBaseUtils.observeValue(self._countValue)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
--[=[
|
|
226
|
+
Adds the item to the list at the specified index
|
|
227
|
+
@param item T
|
|
228
|
+
@param observeValue Observable<Comparable>
|
|
229
|
+
@return callback -- Call to remove
|
|
230
|
+
]=]
|
|
231
|
+
function ObservableSortedList:Add(item, observeValue)
|
|
232
|
+
assert(item ~= nil, "Bad item")
|
|
233
|
+
assert(Observable.isObservable(observeValue), "Bad observeValue")
|
|
234
|
+
|
|
235
|
+
local key = Symbol.named("entryKey")
|
|
236
|
+
local maid = Maid.new()
|
|
237
|
+
|
|
238
|
+
self._contents[key] = item
|
|
239
|
+
|
|
240
|
+
maid:GiveTask(observeValue:Subscribe(function(sortValue)
|
|
241
|
+
self._sortValue[key] = sortValue
|
|
242
|
+
|
|
243
|
+
if sortValue ~= nil then
|
|
244
|
+
local currentIndex = self._indexes[key]
|
|
245
|
+
local targetIndex = self:_findCorrectIndex(sortValue, currentIndex)
|
|
246
|
+
self:_updateIndex(key, item, targetIndex)
|
|
247
|
+
else
|
|
248
|
+
local observableSubs = self._keyObservables[key]
|
|
249
|
+
|
|
250
|
+
-- calling this also may unsubscribe some observables.
|
|
251
|
+
self:_removeItemByKey(key, item)
|
|
252
|
+
|
|
253
|
+
if observableSubs then
|
|
254
|
+
-- fire nil index
|
|
255
|
+
self:_fireSubs(observableSubs, nil)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end))
|
|
259
|
+
|
|
260
|
+
maid:GiveTask(function()
|
|
261
|
+
local observableSubs = self._keyObservables[key]
|
|
262
|
+
self._keyObservables[key] = nil
|
|
263
|
+
|
|
264
|
+
self:_removeItemByKey(key, item)
|
|
265
|
+
|
|
266
|
+
-- Fire off the index change on the value
|
|
267
|
+
if observableSubs then
|
|
268
|
+
self:_completeSubs(observableSubs)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
self._contents[key] = nil
|
|
272
|
+
self._sortValue[key] = nil
|
|
273
|
+
end)
|
|
274
|
+
|
|
275
|
+
self._maid[key] = maid
|
|
276
|
+
|
|
277
|
+
return function()
|
|
278
|
+
self._maid[key] = nil
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
--[=[
|
|
283
|
+
Gets the current item at the index, or nil if it is not defined.
|
|
284
|
+
@param index number
|
|
285
|
+
@return T?
|
|
286
|
+
]=]
|
|
287
|
+
function ObservableSortedList:Get(index)
|
|
288
|
+
assert(type(index) == "number", "Bad index")
|
|
289
|
+
|
|
290
|
+
local key = self._keyList[index]
|
|
291
|
+
if not key then
|
|
292
|
+
return nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
return self._contents[key]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
--[=[
|
|
299
|
+
Removes the item from the list if it exists.
|
|
300
|
+
@param key Symbol
|
|
301
|
+
@return T
|
|
302
|
+
]=]
|
|
303
|
+
function ObservableSortedList:RemoveByKey(key)
|
|
304
|
+
assert(key ~= nil, "Bad key")
|
|
305
|
+
|
|
306
|
+
self._maid[key] = nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
function ObservableSortedList:_updateIndex(key, item, index)
|
|
310
|
+
assert(item ~= nil, "Bad item")
|
|
311
|
+
assert(type(index) == "number", "Bad index")
|
|
312
|
+
|
|
313
|
+
local pastIndex = self._indexes[key]
|
|
314
|
+
if pastIndex == index then
|
|
315
|
+
return
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
self._indexes[key] = index
|
|
319
|
+
|
|
320
|
+
local changed = {}
|
|
321
|
+
|
|
322
|
+
if not pastIndex then
|
|
323
|
+
-- shift everything up to fit this space
|
|
324
|
+
local n = #self._keyList
|
|
325
|
+
for i=n, index, -1 do
|
|
326
|
+
local nextKey = self._keyList[i]
|
|
327
|
+
self._indexes[nextKey] = i + 1
|
|
328
|
+
self._keyList[i + 1] = nextKey
|
|
329
|
+
|
|
330
|
+
table.insert(changed, {
|
|
331
|
+
key = nextKey;
|
|
332
|
+
newIndex = i + 1;
|
|
333
|
+
})
|
|
334
|
+
end
|
|
335
|
+
elseif index > pastIndex then
|
|
336
|
+
-- we're moving up (3 -> 5), so everything shifts down to fill up the pastIndex
|
|
337
|
+
for i=pastIndex + 1, index do
|
|
338
|
+
local nextKey = self._keyList[i]
|
|
339
|
+
self._indexes[nextKey] = i - 1
|
|
340
|
+
self._keyList[i - 1] = nextKey
|
|
341
|
+
|
|
342
|
+
table.insert(changed, {
|
|
343
|
+
key = nextKey;
|
|
344
|
+
newIndex = i - 1;
|
|
345
|
+
})
|
|
346
|
+
end
|
|
347
|
+
else
|
|
348
|
+
-- if index < pastIndex then
|
|
349
|
+
-- we're moving down (5 -> 3) so everything shifts up to fit this space
|
|
350
|
+
for i=pastIndex-1, index, -1 do
|
|
351
|
+
local belowKey = self._keyList[i]
|
|
352
|
+
self._indexes[belowKey] = i + 1
|
|
353
|
+
self._keyList[i + 1] = belowKey
|
|
354
|
+
table.insert(changed, {
|
|
355
|
+
key = belowKey;
|
|
356
|
+
newIndex = i + 1;
|
|
357
|
+
})
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
local itemAdded = {
|
|
362
|
+
key = key;
|
|
363
|
+
newIndex = index;
|
|
364
|
+
item = item;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
-- ensure ourself is considered changed
|
|
368
|
+
table.insert(changed, itemAdded)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
self._keyList[index] = key
|
|
372
|
+
|
|
373
|
+
-- Fire off our count value changed
|
|
374
|
+
-- still O(n^2) but at least we prevent emitting O(n^2) events
|
|
375
|
+
if pastIndex == nil then
|
|
376
|
+
self:_deferChange(1, itemAdded, nil, changed)
|
|
377
|
+
else
|
|
378
|
+
self:_deferChange(0, nil, nil, changed)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
function ObservableSortedList:_removeItemByKey(key, item)
|
|
383
|
+
assert(key ~= nil, "Bad key")
|
|
384
|
+
|
|
385
|
+
local index = self._indexes[key]
|
|
386
|
+
if not index then
|
|
387
|
+
return
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
self._indexes[key] = nil
|
|
391
|
+
self._sortValue[key] = nil
|
|
392
|
+
|
|
393
|
+
local changed = {}
|
|
394
|
+
|
|
395
|
+
-- shift everything down
|
|
396
|
+
local n = #self._keyList
|
|
397
|
+
for i=index, n - 1 do
|
|
398
|
+
local nextKey = self._keyList[i+1]
|
|
399
|
+
self._indexes[nextKey] = i
|
|
400
|
+
self._keyList[i] = nextKey
|
|
401
|
+
|
|
402
|
+
table.insert(changed, {
|
|
403
|
+
key = nextKey;
|
|
404
|
+
newIndex = i;
|
|
405
|
+
})
|
|
406
|
+
end
|
|
407
|
+
self._keyList[n] = nil
|
|
408
|
+
|
|
409
|
+
local itemRemoved = {
|
|
410
|
+
key = key;
|
|
411
|
+
item = item;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
-- TODO: Defer item removed as a changed event?
|
|
415
|
+
|
|
416
|
+
-- still O(n^2) but at least we prevent emitting O(n^2) events
|
|
417
|
+
self:_deferChange(-1, nil, itemRemoved, changed)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
function ObservableSortedList:_deferChange(countChange, itemAdded, itemRemoved, indexChanges)
|
|
421
|
+
self:_queueDeferredChange()
|
|
422
|
+
|
|
423
|
+
if itemAdded then
|
|
424
|
+
self._deferredChange.itemsRemoved[itemAdded.key] = nil
|
|
425
|
+
self._deferredChange.itemsAdded[itemAdded.key] = itemAdded
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
if itemRemoved then
|
|
429
|
+
self._deferredChange.itemsAdded[itemRemoved.key] = nil
|
|
430
|
+
self._deferredChange.itemsRemoved[itemRemoved.key] = itemRemoved
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
self._deferredChange.countChange += countChange
|
|
434
|
+
|
|
435
|
+
for _, data in pairs(indexChanges) do
|
|
436
|
+
self._deferredChange.indexChanges[data.key] = data
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
function ObservableSortedList:_queueDeferredChange()
|
|
441
|
+
if not self._deferredChange then
|
|
442
|
+
self._deferredChange = {
|
|
443
|
+
countChange = 0;
|
|
444
|
+
indexChanges = {};
|
|
445
|
+
itemsAdded = {};
|
|
446
|
+
itemsRemoved = {};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
task.defer(function()
|
|
450
|
+
local snapshot = self._deferredChange
|
|
451
|
+
self._deferredChange = nil
|
|
452
|
+
|
|
453
|
+
self._countValue.Value = self._countValue.Value + snapshot.countChange
|
|
454
|
+
|
|
455
|
+
if self.Destroy then
|
|
456
|
+
-- Fire off last adds
|
|
457
|
+
for _, lastAdded in pairs(snapshot.itemsAdded) do
|
|
458
|
+
self.ItemAdded:Fire(lastAdded.item, lastAdded.newIndex, lastAdded.key)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
for _, lastRemoved in pairs(snapshot.itemsRemoved) do
|
|
462
|
+
self.ItemRemoved:Fire(lastRemoved.item, lastRemoved.key)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
-- Fire off index change on each key list (if the data isn't stale)
|
|
467
|
+
for _, lastChange in pairs(snapshot.indexChanges) do
|
|
468
|
+
if self._indexes[lastChange.key] == lastChange.newIndex then
|
|
469
|
+
local subs = self._keyObservables[lastChange.key]
|
|
470
|
+
if subs then
|
|
471
|
+
self:_fireSubs(subs, lastChange.newIndex)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
function ObservableSortedList:_findCorrectIndex(sortValue, currentIndex)
|
|
480
|
+
-- todo: binary search
|
|
481
|
+
-- todo: stable
|
|
482
|
+
|
|
483
|
+
for i=#self._keyList, 1, -1 do
|
|
484
|
+
local currentKey = self._keyList[i]
|
|
485
|
+
if self._compare(self._sortValue[currentKey], sortValue) then
|
|
486
|
+
|
|
487
|
+
-- include index in this
|
|
488
|
+
if currentIndex and currentIndex <= i then
|
|
489
|
+
return i
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
return i + 1
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
return 1
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
function ObservableSortedList:_fireSubs(list, index)
|
|
500
|
+
for _, sub in pairs(list) do
|
|
501
|
+
if sub:IsPending() then
|
|
502
|
+
task.spawn(function()
|
|
503
|
+
sub:Fire(index)
|
|
504
|
+
end)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
function ObservableSortedList:_completeSubs(list)
|
|
510
|
+
for _, sub in pairs(list) do
|
|
511
|
+
if sub:IsPending() then
|
|
512
|
+
sub:Fire(nil)
|
|
513
|
+
sub:Complete()
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
--[=[
|
|
519
|
+
Cleans up the ObservableSortedList and sets the metatable to nil.
|
|
520
|
+
]=]
|
|
521
|
+
function ObservableSortedList:Destroy()
|
|
522
|
+
self._maid:DoCleaning()
|
|
523
|
+
setmetatable(self, nil)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
return ObservableSortedList
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ObservableSortedList.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local ObservableSortedList = require("ObservableSortedList")
|
|
8
|
+
local Rx = require("Rx")
|
|
9
|
+
|
|
10
|
+
return function()
|
|
11
|
+
describe("ObservableSortedList.new()", function()
|
|
12
|
+
local observableSortedList = ObservableSortedList.new()
|
|
13
|
+
|
|
14
|
+
it("should return nil for unset values", function()
|
|
15
|
+
expect(observableSortedList:Get(1)).to.equal(nil)
|
|
16
|
+
end)
|
|
17
|
+
|
|
18
|
+
it("should allow inserting an value", function()
|
|
19
|
+
expect(observableSortedList:GetCount()).to.equal(0)
|
|
20
|
+
|
|
21
|
+
observableSortedList:Add("b", Rx.of("b"))
|
|
22
|
+
|
|
23
|
+
expect(observableSortedList:Get(1)).to.equal("b")
|
|
24
|
+
expect(observableSortedList:GetCount()).to.equal(1)
|
|
25
|
+
end)
|
|
26
|
+
|
|
27
|
+
it("should sort the items", function()
|
|
28
|
+
expect(observableSortedList:GetCount()).to.equal(1)
|
|
29
|
+
|
|
30
|
+
observableSortedList:Add("a", Rx.of("a"))
|
|
31
|
+
|
|
32
|
+
expect(observableSortedList:Get(1)).to.equal("a")
|
|
33
|
+
expect(observableSortedList:Get(2)).to.equal("b")
|
|
34
|
+
expect(observableSortedList:GetCount()).to.equal(2)
|
|
35
|
+
end)
|
|
36
|
+
end)
|
|
37
|
+
end
|
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
"$className": "DataModel",
|
|
5
5
|
"ServerScriptService": {
|
|
6
6
|
"observablecollection": {
|
|
7
|
-
"$
|
|
7
|
+
"$className": "Folder",
|
|
8
|
+
"observablecollection": {
|
|
9
|
+
"$path": ".."
|
|
10
|
+
},
|
|
11
|
+
"instanceutils": {
|
|
12
|
+
"$path": "../../instanceutils"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"Script": {
|
|
16
|
+
"$path": "scripts/Server"
|
|
8
17
|
}
|
|
9
18
|
}
|
|
10
19
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ServerMain
|
|
3
|
+
]]
|
|
4
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5
|
+
local TweenService = game:GetService("TweenService")
|
|
6
|
+
|
|
7
|
+
local loader = ServerScriptService:FindFirstChild("LoaderUtils", true).Parent
|
|
8
|
+
local packages = require(loader).bootstrapGame(ServerScriptService.observablecollection)
|
|
9
|
+
|
|
10
|
+
local ObservableSortedList = require(packages.ObservableSortedList)
|
|
11
|
+
local RxInstanceUtils = require(packages.RxInstanceUtils)
|
|
12
|
+
local Rx = require(packages.Rx)
|
|
13
|
+
|
|
14
|
+
local observableSortedList = ObservableSortedList.new()
|
|
15
|
+
|
|
16
|
+
observableSortedList.CountChanged:Connect(function(count)
|
|
17
|
+
print("Count", count)
|
|
18
|
+
end)
|
|
19
|
+
|
|
20
|
+
observableSortedList:ObserveItemsBrio():Subscribe(function(brio)
|
|
21
|
+
if brio:IsDead() then
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local part, key = brio:GetValue()
|
|
26
|
+
local maid = brio:ToMaid()
|
|
27
|
+
|
|
28
|
+
local currentTween
|
|
29
|
+
local function setCFrame(cframe, doNotAnimate)
|
|
30
|
+
if currentTween then
|
|
31
|
+
currentTween:Cancel()
|
|
32
|
+
currentTween = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if doNotAnimate then
|
|
36
|
+
part.CFrame = cframe
|
|
37
|
+
else
|
|
38
|
+
local tweenInfo = TweenInfo.new(0.2)
|
|
39
|
+
local tween = TweenService:Create(part, tweenInfo, {
|
|
40
|
+
CFrame = cframe;
|
|
41
|
+
})
|
|
42
|
+
currentTween = tween
|
|
43
|
+
tween:Play()
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
local first = true
|
|
48
|
+
|
|
49
|
+
maid:GiveTask(observableSortedList:ObserveIndexByKey(key):Subscribe(function(index)
|
|
50
|
+
print("change")
|
|
51
|
+
|
|
52
|
+
if index then
|
|
53
|
+
part:SetAttribute("CurrentIndex", index)
|
|
54
|
+
setCFrame(CFrame.new(-5*index, 5, 0) * CFrame.Angles(0, math.pi/2, 0), first)
|
|
55
|
+
first = false
|
|
56
|
+
else
|
|
57
|
+
part:SetAttribute("CurrentIndex", "nil")
|
|
58
|
+
setCFrame(CFrame.new(part.CFrame.x, 10, 0) * CFrame.Angles(0, math.pi/2, 0), first)
|
|
59
|
+
first = false
|
|
60
|
+
end
|
|
61
|
+
end))
|
|
62
|
+
|
|
63
|
+
maid:GiveTask(function()
|
|
64
|
+
part:SetAttribute("CurrentIndex", "nil")
|
|
65
|
+
setCFrame(CFrame.new(part.CFrame.x, 5, 5) * CFrame.Angles(0, math.pi/2, 0), first)
|
|
66
|
+
first = false
|
|
67
|
+
end)
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
local parts = {}
|
|
71
|
+
for i=9, 1, -1 do
|
|
72
|
+
local part = Instance.new("Part")
|
|
73
|
+
part.TopSurface = Enum.SurfaceType.Smooth
|
|
74
|
+
part.BottomSurface = Enum.SurfaceType.Smooth
|
|
75
|
+
part.Anchored = true
|
|
76
|
+
part.Size = Vector3.new(3, 3, 3)
|
|
77
|
+
part.Name = i
|
|
78
|
+
|
|
79
|
+
local surfaceGui = Instance.new("SurfaceGui")
|
|
80
|
+
surfaceGui.Name = "SurfaceGui"
|
|
81
|
+
surfaceGui.Face = Enum.NormalId.Top
|
|
82
|
+
surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
|
|
83
|
+
surfaceGui.Adornee = part
|
|
84
|
+
surfaceGui.Parent = part
|
|
85
|
+
|
|
86
|
+
local textLabel = Instance.new("TextLabel")
|
|
87
|
+
textLabel.Name = "TextLabel"
|
|
88
|
+
textLabel.Size = UDim2.new(1, 0, 1, 0)
|
|
89
|
+
textLabel.TextScaled = true
|
|
90
|
+
textLabel.BackgroundTransparency = 1
|
|
91
|
+
textLabel.TextColor3 = Color3.new(0, 0, 0)
|
|
92
|
+
textLabel.BorderSizePixel = 0
|
|
93
|
+
textLabel.Parent = surfaceGui
|
|
94
|
+
|
|
95
|
+
RxInstanceUtils.observeProperty(part, "Name", nil):Subscribe(function(value)
|
|
96
|
+
textLabel.Text = tostring(value)
|
|
97
|
+
end)
|
|
98
|
+
|
|
99
|
+
parts[i] = part
|
|
100
|
+
part.Parent = workspace
|
|
101
|
+
|
|
102
|
+
observableSortedList:Add(part, RxInstanceUtils.observeProperty(part, "Name", nil):Pipe({
|
|
103
|
+
Rx.map(function(name)
|
|
104
|
+
return tonumber(name)
|
|
105
|
+
end)
|
|
106
|
+
}))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
parts[5].Name = "25"
|
|
110
|
+
parts[9].Name = "3.1"
|
|
111
|
+
parts[2].Name = "remove"
|
|
112
|
+
|