Files
bitvid/docs/app-coordinators.md
T
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

105 lines
5.7 KiB
Markdown

# 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`:
```javascript
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`:
```javascript
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()`.