---
phase: 28-vu-meter-optimization
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - jam-ui/src/stores/vuStore.js
  - jam-ui/src/hooks/useVuStore.js
  - jam-ui/package.json
autonomous: true

must_haves:
  truths:
    - "VU data can be stored outside React state"
    - "External store notifies subscribers on animation frame"
    - "useSyncExternalStore shim installed for React 16 compatibility"
  artifacts:
    - path: "jam-ui/src/stores/vuStore.js"
      provides: "External VU store with RAF batching"
      exports: ["vuStore"]
      min_lines: 60
    - path: "jam-ui/src/hooks/useVuStore.js"
      provides: "React hook for subscribing to VU store"
      exports: ["useVuLevel", "useAllVuLevels"]
      min_lines: 20
  key_links:
    - from: "jam-ui/src/hooks/useVuStore.js"
      to: "jam-ui/src/stores/vuStore.js"
      via: "import { vuStore }"
      pattern: "import.*vuStore.*from.*stores/vuStore"
    - from: "jam-ui/src/hooks/useVuStore.js"
      to: "use-sync-external-store"
      via: "useSyncExternalStore shim"
      pattern: "useSyncExternalStore.*from.*use-sync-external-store"
---

<objective>
Create external VU store infrastructure that bypasses React reconciliation for high-frequency VU meter updates.

Purpose: The native C++ client sends VU data at 50-70 updates/second. Storing this in React state triggers expensive reconciliation cycles causing page freezes. An external store with requestAnimationFrame batching limits updates to 60fps and avoids React overhead.

Output:
- `vuStore.js` - External store module with RAF batching
- `useVuStore.js` - React hook using useSyncExternalStore shim
- npm dependency `use-sync-external-store` installed
</objective>

<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/28-vu-meter-optimization/28-RESEARCH.md

# Current implementation to understand patterns
@jam-ui/src/hooks/useVuHelpers.js
@jam-ui/src/context/VuContext.js
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install useSyncExternalStore shim and create external VU store</name>
  <files>
    jam-ui/package.json
    jam-ui/src/stores/vuStore.js
  </files>
  <action>
1. Install the useSyncExternalStore shim for React 16 compatibility:
   ```bash
   cd jam-ui && npm install use-sync-external-store
   ```

2. Create `jam-ui/src/stores/` directory if it doesn't exist

3. Create `jam-ui/src/stores/vuStore.js` implementing:

   **Core state:**
   - `vuLevels = {}` - Object mapping mixerId to { level, clipping, timestamp }
   - `listeners = new Set()` - Subscriber callbacks
   - `pendingUpdates = {}` - Buffer for incoming updates before RAF flush
   - `rafId = null` - Animation frame ID for cleanup

   **Methods:**
   - `updateLevel(mixerId, level, clipping)` - Called by native bridge at 50-70/sec. Buffers update in pendingUpdates (does NOT emit immediately)
   - `removeLevel(mixerId)` - Removes mixer from store, emits change
   - `getSnapshot()` - Returns vuLevels reference (MUST return same object if unchanged for useSyncExternalStore)
   - `getLevelSnapshot(mixerId)` - Returns single mixer's data for efficient single-meter subscription
   - `subscribe(listener)` - Adds listener, starts RAF loop on first subscriber, returns unsubscribe function
   - `reset()` - Clears all state (for cleanup on session end)

   **RAF batching loop:**
   - `flushUpdates()` - Merges pendingUpdates into vuLevels, emits to listeners, requests next frame
   - Only runs when there are subscribers
   - Cancels on last subscriber removal

   **Critical implementation details:**
   - `getSnapshot` must return the SAME reference if data hasn't changed (Object.is comparison)
   - When merging updates, create new vuLevels object only if pendingUpdates has entries
   - Store timestamp using `performance.now()` for debugging/profiling

   Reference the pattern from 28-RESEARCH.md "Pattern 1: External Store with requestAnimationFrame Batching"
  </action>
  <verify>
    - `npm ls use-sync-external-store` shows package installed
    - `jam-ui/src/stores/vuStore.js` exists
    - File exports `vuStore` object with required methods
  </verify>
  <done>
    - use-sync-external-store installed in package.json
    - vuStore.js created with updateLevel, removeLevel, getSnapshot, getLevelSnapshot, subscribe, reset methods
    - RAF batching loop implemented that only runs with active subscribers
  </done>
</task>

<task type="auto">
  <name>Task 2: Create useVuStore React hook</name>
  <files>
    jam-ui/src/hooks/useVuStore.js
  </files>
  <action>
1. Create `jam-ui/src/hooks/useVuStore.js` implementing:

   **Imports:**
   ```javascript
   import { useSyncExternalStore } from 'use-sync-external-store/shim';
   import { useCallback } from 'react';
   import { vuStore } from '../stores/vuStore';
   ```

   **Export `useAllVuLevels` hook:**
   - Uses useSyncExternalStore with vuStore.subscribe and vuStore.getSnapshot
   - Returns the full vuLevels object
   - Note: Use sparingly - causes re-render on any VU change

   **Export `useVuLevel(mixerId)` hook:**
   - More efficient single-mixer subscription
   - Create stable getSnapshot callback using useCallback with [mixerId] dependency
   - Returns single mixer's data: { level, clipping, timestamp } or null

   **Critical implementation details:**
   - The subscribe function passed to useSyncExternalStore MUST be stable (not recreated each render)
   - vuStore.subscribe is defined outside React, so it's inherently stable
   - The getSnapshot for useVuLevel must be memoized with useCallback to avoid unnecessary resubscription

   Reference the pattern from 28-RESEARCH.md "Pattern 3: useSyncExternalStore Hook Wrapper"
  </action>
  <verify>
    - File exists at `jam-ui/src/hooks/useVuStore.js`
    - Exports useAllVuLevels and useVuLevel functions
    - Imports from 'use-sync-external-store/shim' (not bare 'use-sync-external-store')
  </verify>
  <done>
    - useVuStore.js created with useAllVuLevels and useVuLevel hooks
    - Hooks use useSyncExternalStore shim for React 16 compatibility
    - getSnapshot callbacks properly memoized
  </done>
</task>

</tasks>

<verification>
1. Run `cd jam-ui && npm ls use-sync-external-store` - should show version installed
2. Verify vuStore.js exists: `ls jam-ui/src/stores/vuStore.js`
3. Verify useVuStore.js exists: `ls jam-ui/src/hooks/useVuStore.js`
4. Check exports in vuStore.js: `grep -E "export (const|function)" jam-ui/src/stores/vuStore.js`
5. Check hook exports: `grep "export function" jam-ui/src/hooks/useVuStore.js`
6. Verify shim import path: `grep "use-sync-external-store/shim" jam-ui/src/hooks/useVuStore.js`
</verification>

<success_criteria>
- use-sync-external-store npm package installed
- vuStore.js implements external store with RAF batching loop
- useVuStore.js provides useAllVuLevels and useVuLevel hooks
- Hooks use the official React shim for useSyncExternalStore
- No React state (useState) in store implementation
</success_criteria>

<output>
After completion, create `.planning/phases/28-vu-meter-optimization/28-01-SUMMARY.md`
</output>
