@quenty/observablecollection 12.19.1 → 12.19.2
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,14 @@
|
|
|
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
|
+
## [12.19.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.19.1...@quenty/observablecollection@12.19.2) (2025-03-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/observablecollection
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [12.19.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.19.0...@quenty/observablecollection@12.19.1) (2025-03-09)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @quenty/observablecollection
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/observablecollection",
|
|
3
|
-
"version": "12.19.
|
|
3
|
+
"version": "12.19.2",
|
|
4
4
|
"description": "A set of observable collections, such as sets, maps, sorted lists, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "454a991b6f45e1db7b8030e216a1a8d6efc34083"
|
|
50
50
|
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
@class ObservableSortedList.story
|
|
3
3
|
]]
|
|
4
4
|
|
|
5
|
-
local require =
|
|
5
|
+
local require =
|
|
6
|
+
require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).bootstrapStory(script)
|
|
6
7
|
|
|
7
8
|
local Maid = require("Maid")
|
|
8
9
|
local ObservableSortedList = require("ObservableSortedList")
|
|
9
|
-
local ObservableSortedListOld = require("ObservableSortedListOld")
|
|
10
10
|
|
|
11
11
|
return function(_target)
|
|
12
12
|
local maid = Maid.new()
|
|
@@ -17,23 +17,21 @@ return function(_target)
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
local startTime = os.clock()
|
|
20
|
-
for i=1, n do
|
|
20
|
+
for i = 1, n do
|
|
21
21
|
add(getElement(i))
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
print(string.format("%25s %0.2f ms", label .. " construction", (os.clock() - startTime)*1000))
|
|
24
|
+
print(string.format("%25s %0.2f ms", label .. " construction", (os.clock() - startTime) * 1000))
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
local function cleanup(label, sortedList)
|
|
29
28
|
local startTime = os.clock()
|
|
30
29
|
|
|
31
30
|
sortedList:Destroy()
|
|
32
31
|
|
|
33
|
-
print(string.format("%25s %0.2f ms", label .. " destruction", (os.clock() - startTime)*1000))
|
|
32
|
+
print(string.format("%25s %0.2f ms", label .. " destruction", (os.clock() - startTime) * 1000))
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
|
|
37
35
|
local function getRandomElement()
|
|
38
36
|
return math.random()
|
|
39
37
|
end
|
|
@@ -48,18 +46,13 @@ return function(_target)
|
|
|
48
46
|
|
|
49
47
|
local function runTest(label, n, getElement)
|
|
50
48
|
local observableSortedList = maid:Add(ObservableSortedList.new())
|
|
51
|
-
local observableSortedListOld = maid:Add(ObservableSortedListOld.new())
|
|
52
49
|
|
|
53
50
|
print(string.format("%25s n = %d", label, n))
|
|
54
51
|
print(string.format("%25s %8s", string.rep("-", 25), string.rep("-", 10)))
|
|
55
52
|
|
|
56
|
-
test(n, "prev impl", observableSortedListOld, getElement)
|
|
57
|
-
cleanup("prev impl", observableSortedListOld)
|
|
58
|
-
|
|
59
53
|
test(n, "new impl", observableSortedList, getElement)
|
|
60
54
|
cleanup("new impl", observableSortedList)
|
|
61
55
|
|
|
62
|
-
|
|
63
56
|
print("\n")
|
|
64
57
|
end
|
|
65
58
|
|
|
@@ -71,4 +64,4 @@ return function(_target)
|
|
|
71
64
|
return function()
|
|
72
65
|
maid:DoCleaning()
|
|
73
66
|
end
|
|
74
|
-
end
|
|
67
|
+
end
|
|
@@ -1,725 +0,0 @@
|
|
|
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
|
-
This class always prefers to add equivalent elements to the end of the list if they're not in the list.
|
|
13
|
-
Otherwise it prefers minimal movement.
|
|
14
|
-
|
|
15
|
-
@class ObservableSortedList
|
|
16
|
-
]=]
|
|
17
|
-
|
|
18
|
-
local require = require(script.Parent.loader).load(script)
|
|
19
|
-
|
|
20
|
-
local Brio = require("Brio")
|
|
21
|
-
local Maid = require("Maid")
|
|
22
|
-
local Observable = require("Observable")
|
|
23
|
-
local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
|
|
24
|
-
local Rx = require("Rx")
|
|
25
|
-
local Signal = require("Signal")
|
|
26
|
-
local Symbol = require("Symbol")
|
|
27
|
-
local ValueObject = require("ValueObject")
|
|
28
|
-
local DuckTypeUtils = require("DuckTypeUtils")
|
|
29
|
-
|
|
30
|
-
-- Higher numbers last
|
|
31
|
-
local function defaultCompare(a, b)
|
|
32
|
-
-- equivalent of `return a - b` except it supports comparison of strings and stuff
|
|
33
|
-
if b > a then
|
|
34
|
-
return -1
|
|
35
|
-
elseif b < a then
|
|
36
|
-
return 1
|
|
37
|
-
else
|
|
38
|
-
return 0
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
local ObservableSortedList = {}
|
|
43
|
-
ObservableSortedList.ClassName = "ObservableSortedList"
|
|
44
|
-
ObservableSortedList.__index = ObservableSortedList
|
|
45
|
-
|
|
46
|
-
--[=[
|
|
47
|
-
Constructs a new ObservableSortedList
|
|
48
|
-
@param isReversed boolean
|
|
49
|
-
@param compare function
|
|
50
|
-
@return ObservableSortedList<T>
|
|
51
|
-
]=]
|
|
52
|
-
function ObservableSortedList.new(isReversed, compare)
|
|
53
|
-
assert(type(isReversed) == "boolean" or isReversed == nil, "Bad isReversed")
|
|
54
|
-
|
|
55
|
-
local self = setmetatable({}, ObservableSortedList)
|
|
56
|
-
|
|
57
|
-
self._maid = Maid.new()
|
|
58
|
-
|
|
59
|
-
self._keyList = {} -- { [number]: Symbol } -- immutable
|
|
60
|
-
|
|
61
|
-
self._indexObservers = self._maid:Add(ObservableSubscriptionTable.new())
|
|
62
|
-
self._contentIndexObservers = self._maid:Add(ObservableSubscriptionTable.new())
|
|
63
|
-
self._keyObservables = self._maid:Add(ObservableSubscriptionTable.new())
|
|
64
|
-
|
|
65
|
-
self._sortValue = {} -- { [Symbol]: number }
|
|
66
|
-
self._contents = {} -- { [Symbol]: T }
|
|
67
|
-
self._indexes = {} -- { [Symbol]: number }
|
|
68
|
-
|
|
69
|
-
self._isReversed = isReversed or false
|
|
70
|
-
self._compare = compare or defaultCompare
|
|
71
|
-
|
|
72
|
-
self._countValue = self._maid:Add(ValueObject.new(0, "number"))
|
|
73
|
-
|
|
74
|
-
--[=[
|
|
75
|
-
Fires when an item is added
|
|
76
|
-
@readonly
|
|
77
|
-
@prop ItemAdded Signal<T, number, Symbol>
|
|
78
|
-
@within ObservableSortedList
|
|
79
|
-
]=]
|
|
80
|
-
self.ItemAdded = self._maid:Add(Signal.new())
|
|
81
|
-
|
|
82
|
-
--[=[
|
|
83
|
-
Fires when an item is removed.
|
|
84
|
-
@readonly
|
|
85
|
-
@prop ItemRemoved self._maid:Add(Signal<T, Symbol>)
|
|
86
|
-
@within ObservableSortedList
|
|
87
|
-
]=]
|
|
88
|
-
self.ItemRemoved = self._maid:Add(Signal.new())
|
|
89
|
-
|
|
90
|
-
--[=[
|
|
91
|
-
Fires when an item's order changes.
|
|
92
|
-
@readonly
|
|
93
|
-
@prop OrderChanged self._maid:Add(Signal<T, Symbol>)
|
|
94
|
-
@within ObservableSortedList
|
|
95
|
-
]=]
|
|
96
|
-
self.OrderChanged = self._maid:Add(Signal.new())
|
|
97
|
-
|
|
98
|
-
--[=[
|
|
99
|
-
Fires when the count changes.
|
|
100
|
-
@prop CountChanged RBXScriptSignal
|
|
101
|
-
@within ObservableSortedList
|
|
102
|
-
]=]
|
|
103
|
-
self.CountChanged = self._countValue.Changed
|
|
104
|
-
|
|
105
|
-
return self
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
--[=[
|
|
109
|
-
Observes the list, allocating a new list in the process.
|
|
110
|
-
|
|
111
|
-
@return Observable<{ T }>
|
|
112
|
-
]=]
|
|
113
|
-
function ObservableSortedList:Observe()
|
|
114
|
-
return Rx.combineLatest({
|
|
115
|
-
Rx.fromSignal(self.ItemAdded):Pipe({ Rx.startWith({ true }) });
|
|
116
|
-
Rx.fromSignal(self.ItemRemoved):Pipe({ Rx.startWith({ true }) });
|
|
117
|
-
Rx.fromSignal(self.OrderChanged):Pipe({ Rx.startWith({ true }) });
|
|
118
|
-
}):Pipe({
|
|
119
|
-
Rx.throttleDefer();
|
|
120
|
-
Rx.map(function()
|
|
121
|
-
return self:GetList();
|
|
122
|
-
end);
|
|
123
|
-
})
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
--[=[
|
|
127
|
-
Allows iteration over the observable map
|
|
128
|
-
|
|
129
|
-
@return (T) -> ((T, nextIndex: any) -> ...any, T?)
|
|
130
|
-
]=]
|
|
131
|
-
function ObservableSortedList:__iter()
|
|
132
|
-
return coroutine.wrap(function()
|
|
133
|
-
for index, value in self._keyList do
|
|
134
|
-
coroutine.yield(index, self._contents[value])
|
|
135
|
-
end
|
|
136
|
-
end)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
function ObservableSortedList:Contains(value)
|
|
140
|
-
-- TODO: Binary search
|
|
141
|
-
for _, item in pairs(self._contents) do
|
|
142
|
-
if item == value then
|
|
143
|
-
return true
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
return false
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
--[=[
|
|
151
|
-
Returns whether the value is an observable list
|
|
152
|
-
@param value any
|
|
153
|
-
@return boolean
|
|
154
|
-
]=]
|
|
155
|
-
function ObservableSortedList.isObservableSortedList(value)
|
|
156
|
-
return DuckTypeUtils.isImplementation(ObservableSortedList, value)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
--[=[
|
|
160
|
-
Observes all items in the list
|
|
161
|
-
@return Observable<Brio<T, Symbol>>
|
|
162
|
-
]=]
|
|
163
|
-
function ObservableSortedList:ObserveItemsBrio()
|
|
164
|
-
return Observable.new(function(sub)
|
|
165
|
-
local maid = Maid.new()
|
|
166
|
-
|
|
167
|
-
local function handleItem(item, _index, includeKey)
|
|
168
|
-
local brio = Brio.new(item, includeKey)
|
|
169
|
-
maid[includeKey] = brio
|
|
170
|
-
sub:Fire(brio)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
for index, key in pairs(self._keyList) do
|
|
174
|
-
handleItem(self._contents[key], index, key)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
maid:GiveTask(self.ItemAdded:Connect(handleItem))
|
|
178
|
-
maid:GiveTask(self.ItemRemoved:Connect(function(_item, includeKey)
|
|
179
|
-
maid[includeKey] = nil
|
|
180
|
-
end))
|
|
181
|
-
|
|
182
|
-
self._maid[sub] = maid
|
|
183
|
-
maid:GiveTask(function()
|
|
184
|
-
self._maid[sub] = nil
|
|
185
|
-
sub:Complete()
|
|
186
|
-
end)
|
|
187
|
-
|
|
188
|
-
return maid
|
|
189
|
-
end)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
--[=[
|
|
193
|
-
Gets the first key for a given symbol
|
|
194
|
-
|
|
195
|
-
@param content T
|
|
196
|
-
@return Symbol
|
|
197
|
-
]=]
|
|
198
|
-
function ObservableSortedList:FindFirstKey(content)
|
|
199
|
-
for key, item in pairs(self._contents) do
|
|
200
|
-
if item == content then
|
|
201
|
-
return key
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
return nil
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
--[=[
|
|
209
|
-
Observes the index as it changes, until the entry at the existing
|
|
210
|
-
index is removed.
|
|
211
|
-
|
|
212
|
-
@param indexToObserve number
|
|
213
|
-
@return Observable<number>
|
|
214
|
-
]=]
|
|
215
|
-
function ObservableSortedList:ObserveIndex(indexToObserve)
|
|
216
|
-
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
217
|
-
|
|
218
|
-
local key = self._keyList[indexToObserve]
|
|
219
|
-
if not key then
|
|
220
|
-
error(string.format("No entry at index %q, cannot observe changes", indexToObserve))
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
return self:ObserveIndexByKey(key)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
--[=[
|
|
227
|
-
Observes the current value at a given index. This can be useful for observing
|
|
228
|
-
the first entry, or matching stuff up to a given slot.
|
|
229
|
-
|
|
230
|
-
@param indexToObserve number
|
|
231
|
-
@return Observable<T>
|
|
232
|
-
]=]
|
|
233
|
-
function ObservableSortedList:ObserveAtIndex(indexToObserve)
|
|
234
|
-
assert(type(indexToObserve) == "number", "Bad indexToObserve")
|
|
235
|
-
|
|
236
|
-
return self._indexObservers:Observe(indexToObserve)
|
|
237
|
-
:Pipe({
|
|
238
|
-
Rx.start(function()
|
|
239
|
-
return self:Get(indexToObserve)
|
|
240
|
-
end);
|
|
241
|
-
})
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
--[=[
|
|
245
|
-
Observes the index as it changes, until the entry at the existing
|
|
246
|
-
key is removed.
|
|
247
|
-
|
|
248
|
-
@param key Symbol
|
|
249
|
-
@return Observable<number>
|
|
250
|
-
]=]
|
|
251
|
-
function ObservableSortedList:ObserveIndexByKey(key)
|
|
252
|
-
assert(Symbol.isSymbol(key), "Bad key")
|
|
253
|
-
|
|
254
|
-
return self._keyObservables:Observe(key):Pipe({
|
|
255
|
-
Rx.startFrom(function()
|
|
256
|
-
local currentIndex = self._indexes[key]
|
|
257
|
-
if currentIndex then
|
|
258
|
-
return { currentIndex }
|
|
259
|
-
else
|
|
260
|
-
return {}
|
|
261
|
-
end
|
|
262
|
-
end);
|
|
263
|
-
})
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
--[=[
|
|
267
|
-
Gets the current index from the key
|
|
268
|
-
|
|
269
|
-
@param key Symbol
|
|
270
|
-
@return number
|
|
271
|
-
]=]
|
|
272
|
-
function ObservableSortedList:GetIndexByKey(key)
|
|
273
|
-
local currentIndex = self._indexes[key]
|
|
274
|
-
if currentIndex then
|
|
275
|
-
return currentIndex
|
|
276
|
-
else
|
|
277
|
-
return nil
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
--[=[
|
|
282
|
-
Gets the count of items in the list
|
|
283
|
-
@return number
|
|
284
|
-
]=]
|
|
285
|
-
function ObservableSortedList:GetCount()
|
|
286
|
-
return self._countValue.Value or 0
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
ObservableSortedList.__len = ObservableSortedList.GetCount
|
|
290
|
-
|
|
291
|
-
--[=[
|
|
292
|
-
Gets a list of all entries.
|
|
293
|
-
@return { T }
|
|
294
|
-
]=]
|
|
295
|
-
function ObservableSortedList:GetList()
|
|
296
|
-
local list = table.create(#self._keyList)
|
|
297
|
-
for index, key in pairs(self._keyList) do
|
|
298
|
-
list[index] = self._contents[key]
|
|
299
|
-
end
|
|
300
|
-
return list
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
--[=[
|
|
304
|
-
Observes the count of the list
|
|
305
|
-
@return Observable<number>
|
|
306
|
-
]=]
|
|
307
|
-
function ObservableSortedList:ObserveCount()
|
|
308
|
-
return self._countValue:Observe()
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
--[=[
|
|
312
|
-
Adds the item to the list at the specified index
|
|
313
|
-
@param item T
|
|
314
|
-
@param observeValue Observable<Comparable> | Comparable
|
|
315
|
-
@return callback -- Call to remove
|
|
316
|
-
]=]
|
|
317
|
-
function ObservableSortedList:Add(item, observeValue)
|
|
318
|
-
assert(item ~= nil, "Bad item")
|
|
319
|
-
assert(Observable.isObservable(observeValue) or observeValue ~= nil, "Bad observeValue")
|
|
320
|
-
|
|
321
|
-
local key = Symbol.named("entryKey")
|
|
322
|
-
local maid = Maid.new()
|
|
323
|
-
|
|
324
|
-
self._contents[key] = item
|
|
325
|
-
|
|
326
|
-
if Observable.isObservable(observeValue) then
|
|
327
|
-
maid:GiveTask(observeValue:Subscribe(function(sortValue)
|
|
328
|
-
self:_assignSortValue(key, item, sortValue)
|
|
329
|
-
end))
|
|
330
|
-
elseif observeValue ~= nil then
|
|
331
|
-
self:_assignSortValue(key, item, observeValue)
|
|
332
|
-
else
|
|
333
|
-
error("Bad observeValue")
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
maid:GiveTask(function()
|
|
337
|
-
self:_removeItemByKey(key, item)
|
|
338
|
-
|
|
339
|
-
-- Fire off the index change on the value
|
|
340
|
-
self._keyObservables:Complete(key)
|
|
341
|
-
|
|
342
|
-
self._contents[key] = nil
|
|
343
|
-
self._sortValue[key] = nil
|
|
344
|
-
end)
|
|
345
|
-
|
|
346
|
-
self._maid[key] = maid
|
|
347
|
-
|
|
348
|
-
return function()
|
|
349
|
-
self._maid[key] = nil
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
function ObservableSortedList:_assignSortValue(key, item, sortValue)
|
|
354
|
-
-- self:_debugVerifyIntegrity()
|
|
355
|
-
|
|
356
|
-
if sortValue ~= nil then
|
|
357
|
-
local currentIndex = self._indexes[key]
|
|
358
|
-
local targetIndex = self:_findCorrectIndex(sortValue, currentIndex)
|
|
359
|
-
|
|
360
|
-
self._sortValue[key] = sortValue
|
|
361
|
-
self:_updateIndex(key, item, targetIndex, sortValue)
|
|
362
|
-
else
|
|
363
|
-
-- calling this also may unsubscribe some observables.
|
|
364
|
-
self:_removeItemByKey(key, item)
|
|
365
|
-
self._keyObservables:Complete(key)
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
-- self:_debugVerifyIntegrity()
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
--[=[
|
|
373
|
-
Gets the current item at the index, or nil if it is not defined.
|
|
374
|
-
@param index number
|
|
375
|
-
@return T?
|
|
376
|
-
]=]
|
|
377
|
-
function ObservableSortedList:Get(index)
|
|
378
|
-
assert(type(index) == "number", "Bad index")
|
|
379
|
-
|
|
380
|
-
local key = self._keyList[index]
|
|
381
|
-
if not key then
|
|
382
|
-
return nil
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
return self._contents[key]
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
--[=[
|
|
389
|
-
Removes the item from the list if it exists.
|
|
390
|
-
@param key Symbol
|
|
391
|
-
@return T
|
|
392
|
-
]=]
|
|
393
|
-
function ObservableSortedList:RemoveByKey(key)
|
|
394
|
-
assert(key ~= nil, "Bad key")
|
|
395
|
-
|
|
396
|
-
self._maid[key] = nil
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
function ObservableSortedList:_updateIndex(key, item, newIndex)
|
|
400
|
-
assert(item ~= nil, "Bad item")
|
|
401
|
-
assert(type(newIndex) == "number", "Bad newIndex")
|
|
402
|
-
|
|
403
|
-
local prevIndex = self._indexes[key]
|
|
404
|
-
if prevIndex == newIndex then
|
|
405
|
-
return
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
self._indexes[key] = newIndex
|
|
409
|
-
|
|
410
|
-
local changed = {}
|
|
411
|
-
|
|
412
|
-
if not prevIndex then
|
|
413
|
-
-- shift everything up to fit this space
|
|
414
|
-
local n = #self._keyList
|
|
415
|
-
for i=n, newIndex, -1 do
|
|
416
|
-
local nextKey = self._keyList[i]
|
|
417
|
-
self._indexes[nextKey] = i + 1
|
|
418
|
-
self._keyList[i + 1] = nextKey
|
|
419
|
-
|
|
420
|
-
table.insert(changed, {
|
|
421
|
-
key = nextKey;
|
|
422
|
-
newIndex = i + 1;
|
|
423
|
-
})
|
|
424
|
-
end
|
|
425
|
-
elseif newIndex > prevIndex then
|
|
426
|
-
-- we're shifting down
|
|
427
|
-
for i=prevIndex + 1, newIndex do
|
|
428
|
-
local nextKey = self._keyList[i]
|
|
429
|
-
self._indexes[nextKey] = i - 1
|
|
430
|
-
self._keyList[i - 1] = nextKey
|
|
431
|
-
|
|
432
|
-
table.insert(changed, {
|
|
433
|
-
key = nextKey;
|
|
434
|
-
newIndex = i - 1;
|
|
435
|
-
})
|
|
436
|
-
end
|
|
437
|
-
elseif newIndex < prevIndex then
|
|
438
|
-
-- we're shifting up
|
|
439
|
-
|
|
440
|
-
for i=prevIndex-1, newIndex, -1 do
|
|
441
|
-
local belowKey = self._keyList[i]
|
|
442
|
-
self._indexes[belowKey] = i + 1
|
|
443
|
-
self._keyList[i + 1] = belowKey
|
|
444
|
-
table.insert(changed, {
|
|
445
|
-
key = belowKey;
|
|
446
|
-
newIndex = i + 1;
|
|
447
|
-
})
|
|
448
|
-
end
|
|
449
|
-
else
|
|
450
|
-
error("Bad state")
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
local itemAdded = table.freeze({
|
|
454
|
-
key = key;
|
|
455
|
-
newIndex = newIndex;
|
|
456
|
-
item = item;
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
-- ensure ourself is considered changed
|
|
460
|
-
table.insert(changed, itemAdded)
|
|
461
|
-
|
|
462
|
-
self._keyList[newIndex] = key
|
|
463
|
-
|
|
464
|
-
-- Fire off our count value changed
|
|
465
|
-
-- still O(n^2) but at least we prevent emitting O(n^2) events
|
|
466
|
-
if prevIndex == nil then
|
|
467
|
-
self:_deferChange(1, itemAdded, nil, changed)
|
|
468
|
-
else
|
|
469
|
-
self:_deferChange(0, nil, nil, changed)
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
function ObservableSortedList:_removeItemByKey(key, item)
|
|
474
|
-
assert(key ~= nil, "Bad key")
|
|
475
|
-
|
|
476
|
-
local index = self._indexes[key]
|
|
477
|
-
if not index then
|
|
478
|
-
return
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
self._indexes[key] = nil
|
|
482
|
-
self._sortValue[key] = nil
|
|
483
|
-
|
|
484
|
-
local changed = {}
|
|
485
|
-
|
|
486
|
-
-- shift everything down
|
|
487
|
-
local n = #self._keyList
|
|
488
|
-
for i=index, n - 1 do
|
|
489
|
-
local nextKey = self._keyList[i+1]
|
|
490
|
-
self._indexes[nextKey] = i
|
|
491
|
-
self._keyList[i] = nextKey
|
|
492
|
-
|
|
493
|
-
table.insert(changed, {
|
|
494
|
-
key = nextKey;
|
|
495
|
-
newIndex = i;
|
|
496
|
-
})
|
|
497
|
-
end
|
|
498
|
-
self._keyList[n] = nil
|
|
499
|
-
|
|
500
|
-
local itemRemoved = table.freeze({
|
|
501
|
-
key = key;
|
|
502
|
-
item = item;
|
|
503
|
-
previousIndex = index;
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
-- TODO: Defer item removed as a changed event?
|
|
507
|
-
|
|
508
|
-
-- still O(n^2) but at least we prevent emitting O(n^2) events
|
|
509
|
-
self:_deferChange(-1, nil, itemRemoved, changed)
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
function ObservableSortedList:_deferChange(countChange, itemAdded, itemRemoved, indexChanges)
|
|
513
|
-
self:_queueDeferredChange()
|
|
514
|
-
|
|
515
|
-
if itemAdded then
|
|
516
|
-
self._deferredChange.itemsRemoved[itemAdded.key] = nil
|
|
517
|
-
self._deferredChange.itemsAdded[itemAdded.key] = itemAdded
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
if itemRemoved then
|
|
521
|
-
self._deferredChange.itemsAdded[itemRemoved.key] = nil
|
|
522
|
-
self._deferredChange.itemsRemoved[itemRemoved.key] = itemRemoved
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
self._deferredChange.countChange += countChange
|
|
526
|
-
|
|
527
|
-
for _, data in pairs(indexChanges) do
|
|
528
|
-
self._deferredChange.indexChanges[data.key] = data
|
|
529
|
-
end
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
function ObservableSortedList:_queueDeferredChange()
|
|
533
|
-
if self._deferredChange then
|
|
534
|
-
return
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
self._deferredChange = {
|
|
538
|
-
countChange = 0;
|
|
539
|
-
indexChanges = {};
|
|
540
|
-
itemsAdded = {};
|
|
541
|
-
itemsRemoved = {};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
self._maid._currentDefer = task.defer(function()
|
|
545
|
-
local snapshot = self._deferredChange
|
|
546
|
-
self._deferredChange = nil
|
|
547
|
-
|
|
548
|
-
task.spawn(function()
|
|
549
|
-
self._maid._currentDefer = nil
|
|
550
|
-
local changed = false
|
|
551
|
-
|
|
552
|
-
self._countValue.Value = self._countValue.Value + snapshot.countChange
|
|
553
|
-
|
|
554
|
-
-- Fire off last adds
|
|
555
|
-
for _, lastAdded in pairs(snapshot.itemsAdded) do
|
|
556
|
-
if not self.ItemAdded.Destroy then
|
|
557
|
-
break
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
changed = true
|
|
561
|
-
self.ItemAdded:Fire(lastAdded.item, lastAdded.newIndex, lastAdded.key)
|
|
562
|
-
|
|
563
|
-
-- Item adds are included in indexChanges.
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
for _, lastRemoved in pairs(snapshot.itemsRemoved) do
|
|
567
|
-
if not self.ItemRemoved.Destroy then
|
|
568
|
-
break
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
changed = true
|
|
572
|
-
self.ItemRemoved:Fire(lastRemoved.item, lastRemoved.key)
|
|
573
|
-
|
|
574
|
-
-- Fire only if we aren't handled by an index change.
|
|
575
|
-
if self._keyList[lastRemoved.previousIndex] == nil then
|
|
576
|
-
self._indexObservers:Fire(lastRemoved.previousIndex, nil)
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
-- Fire off index change on each key list (if the data isn't stale)
|
|
581
|
-
for _, lastChange in pairs(snapshot.indexChanges) do
|
|
582
|
-
if self._indexes[lastChange.key] == lastChange.newIndex then
|
|
583
|
-
changed = true
|
|
584
|
-
|
|
585
|
-
self._keyObservables:Fire(lastChange.key, lastChange.newIndex)
|
|
586
|
-
self._indexObservers:Fire(lastChange.newIndex, self._contents[lastChange.key])
|
|
587
|
-
end
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
if changed then
|
|
591
|
-
self.OrderChanged:Fire()
|
|
592
|
-
end
|
|
593
|
-
end)
|
|
594
|
-
end)
|
|
595
|
-
end
|
|
596
|
-
|
|
597
|
-
function ObservableSortedList:_findCorrectIndex(sortValue, currentIndex)
|
|
598
|
-
local highInsertionIndex = self:_highBinarySearch(sortValue)
|
|
599
|
-
|
|
600
|
-
-- we're inserting, so always insert at end
|
|
601
|
-
if not currentIndex then
|
|
602
|
-
return highInsertionIndex
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
local lowInsertionIndex = self:_lowBinarySearch(sortValue)
|
|
606
|
-
|
|
607
|
-
-- remember we get insertion index so we need to subtract one
|
|
608
|
-
if highInsertionIndex > currentIndex then
|
|
609
|
-
highInsertionIndex = highInsertionIndex - 1
|
|
610
|
-
end
|
|
611
|
-
if lowInsertionIndex > currentIndex then
|
|
612
|
-
lowInsertionIndex = lowInsertionIndex - 1
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
-- prioritize the smallest potential movement
|
|
616
|
-
if currentIndex < lowInsertionIndex then
|
|
617
|
-
return lowInsertionIndex
|
|
618
|
-
elseif currentIndex > highInsertionIndex then
|
|
619
|
-
return highInsertionIndex
|
|
620
|
-
else
|
|
621
|
-
return currentIndex
|
|
622
|
-
end
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
function ObservableSortedList:_highBinarySearch(sortValue)
|
|
626
|
-
if #self._keyList == 0 then
|
|
627
|
-
return 1
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
local minIndex = 1
|
|
631
|
-
local maxIndex = #self._keyList
|
|
632
|
-
while true do
|
|
633
|
-
local mid = math.floor((minIndex + maxIndex) / 2)
|
|
634
|
-
local compareValue = self._compare(self._sortValue[self._keyList[mid]], sortValue)
|
|
635
|
-
if type(compareValue) ~= "number" then
|
|
636
|
-
error(string.format("Bad compareValue, expected number, got %q", type(compareValue)))
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
if self._isReversed then
|
|
640
|
-
compareValue = -compareValue
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
if compareValue > 0 then
|
|
644
|
-
maxIndex = mid - 1
|
|
645
|
-
if minIndex > maxIndex then
|
|
646
|
-
return mid
|
|
647
|
-
end
|
|
648
|
-
else
|
|
649
|
-
minIndex = mid + 1
|
|
650
|
-
if minIndex > maxIndex then
|
|
651
|
-
return mid + 1
|
|
652
|
-
end
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
function ObservableSortedList:_lowBinarySearch(sortValue)
|
|
658
|
-
if #self._keyList == 0 then
|
|
659
|
-
return 1
|
|
660
|
-
end
|
|
661
|
-
|
|
662
|
-
local minIndex = 1
|
|
663
|
-
local maxIndex = #self._keyList
|
|
664
|
-
while true do
|
|
665
|
-
local mid = math.floor((minIndex + maxIndex) / 2)
|
|
666
|
-
local compareValue = self._compare(self._sortValue[self._keyList[mid]], sortValue)
|
|
667
|
-
if type(compareValue) ~= "number" then
|
|
668
|
-
error(string.format("Bad compareValue, expected number, got %q", type(compareValue)))
|
|
669
|
-
end
|
|
670
|
-
|
|
671
|
-
if self._isReversed then
|
|
672
|
-
compareValue = -compareValue
|
|
673
|
-
end
|
|
674
|
-
|
|
675
|
-
if compareValue < 0 then
|
|
676
|
-
minIndex = mid + 1
|
|
677
|
-
if minIndex > maxIndex then
|
|
678
|
-
return mid + 1
|
|
679
|
-
end
|
|
680
|
-
else
|
|
681
|
-
maxIndex = mid - 1
|
|
682
|
-
if minIndex > maxIndex then
|
|
683
|
-
return mid
|
|
684
|
-
end
|
|
685
|
-
end
|
|
686
|
-
end
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
function ObservableSortedList:_debugVerifyIntegrity()
|
|
690
|
-
for i=2, #self._keyList do
|
|
691
|
-
local compare = self._compare(self._sortValue[self._keyList[i-1]], self._sortValue[self._keyList[i]])
|
|
692
|
-
if self._isReversed then
|
|
693
|
-
compare = -compare
|
|
694
|
-
end
|
|
695
|
-
if compare > 0 then
|
|
696
|
-
warn(string.format("Bad sorted list state %s at index %d", self:_debugSortValuesToString(), i))
|
|
697
|
-
end
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
for i=1, #self._keyList do
|
|
701
|
-
if self._indexes[self._keyList[i]] ~= i then
|
|
702
|
-
warn(string.format("Index is out of date for %d for %s", i, self:_debugSortValuesToString()))
|
|
703
|
-
end
|
|
704
|
-
end
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
function ObservableSortedList:_debugSortValuesToString()
|
|
708
|
-
local values = {}
|
|
709
|
-
|
|
710
|
-
for _, key in pairs(self._keyList) do
|
|
711
|
-
table.insert(values, string.format("%4d", self._sortValue[key]))
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
return table.concat(values, ", ")
|
|
715
|
-
end
|
|
716
|
-
|
|
717
|
-
--[=[
|
|
718
|
-
Cleans up the ObservableSortedList and sets the metatable to nil.
|
|
719
|
-
]=]
|
|
720
|
-
function ObservableSortedList:Destroy()
|
|
721
|
-
self._maid:DoCleaning()
|
|
722
|
-
setmetatable(self, nil)
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
return ObservableSortedList
|