@pyreon/reactivity 0.24.5 → 0.24.6
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/package.json +1 -4
- package/src/batch.ts +0 -196
- package/src/cell.ts +0 -72
- package/src/computed.ts +0 -313
- package/src/createSelector.ts +0 -109
- package/src/debug.ts +0 -134
- package/src/effect.ts +0 -467
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -60
- package/src/lpih.ts +0 -227
- package/src/manifest.ts +0 -660
- package/src/reactive-devtools.ts +0 -494
- package/src/reactive-trace.ts +0 -142
- package/src/reconcile.ts +0 -118
- package/src/resource.ts +0 -84
- package/src/scope.ts +0 -123
- package/src/signal.ts +0 -261
- package/src/store.ts +0 -250
- package/src/tests/batch.test.ts +0 -751
- package/src/tests/bind.test.ts +0 -84
- package/src/tests/branches.test.ts +0 -343
- package/src/tests/cell.test.ts +0 -159
- package/src/tests/computed.test.ts +0 -436
- package/src/tests/coverage-hardening.test.ts +0 -471
- package/src/tests/createSelector.test.ts +0 -291
- package/src/tests/debug.test.ts +0 -196
- package/src/tests/effect.test.ts +0 -464
- package/src/tests/fanout-repro.test.ts +0 -179
- package/src/tests/lpih-source-location.test.ts +0 -277
- package/src/tests/lpih.test.ts +0 -351
- package/src/tests/manifest-snapshot.test.ts +0 -96
- package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
- package/src/tests/reactive-devtools.test.ts +0 -296
- package/src/tests/reactive-trace.test.ts +0 -102
- package/src/tests/reconcile-security.test.ts +0 -45
- package/src/tests/resource.test.ts +0 -326
- package/src/tests/scope.test.ts +0 -231
- package/src/tests/signal.test.ts +0 -368
- package/src/tests/store.test.ts +0 -286
- package/src/tests/tracking.test.ts +0 -158
- package/src/tests/vue-parity.test.ts +0 -191
- package/src/tests/watch.test.ts +0 -246
- package/src/tracking.ts +0 -139
- package/src/watch.ts +0 -68
package/src/tests/effect.test.ts
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import { effect, onCleanup, renderEffect, setErrorHandler } from '../effect'
|
|
2
|
-
import { effectScope, setCurrentScope } from '../scope'
|
|
3
|
-
import { signal } from '../signal'
|
|
4
|
-
|
|
5
|
-
describe('effect', () => {
|
|
6
|
-
test('runs immediately', () => {
|
|
7
|
-
let ran = false
|
|
8
|
-
effect(() => {
|
|
9
|
-
ran = true
|
|
10
|
-
})
|
|
11
|
-
expect(ran).toBe(true)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('re-runs when tracked signal changes', () => {
|
|
15
|
-
const s = signal(0)
|
|
16
|
-
let count = 0
|
|
17
|
-
effect(() => {
|
|
18
|
-
s() // track
|
|
19
|
-
count++
|
|
20
|
-
})
|
|
21
|
-
expect(count).toBe(1)
|
|
22
|
-
s.set(1)
|
|
23
|
-
expect(count).toBe(2)
|
|
24
|
-
s.set(2)
|
|
25
|
-
expect(count).toBe(3)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('does not re-run after dispose', () => {
|
|
29
|
-
const s = signal(0)
|
|
30
|
-
let count = 0
|
|
31
|
-
const e = effect(() => {
|
|
32
|
-
s()
|
|
33
|
-
count++
|
|
34
|
-
})
|
|
35
|
-
e.dispose()
|
|
36
|
-
s.set(1)
|
|
37
|
-
expect(count).toBe(1) // only the initial run
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('tracks multiple signals', () => {
|
|
41
|
-
const a = signal(1)
|
|
42
|
-
const b = signal(2)
|
|
43
|
-
let result = 0
|
|
44
|
-
effect(() => {
|
|
45
|
-
result = a() + b()
|
|
46
|
-
})
|
|
47
|
-
expect(result).toBe(3)
|
|
48
|
-
a.set(10)
|
|
49
|
-
expect(result).toBe(12)
|
|
50
|
-
b.set(20)
|
|
51
|
-
expect(result).toBe(30)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('does not track signals accessed after conditional branch', () => {
|
|
55
|
-
const toggle = signal(true)
|
|
56
|
-
const a = signal(1)
|
|
57
|
-
const b = signal(100)
|
|
58
|
-
let result = 0
|
|
59
|
-
effect(() => {
|
|
60
|
-
result = toggle() ? a() : b()
|
|
61
|
-
})
|
|
62
|
-
expect(result).toBe(1)
|
|
63
|
-
a.set(2)
|
|
64
|
-
expect(result).toBe(2)
|
|
65
|
-
toggle.set(false)
|
|
66
|
-
expect(result).toBe(100)
|
|
67
|
-
// a is no longer tracked
|
|
68
|
-
a.set(999)
|
|
69
|
-
expect(result).toBe(100)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('catches errors via default error handler', () => {
|
|
73
|
-
const errors: unknown[] = []
|
|
74
|
-
const origError = console.error
|
|
75
|
-
console.error = (...args: unknown[]) => errors.push(args)
|
|
76
|
-
|
|
77
|
-
const s = signal(0)
|
|
78
|
-
effect(() => {
|
|
79
|
-
s()
|
|
80
|
-
throw new Error('boom')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
expect(errors.length).toBe(1)
|
|
84
|
-
console.error = origError
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('calls cleanup before re-run', () => {
|
|
88
|
-
const s = signal(0)
|
|
89
|
-
let cleanups = 0
|
|
90
|
-
effect(() => {
|
|
91
|
-
s()
|
|
92
|
-
return () => {
|
|
93
|
-
cleanups++
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
expect(cleanups).toBe(0)
|
|
97
|
-
s.set(1) // re-run: previous cleanup fires
|
|
98
|
-
expect(cleanups).toBe(1)
|
|
99
|
-
s.set(2)
|
|
100
|
-
expect(cleanups).toBe(2)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test('calls cleanup on dispose', () => {
|
|
104
|
-
const s = signal(0)
|
|
105
|
-
let cleanups = 0
|
|
106
|
-
const e = effect(() => {
|
|
107
|
-
s()
|
|
108
|
-
return () => {
|
|
109
|
-
cleanups++
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
expect(cleanups).toBe(0)
|
|
113
|
-
e.dispose()
|
|
114
|
-
expect(cleanups).toBe(1)
|
|
115
|
-
// Disposing again should not call cleanup again
|
|
116
|
-
e.dispose()
|
|
117
|
-
expect(cleanups).toBe(1)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('cleanup errors are caught by error handler', () => {
|
|
121
|
-
const caught: unknown[] = []
|
|
122
|
-
setErrorHandler((err) => caught.push(err))
|
|
123
|
-
|
|
124
|
-
const s = signal(0)
|
|
125
|
-
effect(() => {
|
|
126
|
-
s()
|
|
127
|
-
return () => {
|
|
128
|
-
throw new Error('cleanup boom')
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
s.set(1) // triggers cleanup which throws
|
|
132
|
-
expect(caught.length).toBe(1)
|
|
133
|
-
expect((caught[0] as Error).message).toBe('cleanup boom')
|
|
134
|
-
|
|
135
|
-
// Restore default handler
|
|
136
|
-
setErrorHandler((_err) => {})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('works with no cleanup return (backwards compatible)', () => {
|
|
140
|
-
const s = signal(0)
|
|
141
|
-
let count = 0
|
|
142
|
-
effect(() => {
|
|
143
|
-
s()
|
|
144
|
-
count++
|
|
145
|
-
// no return
|
|
146
|
-
})
|
|
147
|
-
expect(count).toBe(1)
|
|
148
|
-
s.set(1)
|
|
149
|
-
expect(count).toBe(2)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test('setErrorHandler replaces the error handler', () => {
|
|
153
|
-
const caught: unknown[] = []
|
|
154
|
-
setErrorHandler((err) => caught.push(err))
|
|
155
|
-
|
|
156
|
-
const s = signal(0)
|
|
157
|
-
effect(() => {
|
|
158
|
-
s()
|
|
159
|
-
throw new Error('custom')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
expect(caught.length).toBe(1)
|
|
163
|
-
expect((caught[0] as Error).message).toBe('custom')
|
|
164
|
-
|
|
165
|
-
// Restore default handler
|
|
166
|
-
setErrorHandler((_err) => {})
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('effect notifies scope on re-run (not first run)', async () => {
|
|
170
|
-
const scope = effectScope()
|
|
171
|
-
setCurrentScope(scope)
|
|
172
|
-
|
|
173
|
-
let updateCount = 0
|
|
174
|
-
scope.addUpdateHook(() => {
|
|
175
|
-
updateCount++
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
const s = signal(0)
|
|
179
|
-
effect(() => {
|
|
180
|
-
s()
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
setCurrentScope(null)
|
|
184
|
-
|
|
185
|
-
expect(updateCount).toBe(0) // first run does not notify
|
|
186
|
-
|
|
187
|
-
s.set(1) // re-run triggers notifyEffectRan
|
|
188
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
189
|
-
expect(updateCount).toBe(1)
|
|
190
|
-
|
|
191
|
-
scope.stop()
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
describe('renderEffect', () => {
|
|
196
|
-
test('runs immediately and tracks signals', () => {
|
|
197
|
-
const s = signal(0)
|
|
198
|
-
let count = 0
|
|
199
|
-
renderEffect(() => {
|
|
200
|
-
s()
|
|
201
|
-
count++
|
|
202
|
-
})
|
|
203
|
-
expect(count).toBe(1)
|
|
204
|
-
s.set(1)
|
|
205
|
-
expect(count).toBe(2)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('dispose stops tracking', () => {
|
|
209
|
-
const s = signal(0)
|
|
210
|
-
let count = 0
|
|
211
|
-
const dispose = renderEffect(() => {
|
|
212
|
-
s()
|
|
213
|
-
count++
|
|
214
|
-
})
|
|
215
|
-
expect(count).toBe(1)
|
|
216
|
-
dispose()
|
|
217
|
-
s.set(1)
|
|
218
|
-
expect(count).toBe(1)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test('dispose is idempotent', () => {
|
|
222
|
-
const s = signal(0)
|
|
223
|
-
const dispose = renderEffect(() => {
|
|
224
|
-
s()
|
|
225
|
-
})
|
|
226
|
-
dispose()
|
|
227
|
-
dispose() // should not throw
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test('tracks dynamic dependencies', () => {
|
|
231
|
-
const toggle = signal(true)
|
|
232
|
-
const a = signal(1)
|
|
233
|
-
const b = signal(100)
|
|
234
|
-
let result = 0
|
|
235
|
-
renderEffect(() => {
|
|
236
|
-
result = toggle() ? a() : b()
|
|
237
|
-
})
|
|
238
|
-
expect(result).toBe(1)
|
|
239
|
-
toggle.set(false)
|
|
240
|
-
expect(result).toBe(100)
|
|
241
|
-
a.set(999) // no longer tracked
|
|
242
|
-
expect(result).toBe(100)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test('does not re-run after disposed during signal update', () => {
|
|
246
|
-
const s = signal(0)
|
|
247
|
-
let count = 0
|
|
248
|
-
const dispose = renderEffect(() => {
|
|
249
|
-
s()
|
|
250
|
-
count++
|
|
251
|
-
})
|
|
252
|
-
dispose()
|
|
253
|
-
s.set(5)
|
|
254
|
-
expect(count).toBe(1)
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
describe('onCleanup', () => {
|
|
259
|
-
test('runs cleanup before effect re-runs', () => {
|
|
260
|
-
const s = signal(0)
|
|
261
|
-
const log: string[] = []
|
|
262
|
-
effect(() => {
|
|
263
|
-
const val = s()
|
|
264
|
-
onCleanup(() => log.push(`cleanup-${val}`))
|
|
265
|
-
log.push(`run-${val}`)
|
|
266
|
-
})
|
|
267
|
-
expect(log).toEqual(['run-0'])
|
|
268
|
-
s.set(1)
|
|
269
|
-
expect(log).toEqual(['run-0', 'cleanup-0', 'run-1'])
|
|
270
|
-
s.set(2)
|
|
271
|
-
expect(log).toEqual(['run-0', 'cleanup-0', 'run-1', 'cleanup-1', 'run-2'])
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
test('runs cleanup on dispose', () => {
|
|
275
|
-
let cleaned = false
|
|
276
|
-
const e = effect(() => {
|
|
277
|
-
onCleanup(() => {
|
|
278
|
-
cleaned = true
|
|
279
|
-
})
|
|
280
|
-
})
|
|
281
|
-
expect(cleaned).toBe(false)
|
|
282
|
-
e.dispose()
|
|
283
|
-
expect(cleaned).toBe(true)
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
test('supports multiple onCleanup calls', () => {
|
|
287
|
-
const s = signal(0)
|
|
288
|
-
const log: string[] = []
|
|
289
|
-
effect(() => {
|
|
290
|
-
s()
|
|
291
|
-
onCleanup(() => log.push('a'))
|
|
292
|
-
onCleanup(() => log.push('b'))
|
|
293
|
-
})
|
|
294
|
-
s.set(1)
|
|
295
|
-
expect(log).toEqual(['a', 'b'])
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
test('works alongside return cleanup', () => {
|
|
299
|
-
const s = signal(0)
|
|
300
|
-
const log: string[] = []
|
|
301
|
-
effect(() => {
|
|
302
|
-
s()
|
|
303
|
-
onCleanup(() => log.push('onCleanup'))
|
|
304
|
-
return () => log.push('return')
|
|
305
|
-
})
|
|
306
|
-
s.set(1)
|
|
307
|
-
expect(log).toEqual(['onCleanup', 'return'])
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
test('no-ops outside effect', () => {
|
|
311
|
-
// Should not throw
|
|
312
|
-
onCleanup(() => {})
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
test('cleanup ordering: onCleanup runs before return cleanup', () => {
|
|
316
|
-
const s = signal(0)
|
|
317
|
-
const log: string[] = []
|
|
318
|
-
effect(() => {
|
|
319
|
-
const val = s()
|
|
320
|
-
onCleanup(() => log.push(`onCleanup-${val}`))
|
|
321
|
-
return () => log.push(`return-${val}`)
|
|
322
|
-
})
|
|
323
|
-
s.set(1)
|
|
324
|
-
// onCleanup should fire first, then return cleanup
|
|
325
|
-
expect(log).toEqual(['onCleanup-0', 'return-0'])
|
|
326
|
-
s.set(2)
|
|
327
|
-
expect(log).toEqual(['onCleanup-0', 'return-0', 'onCleanup-1', 'return-1'])
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
test('multiple onCleanup callbacks run in registration order', () => {
|
|
331
|
-
const s = signal(0)
|
|
332
|
-
const log: string[] = []
|
|
333
|
-
effect(() => {
|
|
334
|
-
s()
|
|
335
|
-
onCleanup(() => log.push('first'))
|
|
336
|
-
onCleanup(() => log.push('second'))
|
|
337
|
-
onCleanup(() => log.push('third'))
|
|
338
|
-
})
|
|
339
|
-
s.set(1)
|
|
340
|
-
expect(log).toEqual(['first', 'second', 'third'])
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
test('cleanup runs on dispose even when effect never re-ran', () => {
|
|
344
|
-
const log: string[] = []
|
|
345
|
-
const e = effect(() => {
|
|
346
|
-
onCleanup(() => log.push('disposed'))
|
|
347
|
-
})
|
|
348
|
-
expect(log).toEqual([])
|
|
349
|
-
e.dispose()
|
|
350
|
-
expect(log).toEqual(['disposed'])
|
|
351
|
-
})
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
describe('effect — error handling', () => {
|
|
355
|
-
test('error in effect does not prevent other effects from running', () => {
|
|
356
|
-
const caught: unknown[] = []
|
|
357
|
-
setErrorHandler((err) => caught.push(err))
|
|
358
|
-
|
|
359
|
-
const s = signal(0)
|
|
360
|
-
let goodEffectRuns = 0
|
|
361
|
-
|
|
362
|
-
effect(() => {
|
|
363
|
-
s()
|
|
364
|
-
throw new Error('bad effect')
|
|
365
|
-
})
|
|
366
|
-
effect(() => {
|
|
367
|
-
s()
|
|
368
|
-
goodEffectRuns++
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
expect(caught).toHaveLength(1)
|
|
372
|
-
expect(goodEffectRuns).toBe(1)
|
|
373
|
-
|
|
374
|
-
s.set(1)
|
|
375
|
-
expect(caught).toHaveLength(2)
|
|
376
|
-
expect(goodEffectRuns).toBe(2)
|
|
377
|
-
|
|
378
|
-
setErrorHandler((_err) => {})
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
test('error during cleanup is caught by error handler', () => {
|
|
382
|
-
const caught: unknown[] = []
|
|
383
|
-
setErrorHandler((err) => caught.push(err))
|
|
384
|
-
|
|
385
|
-
const s = signal(0)
|
|
386
|
-
effect(() => {
|
|
387
|
-
s()
|
|
388
|
-
onCleanup(() => {
|
|
389
|
-
throw new Error('cleanup error')
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
s.set(1) // triggers cleanup which throws
|
|
394
|
-
expect(caught).toHaveLength(1)
|
|
395
|
-
expect((caught[0] as Error).message).toBe('cleanup error')
|
|
396
|
-
|
|
397
|
-
setErrorHandler((_err) => {})
|
|
398
|
-
})
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
// ─── Audit bug #1: async effect runtime warning ──────────────────────────────
|
|
402
|
-
//
|
|
403
|
-
// Companion to the `pyreon/no-async-effect` lint rule. The runtime warn
|
|
404
|
-
// catches the case where the lint rule was suppressed or the effect call
|
|
405
|
-
// site was constructed dynamically (e.g. via a higher-order helper that
|
|
406
|
-
// the lint rule's static check doesn't see).
|
|
407
|
-
|
|
408
|
-
describe('effect — async function warning (audit bug #1)', () => {
|
|
409
|
-
test('warns when called with an async arrow function', () => {
|
|
410
|
-
const warns: string[] = []
|
|
411
|
-
const orig = console.warn
|
|
412
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
413
|
-
try {
|
|
414
|
-
// Async effect callbacks are intentionally NOT in `effect()`'s type
|
|
415
|
-
// signature — that's the point. The test deliberately misuses the
|
|
416
|
-
// API to verify the runtime warning catches what the type system
|
|
417
|
-
// would normally reject. Cast through `unknown` to silence TS.
|
|
418
|
-
const asyncFn = async (): Promise<void> => {}
|
|
419
|
-
effect(asyncFn as unknown as () => void)
|
|
420
|
-
} finally {
|
|
421
|
-
console.warn = orig
|
|
422
|
-
}
|
|
423
|
-
expect(warns.some((m) => m.includes('async function'))).toBe(true)
|
|
424
|
-
expect(warns.some((m) => m.includes('await'))).toBe(true)
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
test('does NOT warn for synchronous effect callbacks', () => {
|
|
428
|
-
const warns: string[] = []
|
|
429
|
-
const orig = console.warn
|
|
430
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
431
|
-
try {
|
|
432
|
-
effect(() => {})
|
|
433
|
-
} finally {
|
|
434
|
-
console.warn = orig
|
|
435
|
-
}
|
|
436
|
-
expect(warns.some((m) => m.includes('async function'))).toBe(false)
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
test('renderEffect warns when called with an async arrow function', () => {
|
|
440
|
-
const warns: string[] = []
|
|
441
|
-
const orig = console.warn
|
|
442
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
443
|
-
try {
|
|
444
|
-
const asyncFn = async (): Promise<void> => {}
|
|
445
|
-
renderEffect(asyncFn as unknown as () => void)
|
|
446
|
-
} finally {
|
|
447
|
-
console.warn = orig
|
|
448
|
-
}
|
|
449
|
-
expect(warns.some((m) => m.includes('renderEffect'))).toBe(true)
|
|
450
|
-
expect(warns.some((m) => m.includes('async function'))).toBe(true)
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
test('renderEffect does NOT warn for synchronous callbacks', () => {
|
|
454
|
-
const warns: string[] = []
|
|
455
|
-
const orig = console.warn
|
|
456
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
457
|
-
try {
|
|
458
|
-
renderEffect(() => {})
|
|
459
|
-
} finally {
|
|
460
|
-
console.warn = orig
|
|
461
|
-
}
|
|
462
|
-
expect(warns.some((m) => m.includes('async function'))).toBe(false)
|
|
463
|
-
})
|
|
464
|
-
})
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reproduction of the deferred bug from PR #490 (queryReactiveKey-1000 journey).
|
|
3
|
-
*
|
|
4
|
-
* Symptom from real-app stress: a `reactKey` signal subscribed to by ~100
|
|
5
|
-
* effects (each useQuery's setOptions effect) sees `signalWrite` increment
|
|
6
|
-
* on every `.set(i)` in a tight external loop, but only 1 of N effect runs
|
|
7
|
-
* propagates per .set — `effectRun` stays at the initial-mount count.
|
|
8
|
-
*
|
|
9
|
-
* Hypothesis to test: `notifySubscribers` in tracking.ts iterates the live
|
|
10
|
-
* Set with `originalSize` cap. If an effect's body calls cleanupEffect (which
|
|
11
|
-
* removes itself from the Set) AND re-subscribes (adds itself back at the
|
|
12
|
-
* end), the iteration order shifts so subsequent effects' positions move
|
|
13
|
-
* BEFORE `i`, causing them to be skipped on this pass.
|
|
14
|
-
*
|
|
15
|
-
* See packages/core/reactivity/src/tracking.ts:77-105 (notifySubscribers).
|
|
16
|
-
*/
|
|
17
|
-
import { describe, expect, it } from 'vitest'
|
|
18
|
-
import { effect, effectScope, signal } from '../index'
|
|
19
|
-
|
|
20
|
-
describe('signal fan-out under tight external write loop', () => {
|
|
21
|
-
it('100 effects subscribing to same signal — each fires on every external .set', () => {
|
|
22
|
-
const sig = signal(0)
|
|
23
|
-
const counts = new Array(100).fill(0)
|
|
24
|
-
|
|
25
|
-
for (let i = 0; i < 100; i++) {
|
|
26
|
-
const idx = i
|
|
27
|
-
effect(() => {
|
|
28
|
-
sig() // subscribe
|
|
29
|
-
counts[idx]++
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Each effect ran ONCE at registration.
|
|
34
|
-
for (const c of counts) expect(c).toBe(1)
|
|
35
|
-
|
|
36
|
-
// 10 external writes from outside any batch.
|
|
37
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
38
|
-
|
|
39
|
-
// Each effect should have re-fired 10 times → total = 11.
|
|
40
|
-
for (let i = 0; i < counts.length; i++) {
|
|
41
|
-
expect(counts[i], `effect[${i}] runs after 10 sets`).toBe(11)
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('1 effect — fires on every external .set (control)', () => {
|
|
46
|
-
const sig = signal(0)
|
|
47
|
-
let count = 0
|
|
48
|
-
|
|
49
|
-
effect(() => {
|
|
50
|
-
sig()
|
|
51
|
-
count++
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
expect(count).toBe(1)
|
|
55
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
56
|
-
expect(count).toBe(11)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('5 effects + 5 external sets — each effect fires per set', () => {
|
|
60
|
-
const sig = signal(0)
|
|
61
|
-
const counts = new Array(5).fill(0)
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < 5; i++) {
|
|
64
|
-
const idx = i
|
|
65
|
-
effect(() => {
|
|
66
|
-
sig()
|
|
67
|
-
counts[idx]++
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (let i = 1; i <= 5; i++) sig.set(i)
|
|
72
|
-
|
|
73
|
-
// Each effect: 1 initial + 5 re-runs = 6.
|
|
74
|
-
for (let i = 0; i < counts.length; i++) {
|
|
75
|
-
expect(counts[i], `effect[${i}]`).toBe(6)
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('100 effects inside an EffectScope — each fires on every external .set', () => {
|
|
80
|
-
const sig = signal(0)
|
|
81
|
-
const counts = new Array(100).fill(0)
|
|
82
|
-
const scope = effectScope()
|
|
83
|
-
|
|
84
|
-
scope.runInScope(() => {
|
|
85
|
-
for (let i = 0; i < 100; i++) {
|
|
86
|
-
const idx = i
|
|
87
|
-
effect(() => {
|
|
88
|
-
sig()
|
|
89
|
-
counts[idx]++
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
for (const c of counts) expect(c).toBe(1)
|
|
95
|
-
|
|
96
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
97
|
-
|
|
98
|
-
for (let i = 0; i < counts.length; i++) {
|
|
99
|
-
expect(counts[i], `effect[${i}] runs after 10 sets`).toBe(11)
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('1000 effects subscribing to same signal — each fires per .set', () => {
|
|
104
|
-
const sig = signal(0)
|
|
105
|
-
const counts = new Array(1000).fill(0)
|
|
106
|
-
for (let i = 0; i < 1000; i++) {
|
|
107
|
-
const idx = i
|
|
108
|
-
effect(() => {
|
|
109
|
-
sig()
|
|
110
|
-
counts[idx]++
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
114
|
-
let failed = 0
|
|
115
|
-
for (let i = 0; i < counts.length; i++) {
|
|
116
|
-
if (counts[i] !== 11) failed++
|
|
117
|
-
}
|
|
118
|
-
expect(failed, `effects with wrong count`).toBe(0)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('effects created INSIDE another effect run (inner-effect collector path)', () => {
|
|
122
|
-
const trigger = signal(0)
|
|
123
|
-
const sig = signal(0)
|
|
124
|
-
const counts = new Array(50).fill(0)
|
|
125
|
-
let outerRuns = 0
|
|
126
|
-
|
|
127
|
-
// Outer effect creates 50 INNER effects on each run.
|
|
128
|
-
effect(() => {
|
|
129
|
-
trigger()
|
|
130
|
-
outerRuns++
|
|
131
|
-
for (let i = 0; i < 50; i++) {
|
|
132
|
-
const idx = i
|
|
133
|
-
effect(() => {
|
|
134
|
-
sig()
|
|
135
|
-
counts[idx]++
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// Reset counts after initial outer run created the inners with their initial run.
|
|
141
|
-
expect(outerRuns).toBe(1)
|
|
142
|
-
for (const c of counts) expect(c).toBe(1)
|
|
143
|
-
|
|
144
|
-
// Now flip sig 10 times — every inner should fire 10 more times.
|
|
145
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
146
|
-
|
|
147
|
-
let failed = 0
|
|
148
|
-
for (let i = 0; i < counts.length; i++) {
|
|
149
|
-
if (counts[i] !== 11) failed++
|
|
150
|
-
}
|
|
151
|
-
expect(failed, `inner effects with wrong count`).toBe(0)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('100 effects whose body writes to an unrelated signal during run', () => {
|
|
155
|
-
// Mirrors useQuery's effect: body calls observer.setOptions(options())
|
|
156
|
-
// which fires the observer's subscribe callback, which calls batch()
|
|
157
|
-
// and writes to N "result slot" signals (which may have 0 or N subscribers).
|
|
158
|
-
const sig = signal(0)
|
|
159
|
-
const slot = signal('')
|
|
160
|
-
const counts = new Array(100).fill(0)
|
|
161
|
-
|
|
162
|
-
for (let i = 0; i < 100; i++) {
|
|
163
|
-
const idx = i
|
|
164
|
-
effect(() => {
|
|
165
|
-
sig() // subscribe to sig
|
|
166
|
-
// Simulate observer.setOptions's downstream subscribe-callback work:
|
|
167
|
-
// an inner write to a different signal that has no subscribers.
|
|
168
|
-
slot.set(`run-${idx}`) // 100 different values, each Object.is fails
|
|
169
|
-
counts[idx]++
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (let i = 1; i <= 10; i++) sig.set(i)
|
|
174
|
-
|
|
175
|
-
for (let i = 0; i < counts.length; i++) {
|
|
176
|
-
expect(counts[i], `effect[${i}] runs after 10 sets`).toBe(11)
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
})
|