@pyreon/reactivity 0.16.0 → 0.19.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.
@@ -164,17 +164,25 @@ describe('signal', () => {
164
164
  expect(calls3).toBe(2) // still active
165
165
  })
166
166
 
167
- test('direct updater slot is null after disposal', () => {
167
+ test('direct updater is removed from the set after disposal', () => {
168
168
  const s = signal(0)
169
- const dispose = s.direct(() => {})
169
+ let calls = 0
170
+ const dispose = s.direct(() => {
171
+ calls++
172
+ })
170
173
 
171
- // Access internal _d array via cast
172
- const internal = s as unknown as { _d: ((() => void) | null)[] | null }
174
+ // Internal `_d` is a Set (not an unbounded array see signal.ts).
175
+ const internal = s as unknown as { _d: Set<() => void> | null }
173
176
  expect(internal._d).not.toBeNull()
174
- expect(internal._d![0]).toBeTypeOf('function')
177
+ expect(internal._d!.size).toBe(1)
178
+ s.set(1)
179
+ expect(calls).toBe(1)
175
180
 
176
181
  dispose()
177
- expect(internal._d![0]).toBeNull()
182
+ // O(1) removal — the slot is GONE, not nulled-and-retained.
183
+ expect(internal._d!.size).toBe(0)
184
+ s.set(2)
185
+ expect(calls).toBe(1) // disposed updater not invoked
178
186
  })
179
187
  })
180
188
 
@@ -208,13 +216,31 @@ describe('signal', () => {
208
216
  expect(calls).toBe(1)
209
217
  })
210
218
 
211
- test('direct updater with no prior direct array initializes lazily', () => {
219
+ test('direct updater set initializes lazily', () => {
212
220
  const s = signal(0)
213
- const internal = s as unknown as { _d: ((() => void) | null)[] | null }
221
+ const internal = s as unknown as { _d: Set<() => void> | null }
214
222
  expect(internal._d).toBeNull()
215
223
  s.direct(() => {})
216
224
  expect(internal._d).not.toBeNull()
217
- expect(internal._d).toHaveLength(1)
225
+ expect(internal._d!.size).toBe(1)
226
+ })
227
+
228
+ test('churned direct bindings do not accumulate (no unbounded growth)', () => {
229
+ // Regression for the array-form leak: a long-lived signal whose
230
+ // direct bindings register+dispose repeatedly (e.g. <For> rows
231
+ // re-mounting) must keep `_d` bounded to the LIVE set, not grow
232
+ // one permanent dead slot per ever-registered binding.
233
+ const s = signal(0)
234
+ const internal = s as unknown as { _d: Set<() => void> | null }
235
+ for (let i = 0; i < 10_000; i++) {
236
+ const dispose = s.direct(() => {})
237
+ dispose()
238
+ }
239
+ expect(internal._d!.size).toBe(0)
240
+ // One live binding survives → notify cost is O(live), not O(10_000).
241
+ const dispose = s.direct(() => {})
242
+ expect(internal._d!.size).toBe(1)
243
+ dispose()
218
244
  })
219
245
  })
220
246