Skip to content

Commit 720db0b

Browse files
Nagi-ovoclaude
andcommitted
feat(plugins): add gv:debug:pluginScopes ledger dump
document.dispatchEvent(new Event('gv:debug:pluginScopes')) logs every scope-based plugin's live side-effect ledger (effect labels keyed by plugin id) for leak inspection, following the existing gv:debug:* event pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ed025b7 commit 720db0b

4 files changed

Lines changed: 42 additions & 0 deletions

File tree

src/features/plugins/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,30 @@ export * from './types';
3333

3434
let host: PluginHost | null = null;
3535

36+
/**
37+
* Debug: dump every scope-based plugin's live side-effect ledger. From the
38+
* page console (extension context in DevTools):
39+
* document.dispatchEvent(new Event('gv:debug:pluginScopes'))
40+
*/
41+
export const PLUGIN_SCOPES_DEBUG_EVENT = 'gv:debug:pluginScopes';
42+
43+
const dumpScopeLedgers = (): void => {
44+
logger.info('Plugin scope ledgers', host?.getScopeLedgers() ?? {});
45+
};
46+
3647
export function startPluginHost(): () => void {
3748
if (host) return () => {};
3849
try {
3950
host = new PluginHost();
4051
void host.start();
52+
document.addEventListener(PLUGIN_SCOPES_DEBUG_EVENT, dumpScopeLedgers);
4153
} catch (error) {
4254
if (!isExtensionContextInvalidatedError(error)) {
4355
logger.error('startPluginHost failed', { error: String(error) });
4456
}
4557
}
4658
return () => {
59+
document.removeEventListener(PLUGIN_SCOPES_DEBUG_EVENT, dumpScopeLedgers);
4760
host?.stop();
4861
host = null;
4962
};

src/features/plugins/runtime/PluginHost.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ export class PluginHost {
8383
return this.adapter;
8484
}
8585

86+
/** Live side-effect ledgers of scope-based plugins, keyed by plugin id. */
87+
getScopeLedgers(): Record<string, readonly string[]> {
88+
return this.engine?.getScopeLedgers() ?? {};
89+
}
90+
8691
async start(): Promise<void> {
8792
if (this.started) return;
8893
this.started = true;

src/features/plugins/runtime/declarativeEngine.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,21 @@ describe('DeclarativeEngine', () => {
168168
expect(activate).toHaveBeenCalledOnce();
169169
});
170170

171+
it('getScopeLedgers reports live effect labels per scope-based plugin', () => {
172+
registerNativeHandler('test.scoped-ledger', {
173+
activate: (scope: PluginScope) => {
174+
scope.effect(() => () => {}, 'my-effect');
175+
},
176+
});
177+
const engine = new DeclarativeEngine({ doc: document });
178+
engine.mount(makeManifest({}, 'test.scoped-ledger'));
179+
180+
expect(engine.getScopeLedgers()).toEqual({ 'test.scoped-ledger': ['my-effect'] });
181+
182+
engine.unmount('test.scoped-ledger');
183+
expect(engine.getScopeLedgers()).toEqual({});
184+
});
185+
171186
it('a throwing activation rolls back what it already registered', async () => {
172187
const registered = vi.fn();
173188
registerNativeHandler('test.scoped-throw', {

src/features/plugins/runtime/declarativeEngine.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ export class DeclarativeEngine {
9797
return this.active.size;
9898
}
9999

100+
/** Live side-effect ledger per scope-based plugin — debug/leak inspection. */
101+
getScopeLedgers(): Record<string, readonly string[]> {
102+
const ledgers: Record<string, readonly string[]> = {};
103+
for (const [id, entry] of this.active) {
104+
if (entry.scope) ledgers[id] = entry.scope.getEffects();
105+
}
106+
return ledgers;
107+
}
108+
100109
isActive(id: string): boolean {
101110
return this.active.has(id);
102111
}

0 commit comments

Comments
 (0)