@pyreon/core 0.24.4 → 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.
Files changed (56) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +53 -31
  3. package/package.json +2 -6
  4. package/src/compat-marker.ts +0 -79
  5. package/src/compat-shared.ts +0 -80
  6. package/src/component.ts +0 -98
  7. package/src/context.ts +0 -349
  8. package/src/defer.ts +0 -279
  9. package/src/dynamic.ts +0 -32
  10. package/src/env.d.ts +0 -6
  11. package/src/error-boundary.ts +0 -90
  12. package/src/for.ts +0 -51
  13. package/src/h.ts +0 -80
  14. package/src/index.ts +0 -80
  15. package/src/jsx-dev-runtime.ts +0 -2
  16. package/src/jsx-runtime.ts +0 -747
  17. package/src/lazy.ts +0 -25
  18. package/src/lifecycle.ts +0 -152
  19. package/src/manifest.ts +0 -579
  20. package/src/map-array.ts +0 -42
  21. package/src/portal.ts +0 -39
  22. package/src/props.ts +0 -269
  23. package/src/ref.ts +0 -32
  24. package/src/show.ts +0 -121
  25. package/src/style.ts +0 -102
  26. package/src/suspense.ts +0 -52
  27. package/src/telemetry.ts +0 -120
  28. package/src/tests/compat-marker.test.ts +0 -96
  29. package/src/tests/compat-shared.test.ts +0 -99
  30. package/src/tests/component.test.ts +0 -281
  31. package/src/tests/context.test.ts +0 -629
  32. package/src/tests/core.test.ts +0 -1290
  33. package/src/tests/cx.test.ts +0 -70
  34. package/src/tests/defer.test.ts +0 -359
  35. package/src/tests/dynamic.test.ts +0 -87
  36. package/src/tests/error-boundary.test.ts +0 -181
  37. package/src/tests/extract-props-overloads.types.test.ts +0 -135
  38. package/src/tests/for.test.ts +0 -117
  39. package/src/tests/h.test.ts +0 -221
  40. package/src/tests/jsx-compat.test.tsx +0 -86
  41. package/src/tests/lazy.test.ts +0 -100
  42. package/src/tests/lifecycle.test.ts +0 -350
  43. package/src/tests/manifest-snapshot.test.ts +0 -100
  44. package/src/tests/map-array.test.ts +0 -313
  45. package/src/tests/native-marker-error-boundary.test.ts +0 -12
  46. package/src/tests/portal.test.ts +0 -48
  47. package/src/tests/props-extended.test.ts +0 -157
  48. package/src/tests/props.test.ts +0 -250
  49. package/src/tests/reactive-context.test.ts +0 -69
  50. package/src/tests/reactive-props.test.ts +0 -157
  51. package/src/tests/ref.test.ts +0 -70
  52. package/src/tests/show.test.ts +0 -314
  53. package/src/tests/style.test.ts +0 -157
  54. package/src/tests/suspense.test.ts +0 -139
  55. package/src/tests/telemetry.test.ts +0 -297
  56. package/src/types.ts +0 -116
@@ -1,629 +0,0 @@
1
- import { runWithHooks } from '../component'
2
- import {
3
- captureContextStack,
4
- createContext,
5
- getContextStackLength,
6
- popContext,
7
- provide,
8
- pushContext,
9
- restoreContextStack,
10
- setContextStackProvider,
11
- useContext,
12
- withContext,
13
- } from '../context'
14
- import type { ContextSnapshot } from '../context'
15
- import type { ComponentFn, Props } from '../types'
16
-
17
- describe('createContext', () => {
18
- test('returns context with unique symbol id', () => {
19
- const ctx = createContext('default')
20
- expect(typeof ctx.id).toBe('symbol')
21
- expect(ctx.defaultValue).toBe('default')
22
- })
23
-
24
- test('each context has a unique id', () => {
25
- const a = createContext(1)
26
- const b = createContext(2)
27
- expect(a.id).not.toBe(b.id)
28
- })
29
-
30
- test('undefined default value', () => {
31
- const ctx = createContext<string | undefined>(undefined)
32
- expect(ctx.defaultValue).toBeUndefined()
33
- })
34
-
35
- test('null default value', () => {
36
- const ctx = createContext<null>(null)
37
- expect(ctx.defaultValue).toBeNull()
38
- })
39
-
40
- test('object default value', () => {
41
- const obj = { theme: 'dark', lang: 'en' }
42
- const ctx = createContext(obj)
43
- expect(ctx.defaultValue).toBe(obj)
44
- })
45
-
46
- test('function default value', () => {
47
- const fn = () => 42
48
- const ctx = createContext(fn)
49
- expect(ctx.defaultValue).toBe(fn)
50
- })
51
- })
52
-
53
- describe('useContext', () => {
54
- test('returns default when no provider exists', () => {
55
- const ctx = createContext('fallback')
56
- expect(useContext(ctx)).toBe('fallback')
57
- })
58
-
59
- test('returns provided value from pushContext', () => {
60
- const ctx = createContext('default')
61
- pushContext(new Map([[ctx.id, 'provided']]))
62
- expect(useContext(ctx)).toBe('provided')
63
- popContext()
64
- })
65
-
66
- test('returns innermost value with nested pushContext', () => {
67
- const ctx = createContext('default')
68
- pushContext(new Map([[ctx.id, 'outer']]))
69
- pushContext(new Map([[ctx.id, 'inner']]))
70
- expect(useContext(ctx)).toBe('inner')
71
- popContext()
72
- expect(useContext(ctx)).toBe('outer')
73
- popContext()
74
- })
75
-
76
- test('different contexts in same frame are independent', () => {
77
- const ctxA = createContext('a-default')
78
- const ctxB = createContext('b-default')
79
- const frame = new Map<symbol, unknown>([
80
- [ctxA.id, 'a-value'],
81
- [ctxB.id, 'b-value'],
82
- ])
83
- pushContext(frame)
84
- expect(useContext(ctxA)).toBe('a-value')
85
- expect(useContext(ctxB)).toBe('b-value')
86
- popContext()
87
- })
88
-
89
- test('context not in frame falls through to previous frame', () => {
90
- const ctxA = createContext('a-default')
91
- const ctxB = createContext('b-default')
92
- pushContext(new Map([[ctxA.id, 'a-outer']]))
93
- pushContext(new Map([[ctxB.id, 'b-inner']]))
94
- // ctxA is not in the inner frame, should fall through to outer
95
- expect(useContext(ctxA)).toBe('a-outer')
96
- expect(useContext(ctxB)).toBe('b-inner')
97
- popContext()
98
- popContext()
99
- })
100
- })
101
-
102
- describe('pushContext / popContext', () => {
103
- test('push and pop maintain correct stack order', () => {
104
- const ctx = createContext(0)
105
- pushContext(new Map([[ctx.id, 1]]))
106
- pushContext(new Map([[ctx.id, 2]]))
107
- pushContext(new Map([[ctx.id, 3]]))
108
- expect(useContext(ctx)).toBe(3)
109
- popContext()
110
- expect(useContext(ctx)).toBe(2)
111
- popContext()
112
- expect(useContext(ctx)).toBe(1)
113
- popContext()
114
- expect(useContext(ctx)).toBe(0) // default
115
- })
116
-
117
- test('popContext on empty stack is a silent no-op', () => {
118
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
119
- popContext()
120
- expect(warnSpy).not.toHaveBeenCalled()
121
- warnSpy.mockRestore()
122
- })
123
- })
124
-
125
- describe('withContext', () => {
126
- test('provides value during callback execution', () => {
127
- const ctx = createContext('default')
128
- let captured = ''
129
- withContext(ctx, 'inside', () => {
130
- captured = useContext(ctx)
131
- })
132
- expect(captured).toBe('inside')
133
- // After withContext, should be back to default
134
- expect(useContext(ctx)).toBe('default')
135
- })
136
-
137
- test('restores stack on normal completion', () => {
138
- const ctx = createContext('default')
139
- withContext(ctx, 'temp', () => {
140
- expect(useContext(ctx)).toBe('temp')
141
- })
142
- expect(useContext(ctx)).toBe('default')
143
- })
144
-
145
- test('restores stack even when callback throws', () => {
146
- const ctx = createContext('safe')
147
- try {
148
- withContext(ctx, 'dangerous', () => {
149
- expect(useContext(ctx)).toBe('dangerous')
150
- throw new Error('boom')
151
- })
152
- } catch {
153
- // expected
154
- }
155
- expect(useContext(ctx)).toBe('safe')
156
- })
157
-
158
- test('nested withContext calls', () => {
159
- const ctx = createContext(0)
160
- withContext(ctx, 1, () => {
161
- expect(useContext(ctx)).toBe(1)
162
- withContext(ctx, 2, () => {
163
- expect(useContext(ctx)).toBe(2)
164
- withContext(ctx, 3, () => {
165
- expect(useContext(ctx)).toBe(3)
166
- })
167
- expect(useContext(ctx)).toBe(2)
168
- })
169
- expect(useContext(ctx)).toBe(1)
170
- })
171
- expect(useContext(ctx)).toBe(0)
172
- })
173
-
174
- test('multiple contexts in nested withContext', () => {
175
- const theme = createContext('light')
176
- const lang = createContext('en')
177
-
178
- withContext(theme, 'dark', () => {
179
- withContext(lang, 'fr', () => {
180
- expect(useContext(theme)).toBe('dark')
181
- expect(useContext(lang)).toBe('fr')
182
- })
183
- expect(useContext(lang)).toBe('en')
184
- })
185
- expect(useContext(theme)).toBe('light')
186
- })
187
- })
188
-
189
- describe('provide', () => {
190
- test('pushes context and registers unmount cleanup', () => {
191
- const ctx = createContext('default')
192
- const { hooks } = runWithHooks(
193
- (() => {
194
- provide(ctx, 'provided-value')
195
- expect(useContext(ctx)).toBe('provided-value')
196
- return null
197
- }) as ComponentFn,
198
- {} as Props,
199
- )
200
- // Context should still be available after runWithHooks
201
- expect(useContext(ctx)).toBe('provided-value')
202
- // unmount hooks should include the popContext cleanup
203
- expect(hooks.unmount!.length).toBeGreaterThanOrEqual(1)
204
- // Running unmount cleans up
205
- for (const fn of hooks.unmount!) fn()
206
- expect(useContext(ctx)).toBe('default')
207
- })
208
-
209
- test('multiple provides in same component', () => {
210
- const ctxA = createContext('a')
211
- const ctxB = createContext('b')
212
- const { hooks } = runWithHooks(
213
- (() => {
214
- provide(ctxA, 'A-value')
215
- provide(ctxB, 'B-value')
216
- return null
217
- }) as ComponentFn,
218
- {} as Props,
219
- )
220
- expect(useContext(ctxA)).toBe('A-value')
221
- expect(useContext(ctxB)).toBe('B-value')
222
- // Clean up
223
- for (const fn of hooks.unmount!) fn()
224
- expect(useContext(ctxA)).toBe('a')
225
- expect(useContext(ctxB)).toBe('b')
226
- })
227
- })
228
-
229
- describe('setContextStackProvider', () => {
230
- test('overrides the stack provider', () => {
231
- const customStack: Map<symbol, unknown>[] = []
232
- const ctx = createContext('default')
233
-
234
- setContextStackProvider(() => customStack)
235
-
236
- customStack.push(new Map([[ctx.id, 'custom']]))
237
- expect(useContext(ctx)).toBe('custom')
238
- customStack.pop()
239
- expect(useContext(ctx)).toBe('default')
240
-
241
- // Restore default provider
242
- const freshStack: Map<symbol, unknown>[] = []
243
- setContextStackProvider(() => freshStack)
244
- })
245
-
246
- test('different providers see different stacks', () => {
247
- const ctx = createContext('default')
248
- const stack1: Map<symbol, unknown>[] = []
249
- const stack2: Map<symbol, unknown>[] = []
250
-
251
- setContextStackProvider(() => stack1)
252
- pushContext(new Map([[ctx.id, 'stack1-value']]))
253
- expect(useContext(ctx)).toBe('stack1-value')
254
-
255
- // Switch to stack2 — should not see stack1's value
256
- setContextStackProvider(() => stack2)
257
- expect(useContext(ctx)).toBe('default')
258
-
259
- // Clean up
260
- setContextStackProvider(() => stack1)
261
- popContext()
262
- const freshStack: Map<symbol, unknown>[] = []
263
- setContextStackProvider(() => freshStack)
264
- })
265
- })
266
-
267
- // ─── captureContextStack — dedup semantics ───────────────────────────────────
268
- //
269
- // The capture step deduplicates: only the topmost frame per context-id is
270
- // retained in the snapshot. This is a HEAP-LEAK fix: under deeply-nested
271
- // reactive boundaries, each effect's setup-time snapshot used to grow with
272
- // the live stack's transient duplicates (40k+ entries reported in 0.21.x;
273
- // see context.ts JSDoc for the full story). Dedup collapses the captured
274
- // size to ~N entries where N is the number of distinct context ids in
275
- // scope (typically 2-10 in real apps).
276
- //
277
- // Safety property: `useContext` walks the stack in reverse and stops at
278
- // the first matching frame; any shadowed frame is unreachable. The dedup
279
- // preserves the topmost frame per id, so `useContext` returns the same
280
- // value before and after.
281
-
282
- describe('captureContextStack — dedup', () => {
283
- const restoreStack: Map<symbol, unknown>[][] = []
284
- let testStack: Map<symbol, unknown>[]
285
-
286
- beforeEach(() => {
287
- testStack = []
288
- setContextStackProvider(() => testStack)
289
- })
290
-
291
- afterEach(() => {
292
- while (restoreStack.length > 0) restoreStack.pop()
293
- const freshStack: Map<symbol, unknown>[] = []
294
- setContextStackProvider(() => freshStack)
295
- })
296
-
297
- test('empty stack snapshot is empty', () => {
298
- expect(captureContextStack()).toEqual([])
299
- })
300
-
301
- test('single frame snapshot is identical', () => {
302
- const ctx = createContext('default')
303
- pushContext(new Map([[ctx.id, 'A']]))
304
- const snap = captureContextStack()
305
- expect(snap).toHaveLength(1)
306
- expect(snap[0]).toBe(testStack[0]) // same reference
307
- popContext()
308
- })
309
-
310
- test('stack with no duplicate ids snapshots verbatim', () => {
311
- const a = createContext('a-default')
312
- const b = createContext('b-default')
313
- const c = createContext('c-default')
314
- pushContext(new Map([[a.id, 'A']]))
315
- pushContext(new Map([[b.id, 'B']]))
316
- pushContext(new Map([[c.id, 'C']]))
317
- const snap = captureContextStack()
318
- expect(snap).toHaveLength(3)
319
- expect(snap.map((f) => Array.from(f.values()))).toEqual([['A'], ['B'], ['C']])
320
- popContext()
321
- popContext()
322
- popContext()
323
- })
324
-
325
- test('duplicate ids collapse to topmost', () => {
326
- // Same context-id pushed 3 times — typical of nested restoreContextStack
327
- // windows. Only the topmost should appear in the snapshot.
328
- const ctx = createContext('default')
329
- pushContext(new Map([[ctx.id, 'A']]))
330
- pushContext(new Map([[ctx.id, 'B']]))
331
- pushContext(new Map([[ctx.id, 'C']]))
332
- const snap = captureContextStack()
333
- expect(snap).toHaveLength(1)
334
- expect(snap[0]!.get(ctx.id)).toBe('C') // topmost wins
335
- popContext()
336
- popContext()
337
- popContext()
338
- })
339
-
340
- test('mixed: deep stack with mostly duplicates collapses', () => {
341
- // Simulates the bug shape: same context pushed 40 times via nested
342
- // restore windows + one unique frame at the top.
343
- const repeated = createContext('default')
344
- const unique = createContext('default')
345
- for (let i = 0; i < 40; i++) {
346
- pushContext(new Map([[repeated.id, `dup-${i}`]]))
347
- }
348
- pushContext(new Map([[unique.id, 'unique']]))
349
- expect(testStack).toHaveLength(41)
350
-
351
- const snap = captureContextStack()
352
- // Result: topmost `repeated` frame + the `unique` frame = 2 entries.
353
- // Pre-fix this snapshot would have all 41 frames — the leak.
354
- expect(snap).toHaveLength(2)
355
- // Ordering must match push order (bottom-to-top in the array).
356
- expect(snap[0]!.get(repeated.id)).toBe('dup-39')
357
- expect(snap[1]!.get(unique.id)).toBe('unique')
358
-
359
- for (let i = 0; i < 41; i++) popContext()
360
- })
361
-
362
- test('multi-key frame: kept if it provides ANY un-shadowed id', () => {
363
- // Frame with two contexts; only one is shadowed by a deeper push.
364
- const a = createContext('a')
365
- const b = createContext('b')
366
- pushContext(new Map<symbol, unknown>([[a.id, 'a1'], [b.id, 'b1']]))
367
- pushContext(new Map([[a.id, 'a2']])) // shadows `a`, NOT `b`
368
-
369
- const snap = captureContextStack()
370
- // Both frames should remain: the upper provides `a`, the lower
371
- // still provides un-shadowed `b`.
372
- expect(snap).toHaveLength(2)
373
-
374
- // Verify useContext semantics survive: a→a2, b→b1
375
- setContextStackProvider(() => snap)
376
- expect(useContext(a)).toBe('a2')
377
- expect(useContext(b)).toBe('b1')
378
- setContextStackProvider(() => testStack)
379
-
380
- popContext()
381
- popContext()
382
- })
383
-
384
- test('multi-key frame: dropped if ALL its ids are shadowed', () => {
385
- const a = createContext('a')
386
- const b = createContext('b')
387
- pushContext(new Map<symbol, unknown>([[a.id, 'a1'], [b.id, 'b1']]))
388
- pushContext(new Map<symbol, unknown>([[a.id, 'a2'], [b.id, 'b2']]))
389
-
390
- const snap = captureContextStack()
391
- expect(snap).toHaveLength(1)
392
- expect(snap[0]!.get(a.id)).toBe('a2')
393
- expect(snap[0]!.get(b.id)).toBe('b2')
394
-
395
- popContext()
396
- popContext()
397
- })
398
-
399
- test('useContext returns same value pre/post dedup for arbitrary read patterns', () => {
400
- // Cross-check: build a complex stack, capture, then verify useContext
401
- // returns the same value when reading from the original stack vs the
402
- // deduped snapshot. This is the load-bearing semantic-equivalence
403
- // assertion for the safety argument.
404
- const a = createContext('a-default')
405
- const b = createContext('b-default')
406
- const c = createContext('c-default')
407
- pushContext(new Map([[a.id, 'a1']]))
408
- pushContext(new Map<symbol, unknown>([[a.id, 'a2'], [b.id, 'b1']]))
409
- pushContext(new Map([[c.id, 'c1']]))
410
- pushContext(new Map([[a.id, 'a3']]))
411
- pushContext(new Map([[b.id, 'b2']]))
412
-
413
- // Read against original stack
414
- const beforeA = useContext(a)
415
- const beforeB = useContext(b)
416
- const beforeC = useContext(c)
417
-
418
- // Capture (dedup happens) and read against the snapshot
419
- const snap = captureContextStack()
420
- setContextStackProvider(() => snap)
421
- const afterA = useContext(a)
422
- const afterB = useContext(b)
423
- const afterC = useContext(c)
424
-
425
- expect(afterA).toBe(beforeA) // 'a3' from the topmost frame
426
- expect(afterB).toBe(beforeB) // 'b2' from the topmost frame
427
- expect(afterC).toBe(beforeC) // 'c1' (still the only c-provider)
428
-
429
- // Clean up
430
- setContextStackProvider(() => testStack)
431
- for (let i = 0; i < 5; i++) popContext()
432
- })
433
- })
434
-
435
- // ─── restoreContextStack — works against deduped snapshots ───────────────────
436
-
437
- describe('restoreContextStack — with deduped snapshots', () => {
438
- let testStack: Map<symbol, unknown>[]
439
-
440
- beforeEach(() => {
441
- testStack = []
442
- setContextStackProvider(() => testStack)
443
- })
444
-
445
- afterEach(() => {
446
- const freshStack: Map<symbol, unknown>[] = []
447
- setContextStackProvider(() => freshStack)
448
- })
449
-
450
- test('restores deduped snapshot — fn() sees correct context, stack cleans up', () => {
451
- const ctx = createContext('default')
452
- pushContext(new Map([[ctx.id, 'A']]))
453
- pushContext(new Map([[ctx.id, 'B']]))
454
- pushContext(new Map([[ctx.id, 'C']]))
455
-
456
- const snap = captureContextStack()
457
- expect(snap).toHaveLength(1) // dedup collapsed to topmost
458
-
459
- // Now empty the stack to simulate post-mount state
460
- popContext()
461
- popContext()
462
- popContext()
463
- expect(testStack).toHaveLength(0)
464
-
465
- // Restore the deduped snapshot
466
- const observed = restoreContextStack(snap, () => {
467
- // Inside fn(): stack has the deduped frame
468
- expect(testStack).toHaveLength(1)
469
- return useContext(ctx)
470
- })
471
-
472
- // fn() saw the topmost-frame value, NOT 'default' — semantic equivalence
473
- expect(observed).toBe('C')
474
- // After restore, the snapshot's frames are removed by reference identity
475
- expect(testStack).toHaveLength(0)
476
- })
477
-
478
- test('restoring 40-duplicate stack only pushes/pops 1 frame post-dedup', () => {
479
- // This is the bug-shape regression test. Pre-dedup, this snapshot was
480
- // 40 entries; restore pushed 40 then removed 40. Post-dedup, both
481
- // operations move 1 frame.
482
- const ctx = createContext('default')
483
- for (let i = 0; i < 40; i++) {
484
- pushContext(new Map([[ctx.id, `dup-${i}`]]))
485
- }
486
- const snap = captureContextStack()
487
- expect(snap).toHaveLength(1)
488
-
489
- // Empty the live stack so the restore is observable in isolation.
490
- while (testStack.length > 0) popContext()
491
-
492
- let observedLenInside = -1
493
- restoreContextStack(snap, () => {
494
- observedLenInside = testStack.length
495
- })
496
-
497
- // 1 push during fn, 1 splice after = stack stays balanced.
498
- expect(observedLenInside).toBe(1)
499
- expect(testStack).toHaveLength(0)
500
- })
501
- })
502
-
503
- // ─── Leak audit: snapshot allocations stay bounded under deep stacks ─────────
504
- //
505
- // This is the regression lock for the heap-snapshot finding that motivated
506
- // the dedup. Reported in 0.21.x: 1.22 MB / 321k-entry arrays retained by
507
- // effect closures under deeply-nested reactive boundaries. The 0.23.0
508
- // restoreContextStack fix cleaned the live stack but residual snapshot
509
- // amplification persisted (~3 MB / 20×40k-entry arrays). This dedup
510
- // closes that. The test below makes the bug-shape impossible to
511
- // re-introduce silently: it builds the deep-stack scenario, captures
512
- // N snapshots that previously would each have held the stack-depth, and
513
- // asserts the TOTAL frame count across all snapshots scales with the
514
- // number of DISTINCT context ids in scope, NOT with the stack depth.
515
-
516
- describe('captureContextStack — leak audit (regression lock)', () => {
517
- let testStack: Map<symbol, unknown>[]
518
-
519
- beforeEach(() => {
520
- testStack = []
521
- setContextStackProvider(() => testStack)
522
- })
523
-
524
- afterEach(() => {
525
- const freshStack: Map<symbol, unknown>[] = []
526
- setContextStackProvider(() => freshStack)
527
- })
528
-
529
- test('1000 snapshots of a deep duplicate-heavy stack retain bounded total frames', () => {
530
- // Build a stack of 100 frames, all pushing the same context (simulates
531
- // nested restoreContextStack windows). Then capture 1000 snapshots —
532
- // one per effect setup, as happens in a large component tree.
533
- const ctx = createContext('default')
534
- for (let i = 0; i < 100; i++) {
535
- pushContext(new Map([[ctx.id, `dup-${i}`]]))
536
- }
537
-
538
- const snapshots: ContextSnapshot[] = []
539
- for (let i = 0; i < 1000; i++) {
540
- snapshots.push(captureContextStack())
541
- }
542
-
543
- // Pre-dedup: 1000 snapshots × 100 frames = 100,000 frame references.
544
- // Post-dedup: 1000 snapshots × 1 frame (topmost) = 1,000 frame references.
545
- // The assertion bounds total retention at the dedup-correct ceiling.
546
- const totalFrames = snapshots.reduce((sum, s) => sum + s.length, 0)
547
- expect(totalFrames).toBe(1000) // 1000 snapshots × 1 unique id
548
-
549
- // Clean up
550
- for (let i = 0; i < 100; i++) popContext()
551
- })
552
-
553
- test('mixed deep stack: total frames bounded by distinct id count, not depth', () => {
554
- // 50 unique contexts pushed into a stack of 500 frames (10 duplicates
555
- // per context). Capture 100 snapshots.
556
- const ctxs = Array.from({ length: 50 }, () => createContext('default'))
557
- for (let depth = 0; depth < 10; depth++) {
558
- for (const ctx of ctxs) {
559
- pushContext(new Map([[ctx.id, `d${depth}`]]))
560
- }
561
- }
562
- expect(testStack).toHaveLength(500)
563
-
564
- const snapshots: ContextSnapshot[] = []
565
- for (let i = 0; i < 100; i++) {
566
- snapshots.push(captureContextStack())
567
- }
568
-
569
- // Pre-dedup: 100 × 500 = 50,000 frame references.
570
- // Post-dedup: 100 × 50 (topmost per distinct id) = 5,000 frame
571
- // references. 10× reduction matches the empirical bug-shape.
572
- const totalFrames = snapshots.reduce((sum, s) => sum + s.length, 0)
573
- expect(totalFrames).toBe(100 * 50)
574
-
575
- // Clean up
576
- for (let i = 0; i < 500; i++) popContext()
577
- })
578
- })
579
-
580
- // ─── getContextStackLength ──────────────────────────────────────────────────
581
-
582
- describe('getContextStackLength', () => {
583
- let testStack: Map<symbol, unknown>[]
584
-
585
- beforeEach(() => {
586
- testStack = []
587
- setContextStackProvider(() => testStack)
588
- })
589
-
590
- afterEach(() => {
591
- const freshStack: Map<symbol, unknown>[] = []
592
- setContextStackProvider(() => freshStack)
593
- })
594
-
595
- test('returns the LIVE stack length, not the deduped snapshot length', () => {
596
- // This is the load-bearing distinction: SSR cleanup uses
597
- // `getContextStackLength()` as a position marker, and it must reflect
598
- // the live (un-deduped) stack length so subsequent `popContext` calls
599
- // pop the right number of frames.
600
- const ctx = createContext('default')
601
- pushContext(new Map([[ctx.id, 'A']]))
602
- pushContext(new Map([[ctx.id, 'B']]))
603
- pushContext(new Map([[ctx.id, 'C']]))
604
-
605
- expect(getContextStackLength()).toBe(3) // live length
606
- expect(captureContextStack()).toHaveLength(1) // deduped snapshot length
607
-
608
- popContext()
609
- popContext()
610
- popContext()
611
- })
612
-
613
- test('zero on empty stack', () => {
614
- expect(getContextStackLength()).toBe(0)
615
- })
616
-
617
- test('matches stack array length after push/pop cycles', () => {
618
- const ctx = createContext('default')
619
- expect(getContextStackLength()).toBe(0)
620
- pushContext(new Map([[ctx.id, 'A']]))
621
- expect(getContextStackLength()).toBe(1)
622
- pushContext(new Map([[ctx.id, 'B']]))
623
- expect(getContextStackLength()).toBe(2)
624
- popContext()
625
- expect(getContextStackLength()).toBe(1)
626
- popContext()
627
- expect(getContextStackLength()).toBe(0)
628
- })
629
- })