@quenty/observablecollection 12.47.1 → 12.48.1

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,16 @@
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.48.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.48.0...@quenty/observablecollection@12.48.1) (2026-08-24)
7
+
8
+ ### Bug Fixes
9
+
10
+ - ObservableSortedList handles removal immediately. ([9f115e8](https://github.com/Quenty/NevermoreEngine/commit/9f115e8b32c5450f63904a1a39845e98f423712e))
11
+
12
+ # [12.48.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.47.1...@quenty/observablecollection@12.48.0) (2026-08-24)
13
+
14
+ **Note:** Version bump only for package @quenty/observablecollection
15
+
6
16
  ## [12.47.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/observablecollection@12.47.0...@quenty/observablecollection@12.47.1) (2026-08-14)
7
17
 
8
18
  **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.47.1",
3
+ "version": "12.48.1",
4
4
  "description": "A set of observable collections, such as sets, maps, sorted lists, and more.",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -48,10 +48,10 @@
48
48
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
49
49
  },
50
50
  "devDependencies": {
51
- "@quenty/blend": "12.45.1"
51
+ "@quenty/blend": "12.46.0"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "gitHead": "f084360c3729ea424d49d9bc42f8f0d1c128fa82"
56
+ "gitHead": "1a2c0cc735d06b4adcd324f60302b950e12d68b0"
57
57
  }
@@ -50,6 +50,7 @@ export type ObservableSortedList<T> = typeof(setmetatable(
50
50
 
51
51
  _unifiedTracker: UnifiedChangedSpanTracker.UnifiedChangedSpanTracker,
52
52
 
53
+ _nodeRemovedImmediate: Signal.Signal<SortedNode.SortedNode<T>>,
53
54
  _indexObservers: any,
54
55
  _nodeIndexObservables: any,
55
56
  _mainObservables: any,
@@ -116,6 +117,10 @@ function ObservableSortedList.new<T>(isReversed: boolean?, compare: CompareFunct
116
117
  ]=]
117
118
  self.OrderChanged = self._maid:Add(Signal.new())
118
119
 
120
+ -- Internal: fires the moment a node leaves the tree, ahead of the batched ItemRemoved, for
121
+ -- observers holding the node itself.
122
+ self._nodeRemovedImmediate = self._maid:Add(Signal.new()) :: any
123
+
119
124
  --[=[
120
125
  Fires when the count changes
121
126
 
@@ -286,6 +291,13 @@ function ObservableSortedList.ObserveItemsBrio<T>(
286
291
  -- to so many events
287
292
 
288
293
  local function handleItem(data: T, _index, node)
294
+ -- Nodes added before this subscription are announced from the tree below, and their
295
+ -- ItemAdded still fires on the next flush. Announcing twice would replace the brio the
296
+ -- subscriber is already holding.
297
+ if maid[node] then
298
+ return
299
+ end
300
+
289
301
  local brio = Brio.new(data, node)
290
302
  maid[node] = brio
291
303
  sub:Fire(brio)
@@ -297,7 +309,7 @@ function ObservableSortedList.ObserveItemsBrio<T>(
297
309
  end
298
310
 
299
311
  maid:GiveTask(self.ItemAdded:Connect(handleItem))
300
- maid:GiveTask(self.ItemRemoved:Connect(function(_item, node)
312
+ maid:GiveTask(self._nodeRemovedImmediate:Connect(function(node)
301
313
  maid[node] = nil
302
314
  end))
303
315
 
@@ -494,6 +506,11 @@ function ObservableSortedList._assignSortValue<T>(
494
506
  self:_removeNode(node)
495
507
  node.value = nil
496
508
  self:_queueFireEvents()
509
+
510
+ -- Holders of the node itself have to let go now, not on the next flush. A node added and
511
+ -- removed before that flush cancels both batched events, so ItemRemoved alone would
512
+ -- leave them holding a node that is no longer in the list.
513
+ self._nodeRemovedImmediate:Fire(node)
497
514
  else
498
515
  node.value = nil
499
516
  end
@@ -1013,6 +1013,111 @@ describe("ObservableSortedList", function()
1013
1013
  maid:Destroy()
1014
1014
  end)
1015
1015
 
1016
+ it("should kill the brio of an item added and removed in the same frame", function()
1017
+ local maid = Maid.new()
1018
+ local list = maid:Add(ObservableSortedList.new())
1019
+
1020
+ local remove = list:Add("a", 1)
1021
+
1022
+ local brios: { any } = {}
1023
+ maid:GiveTask(list:ObserveItemsBrio():Subscribe(function(brio)
1024
+ table.insert(brios, brio)
1025
+ end))
1026
+
1027
+ expect(#brios).toEqual(1)
1028
+ expect(brios[1]:IsDead()).toEqual(false)
1029
+
1030
+ remove()
1031
+
1032
+ expect(brios[1]:IsDead()).toEqual(true)
1033
+ expect(list:GetList()).toEqual({})
1034
+
1035
+ list:_testForceFireEvents()
1036
+
1037
+ expect(brios[1]:IsDead()).toEqual(true)
1038
+
1039
+ maid:Destroy()
1040
+ end)
1041
+
1042
+ it("should kill the brio of an item removed after its add has fired", function()
1043
+ local maid = Maid.new()
1044
+ local list = maid:Add(ObservableSortedList.new())
1045
+
1046
+ local remove = list:Add("a", 1)
1047
+ list:_testForceFireEvents()
1048
+
1049
+ local brios: { any } = {}
1050
+ maid:GiveTask(list:ObserveItemsBrio():Subscribe(function(brio)
1051
+ table.insert(brios, brio)
1052
+ end))
1053
+
1054
+ expect(#brios).toEqual(1)
1055
+
1056
+ remove()
1057
+
1058
+ expect(brios[1]:IsDead()).toEqual(true)
1059
+
1060
+ maid:Destroy()
1061
+ end)
1062
+
1063
+ it("should not announce an item added and removed before anyone subscribed", function()
1064
+ local maid = Maid.new()
1065
+ local list = maid:Add(ObservableSortedList.new())
1066
+
1067
+ local remove = list:Add("a", 1)
1068
+ remove()
1069
+
1070
+ local brios: { any } = {}
1071
+ maid:GiveTask(list:ObserveItemsBrio():Subscribe(function(brio)
1072
+ table.insert(brios, brio)
1073
+ end))
1074
+
1075
+ list:_testForceFireEvents()
1076
+
1077
+ expect(#brios).toEqual(0)
1078
+
1079
+ maid:Destroy()
1080
+ end)
1081
+
1082
+ it("should announce an item once when subscribing before its add fires", function()
1083
+ local maid = Maid.new()
1084
+ local list = maid:Add(ObservableSortedList.new())
1085
+
1086
+ list:Add("a", 1)
1087
+
1088
+ local brios: { any } = {}
1089
+ maid:GiveTask(list:ObserveItemsBrio():Subscribe(function(brio)
1090
+ table.insert(brios, brio)
1091
+ end))
1092
+
1093
+ expect(#brios).toEqual(1)
1094
+
1095
+ list:_testForceFireEvents()
1096
+
1097
+ expect(#brios).toEqual(1)
1098
+ expect(brios[1]:IsDead()).toEqual(false)
1099
+
1100
+ maid:Destroy()
1101
+ end)
1102
+
1103
+ it("should keep announcing items added after the subscription", function()
1104
+ local maid = Maid.new()
1105
+ local list = maid:Add(ObservableSortedList.new())
1106
+
1107
+ local brios: { any } = {}
1108
+ maid:GiveTask(list:ObserveItemsBrio():Subscribe(function(brio)
1109
+ table.insert(brios, brio)
1110
+ end))
1111
+
1112
+ list:Add("a", 1)
1113
+ list:_testForceFireEvents()
1114
+
1115
+ expect(#brios).toEqual(1)
1116
+ expect((brios[1]:GetValue())).toEqual("a")
1117
+
1118
+ maid:Destroy()
1119
+ end)
1120
+
1016
1121
  it("should handle removing during a sort value change", function()
1017
1122
  local maid = Maid.new()
1018
1123
  local list = maid:Add(ObservableSortedList.new())