refactor: stricter domain: server-directory

This commit is contained in:
2026-04-11 14:50:38 +02:00
parent 3fb5515c3a
commit c8bb82feb5
16 changed files with 51 additions and 43 deletions

View File

@@ -14,15 +14,23 @@ server-directory/
│ └── server-endpoint-state.service.ts Signal-based endpoint list, reconciliation with defaults, localStorage persistence │ └── server-endpoint-state.service.ts Signal-based endpoint list, reconciliation with defaults, localStorage persistence
├── domain/ ├── domain/
│ ├── server-directory.models.ts ServerEndpoint, ServerInfo, ServerJoinAccessResponse, invite/ban/kick types │ ├── constants/
── server-directory.constants.ts CLIENT_UPDATE_REQUIRED_MESSAGE │ └── server-directory.constants.ts CLIENT_UPDATE_REQUIRED_MESSAGE
── server-endpoint-defaults.ts Default endpoint templates, URL sanitisation, reconciliation helpers ── logic/
│ │ ├── room-signal-source.logic.ts Room → signal-source selector resolution
│ │ ├── room-signal-source.logic.spec.ts Unit tests
│ │ └── server-endpoint-defaults.logic.ts Default endpoint templates, URL sanitisation, reconciliation helpers
│ └── models/
│ └── server-directory.model.ts ServerEndpoint, ServerInfo, ServerJoinAccessResponse, invite/ban/kick types
├── infrastructure/ ├── infrastructure/
│ ├── server-directory-api.service.ts HTTP client for all server API calls │ ├── constants/
── server-endpoint-health.service.ts Health probe (GET /api/health with 5 s timeout, fallback to /api/servers) │ └── server-directory.infrastructure.constants.ts Health-check timeout, localStorage keys
── server-endpoint-compatibility.service.ts Semantic version comparison for client/server compatibility ── services/
── server-endpoint-storage.service.ts localStorage read/write for endpoint list and removed-default tracking ── server-directory-api.service.ts HTTP client for all server API calls
│ ├── server-endpoint-compatibility.service.ts Semantic version comparison for client/server compatibility
│ ├── server-endpoint-health.service.ts Health probe (GET /api/health with 5 s timeout, fallback to /api/servers)
│ └── server-endpoint-storage.service.ts localStorage read/write for endpoint list and removed-default tracking
├── feature/ ├── feature/
│ ├── invite/ Invite creation and resolution UI │ ├── invite/ Invite creation and resolution UI
@@ -45,8 +53,8 @@ graph TD
Health[ServerEndpointHealthService] Health[ServerEndpointHealthService]
Compat[ServerEndpointCompatibilityService] Compat[ServerEndpointCompatibilityService]
Storage[ServerEndpointStorageService] Storage[ServerEndpointStorageService]
Defaults[server-endpoint-defaults] Defaults[server-endpoint-defaults.logic]
Models[server-directory.models] Models[server-directory.model]
Facade --> Service Facade --> Service
Service --> API Service --> API
@@ -61,12 +69,12 @@ graph TD
click Facade "application/facades/server-directory.facade.ts" "Thin domain boundary" _blank click Facade "application/facades/server-directory.facade.ts" "Thin domain boundary" _blank
click Service "application/services/server-directory.service.ts" "Orchestrator" _blank click Service "application/services/server-directory.service.ts" "Orchestrator" _blank
click State "application/services/server-endpoint-state.service.ts" "Signal-based endpoint state" _blank click State "application/services/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 API "infrastructure/services/server-directory-api.service.ts" "HTTP client for server API" _blank
click Health "infrastructure/server-endpoint-health.service.ts" "Health probe" _blank click Health "infrastructure/services/server-endpoint-health.service.ts" "Health probe" _blank
click Compat "infrastructure/server-endpoint-compatibility.service.ts" "Version compatibility" _blank click Compat "infrastructure/services/server-endpoint-compatibility.service.ts" "Version compatibility" _blank
click Storage "infrastructure/server-endpoint-storage.service.ts" "localStorage persistence" _blank click Storage "infrastructure/services/server-endpoint-storage.service.ts" "localStorage persistence" _blank
click Defaults "domain/server-endpoint-defaults.ts" "Default endpoint templates" _blank click Defaults "domain/logic/server-endpoint-defaults.logic.ts" "Default endpoint templates" _blank
click Models "domain/server-directory.models.ts" "Domain types" _blank click Models "domain/models/server-directory.model.ts" "Domain types" _blank
``` ```
## Endpoint lifecycle ## Endpoint lifecycle

View File

@@ -6,7 +6,7 @@ import {
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../../core/constants'; import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../../core/constants';
import { User } from '../../../../shared-kernel'; import { User } from '../../../../shared-kernel';
import { ServerDirectoryApiService } from '../../infrastructure/server-directory-api.service'; import { ServerDirectoryApiService } from '../../infrastructure/services/server-directory-api.service';
import type { import type {
BanServerMemberRequest, BanServerMemberRequest,
CreateServerInviteRequest, CreateServerInviteRequest,
@@ -19,15 +19,15 @@ import type {
ServerJoinAccessResponse, ServerJoinAccessResponse,
ServerSourceSelector, ServerSourceSelector,
UnbanServerMemberRequest UnbanServerMemberRequest
} from '../../domain/server-directory.models'; } from '../../domain/models/server-directory.model';
import { import {
buildRoomSignalSelector, buildRoomSignalSelector,
buildRoomSignalSource, buildRoomSignalSource,
type RoomSignalSource, type RoomSignalSource,
type RoomSignalSourceInput type RoomSignalSourceInput
} from '../../domain/room-signal-source'; } from '../../domain/logic/room-signal-source.logic';
import { ServerEndpointCompatibilityService } from '../../infrastructure/server-endpoint-compatibility.service'; import { ServerEndpointCompatibilityService } from '../../infrastructure/services/server-endpoint-compatibility.service';
import { ServerEndpointHealthService } from '../../infrastructure/server-endpoint-health.service'; import { ServerEndpointHealthService } from '../../infrastructure/services/server-endpoint-health.service';
import { ServerEndpointStateService } from './server-endpoint-state.service'; import { ServerEndpointStateService } from './server-endpoint-state.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })

View File

@@ -16,14 +16,14 @@ import {
hasEndpointForDefault, hasEndpointForDefault,
matchDefaultEndpointTemplate, matchDefaultEndpointTemplate,
sanitiseServerBaseUrl sanitiseServerBaseUrl
} from '../../domain/server-endpoint-defaults'; } from '../../domain/logic/server-endpoint-defaults.logic';
import { ServerEndpointStorageService } from '../../infrastructure/server-endpoint-storage.service'; import { ServerEndpointStorageService } from '../../infrastructure/services/server-endpoint-storage.service';
import type { import type {
ConfiguredDefaultServerDefinition, ConfiguredDefaultServerDefinition,
DefaultEndpointTemplate, DefaultEndpointTemplate,
ServerEndpoint, ServerEndpoint,
ServerEndpointVersions ServerEndpointVersions
} from '../../domain/server-directory.models'; } from '../../domain/models/server-directory.model';
function resolveDefaultHttpProtocol(): 'http' | 'https' { function resolveDefaultHttpProtocol(): 'http' | 'https' {
return typeof window !== 'undefined' && window.location?.protocol === 'https:' return typeof window !== 'undefined' && window.location?.protocol === 'https:'

View File

@@ -3,7 +3,7 @@ import {
buildRoomSignalSelector, buildRoomSignalSelector,
buildRoomSignalSource, buildRoomSignalSource,
getSourceUrlFromSignalingUrl getSourceUrlFromSignalingUrl
} from './room-signal-source'; } from './room-signal-source.logic';
describe('room-signal-source helpers', () => { describe('room-signal-source helpers', () => {
it('converts signaling urls back to normalized source urls', () => { it('converts signaling urls back to normalized source urls', () => {

View File

@@ -1,5 +1,5 @@
import type { ServerEndpoint, ServerSourceSelector } from './server-directory.models'; import type { ServerEndpoint, ServerSourceSelector } from '../models/server-directory.model';
import { normaliseConfiguredServerUrl, sanitiseServerBaseUrl } from './server-endpoint-defaults'; import { normaliseConfiguredServerUrl, sanitiseServerBaseUrl } from './server-endpoint-defaults.logic';
export interface RoomSignalSource { export interface RoomSignalSource {
sourceId?: string; sourceId?: string;

View File

@@ -3,7 +3,7 @@ import type {
DefaultEndpointTemplate, DefaultEndpointTemplate,
DefaultServerDefinition, DefaultServerDefinition,
ServerEndpoint ServerEndpoint
} from './server-directory.models'; } from '../models/server-directory.model';
export function sanitiseServerBaseUrl(rawUrl: string): string { export function sanitiseServerBaseUrl(rawUrl: string): string {
let cleaned = rawUrl.trim().replace(/\/+$/, ''); let cleaned = rawUrl.trim().replace(/\/+$/, '');

View File

@@ -3,7 +3,7 @@ import type {
ChannelPermissionOverride, ChannelPermissionOverride,
RoomRole, RoomRole,
RoomRoleAssignment RoomRoleAssignment
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
export type ServerEndpointStatus = 'online' | 'offline' | 'checking' | 'unknown' | 'incompatible'; export type ServerEndpointStatus = 'online' | 'offline' | 'checking' | 'unknown' | 'incompatible';

View File

@@ -11,7 +11,7 @@ import { Store } from '@ngrx/store';
import { RoomsActions } from '../../../../store/rooms/rooms.actions'; import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import { UsersActions } from '../../../../store/users/users.actions'; import { UsersActions } from '../../../../store/users/users.actions';
import { selectCurrentUser } from '../../../../store/users/users.selectors'; import { selectCurrentUser } from '../../../../store/users/users.selectors';
import type { ServerInviteInfo } from '../../domain/server-directory.models'; import type { ServerInviteInfo } from '../../domain/models/server-directory.model';
import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants'; import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants';
import { DatabaseService } from '../../../../infrastructure/persistence'; import { DatabaseService } from '../../../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade'; import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';

View File

@@ -36,7 +36,7 @@ import {
import { Room, User } from '../../../../shared-kernel'; import { Room, User } from '../../../../shared-kernel';
import { SettingsModalService } from '../../../../core/services/settings-modal.service'; import { SettingsModalService } from '../../../../core/services/settings-modal.service';
import { DatabaseService } from '../../../../infrastructure/persistence'; import { DatabaseService } from '../../../../infrastructure/persistence';
import { type ServerInfo } from '../../domain/server-directory.models'; import { type ServerInfo } from '../../domain/models/server-directory.model';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade'; import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
import { selectCurrentUser } from '../../../../store/users/users.selectors'; import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { ConfirmDialogComponent } from '../../../../shared'; import { ConfirmDialogComponent } from '../../../../shared';

View File

@@ -1,4 +1,4 @@
export * from './application/facades/server-directory.facade'; export * from './application/facades/server-directory.facade';
export * from './domain/server-directory.constants'; export * from './domain/constants/server-directory.constants';
export * from './domain/server-directory.models'; export * from './domain/models/server-directory.model';
export * from './domain/room-signal-source'; export * from './domain/logic/room-signal-source.logic';

View File

@@ -15,8 +15,8 @@ import {
RoomRole, RoomRole,
RoomRoleAssignment, RoomRoleAssignment,
User User
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import { ServerEndpointStateService } from '../application/services/server-endpoint-state.service'; import { ServerEndpointStateService } from '../../application/services/server-endpoint-state.service';
import type { import type {
BanServerMemberRequest, BanServerMemberRequest,
CreateServerInviteRequest, CreateServerInviteRequest,
@@ -28,8 +28,8 @@ import type {
ServerJoinAccessResponse, ServerJoinAccessResponse,
ServerSourceSelector, ServerSourceSelector,
UnbanServerMemberRequest UnbanServerMemberRequest
} from '../domain/server-directory.models'; } from '../../domain/models/server-directory.model';
import type { RoomSignalSourceInput } from '../domain/room-signal-source'; import type { RoomSignalSourceInput } from '../../domain/logic/room-signal-source.logic';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ServerDirectoryApiService { export class ServerDirectoryApiService {

View File

@@ -1,6 +1,6 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import type { ServerVersionCompatibilityResult } from '../domain/server-directory.models'; import type { ServerVersionCompatibilityResult } from '../../domain/models/server-directory.model';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ServerEndpointCompatibilityService { export class ServerEndpointCompatibilityService {

View File

@@ -1,10 +1,10 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { SERVER_HEALTH_CHECK_TIMEOUT_MS } from './server-directory.infrastructure.constants'; import { SERVER_HEALTH_CHECK_TIMEOUT_MS } from '../constants/server-directory.infrastructure.constants';
import type { import type {
ServerEndpoint, ServerEndpoint,
ServerEndpointHealthResult, ServerEndpointHealthResult,
ServerHealthCheckPayload ServerHealthCheckPayload
} from '../domain/server-directory.models'; } from '../../domain/models/server-directory.model';
import { ServerEndpointCompatibilityService } from './server-endpoint-compatibility.service'; import { ServerEndpointCompatibilityService } from './server-endpoint-compatibility.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY, SERVER_ENDPOINTS_STORAGE_KEY } from './server-directory.infrastructure.constants'; import { REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY, SERVER_ENDPOINTS_STORAGE_KEY } from '../constants/server-directory.infrastructure.constants';
import type { ServerEndpoint } from '../domain/server-directory.models'; import type { ServerEndpoint } from '../../domain/models/server-directory.model';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ServerEndpointStorageService { export class ServerEndpointStorageService {