# App-Side Memory Leak Context

## Goal

Use this document as context for the app-side AI agent that owns the React application integrating the `sofya.transcription` SDK.

The SDK-side leak path has already been patched in this repo. The remaining work is to inspect the app code that creates, mounts, reuses, and disposes the transcriber and any app-owned audio resources.

## Repro Flow

Exact repeated flow used during debugging:

1. Render page `/`
2. Click `Choose file`
3. Select a file
4. Click `Start`
5. Let the audio file play for its full duration
6. Click `Stop`
7. Repeat the flow multiple times

## Performance Trace Summary

Single-run trace results:

- Before: heap `11.05 MB`, documents `2`, nodes `338`, listeners `179`
- After 1 run: heap `16.92 MB`, documents `2`, nodes `342`, listeners `447`
- After GC: heap `11.43 MB`, documents `2`, nodes `338`, listeners `179`

Interpretation:

- DOM nodes and event listeners returned to baseline after a single run.
- That suggested transient runtime allocation during playback, but not yet conclusive retained DOM/listener leakage from one cycle.

## Heap Snapshot Summary

Snapshots used:

- Baseline: [/Users/gabriel/Downloads/Heap-20260329T191738.heapsnapshot](/Users/gabriel/Downloads/Heap-20260329T191738.heapsnapshot)
- After 5 runs: [/Users/gabriel/Downloads/Heap-20260329T191743.heapsnapshot](/Users/gabriel/Downloads/Heap-20260329T191743.heapsnapshot)
- After GC: [/Users/gabriel/Downloads/Heap-20260329T192121.heapsnapshot](/Users/gabriel/Downloads/Heap-20260329T192121.heapsnapshot)

Important constructor counts across baseline -> after 5 runs -> after GC:

- `native:AudioContext` `0 -> 10 -> 10`
- `native:AudioWorklet` `0 -> 10 -> 10`
- `native:AudioParam` `0 -> 90 -> 90`
- `native:WebSocket` `1 -> 1 -> 1`
- `native:MessagePort` `2 -> 2 -> 2`
- `object:FiberNode` `342 -> 519 -> 519`
- `native:Text` `103 -> 108 -> 108`
- `detached` total `139 -> 160 -> 160`

Interpretation:

- The leak is real after GC.
- The strongest retained objects were audio graph/runtime objects, not sockets.
- `FiberNode` staying elevated after GC suggests app-side React retention in addition to the SDK leak.
- Detached DOM growth existed but was not the dominant signal.

## SDK-Side Fix Already Applied

This repo already contains a cleanup patch. Relevant files:

- [src/services/transcription/SofyaTranscriber.ts](/Users/gabriel/Projects/lib.sofya.transcription/src/services/transcription/SofyaTranscriber.ts)
- [src/services/transcription/adapters/WhisperTranscriptionAdapter.ts](/Users/gabriel/Projects/lib.sofya.transcription/src/services/transcription/adapters/WhisperTranscriptionAdapter.ts)
- [src/services/transcription/adapters/OracleTranscriptionAdapter.ts](/Users/gabriel/Projects/lib.sofya.transcription/src/services/transcription/adapters/OracleTranscriptionAdapter.ts)
- [src/services/transcription/interfaces/ITranscriptionService.ts](/Users/gabriel/Projects/lib.sofya.transcription/src/services/transcription/interfaces/ITranscriptionService.ts)
- [src/SofyaTranscriber.d.ts](/Users/gabriel/Projects/lib.sofya.transcription/src/SofyaTranscriber.d.ts)

What changed in the SDK:

- Made wrapper-to-service listeners detachable instead of anonymous one-way subscriptions.
- Made Whisper `AudioContext` lazy/session-scoped rather than eagerly created in the adapter constructor.
- Moved internal listener cleanup into the library-managed `startTranscription()` / `stopTranscription()` lifecycle.

App implication:

- The app should not need a special SDK disposal API.
- The app should still ensure it calls `stopTranscription()` consistently for each run and cleans up any app-owned audio resources.

## Most Likely App-Side Leak Sources

The app-side agent should assume the remaining leak is likely one or more of the following:

1. A `SofyaTranscriber` instance is recreated repeatedly and never disposed on unmount.
2. A transcriber is stored in React state, ref, context, or module-level singleton and survives page transitions.
3. The app owns its own `AudioContext`, `MediaElementAudioSourceNode`, `MediaStreamAudioDestinationNode`, or decoded audio buffer and does not close/release it.
4. Object URLs created for uploaded audio files are not revoked.
5. `HTMLAudioElement` instances or file-preview elements are replaced repeatedly without listener cleanup.
6. Event listeners are attached in `useEffect` but not removed on cleanup.
7. Arrays, refs, stores, or caches retain old playback/session objects.
8. StrictMode/dev-only remounts are doubling setup paths without mirrored cleanup.

## Concrete Clues From The Heap Data

There was a retained string in the heap named:

- `uploadedAudioContextRef`

That string does not exist in this SDK repo.

Interpretation:

- The app likely has a ref or state object named `uploadedAudioContextRef`.
- That is a high-priority inspection target.

The elevated `FiberNode` count after GC also strongly suggests retained React component trees or closures.

## What The App-Side Agent Should Inspect First

Search the app codebase for:

- `uploadedAudioContextRef`
- `new AudioContext`
- `createMediaElementSource`
- `createMediaStreamSource`
- `createMediaStreamDestination`
- `URL.createObjectURL`
- `URL.revokeObjectURL`
- `new SofyaTranscriber`
- `startTranscription`
- `stopTranscription`
- `dispose`
- `useRef(`
- `useEffect(`
- `addEventListener`
- `removeEventListener`
- `setInterval`
- `clearInterval`
- `setTimeout`
- `clearTimeout`

Then answer these questions:

1. Where is the transcriber instance created?
2. Is it recreated on every upload or every playback start?
3. Does the page always call `stopTranscription()` before replacing the current session or navigating away?
4. Is there any app-owned audio context separate from the SDK?
5. Are object URLs for uploaded files revoked when replaced or on unmount?
6. Are file/audio element listeners removed when the selected file changes?
7. Are refs or stores keeping old playback sessions alive?

## Expected App-Side Fix Pattern

The app-side solution should probably look like this:

- Create the transcriber once per mounted page/session, not once per render.
- Store it in a `useRef`.
- On teardown or before replacing the session, call `await transcriber.stopTranscription()` if a session is active.
- Null out the ref after the session is no longer needed.
- If the app creates an `AudioContext`, call `await audioContext.close()` in cleanup.
- If the app creates object URLs, call `URL.revokeObjectURL(url)` in cleanup or when replacing the file.
- Remove all DOM and media-element listeners in `useEffect` cleanup.

Example shape:

```ts
useEffect(() => {
  const transcriber = new SofyaTranscriber(connection);
  transcriberRef.current = transcriber;

  return () => {
    const current = transcriberRef.current;
    transcriberRef.current = null;

    void (async () => {
      try {
        await current?.stopTranscription();
      } catch (error) {
        console.error(error);
      }
    })();
  };
}, []);
```

If the app creates its own audio context:

```ts
useEffect(() => {
  const audioContext = new AudioContext();
  uploadedAudioContextRef.current = audioContext;

  return () => {
    const current = uploadedAudioContextRef.current;
    uploadedAudioContextRef.current = null;
    void current?.close();
  };
}, []);
```

## Required Outcome

The app-side agent should not stop at analysis. It should:

1. Find the exact React component(s) implementing the upload/playback/transcription flow.
2. Patch teardown logic.
3. Add or update tests if the app has them.
4. Re-run the same reproduction flow.
5. Verify that after GC the app no longer retains growing counts of:
   - `AudioContext`
   - `AudioWorklet`
   - `AudioParam`
   - `FiberNode`

## Acceptance Criteria

After repeating the upload/start/play/stop flow 5 to 10 times:

- `AudioContext` count should not keep increasing after GC.
- `AudioWorklet` and `AudioParam` counts should not remain elevated after GC.
- `FiberNode` count should return near baseline after GC.
- Detached nodes should not show monotonic growth.

## Notes For The Agent

- The SDK-side patch is already present in this repo, so do not try to fix the issue only by changing the library integration API.
- Focus on app lifecycle ownership and cleanup.
- The most suspicious app-side symbol right now is `uploadedAudioContextRef`.
