LI

lifecycle_guard

Protocols to manage event listeners and prevent memory leaks in VS Code extensions.

Install

mkdir -p .claude/skills/lifecycle-guard && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14201" && unzip -o skill.zip -d .claude/skills/lifecycle-guard && rm skill.zip

Installs to .claude/skills/lifecycle-guard

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Protocols for preventing memory leaks and event listener accumulation in VS Code webview extensions.
100 charsno explicit “when” trigger
Advanced

Key capabilities

  • Implement explicit disposal methods for singletons and components
  • Use global test setup for mocking browser APIs
  • Ensure environmental consistency in test environments
  • Mock HTMLAudioElement.load() to dispatch 'canplay' event
  • Verify test completion without memory leaks

How it works

The skill defines protocols for preventing memory leaks and event listener accumulation in VS Code webview extensions. It mandates explicit disposal methods for components and singletons, and uses global test setup in Vitest to ensure environmental consistency and mock problematic browser APIs.

Inputs & outputs

You give it
VS Code webview extension code with event listeners or singletons
You get back
Code with explicit disposal methods and test setup for memory leak prevention

When to use lifecycle_guard

  • Cleaning up event listeners in extensions
  • Preventing memory leaks in webviews
  • Writing reliable tests for VS Code extensions
  • Disposing of singleton instances

About this skill

Lifecycle Guard Protocol

0. Rationale

Webview components in VS Code often rely on global event listeners (window.addEventListener) and reactive stores. In a test environment like vitest (jsdom), these listeners persist across test suites unless explicitly removed, causing memory leaks, process hangs, and erratic test failures.

1. Implementation Patterns

1.1 Explicit Disposal

Every singleton or component that registers global listeners MUST implement a dispose() or cleanup() method.

export class MessageClient {
  private handlers = new Map<string, Function>();

  constructor() {
    this.onMessage = this.onMessage.bind(this);
    window.addEventListener('message', this.onMessage);
  }

  public dispose() {
    window.removeEventListener('message', this.onMessage);
    this.handlers.clear();
  }
}

1.2 Global Test Setup

Use a vitest.setup.ts file to mock missing browser APIs (e.g., indexedDB, scrollIntoView) and ensure environmental consistency.

import { vi } from 'vitest';

// Global mocks
if (typeof window !== 'undefined') {
  window.scrollIntoView = vi.fn();
}

// Global cleanup hooks
afterEach(() => {
  WebviewStore.getInstance().dispose();
  MessageClient.getInstance().dispose();
});

2. Verification Protocol

  1. Heuristic: If Vitest finishes with "Exit Code 1" despite all tests passing, a leak is present.
  2. Action: Search for setTimeout, setInterval, or addEventListener calls that lack a corresponding remove or clear call.

3. jsdom HTMLAudioElement Contract (Test Infrastructure)

[!WARNING] Scope: Test environment only. This is NOT a production concern.

3.1 The Problem

WebviewAudioEngine.playBlob() sets audio.src, calls audio.load(), then awaits canplayplay()ended. In jsdom, HTMLAudioElement.load() is a stub — it executes synchronously but dispatches no media events. This means canplay never fires and the inner Promise in playBlob() hangs indefinitely.

Symptom: Vitest reports Error: Test timed out in 5000ms for any test directly calling playBlob().

3.2 The Fix

In beforeEach of any test suite that exercises playBlob(), mock load() to synchronously dispatch canplay:

// For instance-specific audio elements (preferred when you have engine.audioElement):
const audio = engine.audioElement;
vi.spyOn(audio, 'load').mockImplementation(function(this: HTMLAudioElement) {
    this.dispatchEvent(new Event('canplay'));
});

// For all HTMLAudioElement instances (use when engine is reconstructed per-test):
vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(function(this: HTMLAudioElement) {
    this.dispatchEvent(new Event('canplay'));
});

3.3 Verification

After applying the mock:

  • The ended listener IS registered by playBlob() (verify via addEventListener spy).
  • Manually call the ended listener to resolve the Promise.
  • Assert engine.isBusy() returns false after await playPromise.

3.4 Affected Tests (resolved as of 2026-04-10)

  • tests/webview/core/RaceCondition.test.ts:47 — "SHOULD allow audio packets that match the current intent"
  • tests/webview/core/WebviewAudioEngine.test.ts:49 — "should acquire lock for playBlob and release it on completion"

When not to use it

  • When not developing VS Code webview extensions
  • When not using Vitest for testing

Limitations

  • Focuses on VS Code webview extensions
  • Specific to Vitest test environment

How it compares

This approach specifically addresses memory leak and event listener issues in VS Code webview extensions within a Vitest test environment, providing targeted solutions unlike general cleanup practices.

Compared to similar skills

lifecycle_guard side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
lifecycle_guard (this skill)04moNo flagsAdvanced
vue-debug-guides46moNo flagsIntermediate
agentation65moReviewBeginner
vrm-springbone-physics07moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry