Skip to content

ADR 20: File Explorer Frontend Service Layer (Interfaces, Singletons, RxJS)

Status: Accepted
Date: 2026-04-27

Context

The Web File Explorer (ADR 15) in apps/file-explorer coordinates session checks, gateway presence, drive listings, tree navigation, uploads, and route-level gates. That logic had accumulated in route resolvers with mutable module-level state, ad-hoc fetch calls, and types split between resolver files and callers. That shape made it harder to:

  • keep one place for side effects and shared state updates;
  • align browser calls with the OpenAPI-derived client (ADR 11);
  • evolve toward testable boundaries without rewriting routing.

We want a small, explicit service-oriented front-end architecture that stays compatible with Mithril route onmatch flows and does not pretend to be a second backend.

Decision

  1. Service layer: Introduce apps/file-explorer/src/services/ as the home for application services (session, gateway presence, drives, tree, upload, auth, telemetry). Each capability exposes a TypeScript interface (contracts in contracts.ts) and a concrete class implementation.

  2. Singleton composition: Export one instance per service from apps/file-explorer/src/services/index.ts. Resolvers and other entry points import those singletons rather than constructing services per route. This matches the current Mithril bootstrap (no global DI container yet) while keeping construction in one file.

  3. Resolvers as adapters: Keep apps/file-explorer/src/resolvers/ as thin route adapters that call services and preserve existing function signatures and route outcomes used by routing.tsx. Where legacy exports (session, gatewayStatus, explorerRouteData, explorerTreeData) remain, they delegate to service snapshots so the UI does not churn in one step.

  4. RxJS for shared state: Services that own cross-route UI state expose BehaviorSubject internally, plus state$ and getSnapshot(). Command methods (resolveSession, resolveExplorerDrives, etc.) update subjects. Components may still read snapshots synchronously during migration; prefer subscribing at layout boundaries when moving to stream-driven redraws.

  5. API URLs and telemetry: Do not duplicate gateway URL construction in explorer-only config types. Use get*Url helpers from @substratum/api-client (generated.ts) so paths stay aligned with utoipa / OpenAPI on the gateway. Optional or debug traffic (e.g. UI telemetry ingest) is still declared on the gateway and generated like any other route.

  6. Twelve-factor alignment (frontend slice): Runtime behavior continues to depend on environment and reverse-proxy wiring (e.g. Vite /api proxy, nginx edge) per ADR 13. The explorer does not introduce a parallel “config server”; the API spec and client are the source of truth for HTTP paths.

Consequences

  • Positive: Clear boundaries for session, connectivity, and explorer data; easier refactors and tests against interfaces; state transitions centralized; HTTP paths stay consistent with the gateway OpenAPI dump.
  • Negative: Singletons are global process state in the browser tab (acceptable for this app size, but not a full DI story); RxJS adds a dependency and a learning curve for contributors; resolver shims remain until all readers move to services or streams.
  • Action category split (panel vs immediate): Context-menu actions now carry a category of panel (Properties, Share, Move, Rename, Delete — opens a route-driven side panel) or immediate (Open — executes synchronously, e.g. navigates a folder or opens a file in a new tab via getGetDriveContentUrl). ExplorerWorkspace.onContextAction branches on category so immediate actions never participate in route panel state, while panel actions continue to use setPanel. The ContextActionService now returns a discriminated { supported, ok, message } result so the workspace can surface real gateway errors instead of a generic "not available yet" message.