mutts 1.0.0 → 1.0.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/dist/chunks/{decorator-BXsign4Z.js → decorator-8qjFb7dw.js} +2 -2
- package/dist/chunks/decorator-8qjFb7dw.js.map +1 -0
- package/dist/chunks/{decorator-CPbZNnsX.esm.js → decorator-AbRkXM5O.esm.js} +2 -2
- package/dist/chunks/decorator-AbRkXM5O.esm.js.map +1 -0
- package/dist/decorator.d.ts +1 -1
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.esm.js +1 -1
- package/dist/destroyable.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/reactive.d.ts +4 -3
- package/dist/reactive.esm.js +61 -57
- package/dist/reactive.esm.js.map +1 -1
- package/dist/reactive.js +61 -56
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.esm.js +1 -1
- package/dist/std-decorators.js +1 -1
- package/docs/reactive.md +616 -0
- package/package.json +1 -2
- package/dist/chunks/decorator-BXsign4Z.js.map +0 -1
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +0 -1
- package/src/decorator.test.ts +0 -495
- package/src/decorator.ts +0 -205
- package/src/destroyable.test.ts +0 -155
- package/src/destroyable.ts +0 -158
- package/src/eventful.test.ts +0 -380
- package/src/eventful.ts +0 -69
- package/src/index.ts +0 -7
- package/src/indexable.test.ts +0 -388
- package/src/indexable.ts +0 -124
- package/src/promiseChain.test.ts +0 -201
- package/src/promiseChain.ts +0 -99
- package/src/reactive/array.test.ts +0 -923
- package/src/reactive/array.ts +0 -352
- package/src/reactive/core.test.ts +0 -1663
- package/src/reactive/core.ts +0 -866
- package/src/reactive/index.ts +0 -28
- package/src/reactive/interface.test.ts +0 -1477
- package/src/reactive/interface.ts +0 -231
- package/src/reactive/map.test.ts +0 -866
- package/src/reactive/map.ts +0 -162
- package/src/reactive/set.test.ts +0 -289
- package/src/reactive/set.ts +0 -142
- package/src/std-decorators.test.ts +0 -679
- package/src/std-decorators.ts +0 -182
- package/src/utils.ts +0 -52
|
@@ -1,1663 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
effect,
|
|
3
|
-
isNonReactive,
|
|
4
|
-
isReactive,
|
|
5
|
-
ReactiveBase,
|
|
6
|
-
reactive,
|
|
7
|
-
reactiveOptions,
|
|
8
|
-
unreactive,
|
|
9
|
-
untracked,
|
|
10
|
-
unwrap,
|
|
11
|
-
} from './index'
|
|
12
|
-
|
|
13
|
-
describe('reactive', () => {
|
|
14
|
-
describe('basic functionality', () => {
|
|
15
|
-
it('should make objects reactive', () => {
|
|
16
|
-
const obj = { count: 0, name: 'test' }
|
|
17
|
-
const reactiveObj = reactive(obj)
|
|
18
|
-
|
|
19
|
-
expect(isReactive(reactiveObj)).toBe(true)
|
|
20
|
-
expect(isReactive(obj)).toBe(false)
|
|
21
|
-
expect(reactiveObj.count).toBe(0)
|
|
22
|
-
expect(reactiveObj.name).toBe('test')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('should not make primitives reactive', () => {
|
|
26
|
-
expect(reactive(42)).toBe(42)
|
|
27
|
-
expect(reactive('string')).toBe('string')
|
|
28
|
-
expect(reactive(true)).toBe(true)
|
|
29
|
-
expect(reactive(null)).toBe(null)
|
|
30
|
-
expect(reactive(undefined)).toBe(undefined)
|
|
31
|
-
expect(reactive(true)).toBe(true)
|
|
32
|
-
expect(reactive(null)).toBe(null)
|
|
33
|
-
expect(reactive(undefined)).toBe(undefined)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should return same proxy for same object', () => {
|
|
37
|
-
const obj = { count: 0 }
|
|
38
|
-
const proxy1 = reactive(obj)
|
|
39
|
-
const proxy2 = reactive(obj)
|
|
40
|
-
|
|
41
|
-
expect(proxy1).toBe(proxy2)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should return same proxy when called on proxy', () => {
|
|
45
|
-
const obj = { count: 0 }
|
|
46
|
-
const proxy1 = reactive(obj)
|
|
47
|
-
const proxy2 = reactive(proxy1)
|
|
48
|
-
|
|
49
|
-
expect(proxy1).toBe(proxy2)
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
describe('property access and modification', () => {
|
|
54
|
-
it('should allow reading properties', () => {
|
|
55
|
-
const obj = { count: 0, name: 'test' }
|
|
56
|
-
const reactiveObj = reactive(obj)
|
|
57
|
-
|
|
58
|
-
expect(reactiveObj.count).toBe(0)
|
|
59
|
-
expect(reactiveObj.name).toBe('test')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('should allow setting properties', () => {
|
|
63
|
-
const obj = { count: 0 }
|
|
64
|
-
const reactiveObj = reactive(obj)
|
|
65
|
-
|
|
66
|
-
reactiveObj.count = 5
|
|
67
|
-
expect(reactiveObj.count).toBe(5)
|
|
68
|
-
expect(obj.count).toBe(5)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should handle numeric properties', () => {
|
|
72
|
-
const obj = { 0: 'zero', 1: 'one' }
|
|
73
|
-
const reactiveObj = reactive(obj)
|
|
74
|
-
|
|
75
|
-
expect(reactiveObj[0]).toBe('zero')
|
|
76
|
-
expect(reactiveObj[1]).toBe('one')
|
|
77
|
-
|
|
78
|
-
reactiveObj[0] = 'ZERO'
|
|
79
|
-
expect(reactiveObj[0]).toBe('ZERO')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('should handle symbol properties', () => {
|
|
83
|
-
const sym = Symbol('test')
|
|
84
|
-
const obj = { [sym]: 'value' }
|
|
85
|
-
const reactiveObj = reactive(obj)
|
|
86
|
-
|
|
87
|
-
expect(reactiveObj[sym]).toBe('value')
|
|
88
|
-
|
|
89
|
-
reactiveObj[sym] = 'new value'
|
|
90
|
-
expect(reactiveObj[sym]).toBe('new value')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
describe('unwrap functionality', () => {
|
|
95
|
-
it('should unwrap reactive objects', () => {
|
|
96
|
-
const obj = { count: 0 }
|
|
97
|
-
const reactiveObj = reactive(obj)
|
|
98
|
-
|
|
99
|
-
const unwrapped = unwrap(reactiveObj)
|
|
100
|
-
expect(unwrapped).toBe(obj)
|
|
101
|
-
expect(unwrapped).not.toBe(reactiveObj)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('should return non-reactive objects as-is', () => {
|
|
105
|
-
const obj = { count: 0 }
|
|
106
|
-
expect(unwrap(obj)).toBe(obj)
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('effect', () => {
|
|
112
|
-
describe('basic effect functionality', () => {
|
|
113
|
-
it('should run effect immediately', () => {
|
|
114
|
-
let count = 0
|
|
115
|
-
const reactiveObj = reactive({ value: 0 })
|
|
116
|
-
|
|
117
|
-
effect(() => {
|
|
118
|
-
count++
|
|
119
|
-
reactiveObj.value
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
expect(count).toBe(1)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('should track dependencies', () => {
|
|
126
|
-
let effectCount = 0
|
|
127
|
-
const reactiveObj = reactive({ count: 0 })
|
|
128
|
-
|
|
129
|
-
effect(() => {
|
|
130
|
-
effectCount++
|
|
131
|
-
reactiveObj.count
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
expect(effectCount).toBe(1)
|
|
135
|
-
|
|
136
|
-
reactiveObj.count = 5
|
|
137
|
-
expect(effectCount).toBe(2)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should only track accessed properties', () => {
|
|
141
|
-
let effectCount = 0
|
|
142
|
-
const reactiveObj = reactive({ count: 0, name: 'test' })
|
|
143
|
-
|
|
144
|
-
effect(() => {
|
|
145
|
-
effectCount++
|
|
146
|
-
reactiveObj.count // Only access count
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
expect(effectCount).toBe(1)
|
|
150
|
-
|
|
151
|
-
reactiveObj.name = 'new name' // Change name
|
|
152
|
-
expect(effectCount).toBe(1) // Should not trigger effect
|
|
153
|
-
|
|
154
|
-
reactiveObj.count = 5 // Change count
|
|
155
|
-
expect(effectCount).toBe(2) // Should trigger effect
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
describe('cascading effects', () => {
|
|
160
|
-
it('should properly handle cascading effects', () => {
|
|
161
|
-
const reactiveObj = reactive({ a: 0, b: 0, c: 0 })
|
|
162
|
-
|
|
163
|
-
effect(() => {
|
|
164
|
-
reactiveObj.b = reactiveObj.a + 1
|
|
165
|
-
})
|
|
166
|
-
effect(() => {
|
|
167
|
-
reactiveObj.c = reactiveObj.b + 1
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
expect(reactiveObj.a).toBe(0)
|
|
171
|
-
expect(reactiveObj.b).toBe(1)
|
|
172
|
-
expect(reactiveObj.c).toBe(2)
|
|
173
|
-
|
|
174
|
-
reactiveObj.b = 5
|
|
175
|
-
expect(reactiveObj.a).toBe(0)
|
|
176
|
-
expect(reactiveObj.b).toBe(5)
|
|
177
|
-
expect(reactiveObj.c).toBe(6)
|
|
178
|
-
|
|
179
|
-
reactiveObj.a = 3
|
|
180
|
-
expect(reactiveObj.a).toBe(3)
|
|
181
|
-
expect(reactiveObj.b).toBe(4)
|
|
182
|
-
expect(reactiveObj.c).toBe(5)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('should allow re-entrant effects (create inner effect inside outer via untracked)', () => {
|
|
186
|
-
const state = reactive({ a: 0, b: 0 })
|
|
187
|
-
let outerRuns = 0
|
|
188
|
-
let innerRuns = 0
|
|
189
|
-
|
|
190
|
-
const stopOuter = effect(() => {
|
|
191
|
-
outerRuns++
|
|
192
|
-
state.a
|
|
193
|
-
// Create/refresh inner effect each time outer runs (re-entrancy)
|
|
194
|
-
// Use untracked to avoid nested-effect guard and dependency coupling
|
|
195
|
-
let stopInner: (() => void) | undefined
|
|
196
|
-
untracked(() => {
|
|
197
|
-
stopInner = effect(() => {
|
|
198
|
-
innerRuns++
|
|
199
|
-
state.b
|
|
200
|
-
})
|
|
201
|
-
})
|
|
202
|
-
// Immediately stop to avoid accumulating watchers
|
|
203
|
-
stopInner?.()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
expect(outerRuns).toBe(1)
|
|
207
|
-
expect(innerRuns).toBe(1)
|
|
208
|
-
|
|
209
|
-
state.a = 1
|
|
210
|
-
expect(outerRuns).toBe(2)
|
|
211
|
-
// inner created again due to re-entrancy
|
|
212
|
-
expect(innerRuns).toBe(2)
|
|
213
|
-
|
|
214
|
-
state.b = 1
|
|
215
|
-
// inner was stopped in the same tick; no rerun expected
|
|
216
|
-
expect(innerRuns).toBe(2)
|
|
217
|
-
|
|
218
|
-
stopOuter()
|
|
219
|
-
})
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
describe('effect cleanup', () => {
|
|
223
|
-
it('should return unwatch function', () => {
|
|
224
|
-
const reactiveObj = reactive({ count: 0 })
|
|
225
|
-
let effectCount = 0
|
|
226
|
-
|
|
227
|
-
const unwatch = effect(() => {
|
|
228
|
-
effectCount++
|
|
229
|
-
reactiveObj.count
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
expect(typeof unwatch).toBe('function')
|
|
233
|
-
expect(effectCount).toBe(1)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('should stop tracking when unwatched', () => {
|
|
237
|
-
const reactiveObj = reactive({ count: 0 })
|
|
238
|
-
let effectCount = 0
|
|
239
|
-
|
|
240
|
-
const unwatch = effect(() => {
|
|
241
|
-
effectCount++
|
|
242
|
-
reactiveObj.count
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
expect(effectCount).toBe(1)
|
|
246
|
-
|
|
247
|
-
unwatch()
|
|
248
|
-
|
|
249
|
-
reactiveObj.count = 5
|
|
250
|
-
expect(effectCount).toBe(1) // Should not trigger effect
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('should clean up dependencies on re-run', () => {
|
|
254
|
-
const reactiveObj = reactive({ count: 0, name: 'test' })
|
|
255
|
-
let effectCount = 0
|
|
256
|
-
|
|
257
|
-
effect(() => {
|
|
258
|
-
effectCount++
|
|
259
|
-
reactiveObj.count
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
expect(effectCount).toBe(1)
|
|
263
|
-
|
|
264
|
-
// Change the effect to only watch name
|
|
265
|
-
effect(() => {
|
|
266
|
-
effectCount++
|
|
267
|
-
reactiveObj.name
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
expect(effectCount).toBe(2)
|
|
271
|
-
|
|
272
|
-
reactiveObj.count = 5
|
|
273
|
-
expect(effectCount).toBe(3) // Should not trigger effect anymore
|
|
274
|
-
|
|
275
|
-
reactiveObj.name = 'new name'
|
|
276
|
-
expect(effectCount).toBe(4) // Should trigger effect
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
describe('error handling', () => {
|
|
281
|
-
it('should propagate errors from effects', () => {
|
|
282
|
-
const reactiveObj = reactive({ count: 0 })
|
|
283
|
-
let effectCount = 0
|
|
284
|
-
|
|
285
|
-
effect(() => {
|
|
286
|
-
effectCount++
|
|
287
|
-
reactiveObj.count
|
|
288
|
-
|
|
289
|
-
if (reactiveObj.count === 1) {
|
|
290
|
-
throw new Error('Test error')
|
|
291
|
-
}
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
expect(effectCount).toBe(1)
|
|
295
|
-
|
|
296
|
-
// This should throw an error when the effect runs
|
|
297
|
-
expect(() => {
|
|
298
|
-
reactiveObj.count = 1
|
|
299
|
-
}).toThrow('Test error')
|
|
300
|
-
|
|
301
|
-
expect(effectCount).toBe(2)
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('complex scenarios', () => {
|
|
306
|
-
it('should handle multiple reactive objects', () => {
|
|
307
|
-
const obj1 = reactive({ count: 0 })
|
|
308
|
-
const obj2 = reactive({ name: 'test' })
|
|
309
|
-
let effectCount = 0
|
|
310
|
-
|
|
311
|
-
effect(() => {
|
|
312
|
-
effectCount++
|
|
313
|
-
obj1.count
|
|
314
|
-
obj2.name
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
expect(effectCount).toBe(1)
|
|
318
|
-
|
|
319
|
-
obj1.count = 5
|
|
320
|
-
expect(effectCount).toBe(2)
|
|
321
|
-
|
|
322
|
-
obj2.name = 'new name'
|
|
323
|
-
expect(effectCount).toBe(3)
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('should handle object identity changes', () => {
|
|
327
|
-
const reactiveObj = reactive({ inner: { count: 0 } })
|
|
328
|
-
let effectCount = 0
|
|
329
|
-
|
|
330
|
-
effect(() => {
|
|
331
|
-
effectCount++
|
|
332
|
-
reactiveObj.inner.count
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
expect(effectCount).toBe(1)
|
|
336
|
-
|
|
337
|
-
reactiveObj.inner = { count: 5 }
|
|
338
|
-
expect(effectCount).toBe(2)
|
|
339
|
-
|
|
340
|
-
reactiveObj.inner.count = 10
|
|
341
|
-
expect(effectCount).toBe(3)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('should manage modifications during effect execution and should not trigger effect', () => {
|
|
345
|
-
const state = reactive({ count: 0, multiplier: 2 })
|
|
346
|
-
let effectCalls = 0
|
|
347
|
-
|
|
348
|
-
const stopEffect = effect(() => {
|
|
349
|
-
effectCalls++
|
|
350
|
-
// Change watched value during effect
|
|
351
|
-
state.count = state.count + 1
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
expect(effectCalls).toBe(1)
|
|
355
|
-
|
|
356
|
-
stopEffect()
|
|
357
|
-
})
|
|
358
|
-
})
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
describe('integration tests', () => {
|
|
362
|
-
it('should work with complex nested structures', () => {
|
|
363
|
-
const state = reactive({
|
|
364
|
-
user: {
|
|
365
|
-
profile: {
|
|
366
|
-
name: 'John',
|
|
367
|
-
age: 30,
|
|
368
|
-
},
|
|
369
|
-
settings: {
|
|
370
|
-
theme: 'dark',
|
|
371
|
-
notifications: true,
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
app: {
|
|
375
|
-
version: '1.0.0',
|
|
376
|
-
features: ['auth', 'chat'],
|
|
377
|
-
},
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
let profileEffectCount = 0
|
|
381
|
-
let settingsEffectCount = 0
|
|
382
|
-
let appEffectCount = 0
|
|
383
|
-
|
|
384
|
-
effect(() => {
|
|
385
|
-
profileEffectCount++
|
|
386
|
-
state.user.profile.name
|
|
387
|
-
state.user.profile.age
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
effect(() => {
|
|
391
|
-
settingsEffectCount++
|
|
392
|
-
state.user.settings.theme
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
effect(() => {
|
|
396
|
-
appEffectCount++
|
|
397
|
-
state.app.version
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
expect(profileEffectCount).toBe(1)
|
|
401
|
-
expect(settingsEffectCount).toBe(1)
|
|
402
|
-
expect(appEffectCount).toBe(1)
|
|
403
|
-
|
|
404
|
-
// Change profile
|
|
405
|
-
state.user.profile.name = 'Jane'
|
|
406
|
-
expect(profileEffectCount).toBe(2)
|
|
407
|
-
expect(settingsEffectCount).toBe(1)
|
|
408
|
-
expect(appEffectCount).toBe(1)
|
|
409
|
-
|
|
410
|
-
// Change settings
|
|
411
|
-
state.user.settings.theme = 'light'
|
|
412
|
-
expect(profileEffectCount).toBe(2)
|
|
413
|
-
expect(settingsEffectCount).toBe(2)
|
|
414
|
-
expect(appEffectCount).toBe(1)
|
|
415
|
-
|
|
416
|
-
// Change app
|
|
417
|
-
state.app.version = '1.1.0'
|
|
418
|
-
expect(profileEffectCount).toBe(2)
|
|
419
|
-
expect(settingsEffectCount).toBe(2)
|
|
420
|
-
expect(appEffectCount).toBe(2)
|
|
421
|
-
})
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
describe('@reactive decorator', () => {
|
|
425
|
-
it('should make class instances reactive using decorator', () => {
|
|
426
|
-
@reactive
|
|
427
|
-
class TestClass {
|
|
428
|
-
count = 0
|
|
429
|
-
name = 'test'
|
|
430
|
-
|
|
431
|
-
increment() {
|
|
432
|
-
this.count++
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
setName(newName: string) {
|
|
436
|
-
this.name = newName
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const instance = new TestClass()
|
|
441
|
-
|
|
442
|
-
let effectCount = 0
|
|
443
|
-
effect(() => {
|
|
444
|
-
effectCount++
|
|
445
|
-
instance.count
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
expect(effectCount).toBe(1)
|
|
449
|
-
expect(instance.count).toBe(0)
|
|
450
|
-
|
|
451
|
-
instance.increment()
|
|
452
|
-
expect(effectCount).toBe(2)
|
|
453
|
-
expect(instance.count).toBe(1)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
it('should track property changes on reactive class instances', () => {
|
|
457
|
-
@reactive
|
|
458
|
-
class User {
|
|
459
|
-
name = 'John'
|
|
460
|
-
age = 30
|
|
461
|
-
|
|
462
|
-
updateProfile(newName: string, newAge: number) {
|
|
463
|
-
this.name = newName
|
|
464
|
-
this.age = newAge
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const user = new User()
|
|
469
|
-
|
|
470
|
-
let nameEffectCount = 0
|
|
471
|
-
let ageEffectCount = 0
|
|
472
|
-
|
|
473
|
-
effect(() => {
|
|
474
|
-
nameEffectCount++
|
|
475
|
-
user.name
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
effect(() => {
|
|
479
|
-
ageEffectCount++
|
|
480
|
-
user.age
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
expect(nameEffectCount).toBe(1)
|
|
484
|
-
expect(ageEffectCount).toBe(1)
|
|
485
|
-
|
|
486
|
-
user.updateProfile('Jane', 25)
|
|
487
|
-
expect(nameEffectCount).toBe(2)
|
|
488
|
-
expect(ageEffectCount).toBe(2)
|
|
489
|
-
expect(user.name).toBe('Jane')
|
|
490
|
-
expect(user.age).toBe(25)
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
it('should work with inheritance', () => {
|
|
494
|
-
// Suppress the expected warning for this test
|
|
495
|
-
const originalWarn = reactiveOptions.warn
|
|
496
|
-
reactiveOptions.warn = () => {}
|
|
497
|
-
try {
|
|
498
|
-
@reactive
|
|
499
|
-
class Animal {
|
|
500
|
-
species = 'unknown'
|
|
501
|
-
energy = 100
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
class Dog extends Animal {
|
|
505
|
-
breed = 'mixed'
|
|
506
|
-
bark() {
|
|
507
|
-
this.energy -= 10
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const dog = new Dog()
|
|
512
|
-
|
|
513
|
-
let energyEffectCount = 0
|
|
514
|
-
effect(() => {
|
|
515
|
-
energyEffectCount++
|
|
516
|
-
dog.energy
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
expect(energyEffectCount).toBe(1)
|
|
520
|
-
expect(dog.energy).toBe(100)
|
|
521
|
-
|
|
522
|
-
dog.bark()
|
|
523
|
-
expect(energyEffectCount).toBe(2)
|
|
524
|
-
expect(dog.energy).toBe(90)
|
|
525
|
-
} finally {
|
|
526
|
-
// Restore original warn function
|
|
527
|
-
reactiveOptions.warn = originalWarn
|
|
528
|
-
}
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
it('should handle method calls that modify properties', () => {
|
|
532
|
-
@reactive
|
|
533
|
-
class Counter {
|
|
534
|
-
value = 0
|
|
535
|
-
|
|
536
|
-
add(amount: number) {
|
|
537
|
-
this.value += amount
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
reset() {
|
|
541
|
-
this.value = 0
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const counter = new Counter()
|
|
546
|
-
|
|
547
|
-
let effectCount = 0
|
|
548
|
-
effect(() => {
|
|
549
|
-
effectCount++
|
|
550
|
-
counter.value
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
expect(effectCount).toBe(1)
|
|
554
|
-
expect(counter.value).toBe(0)
|
|
555
|
-
|
|
556
|
-
counter.add(5)
|
|
557
|
-
expect(effectCount).toBe(2)
|
|
558
|
-
expect(counter.value).toBe(5)
|
|
559
|
-
|
|
560
|
-
counter.reset()
|
|
561
|
-
expect(effectCount).toBe(3)
|
|
562
|
-
expect(counter.value).toBe(0)
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
it('should work with functional syntax', () => {
|
|
566
|
-
class TestClass {
|
|
567
|
-
count = 0
|
|
568
|
-
name = 'test'
|
|
569
|
-
|
|
570
|
-
increment() {
|
|
571
|
-
this.count++
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const ReactiveTestClass = reactive(TestClass)
|
|
576
|
-
const instance = new ReactiveTestClass()
|
|
577
|
-
|
|
578
|
-
let effectCount = 0
|
|
579
|
-
effect(() => {
|
|
580
|
-
effectCount++
|
|
581
|
-
instance.count
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
expect(effectCount).toBe(1)
|
|
585
|
-
expect(instance.count).toBe(0)
|
|
586
|
-
|
|
587
|
-
instance.increment()
|
|
588
|
-
expect(effectCount).toBe(2)
|
|
589
|
-
expect(instance.count).toBe(1)
|
|
590
|
-
})
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
describe('ReactiveBase', () => {
|
|
594
|
-
it('should make classes extending ReactiveBase reactive when decorated', () => {
|
|
595
|
-
class BaseClass extends ReactiveBase {
|
|
596
|
-
baseProp = 'base'
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
@reactive
|
|
600
|
-
class DerivedClass extends BaseClass {
|
|
601
|
-
derivedProp = 'derived'
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const instance = new DerivedClass()
|
|
605
|
-
|
|
606
|
-
let effectCount = 0
|
|
607
|
-
effect(() => {
|
|
608
|
-
effectCount++
|
|
609
|
-
instance.baseProp
|
|
610
|
-
instance.derivedProp
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
expect(effectCount).toBe(1)
|
|
614
|
-
expect(instance.baseProp).toBe('base')
|
|
615
|
-
expect(instance.derivedProp).toBe('derived')
|
|
616
|
-
|
|
617
|
-
instance.baseProp = 'new base'
|
|
618
|
-
expect(effectCount).toBe(2)
|
|
619
|
-
|
|
620
|
-
instance.derivedProp = 'new derived'
|
|
621
|
-
expect(effectCount).toBe(3)
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
it('should solve constructor reactivity issues', () => {
|
|
625
|
-
class BaseClass extends ReactiveBase {
|
|
626
|
-
value = 0
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
@reactive
|
|
630
|
-
class TestClass extends BaseClass {
|
|
631
|
-
constructor() {
|
|
632
|
-
super()
|
|
633
|
-
// In constructor, 'this' is not yet reactive
|
|
634
|
-
// But ReactiveBase ensures the returned instance is reactive
|
|
635
|
-
this.value = 42
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const instance = new TestClass()
|
|
640
|
-
|
|
641
|
-
let effectCount = 0
|
|
642
|
-
effect(() => {
|
|
643
|
-
effectCount++
|
|
644
|
-
instance.value
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
expect(effectCount).toBe(1)
|
|
648
|
-
expect(instance.value).toBe(42)
|
|
649
|
-
|
|
650
|
-
instance.value = 100
|
|
651
|
-
expect(effectCount).toBe(2)
|
|
652
|
-
expect(instance.value).toBe(100)
|
|
653
|
-
})
|
|
654
|
-
|
|
655
|
-
it('should work with complex inheritance trees', () => {
|
|
656
|
-
class GameObject extends ReactiveBase {
|
|
657
|
-
id = 'game-object'
|
|
658
|
-
position = { x: 0, y: 0 }
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
class Entity extends GameObject {
|
|
662
|
-
health = 100
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
@reactive
|
|
666
|
-
class Player extends Entity {
|
|
667
|
-
name = 'Player'
|
|
668
|
-
level = 1
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const player = new Player()
|
|
672
|
-
|
|
673
|
-
let positionEffectCount = 0
|
|
674
|
-
let healthEffectCount = 0
|
|
675
|
-
let levelEffectCount = 0
|
|
676
|
-
|
|
677
|
-
effect(() => {
|
|
678
|
-
positionEffectCount++
|
|
679
|
-
player.position.x
|
|
680
|
-
player.position.y
|
|
681
|
-
})
|
|
682
|
-
|
|
683
|
-
effect(() => {
|
|
684
|
-
healthEffectCount++
|
|
685
|
-
player.health
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
effect(() => {
|
|
689
|
-
levelEffectCount++
|
|
690
|
-
player.level
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
expect(positionEffectCount).toBe(1)
|
|
694
|
-
expect(healthEffectCount).toBe(1)
|
|
695
|
-
expect(levelEffectCount).toBe(1)
|
|
696
|
-
|
|
697
|
-
player.position.x = 10
|
|
698
|
-
expect(positionEffectCount).toBe(2)
|
|
699
|
-
expect(healthEffectCount).toBe(1)
|
|
700
|
-
expect(levelEffectCount).toBe(1)
|
|
701
|
-
|
|
702
|
-
player.health = 80
|
|
703
|
-
expect(positionEffectCount).toBe(2)
|
|
704
|
-
expect(healthEffectCount).toBe(2)
|
|
705
|
-
expect(levelEffectCount).toBe(1)
|
|
706
|
-
|
|
707
|
-
player.level = 2
|
|
708
|
-
expect(positionEffectCount).toBe(2)
|
|
709
|
-
expect(healthEffectCount).toBe(2)
|
|
710
|
-
expect(levelEffectCount).toBe(2)
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
it('should not affect classes that do not extend ReactiveBase', () => {
|
|
714
|
-
class RegularClass {
|
|
715
|
-
value = 0
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
@reactive
|
|
719
|
-
class TestClass extends RegularClass {
|
|
720
|
-
otherValue = 1
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const instance = new TestClass()
|
|
724
|
-
|
|
725
|
-
let effectCount = 0
|
|
726
|
-
effect(() => {
|
|
727
|
-
effectCount++
|
|
728
|
-
instance.value
|
|
729
|
-
instance.otherValue
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
expect(effectCount).toBe(1)
|
|
733
|
-
|
|
734
|
-
instance.value = 10
|
|
735
|
-
expect(effectCount).toBe(2)
|
|
736
|
-
|
|
737
|
-
instance.otherValue = 20
|
|
738
|
-
expect(effectCount).toBe(3)
|
|
739
|
-
})
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
describe('Legacy Reactive mixin', () => {
|
|
743
|
-
it('should still work for backward compatibility', () => {
|
|
744
|
-
class TestClass {
|
|
745
|
-
count = 0
|
|
746
|
-
name = 'test'
|
|
747
|
-
|
|
748
|
-
increment() {
|
|
749
|
-
this.count++
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
setName(newName: string) {
|
|
753
|
-
this.name = newName
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const ReactiveTestClass = reactive(TestClass)
|
|
758
|
-
const instance = new ReactiveTestClass()
|
|
759
|
-
|
|
760
|
-
let effectCount = 0
|
|
761
|
-
effect(() => {
|
|
762
|
-
effectCount++
|
|
763
|
-
instance.count
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
expect(effectCount).toBe(1)
|
|
767
|
-
expect(instance.count).toBe(0)
|
|
768
|
-
|
|
769
|
-
instance.increment()
|
|
770
|
-
expect(effectCount).toBe(2)
|
|
771
|
-
expect(instance.count).toBe(1)
|
|
772
|
-
})
|
|
773
|
-
|
|
774
|
-
it('should warn when used with inheritance', () => {
|
|
775
|
-
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
776
|
-
|
|
777
|
-
class BaseClass {
|
|
778
|
-
baseProp = 'base'
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const ReactiveBaseClass = reactive(BaseClass)
|
|
782
|
-
|
|
783
|
-
// Create a class that extends the reactive class
|
|
784
|
-
class DerivedClass extends ReactiveBaseClass {
|
|
785
|
-
derivedProp = 'derived'
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const _instance = new DerivedClass()
|
|
789
|
-
|
|
790
|
-
// The warning should be triggered when creating the instance
|
|
791
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('has been inherited by'))
|
|
792
|
-
|
|
793
|
-
consoleSpy.mockRestore()
|
|
794
|
-
})
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
describe('non-reactive functionality', () => {
|
|
798
|
-
describe('markNonReactive', () => {
|
|
799
|
-
it('should mark individual objects as non-reactive', () => {
|
|
800
|
-
const obj = { count: 0, name: 'test' }
|
|
801
|
-
unreactive(obj)
|
|
802
|
-
|
|
803
|
-
expect(isNonReactive(obj)).toBe(true)
|
|
804
|
-
expect(reactive(obj)).toBe(obj) // Should not create a proxy
|
|
805
|
-
expect(isReactive(obj)).toBe(false)
|
|
806
|
-
})
|
|
807
|
-
|
|
808
|
-
it('should not affect other objects', () => {
|
|
809
|
-
const obj1 = { count: 0 }
|
|
810
|
-
const obj2 = { count: 0 }
|
|
811
|
-
|
|
812
|
-
unreactive(obj1)
|
|
813
|
-
|
|
814
|
-
expect(isNonReactive(obj1)).toBe(true)
|
|
815
|
-
expect(isNonReactive(obj2)).toBe(false)
|
|
816
|
-
|
|
817
|
-
const reactiveObj2 = reactive(obj2)
|
|
818
|
-
expect(isReactive(reactiveObj2)).toBe(true)
|
|
819
|
-
})
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
describe('markNonReactiveClass', () => {
|
|
823
|
-
it('should mark entire classes as non-reactive', () => {
|
|
824
|
-
class TestClass {
|
|
825
|
-
count = 0
|
|
826
|
-
name = 'test'
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
unreactive(TestClass)
|
|
830
|
-
|
|
831
|
-
const instance1 = new TestClass()
|
|
832
|
-
const instance2 = new TestClass()
|
|
833
|
-
|
|
834
|
-
expect(isNonReactive(instance1)).toBe(true)
|
|
835
|
-
expect(isNonReactive(instance2)).toBe(true)
|
|
836
|
-
expect(reactive(instance1)).toBe(instance1)
|
|
837
|
-
expect(reactive(instance2)).toBe(instance2)
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
it('should work with inheritance', () => {
|
|
841
|
-
class BaseClass {
|
|
842
|
-
baseProp = 'base'
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
class DerivedClass extends BaseClass {
|
|
846
|
-
derivedProp = 'derived'
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
unreactive(BaseClass)
|
|
850
|
-
|
|
851
|
-
const baseInstance = new BaseClass()
|
|
852
|
-
const derivedInstance = new DerivedClass()
|
|
853
|
-
|
|
854
|
-
expect(isNonReactive(baseInstance)).toBe(true)
|
|
855
|
-
expect(isNonReactive(derivedInstance)).toBe(true) // Inherits non-reactive status
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
it('should not affect other classes', () => {
|
|
859
|
-
class NonReactiveClass {
|
|
860
|
-
prop = 'non-reactive'
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
class ReactiveClass {
|
|
864
|
-
prop = 'reactive'
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
unreactive(NonReactiveClass)
|
|
868
|
-
|
|
869
|
-
const unreactiveInstance = new NonReactiveClass()
|
|
870
|
-
const reactiveInstance = new ReactiveClass()
|
|
871
|
-
|
|
872
|
-
expect(isNonReactive(unreactiveInstance)).toBe(true)
|
|
873
|
-
expect(isNonReactive(reactiveInstance)).toBe(false)
|
|
874
|
-
|
|
875
|
-
const reactiveReactiveInstance = reactive(reactiveInstance)
|
|
876
|
-
expect(isReactive(reactiveReactiveInstance)).toBe(true)
|
|
877
|
-
})
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
describe('NonReactive symbol (internal)', () => {
|
|
881
|
-
it('should mark objects with symbol as non-reactive', () => {
|
|
882
|
-
const obj: any = { count: 0 }
|
|
883
|
-
// Since we can't access the internal symbol, test the behavior indirectly
|
|
884
|
-
// by using the public markNonReactive function
|
|
885
|
-
unreactive(obj)
|
|
886
|
-
|
|
887
|
-
expect(isNonReactive(obj)).toBe(true)
|
|
888
|
-
expect(reactive(obj)).toBe(obj)
|
|
889
|
-
expect(isReactive(obj)).toBe(false)
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
it('should work with the Reactive mixin', () => {
|
|
893
|
-
class TestClass {
|
|
894
|
-
count = 0
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Mark the class as non-reactive using the public API
|
|
898
|
-
unreactive(TestClass)
|
|
899
|
-
|
|
900
|
-
const ReactiveTestClass = reactive(TestClass)
|
|
901
|
-
const instance = new ReactiveTestClass()
|
|
902
|
-
|
|
903
|
-
expect(isNonReactive(instance)).toBe(true)
|
|
904
|
-
expect(isReactive(instance)).toBe(false)
|
|
905
|
-
})
|
|
906
|
-
})
|
|
907
|
-
|
|
908
|
-
describe('native objects', () => {
|
|
909
|
-
it('should not make Date objects reactive', () => {
|
|
910
|
-
const date = new Date()
|
|
911
|
-
expect(reactive(date)).toBe(date)
|
|
912
|
-
expect(isReactive(date)).toBe(false)
|
|
913
|
-
})
|
|
914
|
-
|
|
915
|
-
it('should not make RegExp objects reactive', () => {
|
|
916
|
-
const regex = /test/
|
|
917
|
-
expect(reactive(regex)).toBe(regex)
|
|
918
|
-
expect(isReactive(regex)).toBe(false)
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
it('should not make Error objects reactive', () => {
|
|
922
|
-
const error = new Error('test')
|
|
923
|
-
expect(reactive(error)).toBe(error)
|
|
924
|
-
expect(isReactive(error)).toBe(false)
|
|
925
|
-
})
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
describe('integration with existing reactive system', () => {
|
|
929
|
-
it('should work with effects on non-reactive objects', () => {
|
|
930
|
-
const obj = { count: 0 }
|
|
931
|
-
unreactive(obj)
|
|
932
|
-
|
|
933
|
-
let effectCount = 0
|
|
934
|
-
effect(() => {
|
|
935
|
-
effectCount++
|
|
936
|
-
obj.count // Accessing non-reactive object
|
|
937
|
-
})
|
|
938
|
-
|
|
939
|
-
expect(effectCount).toBe(1)
|
|
940
|
-
|
|
941
|
-
obj.count = 5
|
|
942
|
-
expect(effectCount).toBe(1) // Should not trigger effect
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
it('should allow mixing reactive and non-reactive objects', () => {
|
|
946
|
-
const reactiveObj = reactive({ count: 0 })
|
|
947
|
-
const unreactiveObj = { name: 'test' }
|
|
948
|
-
unreactive(unreactiveObj)
|
|
949
|
-
|
|
950
|
-
let effectCount = 0
|
|
951
|
-
effect(() => {
|
|
952
|
-
effectCount++
|
|
953
|
-
reactiveObj.count
|
|
954
|
-
unreactiveObj.name
|
|
955
|
-
})
|
|
956
|
-
|
|
957
|
-
expect(effectCount).toBe(1)
|
|
958
|
-
|
|
959
|
-
reactiveObj.count = 5
|
|
960
|
-
expect(effectCount).toBe(2) // Should trigger effect
|
|
961
|
-
|
|
962
|
-
unreactiveObj.name = 'new name'
|
|
963
|
-
expect(effectCount).toBe(2) // Should not trigger effect
|
|
964
|
-
})
|
|
965
|
-
|
|
966
|
-
it('should work with the Reactive mixin and non-reactive classes', () => {
|
|
967
|
-
class NonReactiveClass {
|
|
968
|
-
count = 0
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
unreactive(NonReactiveClass)
|
|
972
|
-
|
|
973
|
-
const ReactiveNonReactiveClass = reactive(NonReactiveClass)
|
|
974
|
-
const instance = new ReactiveNonReactiveClass()
|
|
975
|
-
|
|
976
|
-
expect(isNonReactive(instance)).toBe(true)
|
|
977
|
-
expect(isReactive(instance)).toBe(false)
|
|
978
|
-
|
|
979
|
-
let effectCount = 0
|
|
980
|
-
effect(() => {
|
|
981
|
-
effectCount++
|
|
982
|
-
instance.count
|
|
983
|
-
})
|
|
984
|
-
|
|
985
|
-
expect(effectCount).toBe(1)
|
|
986
|
-
|
|
987
|
-
instance.count = 5
|
|
988
|
-
expect(effectCount).toBe(1) // Should not trigger effect since it's non-reactive
|
|
989
|
-
})
|
|
990
|
-
})
|
|
991
|
-
})
|
|
992
|
-
|
|
993
|
-
describe('@unreactive decorator', () => {
|
|
994
|
-
describe('class-level decorator syntax', () => {
|
|
995
|
-
it('should mark properties as unreactive using class-level syntax', () => {
|
|
996
|
-
@unreactive('unreactiveProp')
|
|
997
|
-
class TestClass {
|
|
998
|
-
unreactiveProp = 'test'
|
|
999
|
-
|
|
1000
|
-
reactiveProp = 'reactive'
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const instance = new TestClass()
|
|
1004
|
-
const reactiveInstance = reactive(instance)
|
|
1005
|
-
|
|
1006
|
-
// The unreactive property should not trigger effects
|
|
1007
|
-
let effectCount = 0
|
|
1008
|
-
effect(() => {
|
|
1009
|
-
effectCount++
|
|
1010
|
-
reactiveInstance.unreactiveProp
|
|
1011
|
-
reactiveInstance.reactiveProp
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
expect(effectCount).toBe(1)
|
|
1015
|
-
|
|
1016
|
-
// Changing unreactive property should not trigger effect
|
|
1017
|
-
reactiveInstance.unreactiveProp = 'new value'
|
|
1018
|
-
expect(effectCount).toBe(1)
|
|
1019
|
-
|
|
1020
|
-
// Changing reactive property should trigger effect
|
|
1021
|
-
reactiveInstance.reactiveProp = 'new reactive value'
|
|
1022
|
-
expect(effectCount).toBe(2)
|
|
1023
|
-
})
|
|
1024
|
-
|
|
1025
|
-
it('should work with multiple unreactive properties', () => {
|
|
1026
|
-
@unreactive('prop1', 'prop2')
|
|
1027
|
-
class TestClass {
|
|
1028
|
-
prop1 = 'value1'
|
|
1029
|
-
|
|
1030
|
-
prop2 = 'value2'
|
|
1031
|
-
|
|
1032
|
-
reactiveProp = 'reactive'
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const instance = new TestClass()
|
|
1036
|
-
const reactiveInstance = reactive(instance)
|
|
1037
|
-
|
|
1038
|
-
let effectCount = 0
|
|
1039
|
-
effect(() => {
|
|
1040
|
-
effectCount++
|
|
1041
|
-
reactiveInstance.prop1
|
|
1042
|
-
reactiveInstance.prop2
|
|
1043
|
-
reactiveInstance.reactiveProp
|
|
1044
|
-
})
|
|
1045
|
-
|
|
1046
|
-
expect(effectCount).toBe(1)
|
|
1047
|
-
|
|
1048
|
-
// Changing unreactive properties should not trigger effect
|
|
1049
|
-
reactiveInstance.prop1 = 'new value1'
|
|
1050
|
-
reactiveInstance.prop2 = 'new value2'
|
|
1051
|
-
expect(effectCount).toBe(1)
|
|
1052
|
-
|
|
1053
|
-
// Changing reactive property should trigger effect
|
|
1054
|
-
reactiveInstance.reactiveProp = 'new reactive value'
|
|
1055
|
-
expect(effectCount).toBe(2)
|
|
1056
|
-
})
|
|
1057
|
-
|
|
1058
|
-
it('should work with symbol properties', () => {
|
|
1059
|
-
const sym = Symbol('test')
|
|
1060
|
-
|
|
1061
|
-
@unreactive(sym)
|
|
1062
|
-
class TestClass {
|
|
1063
|
-
[sym] = 'symbol value'
|
|
1064
|
-
|
|
1065
|
-
reactiveProp = 'reactive'
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
const instance = new TestClass()
|
|
1069
|
-
const reactiveInstance = reactive(instance)
|
|
1070
|
-
|
|
1071
|
-
let effectCount = 0
|
|
1072
|
-
effect(() => {
|
|
1073
|
-
effectCount++
|
|
1074
|
-
reactiveInstance[sym]
|
|
1075
|
-
reactiveInstance.reactiveProp
|
|
1076
|
-
})
|
|
1077
|
-
|
|
1078
|
-
expect(effectCount).toBe(1)
|
|
1079
|
-
|
|
1080
|
-
// Changing unreactive symbol property should not trigger effect
|
|
1081
|
-
reactiveInstance[sym] = 'new symbol value'
|
|
1082
|
-
expect(effectCount).toBe(1)
|
|
1083
|
-
|
|
1084
|
-
// Changing reactive property should trigger effect
|
|
1085
|
-
reactiveInstance.reactiveProp = 'new reactive value'
|
|
1086
|
-
expect(effectCount).toBe(2)
|
|
1087
|
-
})
|
|
1088
|
-
})
|
|
1089
|
-
|
|
1090
|
-
describe('integration with reactive system', () => {
|
|
1091
|
-
it('should bypass reactivity completely for unreactive properties', () => {
|
|
1092
|
-
@unreactive('unreactiveProp')
|
|
1093
|
-
class TestClass {
|
|
1094
|
-
unreactiveProp = { nested: 'value' }
|
|
1095
|
-
|
|
1096
|
-
reactiveProp = { nested: 'reactive' }
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
const instance = new TestClass()
|
|
1100
|
-
const reactiveInstance = reactive(instance)
|
|
1101
|
-
|
|
1102
|
-
let effectCount = 0
|
|
1103
|
-
effect(() => {
|
|
1104
|
-
effectCount++
|
|
1105
|
-
reactiveInstance.unreactiveProp.nested
|
|
1106
|
-
reactiveInstance.reactiveProp.nested
|
|
1107
|
-
})
|
|
1108
|
-
|
|
1109
|
-
expect(effectCount).toBe(1)
|
|
1110
|
-
|
|
1111
|
-
// Changing nested unreactive property should not trigger effect
|
|
1112
|
-
reactiveInstance.unreactiveProp.nested = 'new nested value'
|
|
1113
|
-
expect(effectCount).toBe(1)
|
|
1114
|
-
|
|
1115
|
-
// Changing nested reactive property should trigger effect
|
|
1116
|
-
reactiveInstance.reactiveProp.nested = 'new nested reactive value'
|
|
1117
|
-
expect(effectCount).toBe(2)
|
|
1118
|
-
})
|
|
1119
|
-
|
|
1120
|
-
it('should work with regular properties', () => {
|
|
1121
|
-
@unreactive('unreactiveProp')
|
|
1122
|
-
class TestClass {
|
|
1123
|
-
unreactiveProp = 'test'
|
|
1124
|
-
|
|
1125
|
-
reactiveProp = 'reactive'
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const instance = new TestClass()
|
|
1129
|
-
const reactiveInstance = reactive(instance)
|
|
1130
|
-
|
|
1131
|
-
let effectCount = 0
|
|
1132
|
-
effect(() => {
|
|
1133
|
-
effectCount++
|
|
1134
|
-
reactiveInstance.unreactiveProp
|
|
1135
|
-
reactiveInstance.reactiveProp
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
expect(effectCount).toBe(1)
|
|
1139
|
-
|
|
1140
|
-
// Changing unreactive property should not trigger effect
|
|
1141
|
-
reactiveInstance.unreactiveProp = 'new value'
|
|
1142
|
-
expect(effectCount).toBe(1)
|
|
1143
|
-
|
|
1144
|
-
// Changing reactive property should trigger effect
|
|
1145
|
-
reactiveInstance.reactiveProp = 'new reactive value'
|
|
1146
|
-
expect(effectCount).toBe(2)
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
it('should work with inheritance', () => {
|
|
1150
|
-
@unreactive('baseUnreactiveProp')
|
|
1151
|
-
class BaseClass {
|
|
1152
|
-
baseUnreactiveProp = 'base unreactive'
|
|
1153
|
-
|
|
1154
|
-
baseReactiveProp = 'base reactive'
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
@unreactive('derivedUnreactiveProp')
|
|
1158
|
-
class DerivedClass extends BaseClass {
|
|
1159
|
-
derivedUnreactiveProp = 'derived unreactive'
|
|
1160
|
-
|
|
1161
|
-
derivedReactiveProp = 'derived reactive'
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const instance = new DerivedClass()
|
|
1165
|
-
const reactiveInstance = reactive(instance)
|
|
1166
|
-
|
|
1167
|
-
let effectCount = 0
|
|
1168
|
-
effect(() => {
|
|
1169
|
-
effectCount++
|
|
1170
|
-
reactiveInstance.baseUnreactiveProp
|
|
1171
|
-
reactiveInstance.baseReactiveProp
|
|
1172
|
-
reactiveInstance.derivedUnreactiveProp
|
|
1173
|
-
reactiveInstance.derivedReactiveProp
|
|
1174
|
-
})
|
|
1175
|
-
|
|
1176
|
-
expect(effectCount).toBe(1)
|
|
1177
|
-
|
|
1178
|
-
// Changing unreactive properties should not trigger effect
|
|
1179
|
-
reactiveInstance.baseUnreactiveProp = 'new base unreactive'
|
|
1180
|
-
reactiveInstance.derivedUnreactiveProp = 'new derived unreactive'
|
|
1181
|
-
expect(effectCount).toBe(1)
|
|
1182
|
-
|
|
1183
|
-
// Changing reactive properties should trigger effect
|
|
1184
|
-
reactiveInstance.baseReactiveProp = 'new base reactive'
|
|
1185
|
-
expect(effectCount).toBe(2)
|
|
1186
|
-
reactiveInstance.derivedReactiveProp = 'new derived reactive'
|
|
1187
|
-
expect(effectCount).toBe(3)
|
|
1188
|
-
})
|
|
1189
|
-
})
|
|
1190
|
-
|
|
1191
|
-
describe('edge cases', () => {
|
|
1192
|
-
it('should handle undefined and null values', () => {
|
|
1193
|
-
@unreactive('unreactiveProp')
|
|
1194
|
-
class TestClass {
|
|
1195
|
-
unreactiveProp: any = undefined
|
|
1196
|
-
|
|
1197
|
-
reactiveProp: any = null
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const instance = new TestClass()
|
|
1201
|
-
const reactiveInstance = reactive(instance)
|
|
1202
|
-
|
|
1203
|
-
let effectCount = 0
|
|
1204
|
-
effect(() => {
|
|
1205
|
-
effectCount++
|
|
1206
|
-
reactiveInstance.unreactiveProp
|
|
1207
|
-
reactiveInstance.reactiveProp
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
expect(effectCount).toBe(1)
|
|
1211
|
-
|
|
1212
|
-
// Setting values should not trigger effects for unreactive properties
|
|
1213
|
-
reactiveInstance.unreactiveProp = 'new value'
|
|
1214
|
-
expect(effectCount).toBe(1)
|
|
1215
|
-
|
|
1216
|
-
// Setting values should trigger effects for reactive properties
|
|
1217
|
-
reactiveInstance.reactiveProp = 'new value'
|
|
1218
|
-
expect(effectCount).toBe(2)
|
|
1219
|
-
})
|
|
1220
|
-
|
|
1221
|
-
it('should work with computed property names', () => {
|
|
1222
|
-
const propName = 'computed'
|
|
1223
|
-
|
|
1224
|
-
@unreactive(propName)
|
|
1225
|
-
class TestClass {
|
|
1226
|
-
[propName] = 'computed unreactive'
|
|
1227
|
-
|
|
1228
|
-
reactiveProp = 'reactive'
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
const instance = new TestClass()
|
|
1232
|
-
const reactiveInstance = reactive(instance)
|
|
1233
|
-
|
|
1234
|
-
let effectCount = 0
|
|
1235
|
-
effect(() => {
|
|
1236
|
-
effectCount++
|
|
1237
|
-
reactiveInstance[propName]
|
|
1238
|
-
reactiveInstance.reactiveProp
|
|
1239
|
-
})
|
|
1240
|
-
|
|
1241
|
-
expect(effectCount).toBe(1)
|
|
1242
|
-
|
|
1243
|
-
// Changing computed unreactive property should not trigger effect
|
|
1244
|
-
reactiveInstance[propName] = 'new computed unreactive'
|
|
1245
|
-
expect(effectCount).toBe(1)
|
|
1246
|
-
|
|
1247
|
-
// Changing reactive property should trigger effect
|
|
1248
|
-
reactiveInstance.reactiveProp = 'new reactive value'
|
|
1249
|
-
expect(effectCount).toBe(2)
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
it('should handle property deletion', () => {
|
|
1253
|
-
@unreactive('unreactiveProp')
|
|
1254
|
-
class TestClass {
|
|
1255
|
-
unreactiveProp?: string = 'test'
|
|
1256
|
-
|
|
1257
|
-
reactiveProp?: string = 'reactive'
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
const instance = new TestClass()
|
|
1261
|
-
const reactiveInstance = reactive(instance)
|
|
1262
|
-
|
|
1263
|
-
let effectCount = 0
|
|
1264
|
-
effect(() => {
|
|
1265
|
-
effectCount++
|
|
1266
|
-
reactiveInstance.unreactiveProp
|
|
1267
|
-
reactiveInstance.reactiveProp
|
|
1268
|
-
})
|
|
1269
|
-
|
|
1270
|
-
expect(effectCount).toBe(1)
|
|
1271
|
-
|
|
1272
|
-
// Deleting unreactive property should not trigger effect
|
|
1273
|
-
delete reactiveInstance.unreactiveProp
|
|
1274
|
-
expect(effectCount).toBe(1)
|
|
1275
|
-
|
|
1276
|
-
// Deleting reactive property should trigger effect
|
|
1277
|
-
delete reactiveInstance.reactiveProp
|
|
1278
|
-
expect(effectCount).toBe(2)
|
|
1279
|
-
})
|
|
1280
|
-
})
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
describe('effect reaction result', () => {
|
|
1284
|
-
it('should support recording the computed result each run (via effect return cleanup)', () => {
|
|
1285
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1286
|
-
|
|
1287
|
-
const received: number[] = []
|
|
1288
|
-
const stop = effect(() => {
|
|
1289
|
-
const sum = state.a + state.b
|
|
1290
|
-
received.push(sum)
|
|
1291
|
-
return () => {}
|
|
1292
|
-
})
|
|
1293
|
-
|
|
1294
|
-
// initial run
|
|
1295
|
-
expect(received).toEqual([3])
|
|
1296
|
-
|
|
1297
|
-
// update triggers rerun and new result
|
|
1298
|
-
state.a = 5
|
|
1299
|
-
expect(received).toEqual([3, 7])
|
|
1300
|
-
|
|
1301
|
-
// another update
|
|
1302
|
-
state.b = 10
|
|
1303
|
-
expect(received).toEqual([3, 7, 15])
|
|
1304
|
-
|
|
1305
|
-
stop()
|
|
1306
|
-
})
|
|
1307
|
-
})
|
|
1308
|
-
|
|
1309
|
-
describe('effect cleanup timing', () => {
|
|
1310
|
-
it('should run previous cleanup before the next execution', () => {
|
|
1311
|
-
const state = reactive({ v: 1 })
|
|
1312
|
-
|
|
1313
|
-
const calls: string[] = []
|
|
1314
|
-
effect(() => {
|
|
1315
|
-
calls.push(`run:${state.v}`)
|
|
1316
|
-
return () => calls.push(`cleanup:${state.v}`)
|
|
1317
|
-
})
|
|
1318
|
-
|
|
1319
|
-
// initial
|
|
1320
|
-
expect(calls).toEqual(['run:1'])
|
|
1321
|
-
|
|
1322
|
-
state.v = 2
|
|
1323
|
-
// cleanup for previous run must happen before new run is recorded
|
|
1324
|
-
// cleanup logs the current value at cleanup time (already updated)
|
|
1325
|
-
expect(calls).toEqual(['run:1', 'cleanup:2', 'run:2'])
|
|
1326
|
-
|
|
1327
|
-
state.v = 3
|
|
1328
|
-
expect(calls).toEqual(['run:1', 'cleanup:2', 'run:2', 'cleanup:3', 'run:3'])
|
|
1329
|
-
})
|
|
1330
|
-
})
|
|
1331
|
-
|
|
1332
|
-
describe('automatic effect cleanup', () => {
|
|
1333
|
-
function tick(ms: number = 100) {
|
|
1334
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
const gc = global.gc
|
|
1338
|
-
|
|
1339
|
-
async function collectGarbages() {
|
|
1340
|
-
await tick()
|
|
1341
|
-
gc!()
|
|
1342
|
-
await tick()
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
describe('parent-child effect cleanup', () => {
|
|
1346
|
-
it('should automatically clean up child effects when parent is cleaned up', () => {
|
|
1347
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1348
|
-
const cleanupCalls: string[] = []
|
|
1349
|
-
|
|
1350
|
-
const stopParent = effect(() => {
|
|
1351
|
-
state.a
|
|
1352
|
-
|
|
1353
|
-
// Create child effect
|
|
1354
|
-
effect(() => {
|
|
1355
|
-
state.b
|
|
1356
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1357
|
-
})
|
|
1358
|
-
|
|
1359
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1360
|
-
})
|
|
1361
|
-
|
|
1362
|
-
expect(cleanupCalls).toEqual([])
|
|
1363
|
-
|
|
1364
|
-
// Stop parent effect - should clean up both parent and child
|
|
1365
|
-
stopParent()
|
|
1366
|
-
expect(cleanupCalls).toEqual(['parent cleanup', 'child cleanup'])
|
|
1367
|
-
})
|
|
1368
|
-
|
|
1369
|
-
it('should clean up all nested child effects when parent is cleaned up', () => {
|
|
1370
|
-
const state = reactive({ a: 1, b: 2, c: 3 })
|
|
1371
|
-
const cleanupCalls: string[] = []
|
|
1372
|
-
|
|
1373
|
-
const stopParent = effect(() => {
|
|
1374
|
-
state.a
|
|
1375
|
-
|
|
1376
|
-
// Create child effect
|
|
1377
|
-
effect(() => {
|
|
1378
|
-
state.b
|
|
1379
|
-
|
|
1380
|
-
// Create grandchild effect
|
|
1381
|
-
effect(() => {
|
|
1382
|
-
state.c
|
|
1383
|
-
return () => cleanupCalls.push('grandchild cleanup')
|
|
1384
|
-
})
|
|
1385
|
-
|
|
1386
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1387
|
-
})
|
|
1388
|
-
|
|
1389
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1390
|
-
})
|
|
1391
|
-
|
|
1392
|
-
expect(cleanupCalls).toEqual([])
|
|
1393
|
-
|
|
1394
|
-
// Stop parent effect - should clean up all nested effects
|
|
1395
|
-
stopParent()
|
|
1396
|
-
expect(cleanupCalls).toEqual(['parent cleanup', 'child cleanup', 'grandchild cleanup'])
|
|
1397
|
-
})
|
|
1398
|
-
|
|
1399
|
-
it('should allow child effects to be cleaned up independently', () => {
|
|
1400
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1401
|
-
const cleanupCalls: string[] = []
|
|
1402
|
-
|
|
1403
|
-
const stopParent = effect(() => {
|
|
1404
|
-
state.a
|
|
1405
|
-
|
|
1406
|
-
// Create child effect and store its cleanup
|
|
1407
|
-
const stopChild = effect(() => {
|
|
1408
|
-
state.b
|
|
1409
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1410
|
-
})
|
|
1411
|
-
|
|
1412
|
-
// Clean up child independently
|
|
1413
|
-
stopChild()
|
|
1414
|
-
|
|
1415
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1416
|
-
})
|
|
1417
|
-
|
|
1418
|
-
expect(cleanupCalls).toEqual(['child cleanup'])
|
|
1419
|
-
|
|
1420
|
-
// Stop parent effect - should only clean up parent
|
|
1421
|
-
stopParent()
|
|
1422
|
-
expect(cleanupCalls).toEqual(['child cleanup', 'parent cleanup'])
|
|
1423
|
-
})
|
|
1424
|
-
|
|
1425
|
-
it('should clean up multiple child effects when parent is cleaned up', () => {
|
|
1426
|
-
const state = reactive({ a: 1, b: 2, c: 3 })
|
|
1427
|
-
const cleanupCalls: string[] = []
|
|
1428
|
-
|
|
1429
|
-
const stopParent = effect(() => {
|
|
1430
|
-
state.a
|
|
1431
|
-
|
|
1432
|
-
// Create multiple child effects
|
|
1433
|
-
effect(() => {
|
|
1434
|
-
state.b
|
|
1435
|
-
return () => cleanupCalls.push('child1 cleanup')
|
|
1436
|
-
})
|
|
1437
|
-
|
|
1438
|
-
effect(() => {
|
|
1439
|
-
state.c
|
|
1440
|
-
return () => cleanupCalls.push('child2 cleanup')
|
|
1441
|
-
})
|
|
1442
|
-
|
|
1443
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1444
|
-
})
|
|
1445
|
-
|
|
1446
|
-
expect(cleanupCalls).toEqual([])
|
|
1447
|
-
|
|
1448
|
-
// Stop parent effect - should clean up all children and parent
|
|
1449
|
-
stopParent()
|
|
1450
|
-
expect(cleanupCalls).toEqual(['parent cleanup', 'child1 cleanup', 'child2 cleanup'])
|
|
1451
|
-
})
|
|
1452
|
-
})
|
|
1453
|
-
|
|
1454
|
-
describe('garbage collection cleanup', () => {
|
|
1455
|
-
it('should clean up unreferenced top-level effects via GC', async () => {
|
|
1456
|
-
const state = reactive({ value: 1 })
|
|
1457
|
-
let cleanupCalled = false
|
|
1458
|
-
|
|
1459
|
-
// Create effect in a scope that will be garbage collected
|
|
1460
|
-
;(() => {
|
|
1461
|
-
const _x = effect(() => {
|
|
1462
|
-
state.value
|
|
1463
|
-
return () => {
|
|
1464
|
-
cleanupCalled = true
|
|
1465
|
-
}
|
|
1466
|
-
})
|
|
1467
|
-
})()
|
|
1468
|
-
|
|
1469
|
-
expect(cleanupCalled).toBe(false)
|
|
1470
|
-
|
|
1471
|
-
// Force garbage collection
|
|
1472
|
-
await collectGarbages()
|
|
1473
|
-
expect(cleanupCalled).toBe(true)
|
|
1474
|
-
})
|
|
1475
|
-
|
|
1476
|
-
it('should clean up parent and child effects when both are unreferenced', async () => {
|
|
1477
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1478
|
-
const cleanupCalls: string[] = []
|
|
1479
|
-
|
|
1480
|
-
// Create parent effect that creates a child, both unreferenced
|
|
1481
|
-
;(() => {
|
|
1482
|
-
effect(() => {
|
|
1483
|
-
state.a
|
|
1484
|
-
|
|
1485
|
-
// Create child effect
|
|
1486
|
-
effect(() => {
|
|
1487
|
-
state.b
|
|
1488
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1489
|
-
})
|
|
1490
|
-
|
|
1491
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1492
|
-
})
|
|
1493
|
-
})()
|
|
1494
|
-
|
|
1495
|
-
expect(cleanupCalls).toEqual([])
|
|
1496
|
-
|
|
1497
|
-
// Force garbage collection
|
|
1498
|
-
await collectGarbages()
|
|
1499
|
-
|
|
1500
|
-
// Both parent and child should be cleaned up
|
|
1501
|
-
expect(cleanupCalls).toContain('parent cleanup')
|
|
1502
|
-
expect(cleanupCalls).toContain('child cleanup')
|
|
1503
|
-
expect(cleanupCalls).toHaveLength(2)
|
|
1504
|
-
})
|
|
1505
|
-
|
|
1506
|
-
it('should clean up orphaned child effects when parent is unreferenced', async () => {
|
|
1507
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1508
|
-
const cleanupCalls: string[] = []
|
|
1509
|
-
|
|
1510
|
-
// Create parent effect that creates a child, both unreferenced
|
|
1511
|
-
;(() => {
|
|
1512
|
-
effect(() => {
|
|
1513
|
-
state.a
|
|
1514
|
-
|
|
1515
|
-
// Create child effect
|
|
1516
|
-
effect(() => {
|
|
1517
|
-
state.b
|
|
1518
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1519
|
-
})
|
|
1520
|
-
|
|
1521
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1522
|
-
})
|
|
1523
|
-
})()
|
|
1524
|
-
|
|
1525
|
-
expect(cleanupCalls).toEqual([])
|
|
1526
|
-
|
|
1527
|
-
// Force garbage collection - both should be cleaned up
|
|
1528
|
-
await collectGarbages()
|
|
1529
|
-
|
|
1530
|
-
expect(cleanupCalls).toContain('parent cleanup')
|
|
1531
|
-
expect(cleanupCalls).toContain('child cleanup')
|
|
1532
|
-
expect(cleanupCalls).toHaveLength(2)
|
|
1533
|
-
})
|
|
1534
|
-
|
|
1535
|
-
it('should handle child effect referenced but parent unreferenced', async () => {
|
|
1536
|
-
const state = reactive({ a: 1, b: 2 })
|
|
1537
|
-
const cleanupCalls: string[] = []
|
|
1538
|
-
|
|
1539
|
-
// Create parent effect that creates a child, but only keep reference to child
|
|
1540
|
-
let stopChild: (() => void) | undefined
|
|
1541
|
-
const createParentWithChild = () => {
|
|
1542
|
-
effect(() => {
|
|
1543
|
-
state.a
|
|
1544
|
-
|
|
1545
|
-
// Create child effect and store its cleanup function
|
|
1546
|
-
stopChild = effect(() => {
|
|
1547
|
-
state.b
|
|
1548
|
-
return () => cleanupCalls.push('child cleanup')
|
|
1549
|
-
})
|
|
1550
|
-
})
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
createParentWithChild()
|
|
1554
|
-
|
|
1555
|
-
expect(cleanupCalls).toEqual([])
|
|
1556
|
-
expect(stopChild).toBeDefined()
|
|
1557
|
-
|
|
1558
|
-
// Force garbage collection - parent should be cleaned up, child should remain
|
|
1559
|
-
await collectGarbages()
|
|
1560
|
-
|
|
1561
|
-
// The child effect should still be alive (parent was GCed but child is referenced)
|
|
1562
|
-
// Note: The child might be cleaned up if it's also unreferenced
|
|
1563
|
-
// This test demonstrates the mechanism, not the exact behavior
|
|
1564
|
-
|
|
1565
|
-
// Explicitly clean up child if it's still alive
|
|
1566
|
-
if (stopChild) {
|
|
1567
|
-
stopChild()
|
|
1568
|
-
expect(cleanupCalls).toContain('child cleanup')
|
|
1569
|
-
}
|
|
1570
|
-
})
|
|
1571
|
-
|
|
1572
|
-
it('should handle mixed explicit and GC cleanup', () => {
|
|
1573
|
-
const state = reactive({ a: 1, b: 2, c: 3 })
|
|
1574
|
-
const cleanupCalls: string[] = []
|
|
1575
|
-
|
|
1576
|
-
// Create parent effect
|
|
1577
|
-
const stopParent = effect(() => {
|
|
1578
|
-
state.a
|
|
1579
|
-
|
|
1580
|
-
// Create child that will be explicitly cleaned up
|
|
1581
|
-
const stopChild = effect(() => {
|
|
1582
|
-
state.b
|
|
1583
|
-
return () => cleanupCalls.push('explicit child cleanup')
|
|
1584
|
-
})
|
|
1585
|
-
|
|
1586
|
-
// Create child that will be GC cleaned up
|
|
1587
|
-
effect(() => {
|
|
1588
|
-
state.c
|
|
1589
|
-
return () => cleanupCalls.push('gc child cleanup')
|
|
1590
|
-
})
|
|
1591
|
-
|
|
1592
|
-
// Explicitly clean up first child
|
|
1593
|
-
stopChild()
|
|
1594
|
-
|
|
1595
|
-
return () => cleanupCalls.push('parent cleanup')
|
|
1596
|
-
})
|
|
1597
|
-
|
|
1598
|
-
expect(cleanupCalls).toEqual(['explicit child cleanup'])
|
|
1599
|
-
|
|
1600
|
-
// Stop parent - should clean up parent and all remaining children
|
|
1601
|
-
stopParent()
|
|
1602
|
-
expect(cleanupCalls).toEqual(['explicit child cleanup', 'parent cleanup', 'gc child cleanup'])
|
|
1603
|
-
})
|
|
1604
|
-
})
|
|
1605
|
-
|
|
1606
|
-
describe('cleanup behavior documentation', () => {
|
|
1607
|
-
it('should demonstrate that cleanup is optional but recommended for side effects', () => {
|
|
1608
|
-
const state = reactive({ value: 1 })
|
|
1609
|
-
let sideEffectExecuted = false
|
|
1610
|
-
|
|
1611
|
-
// Effect with side effect that should be cleaned up
|
|
1612
|
-
const stopEffect = effect(() => {
|
|
1613
|
-
state.value
|
|
1614
|
-
|
|
1615
|
-
// Simulate side effect (e.g., DOM manipulation, timers, etc.)
|
|
1616
|
-
const intervalId = setInterval(() => {
|
|
1617
|
-
sideEffectExecuted = true
|
|
1618
|
-
}, 100)
|
|
1619
|
-
|
|
1620
|
-
// Return cleanup function to prevent memory leaks
|
|
1621
|
-
return () => {
|
|
1622
|
-
clearInterval(intervalId)
|
|
1623
|
-
}
|
|
1624
|
-
})
|
|
1625
|
-
|
|
1626
|
-
// Effect is running, side effect should be active
|
|
1627
|
-
expect(sideEffectExecuted).toBe(false)
|
|
1628
|
-
|
|
1629
|
-
// Stop effect - cleanup should be called
|
|
1630
|
-
stopEffect()
|
|
1631
|
-
|
|
1632
|
-
// Wait a bit to ensure interval would have fired
|
|
1633
|
-
setTimeout(() => {
|
|
1634
|
-
expect(sideEffectExecuted).toBe(false) // Should still be false due to cleanup
|
|
1635
|
-
}, 150)
|
|
1636
|
-
})
|
|
1637
|
-
|
|
1638
|
-
it('should show that effects can be stored and remembered for later cleanup', () => {
|
|
1639
|
-
const state = reactive({ value: 1 })
|
|
1640
|
-
const activeEffects: (() => void)[] = []
|
|
1641
|
-
const cleanupCalls: string[] = []
|
|
1642
|
-
|
|
1643
|
-
// Create multiple effects and store their cleanup functions
|
|
1644
|
-
for (let i = 0; i < 3; i++) {
|
|
1645
|
-
const stopEffect = effect(() => {
|
|
1646
|
-
state.value
|
|
1647
|
-
return () => cleanupCalls.push(`effect ${i} cleanup`)
|
|
1648
|
-
})
|
|
1649
|
-
activeEffects.push(stopEffect)
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
expect(cleanupCalls).toEqual([])
|
|
1653
|
-
|
|
1654
|
-
// Clean up all effects at once
|
|
1655
|
-
activeEffects.forEach((stop) => stop())
|
|
1656
|
-
|
|
1657
|
-
expect(cleanupCalls).toHaveLength(3)
|
|
1658
|
-
expect(cleanupCalls).toContain('effect 0 cleanup')
|
|
1659
|
-
expect(cleanupCalls).toContain('effect 1 cleanup')
|
|
1660
|
-
expect(cleanupCalls).toContain('effect 2 cleanup')
|
|
1661
|
-
})
|
|
1662
|
-
})
|
|
1663
|
-
})
|