@quenty/observablecollection 12.40.0 → 12.42.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,18 @@
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.42.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.41.0...@quenty/observablecollection@12.42.0) (2026-07-23)
7
+
8
+ ### Features
9
+
10
+ - Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
11
+
12
+ # [12.41.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.40.0...@quenty/observablecollection@12.41.0) (2026-07-18)
13
+
14
+ ### Bug Fixes
15
+
16
+ - pin translation behavior with tests and fix camelCase key generation ([#737](https://github.com/Quenty/NevermoreEngine/issues/737)) ([1b7a536](https://github.com/Quenty/NevermoreEngine/commit/1b7a536dde7e124f8432e57612ec8138dd835d75))
17
+
6
18
  # [12.40.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.39.0...@quenty/observablecollection@12.40.0) (2026-07-18)
7
19
 
8
20
  **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.40.0",
3
+ "version": "12.42.0",
4
4
  "description": "A set of observable collections, such as sets, maps, sorted lists, and more.",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -30,28 +30,28 @@
30
30
  "Quenty"
31
31
  ],
32
32
  "dependencies": {
33
- "@quenty/baseobject": "10.13.0",
33
+ "@quenty/baseobject": "10.15.0",
34
34
  "@quenty/binarysearch": "2.4.0",
35
- "@quenty/brio": "14.31.0",
35
+ "@quenty/brio": "14.33.0",
36
36
  "@quenty/ducktype": "5.11.0",
37
- "@quenty/instanceutils": "13.31.0",
37
+ "@quenty/instanceutils": "13.33.0",
38
38
  "@quenty/loader": "10.11.0",
39
- "@quenty/maid": "3.9.0",
40
- "@quenty/nevermore-test-runner": "1.4.0",
41
- "@quenty/promise": "10.19.0",
42
- "@quenty/rx": "13.29.0",
39
+ "@quenty/maid": "3.11.0",
40
+ "@quenty/nevermore-test-runner": "1.5.0",
41
+ "@quenty/promise": "10.21.0",
42
+ "@quenty/rx": "13.31.0",
43
43
  "@quenty/signal": "7.13.1",
44
- "@quenty/steputils": "3.6.3",
44
+ "@quenty/steputils": "3.7.0",
45
45
  "@quenty/symbol": "3.5.2",
46
46
  "@quenty/table": "3.9.2",
47
- "@quenty/valueobject": "13.32.0",
47
+ "@quenty/valueobject": "13.34.0",
48
48
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
49
49
  },
50
50
  "devDependencies": {
51
- "@quenty/blend": "12.38.0"
51
+ "@quenty/blend": "12.40.0"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "gitHead": "cbbb89635bfdcbf32f26a40422ac736292917cca"
56
+ "gitHead": "1de37218a2bedb8e3f8614a2e09bba9eddc812da"
57
57
  }
@@ -327,8 +327,12 @@ function ObservableCountingMap.Add<T>(self: ObservableCountingMap<T>, key: T, am
327
327
  -- Remove item
328
328
  self._map[key] = nil
329
329
 
330
- -- Fire events
331
- self._totalKeyCountValue.Value = self._totalKeyCountValue.Value - 1
330
+ -- Fire events. The backing ValueObject may already be destroyed when a stored removal
331
+ -- callback fires during this map's own teardown, in which case its Value reads nil.
332
+ local totalKeyCount = self._totalKeyCountValue.Value
333
+ if totalKeyCount ~= nil then
334
+ self._totalKeyCountValue.Value = totalKeyCount - 1
335
+ end
332
336
 
333
337
  if self.Destroy then
334
338
  self.KeyRemoved:Fire(key)
@@ -348,8 +352,11 @@ function ObservableCountingMap.Add<T>(self: ObservableCountingMap<T>, key: T, am
348
352
  -- Add item
349
353
  self._map[key] = change
350
354
 
351
- -- Fire events
352
- self._totalKeyCountValue.Value = self._totalKeyCountValue.Value + 1
355
+ -- Fire events (see the removal branch for why Value may read nil here)
356
+ local totalKeyCount = self._totalKeyCountValue.Value
357
+ if totalKeyCount ~= nil then
358
+ self._totalKeyCountValue.Value = totalKeyCount + 1
359
+ end
353
360
 
354
361
  if self.Destroy then
355
362
  self.KeyAdded:Fire(key)
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class ObservableCountingMap.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local ObservableCountingMap = require("ObservableCountingMap")
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class ObservableList.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local ObservableList = require("ObservableList")
@@ -37,7 +35,7 @@ describe("ObservableList.new()", function()
37
35
 
38
36
  it("should allow false as a value", function()
39
37
  expect(observableList:Get(2)).toEqual(nil)
40
- observableList:Add(false)
38
+ observableList:Add(false :: any)
41
39
  expect(observableList:Get(2)).toEqual(false)
42
40
  end)
43
41
 
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class ObservableMap.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local ObservableMap = require("ObservableMap")
@@ -39,9 +37,9 @@ describe("ObservableMap.new()", function()
39
37
  end)
40
38
 
41
39
  it("should allow false as a key", function()
42
- expect(observableMap:Get(false)).toEqual(nil)
43
- observableMap:Set(false, "Hello")
44
- expect(observableMap:Get(false)).toEqual("Hello")
40
+ expect(observableMap:Get(false :: any)).toEqual(nil)
41
+ observableMap:Set(false :: any, "Hello")
42
+ expect(observableMap:Get(false :: any)).toEqual("Hello")
45
43
  end)
46
44
 
47
45
  it("should fire off events for a specific key", function()
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class ObservableMapList.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local ObservableMapList = require("ObservableMapList")
@@ -3,9 +3,7 @@
3
3
  @class ObservableSortedList.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Brio = require("Brio")
11
9
  local Jest = require("Jest")
@@ -59,8 +57,9 @@ describe("ObservableSortedList", function()
59
57
  it("should accept an observable as sort value", function()
60
58
  local maid = Maid.new()
61
59
  local list = maid:Add(ObservableSortedList.new())
62
- list:Add("b", Rx.of(2))
63
- list:Add("a", Rx.of(1))
60
+ -- Cast: the recursive Observable type does not unify structurally inside Add's union.
61
+ list:Add("b", Rx.of(2) :: any)
62
+ list:Add("a", Rx.of(1) :: any)
64
63
 
65
64
  expect(list:Get(1)).toEqual("a")
66
65
  expect(list:Get(2)).toEqual("b")
@@ -121,7 +120,6 @@ describe("ObservableSortedList", function()
121
120
 
122
121
  expect(list:GetList()).toEqual({ "a", "b", "c" })
123
122
 
124
- -- Move A to the end
125
123
  sortValueA.Value = 10
126
124
 
127
125
  expect(list:GetList()).toEqual({ "b", "c", "a" })
@@ -158,7 +156,6 @@ describe("ObservableSortedList", function()
158
156
  local maid = Maid.new()
159
157
  local list = maid:Add(ObservableSortedListTestUtils.fromList({ "a", "b" }))
160
158
 
161
- -- GetCount reads from _countValue which updates via deferred events
162
159
  list:_testForceFireEvents()
163
160
 
164
161
  expect(list:GetCount()).toEqual(2)
@@ -235,7 +232,6 @@ describe("ObservableSortedList", function()
235
232
  expect(#seenIndices).toEqual(1)
236
233
  expect(seenIndices[1]).toEqual(3)
237
234
 
238
- -- Remove B (index 2). C should shift from 3 to 2
239
235
  removeB()
240
236
  list:_testForceFireEvents()
241
237
 
@@ -258,20 +254,17 @@ describe("ObservableSortedList", function()
258
254
 
259
255
  expect(list:GetCount()).toEqual(5)
260
256
 
261
- -- Observe index of the last element (E, currently at index 5)
262
257
  local seenIndicesForE, sub = ObservableSortedListTestUtils.collectValues(list:ObserveIndex(5))
263
258
 
264
259
  expect(#seenIndicesForE).toEqual(1)
265
260
  expect(seenIndicesForE[1]).toEqual(5)
266
261
 
267
- -- Remove B (index 2). This should shift C->2, D->3, E->4
268
262
  removeB()
269
263
  list:_testForceFireEvents()
270
264
 
271
265
  expect(list:GetCount()).toEqual(4)
272
266
  expect(list:GetList()).toEqual({ "a", "c", "d", "e" })
273
267
 
274
- -- E should have been notified that its index changed from 5 to 4
275
268
  expect(#seenIndicesForE).toEqual(2)
276
269
  expect(seenIndicesForE[2]).toEqual(4)
277
270
 
@@ -294,7 +287,6 @@ describe("ObservableSortedList", function()
294
287
  expect(#seenItems).toEqual(1)
295
288
  expect(seenItems[1]).toEqual("b")
296
289
 
297
- -- Remove B. Now C should be at index 2
298
290
  removeB()
299
291
  list:_testForceFireEvents()
300
292
 
@@ -332,7 +324,7 @@ describe("ObservableSortedList", function()
332
324
  local removeA = list:Add("a", 1)
333
325
  list:_testForceFireEvents()
334
326
 
335
- local seenBrios: { Brio.Brio<string> } = {}
327
+ local seenBrios: { Brio.Brio<string, any> } = {}
336
328
  local sub = list:ObserveItemsBrio():Subscribe(function(brio)
337
329
  table.insert(seenBrios, brio)
338
330
  end)
@@ -479,7 +471,6 @@ describe("ObservableSortedList", function()
479
471
  local list = maid:Add(ObservableSortedList.new())
480
472
  list:Add("a", 1)
481
473
 
482
- -- Use Get to verify synchronously, since GetCount requires deferred events
483
474
  expect(list:Get(1)).toEqual("a")
484
475
 
485
476
  maid:Destroy()
@@ -604,7 +595,6 @@ describe("ObservableSortedList", function()
604
595
  expect(#seenItems).toEqual(1)
605
596
  expect(seenItems[1]).toEqual("b")
606
597
 
607
- -- Remove A. B moves to index 1, index 2 is now empty
608
598
  removeA()
609
599
  list:_testForceFireEvents()
610
600
 
@@ -633,17 +623,14 @@ describe("ObservableSortedList", function()
633
623
  fireCount += 1
634
624
  end)
635
625
 
636
- -- Initial fire on subscribe
637
626
  expect(fireCount).toEqual(1)
638
627
 
639
- -- Change all three sort values before deferring
640
628
  sortValueA.Value = 30
641
629
  sortValueB.Value = 20
642
630
  sortValueC.Value = 10
643
631
 
644
632
  list:_testForceFireEvents()
645
633
 
646
- -- Should have batched into a single additional fire
647
634
  expect(fireCount).toEqual(2)
648
635
  expect(list:GetList()).toEqual({ "c", "b", "a" })
649
636
 
@@ -666,7 +653,6 @@ describe("ObservableSortedList", function()
666
653
 
667
654
  expect(completed).toEqual(false)
668
655
 
669
- -- Remove A (the tracked node at index 1)
670
656
  removeA()
671
657
  list:_testForceFireEvents()
672
658
 
@@ -682,7 +668,6 @@ describe("ObservableSortedList", function()
682
668
 
683
669
  local seenBrios, sub = ObservableSortedListTestUtils.collectValues(list:ObserveItemsBrio())
684
670
 
685
- -- Should already have brios without deferWait
686
671
  expect(#seenBrios).toEqual(2)
687
672
 
688
673
  sub:Destroy()
@@ -727,7 +712,6 @@ describe("ObservableSortedList", function()
727
712
 
728
713
  expect(fireCount).toEqual(1)
729
714
 
730
- -- Set to same value - should be a no-op
731
715
  sortValue.Value = 2
732
716
  list:_testForceFireEvents()
733
717
 
@@ -750,17 +734,14 @@ describe("ObservableSortedList", function()
750
734
 
751
735
  expect(list:GetList()).toEqual({ "a", "b", "c" })
752
736
 
753
- -- Change multiple times before defer fires
754
737
  sortValue.Value = 10 -- a goes to end
755
738
  sortValue.Value = 0 -- a goes back to start
756
739
  sortValue.Value = 5 -- a ends up in the middle
757
740
 
758
- -- Tree should reflect the final state synchronously
759
741
  expect(list:GetList()).toEqual({ "b", "c", "a" })
760
742
 
761
743
  list:_testForceFireEvents()
762
744
 
763
- -- Should still be correct after events fire
764
745
  expect(list:GetList()).toEqual({ "b", "c", "a" })
765
746
 
766
747
  maid:Destroy()
@@ -782,15 +763,12 @@ describe("ObservableSortedList", function()
782
763
  end)
783
764
  expect(fireCount).toEqual(1)
784
765
 
785
- -- Oscillate: move to end, then back to start
786
766
  sortValue.Value = 10
787
767
  sortValue.Value = 1
788
768
 
789
769
  expect(list:GetList()).toEqual({ "a", "b", "c" })
790
770
  list:_testForceFireEvents()
791
771
 
792
- -- Events may or may not fire depending on span tracker,
793
- -- but the list should be correct
794
772
  expect(list:GetList()).toEqual({ "a", "b", "c" })
795
773
 
796
774
  sub:Destroy()
@@ -813,7 +791,6 @@ describe("ObservableSortedList", function()
813
791
 
814
792
  expect(list:GetList()).toEqual({ "a", "b", "c", "d" })
815
793
 
816
- -- Reverse all sort values simultaneously
817
794
  sortA.Value = 4
818
795
  sortB.Value = 3
819
796
  sortC.Value = 2
@@ -841,13 +818,10 @@ describe("ObservableSortedList", function()
841
818
 
842
819
  expect(list:GetList()).toEqual({ "a", "b", "c" })
843
820
 
844
- -- Move all to the same sort value
845
821
  sortA.Value = 5
846
822
  sortB.Value = 5
847
823
  sortC.Value = 5
848
824
 
849
- -- All have equal sort values - order may vary based on tree rotation,
850
- -- but should be deterministic
851
825
  local result = list:GetList()
852
826
  expect(#result).toEqual(3)
853
827
  expect(list:Contains("a")).toEqual(true)
@@ -870,11 +844,9 @@ describe("ObservableSortedList", function()
870
844
  local seenIndices, sub = ObservableSortedListTestUtils.collectValues(list:ObserveIndex(2))
871
845
  expect(seenIndices[1]).toEqual(2)
872
846
 
873
- -- Change from 5 to 7 - still between 1 and 10, no movement needed
874
847
  sortB.Value = 7
875
848
  list:_testForceFireEvents()
876
849
 
877
- -- Index should not have changed
878
850
  expect(#seenIndices).toEqual(1)
879
851
  expect(list:GetList()).toEqual({ "a", "b", "c" })
880
852
 
@@ -893,11 +865,9 @@ describe("ObservableSortedList", function()
893
865
  list:Add("b", sortValue:Observe())
894
866
  list:Add("c", 3)
895
867
 
896
- -- B should not be in the tree since its sort value is nil
897
868
  expect(list:GetList()).toEqual({ "a", "c" })
898
869
  expect(list:Contains("b")).toEqual(false)
899
870
 
900
- -- Now emit a real value - B should appear
901
871
  sortValue.Value = 2
902
872
  expect(list:GetList()).toEqual({ "a", "b", "c" })
903
873
  expect(list:Contains("b")).toEqual(true)
@@ -916,12 +886,10 @@ describe("ObservableSortedList", function()
916
886
 
917
887
  expect(list:GetList()).toEqual({ "a", "b", "c" })
918
888
 
919
- -- Emit nil - B should be removed from tree
920
- sortValue.Value = nil
889
+ sortValue.Value = nil :: any
921
890
  expect(list:GetList()).toEqual({ "a", "c" })
922
891
  expect(list:Contains("b")).toEqual(false)
923
892
 
924
- -- Re-emit a value - B should come back
925
893
  sortValue.Value = 2
926
894
  expect(list:GetList()).toEqual({ "a", "b", "c" })
927
895
 
@@ -942,8 +910,7 @@ describe("ObservableSortedList", function()
942
910
  expect(seenBrios[1]:IsDead()).toEqual(false)
943
911
  expect(seenBrios[2]:IsDead()).toEqual(false)
944
912
 
945
- -- Emit nil - B's brio should die
946
- sortValue.Value = nil
913
+ sortValue.Value = nil :: any
947
914
  list:_testForceFireEvents()
948
915
 
949
916
  expect(seenBrios[2]:IsDead()).toEqual(true)
@@ -958,16 +925,14 @@ describe("ObservableSortedList", function()
958
925
  local list = maid:Add(ObservableSortedList.new())
959
926
 
960
927
  list:Add("a", 1)
961
- list:Add("b", Rx.EMPTY)
928
+ list:Add("b", Rx.EMPTY :: any)
962
929
  list:Add("c", 3)
963
930
 
964
- -- B should never enter the tree
965
931
  expect(list:GetList()).toEqual({ "a", "c" })
966
932
  expect(list:Contains("b")).toEqual(false)
967
933
 
968
934
  list:_testForceFireEvents()
969
935
 
970
- -- Still not there after events fire
971
936
  expect(list:GetList()).toEqual({ "a", "c" })
972
937
 
973
938
  maid:Destroy()
@@ -984,15 +949,12 @@ describe("ObservableSortedList", function()
984
949
 
985
950
  expect(list:GetList()).toEqual({ "a", "c" })
986
951
 
987
- -- First appearance
988
952
  sortValue.Value = 2
989
953
  expect(list:GetList()).toEqual({ "a", "b", "c" })
990
954
 
991
- -- Disappear
992
- sortValue.Value = nil
955
+ sortValue.Value = nil :: any
993
956
  expect(list:GetList()).toEqual({ "a", "c" })
994
957
 
995
- -- Reappear at different position
996
958
  sortValue.Value = 4
997
959
  expect(list:GetList()).toEqual({ "a", "c", "b" })
998
960
 
@@ -1011,8 +973,7 @@ describe("ObservableSortedList", function()
1011
973
 
1012
974
  expect(list:GetList()).toEqual({ "a", "b", "c" })
1013
975
 
1014
- -- Go nil then come back in the same frame
1015
- sortValue.Value = nil
976
+ sortValue.Value = nil :: any
1016
977
  sortValue.Value = 2
1017
978
 
1018
979
  expect(list:GetList()).toEqual({ "a", "b", "c" })
@@ -1040,13 +1001,11 @@ describe("ObservableSortedList", function()
1040
1001
  removeCount += 1
1041
1002
  end))
1042
1003
 
1043
- -- Add and remove before events fire
1044
1004
  local remove = list:Add("b", 2)
1045
1005
  remove()
1046
1006
 
1047
1007
  list:_testForceFireEvents()
1048
1008
 
1049
- -- Should have cancelled out - no add or remove events
1050
1009
  expect(addCount).toEqual(0)
1051
1010
  expect(removeCount).toEqual(0)
1052
1011
  expect(list:GetList()).toEqual({ "a" })
@@ -1065,12 +1024,10 @@ describe("ObservableSortedList", function()
1065
1024
 
1066
1025
  expect(list:GetList()).toEqual({ "a", "b", "c" })
1067
1026
 
1068
- -- Remove while sort value is "live" - should cleanly unsubscribe
1069
1027
  remove()
1070
1028
 
1071
1029
  expect(list:GetList()).toEqual({ "a", "c" })
1072
1030
 
1073
- -- Sort value change after removal should have no effect
1074
1031
  sortValue.Value = 10
1075
1032
  expect(list:GetList()).toEqual({ "a", "c" })
1076
1033
 
@@ -1085,12 +1042,10 @@ describe("ObservableSortedList", function()
1085
1042
 
1086
1043
  list:Add("a", 1)
1087
1044
  list:Add("b", 2)
1088
- -- Don't wait - add more before events fire
1089
1045
  list:Add("c", 3)
1090
1046
 
1091
1047
  list:_testForceFireEvents()
1092
1048
 
1093
- -- All three should batch into one count update
1094
1049
  expect(counts[1]).toEqual(0)
1095
1050
  expect(counts[2]).toEqual(3)
1096
1051
  expect(#counts).toEqual(2)
@@ -1115,7 +1070,6 @@ describe("ObservableSortedList", function()
1115
1070
 
1116
1071
  list:_testForceFireEvents()
1117
1072
 
1118
- -- Net result: b, c, d (3 items)
1119
1073
  expect(list:GetList()).toEqual({ "b", "c", "d" })
1120
1074
  expect(counts[2]).toEqual(3)
1121
1075
 
@@ -1133,8 +1087,7 @@ describe("ObservableSortedList", function()
1133
1087
 
1134
1088
  expect(list:GetList()).toEqual({ "a", "b" })
1135
1089
 
1136
- -- Set nil (removes from tree) then also call cleanup
1137
- sortValue.Value = nil
1090
+ sortValue.Value = nil :: any
1138
1091
  remove()
1139
1092
 
1140
1093
  expect(list:GetList()).toEqual({ "a" })
@@ -1205,7 +1158,6 @@ describe("ObservableSortedList", function()
1205
1158
  local indexChanges, sub = ObservableSortedListTestUtils.collectValues(list:ObserveIndex(2))
1206
1159
  expect(#indexChanges).toEqual(1)
1207
1160
 
1208
- -- Change value but stay in same position (between 1 and 10)
1209
1161
  sortB.Value = 8
1210
1162
  list:_testForceFireEvents()
1211
1163
 
@@ -1229,7 +1181,6 @@ describe("ObservableSortedList", function()
1229
1181
  local seenAtIndex1, sub = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(1))
1230
1182
  expect(seenAtIndex1[1]).toEqual("a")
1231
1183
 
1232
- -- Move A to the end
1233
1184
  sortA.Value = 10
1234
1185
  list:_testForceFireEvents()
1235
1186
 
@@ -1248,24 +1199,21 @@ describe("ObservableSortedList", function()
1248
1199
  list:Add("b", sortValue:Observe())
1249
1200
  list:_testForceFireEvents()
1250
1201
 
1251
- local seenBrios: { Brio.Brio<string> } = {}
1202
+ local seenBrios: { Brio.Brio<string, any> } = {}
1252
1203
  local sub = list:ObserveItemsBrio():Subscribe(function(brio)
1253
1204
  table.insert(seenBrios, brio)
1254
1205
  end)
1255
1206
 
1256
1207
  expect(#seenBrios).toEqual(2)
1257
1208
 
1258
- -- Remove via nil
1259
- sortValue.Value = nil
1209
+ sortValue.Value = nil :: any
1260
1210
  list:_testForceFireEvents()
1261
1211
 
1262
1212
  expect(seenBrios[2]:IsDead()).toEqual(true)
1263
1213
 
1264
- -- Re-add via value
1265
1214
  sortValue.Value = 2
1266
1215
  list:_testForceFireEvents()
1267
1216
 
1268
- -- Should have gotten a new brio
1269
1217
  expect(#seenBrios).toEqual(3)
1270
1218
  expect(seenBrios[3]:IsDead()).toEqual(false)
1271
1219
 
@@ -1282,7 +1230,6 @@ describe("ObservableSortedList", function()
1282
1230
  maid:Destroy()
1283
1231
  end))
1284
1232
 
1285
- -- Should not error when the list is destroyed during event firing
1286
1233
  list:Add("a", 1)
1287
1234
  list:_testForceFireEvents()
1288
1235
 
@@ -1317,7 +1264,6 @@ describe("ObservableSortedList", function()
1317
1264
 
1318
1265
  expect(list:GetList()).toEqual({ "a", "b", "c", "d", "e" })
1319
1266
 
1320
- -- Move C to the end - a, b, d, e should keep their relative order
1321
1267
  sortC.Value = 10
1322
1268
 
1323
1269
  expect(list:GetList()).toEqual({ "a", "b", "d", "e", "c" })
@@ -1341,7 +1287,6 @@ describe("ObservableSortedList", function()
1341
1287
 
1342
1288
  expect(list:GetList()).toEqual({ "a", "b", "c", "d", "e" })
1343
1289
 
1344
- -- Swap B and D's positions
1345
1290
  sortB.Value = 4
1346
1291
  sortD.Value = 2
1347
1292
 
@@ -1364,15 +1309,12 @@ describe("ObservableSortedList", function()
1364
1309
  list:Add("d", 4)
1365
1310
  local removeE = list:Add("e", 5)
1366
1311
 
1367
- -- Remove last
1368
1312
  removeE()
1369
1313
  expect(list:GetList()).toEqual({ "a", "b", "c", "d" })
1370
1314
 
1371
- -- Remove first
1372
1315
  removeA()
1373
1316
  expect(list:GetList()).toEqual({ "b", "c", "d" })
1374
1317
 
1375
- -- Remove middle
1376
1318
  removeC()
1377
1319
  expect(list:GetList()).toEqual({ "b", "d" })
1378
1320
 
@@ -1399,7 +1341,6 @@ describe("ObservableSortedList", function()
1399
1341
  expect(#fires1).toEqual(1)
1400
1342
  expect(#fires5).toEqual(1)
1401
1343
 
1402
- -- Move B past C and D: changed span [2, 4], indices 1, 5 unaffected
1403
1344
  sortB.Value = 4.5
1404
1345
  list:_testForceFireEvents()
1405
1346
 
@@ -1434,7 +1375,6 @@ describe("ObservableSortedList", function()
1434
1375
  expect(#firesA).toEqual(1)
1435
1376
  expect(#firesE).toEqual(1)
1436
1377
 
1437
- -- Move B past C and D: only span [2, 4] changed
1438
1378
  sortB.Value = 4.5
1439
1379
  list:_testForceFireEvents()
1440
1380
 
@@ -1531,7 +1471,6 @@ describe("ObservableSortedList", function()
1531
1471
  expect(#fires1).toEqual(1)
1532
1472
  expect(#fires2).toEqual(1)
1533
1473
 
1534
- -- Remove c from middle (index 3). Indices 1, 2 unaffected.
1535
1474
  removeC()
1536
1475
  list:_testForceFireEvents()
1537
1476
 
@@ -1555,7 +1494,6 @@ describe("ObservableSortedList", function()
1555
1494
  list:Add("e", 5)
1556
1495
  list:_testForceFireEvents()
1557
1496
 
1558
- -- Index 4 (currently "d") will shift to index 3 after c is removed
1559
1497
  local fires4, sub4 = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(4))
1560
1498
  expect(#fires4).toEqual(1)
1561
1499
  expect(fires4[1]).toEqual("d")
@@ -1564,7 +1502,6 @@ describe("ObservableSortedList", function()
1564
1502
  list:_testForceFireEvents()
1565
1503
 
1566
1504
  expect(list:GetList()).toEqual({ "a", "b", "d", "e" })
1567
- -- Index 4 should have fired — now contains "e" instead of "d"
1568
1505
  expect(#fires4).toEqual(2)
1569
1506
  expect(fires4[2]).toEqual("e")
1570
1507
 
@@ -1596,7 +1533,6 @@ describe("ObservableSortedList", function()
1596
1533
  removeC()
1597
1534
  list:_testForceFireEvents()
1598
1535
 
1599
- -- D shifted 4->3, E shifted 5->4
1600
1536
  expect(#firesD).toEqual(2)
1601
1537
  expect(firesD[2]).toEqual(3)
1602
1538
  expect(#firesE).toEqual(2)
@@ -1626,14 +1562,12 @@ describe("ObservableSortedList", function()
1626
1562
  expect(#fires4).toEqual(1)
1627
1563
  expect(#fires5).toEqual(1)
1628
1564
 
1629
- -- Remove b at index 2, add f at index 2. Count unchanged, only index 2 content changed.
1630
1565
  removeB()
1631
1566
  list:Add("f", 2)
1632
1567
  list:_testForceFireEvents()
1633
1568
 
1634
1569
  expect(list:GetList()).toEqual({ "a", "f", "c", "d", "e" })
1635
1570
 
1636
- -- Indices 3-5 unchanged — should not fire
1637
1571
  expect(#fires3).toEqual(1)
1638
1572
  expect(#fires4).toEqual(1)
1639
1573
  expect(#fires5).toEqual(1)
@@ -1668,7 +1602,6 @@ describe("ObservableSortedList", function()
1668
1602
  expect(#firesD).toEqual(1)
1669
1603
  expect(#firesE).toEqual(1)
1670
1604
 
1671
- -- Replace b with f at same index. C, D, E positions unchanged.
1672
1605
  removeB()
1673
1606
  list:Add("f", 2)
1674
1607
  list:_testForceFireEvents()
@@ -1695,20 +1628,16 @@ describe("ObservableSortedList", function()
1695
1628
  list:Add("e", 5)
1696
1629
  list:_testForceFireEvents()
1697
1630
 
1698
- -- Observe index 3 (currently "c")
1699
1631
  local fires3, sub3 = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(3))
1700
1632
  expect(#fires3).toEqual(1)
1701
1633
  expect(fires3[1]).toEqual("c")
1702
1634
 
1703
- -- Remove b (index 2), add f at index 4. Count unchanged, but index 3 shifts: c was at 3, now d is at 3
1704
1635
  removeB()
1705
1636
  list:Add("f", 4.5)
1706
1637
  list:_testForceFireEvents()
1707
1638
 
1708
- -- List is now: a, c, d, f, e
1709
1639
  expect(list:GetList()).toEqual({ "a", "c", "d", "f", "e" })
1710
1640
 
1711
- -- Index 3 changed from "c" to "d" — must fire
1712
1641
  expect(#fires3).toEqual(2)
1713
1642
  expect(fires3[2]).toEqual("d")
1714
1643
 
@@ -1737,15 +1666,12 @@ describe("ObservableSortedList", function()
1737
1666
  expect(firesC[1]).toEqual(3)
1738
1667
  expect(firesD[1]).toEqual(4)
1739
1668
 
1740
- -- Remove b (index 2), add f at index 4.5. Count unchanged, but c shifts 3->2, d shifts 4->3
1741
1669
  removeB()
1742
1670
  list:Add("f", 4.5)
1743
1671
  list:_testForceFireEvents()
1744
1672
 
1745
- -- List is now: a, c, d, f, e
1746
1673
  expect(list:GetList()).toEqual({ "a", "c", "d", "f", "e" })
1747
1674
 
1748
- -- C shifted 3->2, D shifted 4->3
1749
1675
  expect(#firesC).toEqual(2)
1750
1676
  expect(firesC[2]).toEqual(2)
1751
1677
  expect(#firesD).toEqual(2)
@@ -1770,25 +1696,20 @@ describe("ObservableSortedList", function()
1770
1696
  local fires3, sub3 = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(3))
1771
1697
  expect(fires3[1]).toEqual("c")
1772
1698
 
1773
- -- Remove b (index 2) and d (index 4), add f (1.5) and g (4.5). Count unchanged.
1774
1699
  removeB()
1775
1700
  removeD()
1776
1701
  list:Add("f", 1.5)
1777
1702
  list:Add("g", 4.5)
1778
1703
  list:_testForceFireEvents()
1779
1704
 
1780
- -- List: a, f, c, g, e
1781
1705
  expect(list:GetList()).toEqual({ "a", "f", "c", "g", "e" })
1782
1706
 
1783
- -- Index 3 still has "c" — should not fire extra
1784
1707
  expect(#fires3).toEqual(1)
1785
1708
 
1786
1709
  sub3:Destroy()
1787
1710
  maid:Destroy()
1788
1711
  end)
1789
1712
 
1790
- -- Should-fire tests: verify events ARE emitted when content/index actually changes
1791
-
1792
1713
  it("should fire ObserveAtIndex for ALL shifted indices when removing from beginning", function()
1793
1714
  local maid = Maid.new()
1794
1715
  local list = maid:Add(ObservableSortedList.new())
@@ -1810,7 +1731,6 @@ describe("ObservableSortedList", function()
1810
1731
  removeA()
1811
1732
  list:_testForceFireEvents()
1812
1733
 
1813
- -- Every index shifted: a removed, b->1, c->2, d->3
1814
1734
  expect(list:GetList()).toEqual({ "b", "c", "d" })
1815
1735
  expect(#fires1).toEqual(2)
1816
1736
  expect(fires1[2]).toEqual("b")
@@ -1848,7 +1768,6 @@ describe("ObservableSortedList", function()
1848
1768
  removeC()
1849
1769
  list:_testForceFireEvents()
1850
1770
 
1851
- -- List: a, d, e — indices 2-4 all changed
1852
1771
  expect(list:GetList()).toEqual({ "a", "d", "e" })
1853
1772
  expect(#fires2).toEqual(2)
1854
1773
  expect(fires2[2]).toEqual("d")
@@ -1883,7 +1802,6 @@ describe("ObservableSortedList", function()
1883
1802
  list:Add("a", 1)
1884
1803
  list:_testForceFireEvents()
1885
1804
 
1886
- -- List: a, b, c, d — everything shifted right
1887
1805
  expect(list:GetList()).toEqual({ "a", "b", "c", "d" })
1888
1806
  expect(#fires1).toEqual(2)
1889
1807
  expect(fires1[2]).toEqual("a")
@@ -1922,7 +1840,6 @@ describe("ObservableSortedList", function()
1922
1840
  expect(firesC[1]).toEqual(3)
1923
1841
  expect(firesD[1]).toEqual(4)
1924
1842
 
1925
- -- Move A from index 1 to index 4 — all others shift left
1926
1843
  sortA.Value = 10
1927
1844
  list:_testForceFireEvents()
1928
1845
 
@@ -1940,8 +1857,6 @@ describe("ObservableSortedList", function()
1940
1857
  maid:Destroy()
1941
1858
  end)
1942
1859
 
1943
- -- Should-NOT-fire tests: verify events are NOT emitted when nothing changed at the observed position
1944
-
1945
1860
  it("should not fire ObserveAtIndex when remove+add cancel out at the same index", function()
1946
1861
  local maid = Maid.new()
1947
1862
  local list = maid:Add(ObservableSortedList.new())
@@ -1960,7 +1875,6 @@ describe("ObservableSortedList", function()
1960
1875
  expect(fires3[1]).toEqual("c")
1961
1876
  expect(fires4[1]).toEqual("d")
1962
1877
 
1963
- -- Remove b (index 2), add f at same sort position. c, d stay put.
1964
1878
  removeB()
1965
1879
  list:Add("f", 2)
1966
1880
  list:_testForceFireEvents()
@@ -1996,7 +1910,6 @@ describe("ObservableSortedList", function()
1996
1910
  expect(fires2[1]).toEqual("b")
1997
1911
  expect(fires3[1]).toEqual("c")
1998
1912
 
1999
- -- Move D past E: only indices 4 and 5 change
2000
1913
  sortD.Value = 6
2001
1914
  list:_testForceFireEvents()
2002
1915
 
@@ -2034,14 +1947,12 @@ describe("ObservableSortedList", function()
2034
1947
  expect(firesA[1]).toEqual(1)
2035
1948
  expect(firesE[1]).toEqual(5)
2036
1949
 
2037
- -- Remove b and c (indices 2-3), add f(2) and g(3). Count stays at 5.
2038
1950
  removeB()
2039
1951
  removeC()
2040
1952
  list:Add("f", 2)
2041
1953
  list:Add("g", 3)
2042
1954
  list:_testForceFireEvents()
2043
1955
 
2044
- -- List: a, f, g, d, e — A at 1 and E at 5, unchanged
2045
1956
  expect(list:GetList()).toEqual({ "a", "f", "g", "d", "e" })
2046
1957
  expect(#firesA).toEqual(1)
2047
1958
  expect(#firesE).toEqual(1)
@@ -2073,15 +1984,12 @@ describe("ObservableSortedList", function()
2073
1984
  expect(fires3[1]).toEqual("c")
2074
1985
  expect(fires4[1]).toEqual("d")
2075
1986
 
2076
- -- Remove first and last: a(1) and e(5). Add f(0.5) at beginning and g(5.5) at end.
2077
- -- Net effect: f replaces a's slot, g replaces e's slot. Middle is untouched.
2078
1987
  removeA()
2079
1988
  removeE()
2080
1989
  list:Add("f", 0.5)
2081
1990
  list:Add("g", 5.5)
2082
1991
  list:_testForceFireEvents()
2083
1992
 
2084
- -- List: f, b, c, d, g — indices 2, 3, 4 unchanged
2085
1993
  expect(list:GetList()).toEqual({ "f", "b", "c", "d", "g" })
2086
1994
  expect(#fires2).toEqual(1)
2087
1995
  expect(#fires3).toEqual(1)
@@ -2115,13 +2023,10 @@ describe("ObservableSortedList", function()
2115
2023
  expect(fires3[1]).toEqual("c")
2116
2024
  expect(fires4[1]).toEqual("d")
2117
2025
 
2118
- -- Swap a and b (sort 1<->2), swap d and e (sort 4<->5). Index 3 unchanged.
2119
2026
  sortA.Value = 2.5
2120
2027
  sortE.Value = 3.5
2121
2028
  list:_testForceFireEvents()
2122
2029
 
2123
- -- List: b, a, e, c, d -- wait, let me reconsider
2124
- -- Actually a at 2.5, e at 3.5: b(2), a(2.5), c(3), e(3.5), d(4)
2125
2030
  expect(list:GetList()).toEqual({ "b", "a", "c", "e", "d" })
2126
2031
  expect(#fires3).toEqual(1)
2127
2032
 
@@ -2150,15 +2055,12 @@ describe("ObservableSortedList", function()
2150
2055
  local firesC, subC = ObservableSortedListTestUtils.collectValues(list:ObserveIndexByKey(nodeC))
2151
2056
  expect(firesC[1]).toEqual(3)
2152
2057
 
2153
- -- Remove a (before c), remove e (after c). Add f(0.5) before c, add g(5.5) after c.
2154
- -- C stays at index 3.
2155
2058
  removeA()
2156
2059
  removeE()
2157
2060
  list:Add("f", 0.5)
2158
2061
  list:Add("g", 5.5)
2159
2062
  list:_testForceFireEvents()
2160
2063
 
2161
- -- List: f, b, c, d, g — c is still at index 3
2162
2064
  expect(list:GetList()).toEqual({ "f", "b", "c", "d", "g" })
2163
2065
  expect(#firesC).toEqual(1)
2164
2066
 
@@ -2169,14 +2071,7 @@ describe("ObservableSortedList", function()
2169
2071
  end)
2170
2072
 
2171
2073
  describe("coordinate system correctness", function()
2172
- -- These tests reproduce scenarios where intermediate tree mutations cause
2173
- -- span indices to be recorded in different coordinate systems, potentially
2174
- -- leading to under-firing (missed notifications).
2175
-
2176
2074
  it("should fire ObserveAtIndex for tail index when two removes collapse recorded indices", function()
2177
- -- Removing d then e records removed index 4 twice (from different tree states),
2178
- -- losing the information that e was originally at index 5. The effective span
2179
- -- must still cover index 5.
2180
2075
  local maid = Maid.new()
2181
2076
  local list = maid:Add(ObservableSortedList.new())
2182
2077
 
@@ -2190,18 +2085,14 @@ describe("ObservableSortedList", function()
2190
2085
  local fires5, sub5 = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(5))
2191
2086
  expect(fires5[1]).toEqual("e")
2192
2087
 
2193
- -- Remove d (index 4 in 5-element tree), then e (index 4 in 4-element tree).
2194
- -- Add f(1.5) and g(2.5). Count stays at 5.
2195
2088
  removeD()
2196
2089
  removeE()
2197
2090
  list:Add("f", 1.5)
2198
2091
  list:Add("g", 2.5)
2199
2092
  list:_testForceFireEvents()
2200
2093
 
2201
- -- List: a, f, b, g, c — index 5 changed from "e" to "c"
2202
2094
  expect(list:GetList()).toEqual({ "a", "f", "b", "g", "c" })
2203
2095
 
2204
- -- Index 5 MUST fire — content changed from "e" to "c"
2205
2096
  expect(#fires5).toEqual(2)
2206
2097
  expect(fires5[2]).toEqual("c")
2207
2098
 
@@ -2210,8 +2101,6 @@ describe("ObservableSortedList", function()
2210
2101
  end)
2211
2102
 
2212
2103
  it("should fire ObserveIndexByKey for node shifted beyond recorded span", function()
2213
- -- When two removes from the end record the same intermediate index,
2214
- -- the span may not extend far enough to cover nodes that shifted.
2215
2104
  local maid = Maid.new()
2216
2105
  local list = maid:Add(ObservableSortedList.new())
2217
2106
 
@@ -2229,15 +2118,12 @@ describe("ObservableSortedList", function()
2229
2118
  local firesF, subF = ObservableSortedListTestUtils.collectValues(list:ObserveIndexByKey(nodeF))
2230
2119
  expect(firesF[1]).toEqual(6)
2231
2120
 
2232
- -- Remove d (index 4 in 6-element tree), then e (index 4 in 5-element tree).
2233
- -- Count goes from 6 to 4. F shifts from index 6 to index 4.
2234
2121
  removeD()
2235
2122
  removeE()
2236
2123
  list:_testForceFireEvents()
2237
2124
 
2238
2125
  expect(list:GetList()).toEqual({ "a", "b", "c", "f" })
2239
2126
 
2240
- -- F MUST be notified: its index changed from 6 to 4
2241
2127
  expect(#firesF).toEqual(2)
2242
2128
  expect(firesF[2]).toEqual(4)
2243
2129
 
@@ -2246,8 +2132,6 @@ describe("ObservableSortedList", function()
2246
2132
  end)
2247
2133
 
2248
2134
  it("should fire ObserveAtIndex for all affected indices when three consecutive items are removed", function()
2249
- -- Removing items 3, 4, 5 sequentially records indices 3, 3, 3 in intermediate
2250
- -- tree states, but indices 4 and 5 also changed content.
2251
2135
  local maid = Maid.new()
2252
2136
  local list = maid:Add(ObservableSortedList.new())
2253
2137
 
@@ -2266,20 +2150,16 @@ describe("ObservableSortedList", function()
2266
2150
  expect(fires4[1]).toEqual("d")
2267
2151
  expect(fires5[1]).toEqual("e")
2268
2152
 
2269
- -- Remove c, d, e. Each removal records index 3 in the shrinking tree.
2270
2153
  removeC()
2271
2154
  removeD()
2272
2155
  removeE()
2273
2156
  list:_testForceFireEvents()
2274
2157
 
2275
- -- List: a, b, f, g — indices 4 and 5 changed
2276
2158
  expect(list:GetList()).toEqual({ "a", "b", "f", "g" })
2277
2159
 
2278
- -- Index 4 changed from "d" to "g"
2279
2160
  expect(#fires4).toEqual(2)
2280
2161
  expect(fires4[2]).toEqual("g")
2281
2162
 
2282
- -- Index 5 went out of bounds — must fire nil
2283
2163
  expect(#fires5).toEqual(2)
2284
2164
  expect(fires5[2]).toEqual(NIL_VALUE)
2285
2165
 
@@ -2289,8 +2169,6 @@ describe("ObservableSortedList", function()
2289
2169
  end)
2290
2170
 
2291
2171
  it("should fire ObserveAtIndex when adding at beginning shifts tail beyond recorded add index", function()
2292
- -- Adding three items at sort values 0.1, 0.2, 0.3 records add indices 1, 2, 3
2293
- -- in intermediate trees. But the original item at index 3 shifts to index 6.
2294
2172
  local maid = Maid.new()
2295
2173
  local list = maid:Add(ObservableSortedList.new())
2296
2174
 
@@ -2302,16 +2180,13 @@ describe("ObservableSortedList", function()
2302
2180
  local fires3, sub3 = ObservableSortedListTestUtils.collectValues(list:ObserveAtIndex(3))
2303
2181
  expect(fires3[1]).toEqual("c")
2304
2182
 
2305
- -- Add three items before everything. Each records a low add index.
2306
2183
  list:Add("x", 0.1)
2307
2184
  list:Add("y", 0.2)
2308
2185
  list:Add("z", 0.3)
2309
2186
  list:_testForceFireEvents()
2310
2187
 
2311
- -- List: x, y, z, a, b, c — index 3 changed from "c" to "z"
2312
2188
  expect(list:GetList()).toEqual({ "x", "y", "z", "a", "b", "c" })
2313
2189
 
2314
- -- Index 3 MUST fire — content changed from "c" to "z"
2315
2190
  expect(#fires3).toEqual(2)
2316
2191
  expect(fires3[2]).toEqual("z")
2317
2192
 
@@ -2320,8 +2195,6 @@ describe("ObservableSortedList", function()
2320
2195
  end)
2321
2196
 
2322
2197
  it("should fire ObserveIndexByKey when multiple removes from same position shift a distant node", function()
2323
- -- Removing 3 items at position 1 records index 1 three times.
2324
- -- A node originally at index 5 should shift to index 2.
2325
2198
  local maid = Maid.new()
2326
2199
  local list = maid:Add(ObservableSortedList.new())
2327
2200
 
@@ -2338,7 +2211,6 @@ describe("ObservableSortedList", function()
2338
2211
  local firesE, subE = ObservableSortedListTestUtils.collectValues(list:ObserveIndexByKey(nodeE))
2339
2212
  expect(firesE[1]).toEqual(5)
2340
2213
 
2341
- -- Remove a, b, c. Each records removal at index 1 in the shrinking tree.
2342
2214
  removeA()
2343
2215
  removeB()
2344
2216
  removeC()
@@ -2346,7 +2218,6 @@ describe("ObservableSortedList", function()
2346
2218
 
2347
2219
  expect(list:GetList()).toEqual({ "d", "e" })
2348
2220
 
2349
- -- E shifted from 5 to 2 — must be notified
2350
2221
  expect(#firesE).toEqual(2)
2351
2222
  expect(firesE[2]).toEqual(2)
2352
2223
 
@@ -3,9 +3,7 @@
3
3
  @class SortFunctionUtils.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local SortFunctionUtils = require("SortFunctionUtils")
@@ -59,7 +57,7 @@ describe("SortFunctionUtils", function()
59
57
  describe("emptyIterator", function()
60
58
  it("should return nothing", function()
61
59
  local count = 0
62
- for _ in SortFunctionUtils.emptyIterator do
60
+ for _ in SortFunctionUtils.emptyIterator :: any do
63
61
  count += 1
64
62
  end
65
63
  expect(count).toEqual(0)
@@ -3,9 +3,7 @@
3
3
  @class SortedNode.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local SortedNode = require("SortedNode")
@@ -114,7 +112,6 @@ describe("SortedNode", function()
114
112
  second.value = 1
115
113
  root = root:InsertNode(second)
116
114
 
117
- -- Both should be present
118
115
  expect(root.descendantCount).toEqual(2)
119
116
 
120
117
  local result = {}
@@ -278,7 +275,6 @@ describe("SortedNode", function()
278
275
  describe("NeedsToMove", function()
279
276
  it("should return false when value stays valid relative to neighbors", function()
280
277
  local root, nodes = buildTree({ 1, 3, 5 })
281
- -- nodes[2] = "c" (value=3), between 1 and 5
282
278
 
283
279
  expect(nodes[2]:NeedsToMove(root, 2)).toEqual(false)
284
280
  expect(nodes[2]:NeedsToMove(root, 4)).toEqual(false)
@@ -287,7 +283,6 @@ describe("SortedNode", function()
287
283
  it("should return true when value violates parent constraint", function()
288
284
  local root, nodes = buildTree({ 1, 3, 5 })
289
285
 
290
- -- Try to move "a" (value=1, leftmost) past its parent
291
286
  expect(nodes[1]:NeedsToMove(root, 100)).toEqual(true)
292
287
  end)
293
288
 
@@ -373,7 +368,6 @@ describe("SortedNode", function()
373
368
  table.insert(result, node.data)
374
369
  end
375
370
 
376
- -- -2 means up to second-to-last (index 4)
377
371
  expect(result).toEqual({ "a", "b", "c", "d" })
378
372
  end)
379
373
 
@@ -412,7 +406,6 @@ describe("SortedNode", function()
412
406
  it("should maintain sorted order through a sequence of inserts and removes", function()
413
407
  local root, nodes = buildTree({ 5, 2, 8, 1, 3, 7, 9 })
414
408
 
415
- -- Remove some internal nodes
416
409
  root = root:RemoveNode(nodes[3]) -- value=8
417
410
  root = root:RemoveNode(nodes[4]) -- value=1
418
411
 
@@ -425,25 +418,18 @@ describe("SortedNode", function()
425
418
  end)
426
419
 
427
420
  it("should maintain sorted order when swapping two nodes' values via remove-reinsert", function()
428
- -- Simulates what ObservableSortedList does when two sort values swap:
429
- -- Tree: a(1), b(2), c(3), d(4), e(5)
430
- -- Swap B(2→4) and D(4→2)
431
- -- Expected result: a(1), d(2), c(3), b(4), e(5)
432
421
  local root, nodes = buildTree({ 1, 2, 3, 4, 5 })
433
422
  local nodeB = nodes[2] -- data="b", value=2
434
423
  local nodeD = nodes[4] -- data="d", value=4
435
424
 
436
- -- Step 1: Move B from 2 to 4 (remove, change value, reinsert)
437
425
  root = root:RemoveNode(nodeB)
438
426
  nodeB.value = 4
439
427
  root = root:InsertNode(nodeB)
440
428
 
441
- -- Step 2: Move D from 4 to 2 (remove, change value, reinsert)
442
429
  root = root:RemoveNode(nodeD)
443
430
  nodeD.value = 2
444
431
  root = root:InsertNode(nodeD)
445
432
 
446
- -- Should be sorted: a(1), d(2), c(3), b(4), e(5)
447
433
  local result = {}
448
434
  for _, data in root:IterateData() do
449
435
  table.insert(result, data)
@@ -453,13 +439,10 @@ describe("SortedNode", function()
453
439
  end)
454
440
 
455
441
  it("should maintain sorted order when swapping via NeedsToMove gated remove-reinsert", function()
456
- -- Same as above but uses NeedsToMove to gate whether to remove-reinsert,
457
- -- matching the exact logic of ObservableSortedList._assignSortValue
458
442
  local root, nodes = buildTree({ 1, 2, 3, 4, 5 })
459
443
  local nodeB = nodes[2] -- data="b", value=2
460
444
  local nodeD = nodes[4] -- data="d", value=4
461
445
 
462
- -- Step 1: Move B from 2 to 4
463
446
  if nodeB:NeedsToMove(root, 4) then
464
447
  root = root:RemoveNode(nodeB)
465
448
  nodeB.value = 4
@@ -468,7 +451,6 @@ describe("SortedNode", function()
468
451
  nodeB.value = 4
469
452
  end
470
453
 
471
- -- Step 2: Move D from 4 to 2
472
454
  if nodeD:NeedsToMove(root, 2) then
473
455
  root = root:RemoveNode(nodeD)
474
456
  nodeD.value = 2
@@ -477,7 +459,6 @@ describe("SortedNode", function()
477
459
  nodeD.value = 2
478
460
  end
479
461
 
480
- -- Should be sorted: a(1), d(2), c(3), b(4), e(5)
481
462
  local result = {}
482
463
  for _, data in root:IterateData() do
483
464
  table.insert(result, data)
@@ -70,6 +70,13 @@ function SortedNodeValue.__lt<T>(self: SortedNodeValue<T>, other: SortedNodeValu
70
70
  return self._compare(self._value, other._value) < 0
71
71
  end
72
72
 
73
+ function SortedNodeValue.__le<T>(self: SortedNodeValue<T>, other: SortedNodeValue<T>): boolean
74
+ assert(SortedNodeValue.isSortedNodeValue(other), "Bad other")
75
+ assert(other._compare == self._compare, "Bad compare")
76
+
77
+ return self._compare(self._value, other._value) <= 0
78
+ end
79
+
73
80
  function SortedNodeValue.__gt<T>(self: SortedNodeValue<T>, other: SortedNodeValue<T>): boolean
74
81
  assert(SortedNodeValue.isSortedNodeValue(other), "Bad other")
75
82
  assert(other._compare == self._compare, "Bad compare")
@@ -3,9 +3,7 @@
3
3
  @class SortedNodeValue.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local SortFunctionUtils = require("SortFunctionUtils")
@@ -3,9 +3,7 @@
3
3
  @class UnifiedChangedSpanTracker.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Jest = require("Jest")
11
9
  local UnifiedChangedSpanTracker = require("UnifiedChangedSpanTracker")
@@ -229,7 +227,6 @@ describe("UnifiedChangedSpanTracker", function()
229
227
 
230
228
  local result = tracker:ComputeEffectiveSpans(5, 5)
231
229
 
232
- -- Index 3 stays as item c, so it's not dirty
233
230
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 1)).toEqual(false)
234
231
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 2)).toEqual(true)
235
232
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 3)).toEqual(false)
@@ -298,7 +295,6 @@ describe("UnifiedChangedSpanTracker", function()
298
295
  local result1 = tracker:ComputeEffectiveSpans(3, 4)
299
296
  expect(#result1 > 0).toEqual(true)
300
297
 
301
- -- Second call should return nothing since state was cleared
302
298
  local result2 = tracker:ComputeEffectiveSpans(4, 4)
303
299
  expect(#result2).toEqual(0)
304
300
  end)
@@ -308,10 +304,6 @@ describe("UnifiedChangedSpanTracker", function()
308
304
  it(
309
305
  "should cover index 5 when two end-removes collapse to same recorded index and count is unchanged",
310
306
  function()
311
- -- List [a,b,c,d,e] (5 items)
312
- -- Remove d at index 4 → [a,b,c,e], remove e at index 4 → [a,b,c]
313
- -- Add f at index 2 → [a,f,b,c], add g at index 4 → [a,f,b,g,c]
314
- -- Count stays 5. Both removes record index 4.
315
307
  local tracker = UnifiedChangedSpanTracker.new()
316
308
  tracker:LogRemove(4)
317
309
  tracker:LogRemove(4)
@@ -320,7 +312,6 @@ describe("UnifiedChangedSpanTracker", function()
320
312
 
321
313
  local result = tracker:ComputeEffectiveSpans(5, 5)
322
314
 
323
- -- Index 5 changed content (was "e", now "c"). Must be covered.
324
315
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 1)).toEqual(false)
325
316
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 2)).toEqual(true)
326
317
  expect(UnifiedChangedSpanTracker.isIndexInSpan(result, 3)).toEqual(true)
@@ -330,10 +321,6 @@ describe("UnifiedChangedSpanTracker", function()
330
321
  )
331
322
 
332
323
  it("should only cover edges when removing first and last and replacing at edges", function()
333
- -- List [a,b,c,d,e] (5 items)
334
- -- Remove a at 1 → [b,c,d,e], remove e at 4 → [b,c,d]
335
- -- Add f at 1 → [f,b,c,d], add g at 5 → [f,b,c,d,g]
336
- -- Count stays 5. Middle indices 2,3,4 are unchanged.
337
324
  local tracker = UnifiedChangedSpanTracker.new()
338
325
  tracker:LogRemove(1)
339
326
  tracker:LogRemove(4)
@@ -350,7 +337,6 @@ describe("UnifiedChangedSpanTracker", function()
350
337
  end)
351
338
 
352
339
  it("should produce two separate spans for non-overlapping moves when count is unchanged", function()
353
- -- Item at 2 moves to 3, item at 8 moves to 9. Count stays 10.
354
340
  local tracker = UnifiedChangedSpanTracker.new()
355
341
  tracker:LogMove(2, 3)
356
342
  tracker:LogMove(8, 9)
@@ -368,10 +354,6 @@ describe("UnifiedChangedSpanTracker", function()
368
354
  end)
369
355
 
370
356
  it("should not dirty index 3 when removing two and adding two leaves c at index 3", function()
371
- -- List [a,b,c,d,e] (5 items)
372
- -- Remove b at index 2 → [a,c,d,e], remove d at index 3 → [a,c,e]
373
- -- Add f at index 2 (sort 1.5) → [a,f,c,e], add g at index 4 (sort 4.5) → [a,f,c,g,e]
374
- -- Index 3 still has c.
375
357
  local tracker = UnifiedChangedSpanTracker.new()
376
358
  tracker:LogRemove(2)
377
359
  tracker:LogRemove(3)
@@ -388,10 +370,6 @@ describe("UnifiedChangedSpanTracker", function()
388
370
  end)
389
371
 
390
372
  it("should not dirty middle indices when removing first and last and adding at edges", function()
391
- -- List [a,b,c,d,e] (5 items)
392
- -- Remove a at index 1 → [b,c,d,e], remove e at index 4 → [b,c,d]
393
- -- Add f at index 1 (sort 0.5) → [f,b,c,d], add g at index 5 (sort 5.5) → [f,b,c,d,g]
394
- -- Indices 2,3,4 unchanged.
395
373
  local tracker = UnifiedChangedSpanTracker.new()
396
374
  tracker:LogRemove(1)
397
375
  tracker:LogRemove(4)
@@ -408,10 +386,6 @@ describe("UnifiedChangedSpanTracker", function()
408
386
  end)
409
387
 
410
388
  it("should not dirty index 3 when two opposite-end moves leave middle unchanged", function()
411
- -- List [a,b,c,d,e] (5 items)
412
- -- a moves from index 1 → index 2 (sort 1 → 2.5): remove at 1, insert at 2
413
- -- e moves from index 5 → index 4 (sort 5 → 3.5): remove at 5, insert at 4
414
- -- List becomes [b,a,c,e,d]. Index 3 still has c.
415
389
  local tracker = UnifiedChangedSpanTracker.new()
416
390
  tracker:LogMove(1, 2)
417
391
  tracker:LogMove(5, 4)
@@ -426,10 +400,6 @@ describe("UnifiedChangedSpanTracker", function()
426
400
  end)
427
401
 
428
402
  it("should dirty index 5 when two removes from end collapse and adds shift content", function()
429
- -- List [a,b,c,d,e] (5 items)
430
- -- Remove d at index 4 → [a,b,c,e], remove e at index 4 → [a,b,c]
431
- -- Add f at index 2 (sort 1.5) → [a,f,b,c], add g at index 3 (sort 2.5) → [a,f,g,b,c]
432
- -- Index 5 changed from "e" to "c".
433
403
  local tracker = UnifiedChangedSpanTracker.new()
434
404
  tracker:LogRemove(4)
435
405
  tracker:LogRemove(4)