Files
bitvid/docs/app-coordinators.md
Claude 41a12669f6 refactor: extract Application method clusters into coordinator modules
Move cohesive method groups out of the monolithic Application class
(js/app.js, ~10K lines) into five coordinator modules under js/app/:

- feedCoordinator: feed registration, runtime building, refresh,
  video loading, URL-health helpers, and feed telemetry
- playbackCoordinator: URL-first + magnet fallback pipeline, probing,
  torrent status intervals, and video timestamp resolution
- authSessionCoordinator: signer lifecycle, profile switching,
  relay/block/wallet management, and cleanup
- modalCoordinator: modal open/close, view-count subscriptions,
  watch-history metadata, and playback logging
- moderationCoordinator: moderation settings, video decoration,
  override/block/hide action routing

Each coordinator factory receives injected dependencies instead of
importing globals. Application retains thin delegator methods so all
public call-sites remain unchanged. Coordinator initialisation is
idempotent (via _initCoordinators) and supports both constructor-based
and Object.create()-based instantiation used by test harnesses.

All existing unit tests, lint checks, and syntax validation pass.

https://claude.ai/code/session_01C3PKJTCdavsGbk5jjC85EB
2026-02-06 20:47:10 +00:00

5.7 KiB

Application Coordinator Architecture

The Application class (js/app.js) acts as the composition root for bitvid. Business logic that previously lived directly on Application has been extracted into coordinator modules under js/app/. Application retains thin delegator methods so that every public call-site continues to work unchanged.

Coordinator Modules

Module File Responsibility
Feed js/app/feedCoordinator.js Feed registration, runtime building, refresh, video loading, URL-health helpers, feed telemetry
Playback js/app/playbackCoordinator.js URL-first + magnet fallback pipeline, probing, torrent status, video timestamps
Auth/Session js/app/authSessionCoordinator.js Signer lifecycle, profile switching, relay/block/wallet management, cleanup
Modal js/app/modalCoordinator.js Modal open/close, view-count subscriptions, watch-history metadata, playback logging
Moderation js/app/moderationCoordinator.js Moderation settings, video decoration, override/block/hide actions

A shared helper (js/app/bindCoordinator.js) binds every method returned by a coordinator factory to the Application instance so that this works as expected.

Dependency Injection

Each coordinator is a factory function that receives a deps object containing every module-level dependency it needs. Coordinators never import globals themselves; all references come through the closure created by deps:

export function createFeedCoordinator(deps) {
  const { devLogger, nostrClient, watchHistoryService, ... } = deps;

  return {
    async loadVideos(forceFetch = false) {
      devLogger.log("Starting loadVideos...");  // closure var, not import
      // `this` is the Application instance (bound by caller)
      this.checkRelayHealthWarning();
      // ...
    },
  };
}

Application._initCoordinators() creates all five coordinators and injects their dependencies from the module scope of js/app.js. It is called eagerly in the constructor and idempotently from every delegator, so test harnesses that use Object.create(Application.prototype) also work.

Delegation Pattern

Every extracted method has a matching thin delegator on Application:

class Application {
  async loadVideos(...args) {
    this._initCoordinators();
    return this._feed.loadVideos(...args);
  }
}

This preserves the public API surface. Callers (views, services, test harnesses) do not need to know about coordinators.

Old-to-New Mapping

Old location (Application method) New location
registerRecentFeed, registerForYouFeed, registerKidsFeed, registerExploreFeed, registerSubscriptionsFeed, registerWatchHistoryFeed feedCoordinator
buildForYouFeedRuntime, buildExploreFeedRuntime, buildRecentFeedRuntime, buildKidsFeedRuntime feedCoordinator
refreshForYouFeed, refreshKidsFeed, refreshExploreFeed, refreshRecentFeed feedCoordinator
loadVideos, loadForYouVideos, loadKidsVideos, loadExploreVideos, loadOlderVideos feedCoordinator
renderVideoList, refreshVideoDiscussionCounts feedCoordinator
checkRelayHealthWarning, hasOlderVersion feedCoordinator
Feed telemetry (getFeedTelemetryState, emitFeedImpressions, recordFeedClick, ...) feedCoordinator
playHttp, playViaWebTorrent, playVideoWithFallback playbackCoordinator
playVideoByEventId, playVideoWithoutEvent playbackCoordinator
probeUrl, probeUrlWithVideoElement, checkUrlParams playbackCoordinator
autoplayModalVideo, startTorrentStatusMirrors playbackCoordinator
buildModalTimestampPayload, resolveVideoPostedAt, ensureModalPostedTimestamp playbackCoordinator
shouldDeferModeratedPlayback, resumePendingModeratedPlayback playbackCoordinator
handleAuthLogin, handleAuthLogout, requestLogout authSessionCoordinator
handleBlocksLoaded, handleRelaysLoaded, scheduleRelayUiRefresh authSessionCoordinator
cleanup, waitForCleanup, clearActiveIntervals authSessionCoordinator
handleProfileSwitchRequest, waitForIdentityRefresh, handleProfileLogoutRequest authSessionCoordinator
handleProfileRelayOperation, handleProfileBlocklistMutation, handleProfileAdminMutation authSessionCoordinator
handleProfileWalletPersist, handleProfileWalletTest, handleProfileWalletDisconnect authSessionCoordinator
showModalWithPoster, hideModal, ensureVideoModalReady modalCoordinator
subscribeModalViewCount, teardownModalViewCountSubscription modalCoordinator
preparePlaybackLogging, teardownVideoElement modalCoordinator
persistWatchHistoryMetadataForVideo, handleRemoveHistoryAction modalCoordinator
setShareButtonState, getShareUrlBase modalCoordinator
handleModerationSettingsChange, refreshVisibleModerationUi moderationCoordinator
decorateVideoModeration, initializeModerationActionController moderationCoordinator
handleModerationOverride, handleModerationBlock, handleModerationHide moderationCoordinator
normalizeModerationSettings, getActiveModerationThresholds moderationCoordinator

Adding New Methods

When adding a method that belongs to an existing coordinator:

  1. Add the method to the coordinator's return object.
  2. Add a thin delegator on Application that calls this._initCoordinators() then delegates to the coordinator.
  3. If the method uses a new module-level import, add it to the coordinator's deps destructuring and pass it from _initCoordinators().