@quenty/observablecollection 12.11.1 → 12.12.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.
@@ -0,0 +1,586 @@
1
+ --[=[
2
+ A list that can be observed for blend and other components and maintains sorting order.
3
+
4
+ This allows you to observe both an index, observe a value at an index, and more.
5
+
6
+ This class is a red-black binary sorted tree. Unlike previous iterations of this class, we can add
7
+ values in log(n) time, and remove in log(n) time, and it uses less memory.
8
+
9
+ Previously we'd use O(n^2) processing time when constructing this class.
10
+
11
+ We reuse the node itself as the indexing key.
12
+
13
+ This class always prefers to add equivalent elements to the end of the list if they're not in the list.
14
+ Otherwise it prefers minimal movement.
15
+
16
+ @class ObservableSortedList
17
+ ]=]
18
+
19
+ local require = require(script.Parent.loader).load(script)
20
+
21
+ local Brio = require("Brio")
22
+ local DuckTypeUtils = require("DuckTypeUtils")
23
+ local Maid = require("Maid")
24
+ local Observable = require("Observable")
25
+ local ObservableSubscriptionTable = require("ObservableSubscriptionTable")
26
+ local Rx = require("Rx")
27
+ local Signal = require("Signal")
28
+ local SortedNode = require("SortedNode")
29
+ local SortedNodeValue = require("SortedNodeValue")
30
+ local ValueObject = require("ValueObject")
31
+ local SortFunctionUtils = require("SortFunctionUtils")
32
+ local ListIndexUtils = require("ListIndexUtils")
33
+
34
+ local ObservableSortedList = {}
35
+ ObservableSortedList.ClassName = "ObservableSortedList"
36
+ ObservableSortedList.__index = ObservableSortedList
37
+
38
+ --[=[
39
+ Constructs a new ObservableSortedList
40
+ @param isReversed boolean
41
+ @param compare function
42
+ @return ObservableSortedList<T>
43
+ ]=]
44
+ function ObservableSortedList.new(isReversed, compare)
45
+ assert(type(isReversed) == "boolean" or isReversed == nil, "Bad isReversed")
46
+
47
+ local self = setmetatable({}, ObservableSortedList)
48
+
49
+ self._maid = Maid.new()
50
+
51
+ self._indexObservers = self._maid:Add(ObservableSubscriptionTable.new())
52
+ self._nodeIndexObservables = self._maid:Add(ObservableSubscriptionTable.new())
53
+
54
+ self._mainObservables = self._maid:Add(ObservableSubscriptionTable.new())
55
+
56
+ self._nodesAdded = {}
57
+ self._nodesRemoved = {}
58
+ self._lowestIndexChanged = nil
59
+
60
+ self._compare = if isReversed then SortFunctionUtils.reverse(compare) else compare
61
+
62
+ self._countValue = self._maid:Add(ValueObject.new(0, "number"))
63
+
64
+ --[=[
65
+ Fires when an item is added
66
+
67
+ @readonly
68
+ @prop ItemAdded Signal<T, number, Symbol>
69
+ @within ObservableSortedList
70
+ ]=]
71
+ self.ItemAdded = self._maid:Add(Signal.new())
72
+
73
+ --[=[
74
+ Fires when an item is removed.
75
+
76
+ @readonly
77
+ @prop ItemRemoved Signal<T, Symbol>
78
+ @within ObservableSortedList
79
+ ]=]
80
+ self.ItemRemoved = Signal.new()
81
+
82
+ --[=[
83
+ Fires when the order could have changed
84
+
85
+ @readonly
86
+ @prop OrderChanged Signal
87
+ @within ObservableSortedList
88
+ ]=]
89
+ self.OrderChanged = self._maid:Add(Signal.new())
90
+
91
+
92
+ --[=[
93
+ Fires when the count changes
94
+
95
+ @readonly
96
+ @prop CountChanged Signal<number>
97
+ @within ObservableSortedList
98
+ ]=]
99
+ self.CountChanged = self._countValue.Changed
100
+
101
+ return self
102
+ end
103
+
104
+ --[=[
105
+ Returns whether the value is an observable list
106
+ @param value any
107
+ @return boolean
108
+ ]=]
109
+ function ObservableSortedList.isObservableSortedList(value)
110
+ return DuckTypeUtils.isImplementation(ObservableSortedList, value)
111
+ end
112
+
113
+ --[=[
114
+ Observes the list, allocating a new list in the process.
115
+
116
+ @return Observable<{ T }>
117
+ ]=]
118
+ function ObservableSortedList:Observe()
119
+ return self._mainObservables:Observe("list")
120
+ end
121
+
122
+ --[=[
123
+ Allows iteration over the observable map
124
+
125
+ @return (T) -> ((T, nextIndex: any) -> ...any, T?)
126
+ ]=]
127
+ function ObservableSortedList:__iter()
128
+ if self._root then
129
+ return self._root:IterateData()
130
+ else
131
+ return SortFunctionUtils.emptyIterator
132
+ end
133
+ end
134
+
135
+ --[=[
136
+ Iterates over an index range
137
+
138
+ @param start number
139
+ @param finish number
140
+ @return (T) -> ((T, nextIndex: any) -> ...any, T?)
141
+ ]=]
142
+ function ObservableSortedList:IterateRange(start, finish)
143
+ return coroutine.wrap(function()
144
+ for index, node in self:_iterateNodesRange(start, finish) do
145
+ coroutine.yield(index, node.data)
146
+ end
147
+ end)
148
+ end
149
+
150
+ function ObservableSortedList:_iterateNodes()
151
+ if self._root then
152
+ return self._root:IterateNodes()
153
+ else
154
+ return SortFunctionUtils.emptyIterator
155
+ end
156
+ end
157
+
158
+ function ObservableSortedList:_iterateNodesRange(start, finish)
159
+ if self._root then
160
+ return self._root:IterateNodesRange(start, finish)
161
+ else
162
+ return SortFunctionUtils.emptyIterator
163
+ end
164
+ end
165
+
166
+ function ObservableSortedList:_containsNode(node)
167
+ assert(SortedNode.isSortedNode(node), "Bad node")
168
+
169
+ if self._root then
170
+ return self._root:ContainsNode(node)
171
+ else
172
+ return false
173
+ end
174
+ end
175
+
176
+ function ObservableSortedList:_findNodeForDataLinearSearchSlow(data)
177
+ if self._root then
178
+ return self._root:FindFirstNodeForData(data)
179
+ else
180
+ return nil
181
+ end
182
+ end
183
+
184
+ function ObservableSortedList:_findNodeAtIndex(index)
185
+ assert(type(index) == "number", "Bad index")
186
+
187
+ if self._root then
188
+ return self._root:FindNodeAtIndex(index)
189
+ else
190
+ return nil
191
+ end
192
+ end
193
+
194
+ function ObservableSortedList:_findNodeIndex(node)
195
+ assert(SortedNode.isSortedNode(node), "Bad node")
196
+
197
+ if self._root then
198
+ return self._root:FindNodeIndex(node)
199
+ else
200
+ return nil
201
+ end
202
+ end
203
+
204
+ --[=[
205
+ Gets the first node for a given symbol
206
+
207
+ @param content T
208
+ @return Symbol
209
+ ]=]
210
+ function ObservableSortedList:FindFirstKey(content)
211
+ return self:_findNodeForDataLinearSearchSlow(content)
212
+ end
213
+
214
+ function ObservableSortedList:PrintDebug()
215
+ print(self._root)
216
+ end
217
+
218
+ --[=[
219
+ Returns true if the value exists
220
+
221
+ @param content T
222
+ @return boolean
223
+ ]=]
224
+ function ObservableSortedList:Contains(content)
225
+ assert(content ~= nil, "Bad content")
226
+
227
+ -- TODO: Speed up
228
+ return self:_findNodeForDataLinearSearchSlow(content) ~= nil
229
+ end
230
+
231
+ --[=[
232
+ Observes all items in the list
233
+ @return Observable<Brio<T, Symbol>>
234
+ ]=]
235
+ function ObservableSortedList:ObserveItemsBrio()
236
+ return Observable.new(function(sub)
237
+ local maid = Maid.new()
238
+
239
+ -- TODO: Optimize this so we don't have to make so many brios and connect
240
+ -- to so many events
241
+
242
+ local function handleItem(data, _index, node)
243
+ local brio = Brio.new(data, node)
244
+ maid[node] = brio
245
+ sub:Fire(brio)
246
+ end
247
+
248
+ -- NOTE: This can modify the list...?
249
+ for index, node in self:_iterateNodes() do
250
+ handleItem(node.data, index, node)
251
+ end
252
+
253
+ maid:GiveTask(self.ItemAdded:Connect(handleItem))
254
+ maid:GiveTask(self.ItemRemoved:Connect(function(_item, node)
255
+ maid[node] = nil
256
+ end))
257
+
258
+ -- TODO: Prevent this stuff from happening too
259
+ self._maid[sub] = maid
260
+ maid:GiveTask(function()
261
+ self._maid[sub] = nil
262
+ sub:Complete()
263
+ end)
264
+
265
+ return maid
266
+ end)
267
+ end
268
+
269
+ --[=[
270
+ Observes the index as it changes, until the entry at the existing
271
+ index is removed.
272
+
273
+ @param indexToObserve number
274
+ @return Observable<number>
275
+ ]=]
276
+ function ObservableSortedList:ObserveIndex(indexToObserve)
277
+ assert(type(indexToObserve) == "number", "Bad indexToObserve")
278
+
279
+ local node = self:_findNodeAtIndex(indexToObserve)
280
+ if not node then
281
+ error(string.format("[ObservableSortedList.ObserveIndex] - No entry at index %q, cannot observe changes", indexToObserve))
282
+ end
283
+
284
+ return self:ObserveIndexByKey(node)
285
+ end
286
+
287
+ --[=[
288
+ Observes the current value at a given index. This can be useful for observing
289
+ the first entry, or matching stuff up to a given slot.
290
+
291
+ @param indexToObserve number
292
+ @return Observable<(T, Key)>
293
+ ]=]
294
+ function ObservableSortedList:ObserveAtIndex(indexToObserve)
295
+ assert(type(indexToObserve) == "number", "Bad indexToObserve")
296
+
297
+ return self._indexObservers:Observe(indexToObserve)
298
+ :Pipe({
299
+ Rx.start(function()
300
+ local node = self:_findNodeAtIndex(indexToObserve)
301
+ if node then
302
+ return node.data, node
303
+ else
304
+ return nil
305
+ end
306
+ end);
307
+ })
308
+ end
309
+
310
+ --[=[
311
+ Observes the index as it changes, until the entry at the existing
312
+ node is removed.
313
+
314
+ @param node SortedNode
315
+ @return Observable<number>
316
+ ]=]
317
+ function ObservableSortedList:ObserveIndexByKey(node)
318
+ assert(SortedNode.isSortedNode(node), "Bad node")
319
+
320
+ return self._nodeIndexObservables:Observe(node):Pipe({
321
+ Rx.startFrom(function()
322
+ local currentIndex = self:_findNodeIndex(node)
323
+ if currentIndex then
324
+ return { currentIndex }
325
+ else
326
+ return {}
327
+ end
328
+ end);
329
+ })
330
+ end
331
+
332
+ --[=[
333
+ Gets the current index from the node
334
+
335
+ @param node SortedNode
336
+ @return number
337
+ ]=]
338
+ function ObservableSortedList:GetIndexByKey(node)
339
+ assert(SortedNode.isSortedNode(node), "Bad node")
340
+
341
+ return self:_findNodeIndex(node)
342
+ end
343
+
344
+ --[=[
345
+ Gets the count of items in the list
346
+ @return number
347
+ ]=]
348
+ function ObservableSortedList:GetCount()
349
+ return self._countValue.Value or 0
350
+ end
351
+
352
+ --[=[
353
+ Gets a list of all entries.
354
+ @return { T }
355
+ ]=]
356
+ function ObservableSortedList:GetList()
357
+ local list = table.create(self._countValue.Value)
358
+ for index, data in self:__iter() do
359
+ list[index] = data
360
+ end
361
+ return table.freeze(list)
362
+ end
363
+
364
+ --[=[
365
+ Observes the count of the list
366
+ @return Observable<number>
367
+ ]=]
368
+ function ObservableSortedList:ObserveCount()
369
+ return self._countValue:Observe()
370
+ end
371
+
372
+ --[=[
373
+ Adds the item to the list at the specified index
374
+ @param data T
375
+ @param observeValue Observable<Comparable> | Comparable
376
+ @return callback -- Call to remove
377
+ ]=]
378
+ function ObservableSortedList:Add(data, observeValue)
379
+ assert(data ~= nil, "Bad data")
380
+ assert(Observable.isObservable(observeValue) or observeValue ~= nil, "Bad observeValue")
381
+
382
+ local node = SortedNode.new(data)
383
+
384
+ -- TODO: Store maid in node to prevent lookup of node -> index
385
+ local maid = Maid.new()
386
+
387
+ if Observable.isObservable(observeValue) then
388
+ maid:GiveTask(observeValue:Subscribe(function(sortValue)
389
+ self:_assignSortValue(node, sortValue)
390
+ end))
391
+ elseif observeValue ~= nil then
392
+ self:_assignSortValue(node, observeValue)
393
+ else
394
+ error("Bad observeValue")
395
+ end
396
+
397
+ maid:GiveTask(function()
398
+ -- TODO: Avoid cleaning up all these nodes when global maid cleans up
399
+ self:_assignSortValue(node, nil)
400
+ self._nodeIndexObservables:Complete(node)
401
+ end)
402
+
403
+ self._maid[node] = maid
404
+
405
+ return function()
406
+ self._maid[node] = nil
407
+ end
408
+ end
409
+
410
+ function ObservableSortedList:_assignSortValue(node, value)
411
+ if SortedNodeValue.isSortedNodeValue(node.value) then
412
+ if node.value:GetValue() == value then
413
+ return
414
+ end
415
+ elseif node.value == value then
416
+ return
417
+ end
418
+
419
+ if value == nil then
420
+ if self._root and self._root:ContainsNode(node) then
421
+ self._nodesRemoved[node] = true
422
+ self:_applyLowestIndexChanged(node:GetIndex())
423
+ self:_removeNode(node)
424
+ node.value = nil
425
+ self:_queueFireEvents()
426
+ else
427
+ node.value = nil
428
+ end
429
+
430
+ return
431
+ end
432
+
433
+ if self._compare ~= nil then
434
+ value = SortedNodeValue.new(value, self._compare)
435
+ end
436
+
437
+ -- our value changing didn't change anything
438
+ if not node:NeedsToMove(self._root, value) then
439
+ node.value = value
440
+ return
441
+ end
442
+
443
+ self._nodesRemoved[node] = nil
444
+
445
+ if self._root and self._root:ContainsNode(node) then
446
+ self:_applyLowestIndexChanged(node:GetIndex())
447
+ self:_removeNode(node)
448
+ else
449
+ self._nodesAdded[node] = true
450
+ end
451
+
452
+ node.value = value
453
+
454
+ self:_insertNode(node)
455
+ self:_applyLowestIndexChanged(node:GetIndex())
456
+ self:_queueFireEvents()
457
+ end
458
+
459
+ function ObservableSortedList:_applyLowestIndexChanged(index)
460
+ if self._lowestIndexChanged == nil then
461
+ self._lowestIndexChanged = index
462
+ return
463
+ end
464
+
465
+ if index < self._lowestIndexChanged then
466
+ self._lowestIndexChanged = index
467
+ end
468
+ end
469
+
470
+ function ObservableSortedList:_queueFireEvents()
471
+ if self._maid._fireEvents then
472
+ return
473
+ end
474
+
475
+ self._maid._fireEvents = task.defer(function()
476
+ self._maid._fireEvents = nil
477
+ self:_fireEvents()
478
+ end)
479
+ end
480
+
481
+ function ObservableSortedList:_fireEvents()
482
+ -- print(self._root)
483
+
484
+ local lowestIndexChanged = self._lowestIndexChanged
485
+ self._lowestIndexChanged = nil
486
+
487
+ local nodesAdded = self._nodesAdded
488
+ self._nodesAdded = {}
489
+
490
+ local nodesRemoved = self._nodesRemoved
491
+ self._nodesRemoved = {}
492
+
493
+ -- Fire count changed first
494
+ if self._root then
495
+ self._countValue.Value = self._root.descendantCount
496
+ else
497
+ self._countValue.Value = 0
498
+ end
499
+
500
+ -- TODO: Prevent Rx.of(itemAdded) stuff in our UI
501
+ for node in nodesAdded do
502
+ -- TODO: Prevent query slow here...?
503
+ local index = node:GetIndex()
504
+ self.ItemAdded:Fire(node.data, index, node)
505
+ end
506
+
507
+ for node in nodesRemoved do
508
+ self.ItemRemoved:Fire(node.data, node)
509
+ end
510
+
511
+ self.OrderChanged:Fire()
512
+
513
+ do
514
+ for index, node in self:_iterateNodesRange(lowestIndexChanged) do
515
+ -- TODO: Handle negative observations to avoid refiring upon insertion
516
+ -- TODO: Handle our state changing while we're firing
517
+ -- TODO: Avoid looping over nodes if we don't need to (track observations in node itself?)
518
+ local negative = ListIndexUtils.toNegativeIndex(self._root.descendantCount, index)
519
+ self._nodeIndexObservables:Fire(node, index)
520
+ self._indexObservers:Fire(index, node)
521
+ self._indexObservers:Fire(negative, node)
522
+ end
523
+ end
524
+
525
+ if self._mainObservables:HasSubscriptions("list") then
526
+ -- TODO: Reuse list
527
+ local list = self:GetList()
528
+ self._mainObservables:Fire("list", list)
529
+ end
530
+ end
531
+
532
+ function ObservableSortedList:_insertNode(node)
533
+ assert(SortedNode.isSortedNode(node), "Bad SortedNode")
534
+
535
+ if self._root == nil then
536
+ node:MarkBlack()
537
+ self._root = node
538
+ else
539
+ self._root = self._root:InsertNode(node)
540
+ end
541
+ end
542
+
543
+ function ObservableSortedList:_removeNode(nodeToRemove)
544
+ assert(SortedNode.isSortedNode(nodeToRemove), "Bad SortedNode")
545
+
546
+ if self._root ~= nil then
547
+ self._root = self._root:RemoveNode(nodeToRemove)
548
+ end
549
+ end
550
+
551
+ --[=[
552
+ Gets the current item at the index, or nil if it is not defined.
553
+ @param index number
554
+ @return T?
555
+ ]=]
556
+ function ObservableSortedList:Get(index)
557
+ assert(type(index) == "number", "Bad index")
558
+
559
+ local node = self:_findNodeAtIndex(index)
560
+ if not node then
561
+ return nil
562
+ end
563
+
564
+ return node.data
565
+ end
566
+
567
+ --[=[
568
+ Removes the item from the list if it exists.
569
+ @param node SortedNode
570
+ @return T
571
+ ]=]
572
+ function ObservableSortedList:RemoveByKey(node)
573
+ assert(SortedNode.isSortedNode(node), "Bad node")
574
+
575
+ self._maid[node] = nil
576
+ end
577
+
578
+ --[=[
579
+ Cleans up the ObservableSortedList and sets the metatable to nil.
580
+ ]=]
581
+ function ObservableSortedList:Destroy()
582
+ self._maid:DoCleaning()
583
+ setmetatable(self, nil)
584
+ end
585
+
586
+ return ObservableSortedList
@@ -19,11 +19,11 @@ return function(target)
19
19
 
20
20
  local observableSortedList = maid:Add(ObservableSortedList.new())
21
21
 
22
- local random = Random.new()
22
+ local random = Random.new(35)
23
23
 
24
24
  local values = {}
25
25
  for i=1, ENTRIES do
26
- local scoreValue = maid:Add(ValueObject.new(0, "number"))
26
+ local scoreValue = maid:Add(ValueObject.new(0 or random:NextNumber(), "number"))
27
27
 
28
28
  local data = {
29
29
  originalIndex = i;
@@ -105,6 +105,7 @@ return function(target)
105
105
  };
106
106
 
107
107
  Blend.New "TextLabel" {
108
+ Name = "Score";
108
109
  Text = data.scoreValue:Observe():Pipe({
109
110
  Rx.map(tostring)
110
111
  });
@@ -116,17 +117,8 @@ return function(target)
116
117
  TextXAlignment = Enum.TextXAlignment.Left;
117
118
  };
118
119
 
119
- Blend.New "TextLabel" {
120
- Text = data.originalIndex;
121
- Size = UDim2.fromScale(1, 1);
122
- BackgroundTransparency = 1;
123
- Position = UDim2.new(0, -10, 0.5, 0);
124
- AnchorPoint = Vector2.new(1, 0.5);
125
- TextColor3 = Color3.new(1, 1, 1);
126
- TextXAlignment = Enum.TextXAlignment.Right;
127
- };
128
-
129
120
  Blend.New "TextBox" {
121
+ Name = "SetScore";
130
122
  Size = UDim2.fromScale(1, 1);
131
123
  Text = tostring(data.scoreValue.Value);
132
124
  BackgroundTransparency = 1;
@@ -139,6 +131,17 @@ return function(target)
139
131
  end
140
132
  end;
141
133
  };
134
+
135
+ Blend.New "TextLabel" {
136
+ Name = "OriginalIndex";
137
+ Text = data.originalIndex;
138
+ Size = UDim2.fromScale(1, 1);
139
+ BackgroundTransparency = 1;
140
+ Position = UDim2.new(0, -10, 0.5, 0);
141
+ AnchorPoint = Vector2.new(1, 0.5);
142
+ TextColor3 = Color3.new(1, 1, 1);
143
+ TextXAlignment = Enum.TextXAlignment.Right;
144
+ };
142
145
  }
143
146
  end)
144
147
  })