Files
Toju/toju-app/src/app/domains/server-directory/README.md

8.4 KiB

Server Directory Domain

Manages the list of server endpoints the client can connect to, health-checking them, resolving API URLs, and providing server CRUD, search, invites, and moderation. This is the central domain that other domains (auth, chat, attachment) depend on for knowing where the backend is.

Module map

server-directory/
├── application/
│   ├── server-directory.facade.ts           High-level API: server CRUD, search, health, invites, moderation
│   └── server-endpoint-state.service.ts     Signal-based endpoint list, reconciliation with defaults, localStorage persistence
│
├── domain/
│   ├── server-directory.models.ts           ServerEndpoint, ServerInfo, ServerJoinAccessResponse, invite/ban/kick types
│   ├── server-directory.constants.ts        CLIENT_UPDATE_REQUIRED_MESSAGE
│   └── server-endpoint-defaults.ts          Default endpoint templates, URL sanitisation, reconciliation helpers
│
├── infrastructure/
│   ├── server-directory-api.service.ts            HTTP client for all server API calls
│   ├── server-endpoint-health.service.ts          Health probe (GET /api/health with 5 s timeout, fallback to /api/servers)
│   ├── server-endpoint-compatibility.service.ts   Semantic version comparison for client/server compatibility
│   └── server-endpoint-storage.service.ts         localStorage read/write for endpoint list and removed-default tracking
│
├── feature/
│   ├── invite/                              Invite creation and resolution UI
│   ├── server-search/                       Server search/browse panel
│   └── settings/                            Server endpoint management settings
│
└── index.ts                                 Barrel exports

Layer composition

The facade delegates HTTP work to the API service and endpoint state to the state service. Health probing combines the health service and compatibility service. Storage is accessed only through the state service.

graph TD
    Facade[ServerDirectoryFacade]
    State[ServerEndpointStateService]
    API[ServerDirectoryApiService]
    Health[ServerEndpointHealthService]
    Compat[ServerEndpointCompatibilityService]
    Storage[ServerEndpointStorageService]
    Defaults[server-endpoint-defaults]
    Models[server-directory.models]

    Facade --> API
    Facade --> State
    Facade --> Health
    Facade --> Compat
    API --> State
    State --> Storage
    State --> Defaults
    Health --> Compat

    click Facade "application/server-directory.facade.ts" "High-level API" _blank
    click State "application/server-endpoint-state.service.ts" "Signal-based endpoint state" _blank
    click API "infrastructure/server-directory-api.service.ts" "HTTP client for server API" _blank
    click Health "infrastructure/server-endpoint-health.service.ts" "Health probe" _blank
    click Compat "infrastructure/server-endpoint-compatibility.service.ts" "Version compatibility" _blank
    click Storage "infrastructure/server-endpoint-storage.service.ts" "localStorage persistence" _blank
    click Defaults "domain/server-endpoint-defaults.ts" "Default endpoint templates" _blank
    click Models "domain/server-directory.models.ts" "Domain types" _blank

Endpoint lifecycle

On startup, ServerEndpointStateService loads endpoints from localStorage, reconciles them with the configured defaults from the environment, and ensures at least one endpoint is active.

stateDiagram-v2
    [*] --> Load: constructor
    Load --> HasStored: localStorage has endpoints
    Load --> InitDefaults: no stored endpoints
    InitDefaults --> Ready: save default endpoints
    HasStored --> Reconcile: compare stored vs defaults
    Reconcile --> Ready: merge, ensure active
    Ready --> HealthCheck: facade.testAllServers()

    state HealthCheck {
        [*] --> Probing
        Probing --> Online: /api/health 200 OK
        Probing --> Incompatible: version mismatch
        Probing --> Offline: request failed
    }

Health probing

The facade exposes testServer(endpointId) and testAllServers(). Both delegate to ServerEndpointHealthService.probeEndpoint(), which:

  1. Sends GET /api/health with a 5-second timeout
  2. On success, checks the response's serverVersion against the client version via ServerEndpointCompatibilityService
  3. If versions are incompatible, the endpoint is marked incompatible and deactivated
  4. If /api/health fails, falls back to GET /api/servers as a basic liveness check
  5. Updates the endpoint's status, latency, and version info in the state service
sequenceDiagram
    participant Facade
    participant Health as HealthService
    participant Compat as CompatibilityService
    participant API as Server

    Facade->>Health: probeEndpoint(endpoint, clientVersion)
    Health->>API: GET /api/health (5s timeout)

    alt 200 OK
        API-->>Health: { serverVersion }
        Health->>Compat: evaluateServerVersion(serverVersion, clientVersion)
        Compat-->>Health: { isCompatible, serverVersion }
        Health-->>Facade: online / incompatible + latency + versions
    else Request failed
        Health->>API: GET /api/servers (fallback)
        alt 200 OK
            API-->>Health: servers list
            Health-->>Facade: online + latency
        else Also failed
            Health-->>Facade: offline
        end
    end

    Facade->>Facade: updateServerStatus(id, status, latency, versions)

The facade's searchServers(query) method supports two modes controlled by a searchAllServers flag:

  • Single endpoint: searches only the active server's API
  • All endpoints: fans out the query to every online active endpoint via forkJoin, then deduplicates results by server ID

The API service normalises every ServerInfo response, filling in sourceId, sourceName, and sourceUrl so the UI knows which endpoint each server came from.

Server-owned room metadata

ServerInfo also carries the server-owned channels list for each room. Register and update calls persist this channel metadata on the server, and search or hydration responses return the normalised channel list so text and voice channel topology survives reloads, reconnects, and fresh joins.

The renderer may cache room data locally, but channel creation, rename, and removal must round-trip through the server-directory API instead of being treated as client-only state. Server-side normalisation also deduplicates channel names before persistence.

Default endpoint management

Default servers are configured in the environment file. The state service builds DefaultEndpointTemplate objects from the configuration and uses them during reconciliation:

  • Stored endpoints are matched to defaults by defaultKey or URL
  • Missing defaults are added unless the user explicitly removed them (tracked in a separate localStorage key)
  • restoreDefaultServers() re-adds any removed defaults and clears the removal tracking
  • The primary default URL is used as a fallback when no endpoint is resolved

URL sanitisation strips trailing slashes and /api suffixes. Protocol-less URLs get http or https based on the current page protocol.

Server administration

The facade provides methods for server registration, updates, and unregistration. These map directly to the API service's HTTP calls:

Method HTTP Endpoint
registerServer POST /api/servers
updateServer PUT /api/servers/:id
unregisterServer DELETE /api/servers/:id

Invites and moderation

Method Purpose
createInvite(serverId, request) Creates a time-limited invite link
getInvite(inviteId) Resolves invite metadata
requestServerAccess(request) Joins a server (via membership, password, invite, or public access)
kickServerMember(serverId, request) Removes a user from the server
banServerMember(serverId, request) Bans a user with optional reason and expiry
unbanServerMember(serverId, request) Lifts a ban

Persistence

All endpoint state is persisted to localStorage under two keys:

Key Contents
metoyou_server_endpoints Full ServerEndpoint[] array
metoyou_removed_default_server_keys Set of default endpoint keys the user explicitly removed

The storage service handles JSON serialisation and defensive parsing. Invalid data falls back to empty state rather than throwing.