Skip to content

Commit c721a27

Browse files
committed
fix(vuebugger): no memory leak on module level debugs and HMR
1 parent eb3f6fd commit c721a27

3 files changed

Lines changed: 75 additions & 6 deletions

File tree

.changeset/strict-lights-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vingy/vuebugger": patch
3+
---
4+
5+
fix: module level debug calls no longer leak on HMR

packages/vuebugger/src/debug.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export type DebugOptions = {
2020
enable?: MaybeRefOrGetter<boolean>
2121
}
2222

23+
export const moduleScopes = new Map<
24+
string,
25+
ReturnType<typeof effectScope>
26+
>()
27+
2328
const createDebugScope = <T extends Record<string, any>>(
2429
groupId: VuebuggerEntry['groupId'],
2530
state: T,
@@ -41,11 +46,24 @@ const createDebugScope = <T extends Record<string, any>>(
4146
debugState: state,
4247
}
4348

44-
const scope = getCurrentScope() ?? effectScope()
45-
scope.run(() => {
46-
onScopeDispose(() => remove(entry))
47-
run(entry)
48-
})
49+
const currentScope = getCurrentScope()
50+
51+
if (currentScope) {
52+
currentScope.run(() => {
53+
onScopeDispose(() => remove(entry))
54+
run(entry)
55+
})
56+
} else {
57+
const existing = moduleScopes.get(groupId)
58+
if (existing) existing.stop()
59+
60+
const scope = effectScope()
61+
moduleScopes.set(groupId, scope)
62+
scope.run(() => {
63+
onScopeDispose(() => remove(entry))
64+
run(entry)
65+
})
66+
}
4967
}
5068

5169
export const debug = <T extends Record<string, any>>(

packages/vuebugger/src/debug.unit.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { expect, test, vi } from 'vitest'
22
import { effectScope, nextTick, ref } from 'vue'
33

4-
import { debug, setUidGenerator } from './debug'
4+
import {
5+
debug,
6+
moduleScopes,
7+
setUidGenerator,
8+
} from './debug'
59
import { byGroupId, byUid } from './registry'
610

711
// ---- helpers ----------------------------------------------------------------
@@ -17,6 +21,7 @@ const setup = () => {
1721
const teardown = () => {
1822
byUid.clear()
1923
byGroupId.clear()
24+
moduleScopes.clear()
2025
}
2126

2227
// ---- debug() without options ------------------------------------------------
@@ -341,6 +346,47 @@ test('enable boolean false — never registers', async () => {
341346
teardown()
342347
})
343348

349+
// ---- module-level / HMR -----------------------------------------------------
350+
351+
test('HMR: re-executing debug outside a scope with same groupId stops old scope and keeps only one entry', async () => {
352+
setup()
353+
const state = { count: 1 }
354+
355+
// First "module execution"
356+
debug('myStore', state)
357+
await nextTick()
358+
expect(byUid.size).toBe(1)
359+
expect(moduleScopes.size).toBe(1)
360+
const firstScope = moduleScopes.get('myStore')
361+
362+
// Simulate HMR: module re-executes, same groupId
363+
setup()
364+
const state2 = { count: 2 }
365+
debug('myStore', state2)
366+
await nextTick()
367+
368+
// Old scope should be stopped, only one entry remains
369+
expect(firstScope?.active).toBe(false)
370+
expect(byUid.size).toBe(1)
371+
expect(moduleScopes.size).toBe(1)
372+
expect(moduleScopes.get('myStore')).not.toBe(firstScope)
373+
374+
teardown()
375+
})
376+
377+
test('module-level: registers entry outside a component scope', async () => {
378+
setup()
379+
const state = { count: 1 }
380+
381+
debug('myStore', state)
382+
await nextTick()
383+
384+
expect(byUid.size).toBe(1)
385+
expect(moduleScopes.has('myStore')).toBe(true)
386+
387+
teardown()
388+
})
389+
344390
// ---- production guard -------------------------------------------------------
345391

346392
test('production guard: returns state but registers nothing when DEV is false', async () => {

0 commit comments

Comments
 (0)