@quenty/binder 14.44.0 → 14.46.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.
@@ -21,6 +21,7 @@ export type BoundChildCollection<T> =
21
21
  _binder: Binder.Binder<T>,
22
22
  _parent: Instance,
23
23
  _classes: Set.Set<T>,
24
+ _childToClass: { [Instance]: T },
24
25
  ClassAdded: Signal.Signal<T>,
25
26
  ClassRemoved: Signal.Signal<T>,
26
27
  _size: number,
@@ -56,15 +57,9 @@ function BoundChildCollection.new<T>(binder: Binder.Binder<T>, parent: Instance)
56
57
  self.ClassRemoved = self._maid:Add(Signal.new() :: any) -- :Fire(class)
57
58
 
58
59
  self._classes = {} -- [class] = true
60
+ self._childToClass = {}
59
61
  self._size = 0
60
62
 
61
- self._maid:GiveTask(self._binder:GetClassAddedSignal():Connect(function(...)
62
- self:_handleNewClassBound(...)
63
- end))
64
- self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
65
- self:_removeClass(class)
66
- end))
67
-
68
63
  self:_startTracking()
69
64
 
70
65
  return self
@@ -128,29 +123,39 @@ function BoundChildCollection._startTracking<T>(self: BoundChildCollection<T>)
128
123
  end
129
124
 
130
125
  function BoundChildCollection._addChild<T>(self: BoundChildCollection<T>, inst: Instance, doNotFire: boolean?): ()
131
- local class = self._binder:Get(inst)
132
- if not class then
133
- return
134
- end
126
+ self._maid[inst] = self._binder:ObserveInstance(inst, function(class)
127
+ self:_setChildClass(inst, class)
128
+ end)
135
129
 
136
- self:_addClass(class, doNotFire)
130
+ self:_setChildClass(inst, self._binder:Get(inst), doNotFire)
137
131
  end
138
132
 
139
- function BoundChildCollection._handleNewClassBound<T>(self: BoundChildCollection<T>, class: T, inst: Instance): ()
140
- if inst.Parent ~= self._parent then
141
- return
142
- end
133
+ function BoundChildCollection._removeChild<T>(self: BoundChildCollection<T>, inst: Instance): ()
134
+ self._maid[inst] = nil
143
135
 
144
- self:_addClass(class)
136
+ self:_setChildClass(inst, nil)
145
137
  end
146
138
 
147
- function BoundChildCollection._removeChild<T>(self: BoundChildCollection<T>, inst: Instance): ()
148
- local class = self._binder:Get(inst)
149
- if not class then
139
+ function BoundChildCollection._setChildClass<T>(
140
+ self: BoundChildCollection<T>,
141
+ inst: Instance,
142
+ class: T?,
143
+ doNotFire: boolean?
144
+ ): ()
145
+ local existing = self._childToClass[inst]
146
+ if existing == class then
150
147
  return
151
148
  end
152
149
 
153
- self:_removeClass(class)
150
+ if existing ~= nil then
151
+ self._childToClass[inst] = nil
152
+ self:_removeClass(existing)
153
+ end
154
+
155
+ if class ~= nil then
156
+ self._childToClass[inst] = class
157
+ self:_addClass(class, doNotFire)
158
+ end
154
159
  end
155
160
 
156
161
  function BoundChildCollection._addClass<T>(self: BoundChildCollection<T>, class: T, doNotFire: boolean?): ()
@@ -9,6 +9,8 @@ local Binder = require("Binder")
9
9
  local BinderProvider = require("BinderProvider")
10
10
  local BoundChildCollection = require("BoundChildCollection")
11
11
  local Jest = require("Jest")
12
+ local JestUtils = require("JestUtils")
13
+ local Maid = require("Maid")
12
14
  local ServiceBag = require("ServiceBag")
13
15
 
14
16
  local describe = Jest.Globals.describe
@@ -29,6 +31,8 @@ local function makeClass()
29
31
  end
30
32
 
31
33
  local function setup()
34
+ local maid = Maid.new()
35
+
32
36
  specCounter += 1
33
37
  local suffix = specCounter
34
38
 
@@ -37,8 +41,8 @@ local function setup()
37
41
  container.Name = "BoundChildCollectionSpecContainer"
38
42
  container.Parent = workspace
39
43
 
40
- local instances = {}
41
- local cleanups = {}
44
+ local instances: { Instance } = {}
45
+ local cleanups: { any } = {}
42
46
  local booted = false
43
47
 
44
48
  local binder = Binder.new(string.format("BoundChildCollectionSpecTag_%d", suffix), makeClass() :: any)
@@ -67,27 +71,35 @@ local function setup()
67
71
  serviceBag:Start()
68
72
  end
69
73
 
70
- return {
74
+ maid:GiveTask(function()
75
+ for _, item in cleanups do
76
+ pcall(function()
77
+ item:Destroy()
78
+ end)
79
+ end
80
+ serviceBag:Destroy()
81
+ for _, inst in instances do
82
+ pcall(function()
83
+ inst:Destroy()
84
+ end)
85
+ end
86
+ container:Destroy()
87
+ end)
88
+
89
+ local controller = {
71
90
  container = container,
72
91
  binder = binder,
73
92
  newInstance = newInstance,
74
93
  track = track,
75
94
  boot = boot,
76
- destroy = function()
77
- for _, item in cleanups do
78
- pcall(function()
79
- item:Destroy()
80
- end)
81
- end
82
- serviceBag:Destroy()
83
- for _, inst in instances do
84
- pcall(function()
85
- inst:Destroy()
86
- end)
87
- end
88
- container:Destroy()
95
+ Destroy = function(_self)
96
+ maid:DoCleaning()
89
97
  end,
90
98
  }
99
+
100
+ maid:GiveTask(JestUtils.afterThis(controller))
101
+
102
+ return controller
91
103
  end
92
104
 
93
105
  describe("BoundChildCollection construction", function()
@@ -114,7 +126,7 @@ describe("BoundChildCollection construction", function()
114
126
  expect(#collection:GetClasses()).toEqual(2)
115
127
  expect(collection:HasClass(controller.binder:Get(childA))).toEqual(true)
116
128
 
117
- controller.destroy()
129
+ controller:Destroy()
118
130
  end)
119
131
  end)
120
132
 
@@ -146,7 +158,97 @@ describe("BoundChildCollection dynamic updates", function()
146
158
  expect(addedClass).toEqual(class)
147
159
  expect(collection:GetSize()).toEqual(1)
148
160
 
149
- controller.destroy()
161
+ controller:Destroy()
162
+ end)
163
+
164
+ it("fires ClassAdded when an existing child binds after construction", function()
165
+ local controller = setup()
166
+
167
+ local parent = controller.newInstance()
168
+ local child = controller.newInstance(parent)
169
+ controller.boot()
170
+
171
+ local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
172
+ expect(collection:GetSize()).toEqual(0)
173
+
174
+ local added = {}
175
+ collection.ClassAdded:Connect(function(class)
176
+ table.insert(added, class)
177
+ end)
178
+
179
+ controller.binder:Tag(child)
180
+
181
+ expect(collection:GetSize()).toEqual(1)
182
+ expect(#added).toEqual(1)
183
+ expect(added[1]).toEqual(controller.binder:Get(child))
184
+
185
+ controller:Destroy()
186
+ end)
187
+
188
+ it("ignores a bind on an instance that is not a child of the parent", function()
189
+ local controller = setup()
190
+
191
+ local parent = controller.newInstance()
192
+ local outsider = controller.newInstance()
193
+ local grandchild = controller.newInstance(controller.newInstance(parent))
194
+ controller.boot()
195
+
196
+ local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
197
+
198
+ controller.binder:Tag(outsider)
199
+ controller.binder:Tag(grandchild)
200
+
201
+ expect(collection:GetSize()).toEqual(0)
202
+
203
+ controller:Destroy()
204
+ end)
205
+
206
+ it("stops following a child that was reparented out", function()
207
+ local controller = setup()
208
+
209
+ local parent = controller.newInstance()
210
+ local child = controller.newInstance(parent)
211
+ controller.binder:Tag(child)
212
+ controller.boot()
213
+
214
+ local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
215
+ expect(collection:GetSize()).toEqual(1)
216
+
217
+ child.Parent = controller.container
218
+ expect(collection:GetSize()).toEqual(0)
219
+
220
+ local removed = 0
221
+ collection.ClassRemoved:Connect(function()
222
+ removed += 1
223
+ end)
224
+
225
+ controller.binder:Untag(child)
226
+
227
+ expect(collection:GetSize()).toEqual(0)
228
+ expect(removed).toEqual(0)
229
+
230
+ controller:Destroy()
231
+ end)
232
+
233
+ it("re-adds a child that leaves and comes back", function()
234
+ local controller = setup()
235
+
236
+ local parent = controller.newInstance()
237
+ local child = controller.newInstance(parent)
238
+ controller.binder:Tag(child)
239
+ controller.boot()
240
+
241
+ local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
242
+ expect(collection:GetSize()).toEqual(1)
243
+
244
+ child.Parent = controller.container
245
+ expect(collection:GetSize()).toEqual(0)
246
+
247
+ child.Parent = parent
248
+ expect(collection:GetSize()).toEqual(1)
249
+ expect(collection:HasClass(controller.binder:Get(child))).toEqual(true)
250
+
251
+ controller:Destroy()
150
252
  end)
151
253
 
152
254
  it("fires ClassRemoved when a tracked child is reparented out", function()
@@ -175,7 +277,7 @@ describe("BoundChildCollection dynamic updates", function()
175
277
  expect(removedClass).toEqual(class)
176
278
  expect(collection:GetSize()).toEqual(0)
177
279
 
178
- controller.destroy()
280
+ controller:Destroy()
179
281
  end)
180
282
 
181
283
  it("fires ClassRemoved when a tracked child is unbound", function()
@@ -198,6 +300,6 @@ describe("BoundChildCollection dynamic updates", function()
198
300
 
199
301
  expect(collection:GetSize()).toEqual(0)
200
302
 
201
- controller.destroy()
303
+ controller:Destroy()
202
304
  end)
203
305
  end)
@@ -8,6 +8,8 @@ local require = require(script.Parent.loader).load(script)
8
8
  local Binder = require("Binder")
9
9
  local BinderProvider = require("BinderProvider")
10
10
  local Jest = require("Jest")
11
+ local JestUtils = require("JestUtils")
12
+ local Maid = require("Maid")
11
13
  local ServiceBag = require("ServiceBag")
12
14
  local promiseBoundClass = require("promiseBoundClass")
13
15
 
@@ -29,6 +31,8 @@ local function makeClass()
29
31
  end
30
32
 
31
33
  local function setup()
34
+ local maid = Maid.new()
35
+
32
36
  specCounter += 1
33
37
  local suffix = specCounter
34
38
 
@@ -37,7 +41,7 @@ local function setup()
37
41
  container.Name = "PromiseBoundClassSpecContainer"
38
42
  container.Parent = workspace
39
43
 
40
- local instances = {}
44
+ local instances: { Instance } = {}
41
45
  local booted = false
42
46
 
43
47
  local binder = Binder.new(string.format("PromiseBoundClassSpecTag_%d", suffix), makeClass() :: any)
@@ -61,20 +65,28 @@ local function setup()
61
65
  serviceBag:Start()
62
66
  end
63
67
 
64
- return {
68
+ maid:GiveTask(function()
69
+ serviceBag:Destroy()
70
+ for _, inst in instances do
71
+ pcall(function()
72
+ inst:Destroy()
73
+ end)
74
+ end
75
+ container:Destroy()
76
+ end)
77
+
78
+ local controller = {
65
79
  binder = binder,
66
80
  newInstance = newInstance,
67
81
  boot = boot,
68
- destroy = function()
69
- serviceBag:Destroy()
70
- for _, inst in instances do
71
- pcall(function()
72
- inst:Destroy()
73
- end)
74
- end
75
- container:Destroy()
82
+ Destroy = function(_self)
83
+ maid:DoCleaning()
76
84
  end,
77
85
  }
86
+
87
+ maid:GiveTask(JestUtils.afterThis(controller))
88
+
89
+ return controller
78
90
  end
79
91
 
80
92
  describe("promiseBoundClass()", function()
@@ -89,7 +101,7 @@ describe("promiseBoundClass()", function()
89
101
  assert(ok, "Never bound")
90
102
  expect(class).toEqual(controller.binder:Get(inst))
91
103
 
92
- controller.destroy()
104
+ controller:Destroy()
93
105
  end)
94
106
 
95
107
  it("resolves once an instance is bound after start", function()
@@ -102,7 +114,7 @@ describe("promiseBoundClass()", function()
102
114
  local ok = promiseBoundClass(controller.binder, inst):Yield()
103
115
  expect(ok).toEqual(true)
104
116
 
105
- controller.destroy()
117
+ controller:Destroy()
106
118
  end)
107
119
 
108
120
  it("throws when the binder is not a binder", function()
@@ -113,7 +125,7 @@ describe("promiseBoundClass()", function()
113
125
  promiseBoundClass({} :: any, controller.newInstance())
114
126
  end).toThrow()
115
127
 
116
- controller.destroy()
128
+ controller:Destroy()
117
129
  end)
118
130
 
119
131
  it("throws when the instance is not an Instance", function()
@@ -124,6 +136,6 @@ describe("promiseBoundClass()", function()
124
136
  promiseBoundClass(controller.binder, 5 :: any)
125
137
  end).toThrow()
126
138
 
127
- controller.destroy()
139
+ controller:Destroy()
128
140
  end)
129
141
  end)
@@ -1,83 +0,0 @@
1
- --!strict
2
- --[=[
3
- Tracks a parent bound to a specific binder
4
- @class BoundAncestorTracker
5
- ]=]
6
-
7
- local require = require(script.Parent.loader).load(script)
8
-
9
- local BaseObject = require("BaseObject")
10
- local Binder = require("Binder")
11
- local BinderUtils = require("BinderUtils")
12
- local ValueObject = require("ValueObject")
13
-
14
- local BoundAncestorTracker = setmetatable({}, BaseObject)
15
- BoundAncestorTracker.ClassName = "BoundAncestorTracker"
16
- BoundAncestorTracker.__index = BoundAncestorTracker
17
-
18
- export type BoundAncestorTracker<T> =
19
- typeof(setmetatable(
20
- {} :: {
21
- _child: Instance,
22
- _binder: Binder.Binder<T>,
23
- Class: ValueObject.ValueObject<T?>,
24
- },
25
- {} :: typeof({ __index = BoundAncestorTracker })
26
- ))
27
- & BaseObject.BaseObject
28
-
29
- --[=[
30
- Constructs a new BoundAncestorTracker
31
-
32
- @param binder Binder<T>
33
- @param child Instance
34
- @return BoundAncestorTracker
35
- ]=]
36
- function BoundAncestorTracker.new<T>(binder: Binder.Binder<T>, child: Instance): BoundAncestorTracker<T>
37
- local self: BoundAncestorTracker<T> = setmetatable(BaseObject.new() :: any, BoundAncestorTracker)
38
-
39
- self._child = child or error("No child")
40
- self._binder = binder or error("No binder")
41
-
42
- --[=[
43
- @prop Class ValueObject<T>
44
- @readonly
45
- @within BoundAncestorTracker
46
- Bound value
47
- ]=]
48
- self.Class = ValueObject.new() :: ValueObject.ValueObject<T?>
49
- self._maid:GiveTask(self.Class)
50
-
51
- -- Handle instance removing
52
- self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
53
- if class == self.Class.Value then
54
- self.Class.Value = nil
55
- end
56
- end))
57
-
58
- self._maid:GiveTask(self._binder:GetClassAddedSignal():Connect(function(_, instance)
59
- if self._child:IsDescendantOf(instance) then
60
- self:_update()
61
- end
62
- end))
63
-
64
- -- Perform update
65
- self._maid:GiveTask(self._child.AncestryChanged:Connect(function()
66
- self:_update()
67
- end))
68
- self:_update()
69
-
70
- return self
71
- end
72
-
73
- function BoundAncestorTracker._update<T>(self: BoundAncestorTracker<T>): ()
74
- local parent = self._child.Parent
75
- if not parent then
76
- self.Class.Value = nil
77
- return
78
- end
79
-
80
- self.Class.Value = BinderUtils.findFirstAncestor(self._binder, parent)
81
- end
82
-
83
- return BoundAncestorTracker
@@ -1,167 +0,0 @@
1
- --!strict
2
- --[[
3
- @class BoundAncestorTracker.spec.lua
4
- ]]
5
-
6
- local require = require(script.Parent.loader).load(script)
7
-
8
- local Binder = require("Binder")
9
- local BinderProvider = require("BinderProvider")
10
- local BoundAncestorTracker = require("BoundAncestorTracker")
11
- local Jest = require("Jest")
12
- local ServiceBag = require("ServiceBag")
13
-
14
- local describe = Jest.Globals.describe
15
- local expect = Jest.Globals.expect
16
- local it = Jest.Globals.it
17
-
18
- local specCounter = 0
19
-
20
- local function makeClass()
21
- local Class = {}
22
- Class.__index = Class
23
- Class.ClassName = "BoundAncestorTrackerSpecClass"
24
- function Class.new(inst)
25
- return setmetatable({ instance = inst }, Class)
26
- end
27
- function Class:Destroy() end
28
- return Class
29
- end
30
-
31
- local function setup()
32
- specCounter += 1
33
- local suffix = specCounter
34
-
35
- local serviceBag = ServiceBag.new()
36
- local container = Instance.new("Folder")
37
- container.Name = "BoundAncestorTrackerSpecContainer"
38
- container.Parent = workspace
39
-
40
- local instances = {}
41
- local cleanups = {}
42
- local booted = false
43
-
44
- local binder = Binder.new(string.format("BoundAncestorTrackerSpecTag_%d", suffix), makeClass() :: any)
45
-
46
- local function newInstance(parent: Instance?): Instance
47
- local inst = Instance.new("Folder")
48
- inst.Parent = parent or container
49
- table.insert(instances, inst)
50
- return inst
51
- end
52
-
53
- local function track(item: any): any
54
- table.insert(cleanups, item)
55
- return item
56
- end
57
-
58
- local function boot()
59
- assert(not booted, "Already booted")
60
- booted = true
61
-
62
- local provider = BinderProvider.new(string.format("BoundAncestorTrackerSpecProvider_%d", suffix), function(self)
63
- self:Add(binder)
64
- end)
65
- serviceBag:GetService(provider)
66
- serviceBag:Init()
67
- serviceBag:Start()
68
- end
69
-
70
- local function awaitChange(tracker, previous)
71
- if tracker.Class.Value == previous then
72
- tracker.Class.Changed:Wait()
73
- end
74
- return tracker.Class.Value
75
- end
76
-
77
- return {
78
- container = container,
79
- binder = binder,
80
- newInstance = newInstance,
81
- track = track,
82
- boot = boot,
83
- awaitChange = awaitChange,
84
- destroy = function()
85
- for _, item in cleanups do
86
- pcall(function()
87
- item:Destroy()
88
- end)
89
- end
90
- serviceBag:Destroy()
91
- for _, inst in instances do
92
- pcall(function()
93
- inst:Destroy()
94
- end)
95
- end
96
- container:Destroy()
97
- end,
98
- }
99
- end
100
-
101
- describe("BoundAncestorTracker tracking", function()
102
- it("exposes the nearest bound ancestor's class", function()
103
- local controller = setup()
104
-
105
- local grandparent = controller.newInstance()
106
- local parent = controller.newInstance(grandparent)
107
- local child = controller.newInstance(parent)
108
- controller.binder:Tag(grandparent)
109
- controller.boot()
110
-
111
- local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
112
- expect(tracker.Class.Value).toEqual(controller.binder:Get(grandparent))
113
-
114
- controller.destroy()
115
- end)
116
-
117
- it("has no value when no ancestor is bound", function()
118
- local controller = setup()
119
-
120
- local parent = controller.newInstance()
121
- local child = controller.newInstance(parent)
122
- controller.boot()
123
-
124
- local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
125
- expect(tracker.Class.Value).toBeNil()
126
-
127
- controller.destroy()
128
- end)
129
-
130
- it("updates when an ancestor becomes bound", function()
131
- local controller = setup()
132
-
133
- -- The tracker resolves ancestors above the child's direct parent, so bind the grandparent.
134
- local ancestor = controller.newInstance()
135
- local parent = controller.newInstance(ancestor)
136
- local child = controller.newInstance(parent)
137
- controller.boot()
138
-
139
- local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
140
- expect(tracker.Class.Value).toBeNil()
141
-
142
- controller.binder:Tag(ancestor)
143
- local value = controller.awaitChange(tracker, nil)
144
- expect(value).toEqual(controller.binder:Get(ancestor))
145
-
146
- controller.destroy()
147
- end)
148
-
149
- it("clears the value when the child leaves the bound ancestry", function()
150
- local controller = setup()
151
-
152
- local ancestor = controller.newInstance()
153
- local parent = controller.newInstance(ancestor)
154
- local child = controller.newInstance(parent)
155
- controller.binder:Tag(ancestor)
156
- controller.boot()
157
-
158
- local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
159
- local class = controller.binder:Get(ancestor)
160
- expect(tracker.Class.Value).toEqual(class)
161
-
162
- child.Parent = controller.container
163
- expect(controller.awaitChange(tracker, class)).toBeNil()
164
-
165
- controller.destroy()
166
- end)
167
- end)
@@ -1,64 +0,0 @@
1
- --!strict
2
- --[=[
3
- Tracks a parent bound to a specific binder
4
- @class BoundParentTracker
5
- ]=]
6
-
7
- local require = require(script.Parent.loader).load(script)
8
-
9
- local BaseObject = require("BaseObject")
10
- local Binder = require("Binder")
11
- local ValueObject = require("ValueObject")
12
-
13
- local BoundParentTracker = setmetatable({}, BaseObject)
14
- BoundParentTracker.ClassName = "BoundParentTracker"
15
- BoundParentTracker.__index = BoundParentTracker
16
-
17
- export type BoundParentTracker<T> =
18
- typeof(setmetatable(
19
- {} :: {
20
- _child: Instance,
21
- _binder: Binder.Binder<T>,
22
- Class: ValueObject.ValueObject<T?>,
23
- },
24
- {} :: typeof({ __index = BoundParentTracker })
25
- ))
26
- & BaseObject.BaseObject
27
-
28
- function BoundParentTracker.new<T>(binder: Binder.Binder<T>, child: Instance): BoundParentTracker<T>
29
- local self: BoundParentTracker<T> = setmetatable(BaseObject.new() :: any, BoundParentTracker)
30
-
31
- self._child = child or error("No child")
32
- self._binder = binder or error("No binder")
33
-
34
- -- Bound value
35
- self.Class = ValueObject.new(nil :: T?)
36
- self._maid:GiveTask(self.Class)
37
-
38
- -- Handle instance removing
39
- self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
40
- if class == self.Class.Value then
41
- self.Class.Value = nil
42
- end
43
- end))
44
-
45
- -- Perform update
46
- self._maid:GiveTask(self._child:GetPropertyChangedSignal("Parent"):Connect(function()
47
- self:_update()
48
- end))
49
- self:_update()
50
-
51
- return self
52
- end
53
-
54
- function BoundParentTracker._update<T>(self: BoundParentTracker<T>): ()
55
- local parent = self._child.Parent
56
- if not parent then
57
- self.Class.Value = nil
58
- return
59
- end
60
-
61
- self.Class.Value = self._binder:Get(parent)
62
- end
63
-
64
- return BoundParentTracker