Skip to content

refactor(frontend): singleton consolidation phase 2 — API clients, observer patterns, module-scoped composables, cache boundary (from #11634 audit) #11682

Description

@mrveiss

Phase-2 of the frontend singleton consolidation from the #11634 audit (phase 1 = #11640: health monitors + repository instances, merged). Evidence from the 2026-07-11 frontend singleton inventory; WebSocket manager consolidation stays in #6488 (excluded here).

Confirmed duplications / drift (file evidence)

  1. Five API-client singletons overlap the main apiClient (src/utils/ApiClient.ts Proxy singleton): AdvancedControlApiClient.ts, VisionMultimodalApiClient.ts, SecretsApiClient.js, FeatureFlagsApiClient.ts — each export const x = new X(). No documented rule for when to use which; risk of divergent auth/base-url handling.
  2. Custom observer singletons vs canonical state: src/utils/ObserverPatterns.js exports eventObserver + stateObserver (module-level Map-based pub/sub and state watchers) — responsibilities that belong to Pinia stores / the event bus (useEventBus wrapping LiveEventService per refactor(frontend/ws): unify three WebSocket managers (useWebSocket + useGlobalWebSocket + useLiveEvents) into single useEventBus #6488 phase 1). Module-level Maps also accumulate listeners across HMR.
  3. Module-scoped composable state acting as unmanaged singletons: useConfirmDialog, useActionQueue (localStorage-persisted), usePushNotifications, useVoiceConversation hold ref() state at module scope — no test isolation, survives unmount; candidates for Pinia stores or documented intentional singletons with reset seams.
  4. Cache split undocumented: CacheManager (browser-level: storage/service-worker caches) vs CacheService (in-app LRU) — likely intentional separation, but nothing states the boundary; name proximity invites misuse.

Acceptance Criteria

  • API clients: one documented decision — either fold specialized clients into apiClient modules or keep them with an explicit "when to use which" note + shared base (no divergent auth/base-url logic); grep evidence of consumers migrated
  • eventObserver/stateObserver: consumers inventoried; migrate to Pinia/useEventBus or delete-if-orphaned per dead-code rule (wire-in first)
  • Module-scoped composables: each either moves state to a Pinia store or gains a documented reset seam for tests
  • Cache boundary documented at both class docblocks
  • vue-tsc + frontend suites clean

Activity

  1. mrveiss commented on Jul 22, 2026

    @mrveiss
    OwnerAuthor

    Read-only audit (audit-first, awaiting approval before any code change)

    API-client singletons vs canonical apiClient (utils/ApiClient.ts Proxy singleton)

    Client Real call-sites Verdict
    AdvancedControlApiClient.ts 0 DEAD CODE — wire-in-or-remove decision (dead-code rule)
    VisionMultimodalApiClient.ts ~8 SAFE-TO-CONSOLIDATE but HIGH-risk — lacks 401-auto-logout + token-expiry that canonical has; requestFormData multi-field upload must fold into ApiClient.uploadFile (currently single-file) to avoid regressing combineModalities
    SecretsApiClient.js 5 SAFE — mechanical: instantiates a second new ApiClient() instead of the shared apiClient; 1-line swap
    FeatureFlagsApiClient.ts 1 runtime (FeatureFlagsSettingsPanel) SAFE-TO-CONSOLIDATE; shares a verbatim-duplicated base-URL-init block with Vision

    Orphaned singletons (0 consumers — dead-code decisions)

    • utils/ObserverPatterns.js (eventObserver/stateObserver + 6 more exports) — 0 imports anywhere. Never wired since introduction. HMR listener-accumulation risk is moot (no subscribers).
    • utils/CacheManager.ts / cacheManager — 0 call-sites. (Distinct from services/CacheService.ts, which has 1 real caller, SettingsPanel.)

    Latent composable bug (real, worth fixing)

    useVoiceConversation.ts registers watch(isSpeaking) (L842) + watch(prefLanguage) (L850) inside the composable body → one watcher per call. With 3 real callers (Panel + Overlay + ChatInterface) potentially mounted together, _resumeAutoListening() can double-fire on TTS-completion — the exact double-fire class the code's own comments say was fixed once (#6823) on a different path. Same pattern (unguarded per-call watch) in useActionQueue.ts:99 (1 caller today, latent). useConfirmDialog/useVoiceConversation also lack test-reset seams.

    Proposed staged plan (each a separate reviewable PR)

    LOW-risk / mechanical (do first, on approval):

    1. SecretsApiClient → use shared apiClient (1-line, transparent).
    2. Document CacheManager vs CacheService boundary (docblocks, no code).
    3. File wire-in-or-remove discovery issues for the 3 orphans (AdvancedControlApiClient, ObserverPatterns, CacheManager).
    4. Add _resetForTests() seams to the 4 module-scoped composables (additive).
    5. Extract the duplicated base-URL-init block shared by Vision + FeatureFlags.

    HIGH-risk / app-wide (needs manual QA, separate PRs):
    6. Fold Vision + FeatureFlags clients into apiClient (picks up 401-redirect + expiry; ~14 files; needs QA on screen-analysis/OCR/image-audio + feature-flag admin).
    7. Fix the watch()-per-call accumulation in useVoiceConversation/useActionQueue (needs live QA of voice modes; intricate state machine).

    WebSocket consolidation (#6488) explicitly untouched. No code changed — awaiting your go-ahead on which steps to take (I'd suggest starting with steps 1-4, and the 3 orphan-disposition decisions).

  2. mrveiss commented on Jul 23, 2026

    @mrveiss
    OwnerAuthor

    Status: Tier-1 merged (#12106 SecretsApiClient; #12123 removed orphans ObserverPatterns/CacheManager; AdvancedControlApiClient → wire-in #12102). Tier-2 in progress (owner-approved, owner QA before merge): #12152 (fold Vision/FeatureFlags→apiClient), #12153 (useVoiceConversation watcher fix).

  3. added this to the v0.10.0 milestone on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions