refactor: true facades

This commit is contained in:
2026-04-11 14:35:02 +02:00
parent db7e683504
commit a6bdac1a25
10 changed files with 261 additions and 794 deletions

View File

@@ -7,8 +7,11 @@ Manages the list of server endpoints the client can connect to, health-checking
```
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
│ ├── facades/
│ └── server-directory.facade.ts Thin domain boundary, delegates to ServerDirectoryService
│ └── services/
│ ├── server-directory.service.ts Orchestrator: 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
@@ -31,11 +34,12 @@ server-directory/
## 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.
The facade is a thin pass-through that delegates to `ServerDirectoryService`. The service 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.
```mermaid
graph TD
Facade[ServerDirectoryFacade]
Service[ServerDirectoryService]
State[ServerEndpointStateService]
API[ServerDirectoryApiService]
Health[ServerEndpointHealthService]
@@ -44,17 +48,19 @@ graph TD
Defaults[server-endpoint-defaults]
Models[server-directory.models]
Facade --> API
Facade --> State
Facade --> Health
Facade --> Compat
Facade --> Service
Service --> API
Service --> State
Service --> Health
Service --> 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 Facade "application/facades/server-directory.facade.ts" "Thin domain boundary" _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 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
@@ -87,7 +93,7 @@ stateDiagram-v2
## Health probing
The facade exposes `testServer(endpointId)` and `testAllServers()`. Both delegate to `ServerEndpointHealthService.probeEndpoint()`, which:
The facade exposes `testServer(endpointId)` and `testAllServers()`. Both delegate through the service to `ServerEndpointHealthService.probeEndpoint()`, which:
1. Sends `GET /api/health` with a 5-second timeout
2. Reads the response's `serverVersion` and stable `serverInstanceId`

View File

@@ -0,0 +1,229 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { Injectable, inject } from '@angular/core';
import { ServerDirectoryService } from '../services/server-directory.service';
@Injectable({ providedIn: 'root' })
export class ServerDirectoryFacade {
private readonly service = inject(ServerDirectoryService);
readonly servers = this.service.servers;
readonly activeServers = this.service.activeServers;
readonly hasMissingDefaultServers = this.service.hasMissingDefaultServers;
readonly activeServer = this.service.activeServer;
awaitInitialServerHealthCheck(
...args: Parameters<ServerDirectoryService['awaitInitialServerHealthCheck']>
): ReturnType<ServerDirectoryService['awaitInitialServerHealthCheck']> {
return this.service.awaitInitialServerHealthCheck(...args);
}
addServer(
...args: Parameters<ServerDirectoryService['addServer']>
): ReturnType<ServerDirectoryService['addServer']> {
return this.service.addServer(...args);
}
ensureServerEndpoint(
...args: Parameters<ServerDirectoryService['ensureServerEndpoint']>
): ReturnType<ServerDirectoryService['ensureServerEndpoint']> {
return this.service.ensureServerEndpoint(...args);
}
findServerByUrl(
...args: Parameters<ServerDirectoryService['findServerByUrl']>
): ReturnType<ServerDirectoryService['findServerByUrl']> {
return this.service.findServerByUrl(...args);
}
removeServer(
...args: Parameters<ServerDirectoryService['removeServer']>
): ReturnType<ServerDirectoryService['removeServer']> {
return this.service.removeServer(...args);
}
restoreDefaultServers(
...args: Parameters<ServerDirectoryService['restoreDefaultServers']>
): ReturnType<ServerDirectoryService['restoreDefaultServers']> {
return this.service.restoreDefaultServers(...args);
}
setActiveServer(
...args: Parameters<ServerDirectoryService['setActiveServer']>
): ReturnType<ServerDirectoryService['setActiveServer']> {
return this.service.setActiveServer(...args);
}
deactivateServer(
...args: Parameters<ServerDirectoryService['deactivateServer']>
): ReturnType<ServerDirectoryService['deactivateServer']> {
return this.service.deactivateServer(...args);
}
updateServerStatus(
...args: Parameters<ServerDirectoryService['updateServerStatus']>
): ReturnType<ServerDirectoryService['updateServerStatus']> {
return this.service.updateServerStatus(...args);
}
ensureEndpointVersionCompatibility(
...args: Parameters<ServerDirectoryService['ensureEndpointVersionCompatibility']>
): ReturnType<ServerDirectoryService['ensureEndpointVersionCompatibility']> {
return this.service.ensureEndpointVersionCompatibility(...args);
}
resolveRoomEndpoint(
...args: Parameters<ServerDirectoryService['resolveRoomEndpoint']>
): ReturnType<ServerDirectoryService['resolveRoomEndpoint']> {
return this.service.resolveRoomEndpoint(...args);
}
normaliseRoomSignalSource(
...args: Parameters<ServerDirectoryService['normaliseRoomSignalSource']>
): ReturnType<ServerDirectoryService['normaliseRoomSignalSource']> {
return this.service.normaliseRoomSignalSource(...args);
}
buildRoomSignalSelector(
...args: Parameters<ServerDirectoryService['buildRoomSignalSelector']>
): ReturnType<ServerDirectoryService['buildRoomSignalSelector']> {
return this.service.buildRoomSignalSelector(...args);
}
getFallbackRoomEndpoints(
...args: Parameters<ServerDirectoryService['getFallbackRoomEndpoints']>
): ReturnType<ServerDirectoryService['getFallbackRoomEndpoints']> {
return this.service.getFallbackRoomEndpoints(...args);
}
setSearchAllServers(
...args: Parameters<ServerDirectoryService['setSearchAllServers']>
): ReturnType<ServerDirectoryService['setSearchAllServers']> {
return this.service.setSearchAllServers(...args);
}
testServer(
...args: Parameters<ServerDirectoryService['testServer']>
): ReturnType<ServerDirectoryService['testServer']> {
return this.service.testServer(...args);
}
testAllServers(
...args: Parameters<ServerDirectoryService['testAllServers']>
): ReturnType<ServerDirectoryService['testAllServers']> {
return this.service.testAllServers(...args);
}
getApiBaseUrl(
...args: Parameters<ServerDirectoryService['getApiBaseUrl']>
): ReturnType<ServerDirectoryService['getApiBaseUrl']> {
return this.service.getApiBaseUrl(...args);
}
getWebSocketUrl(
...args: Parameters<ServerDirectoryService['getWebSocketUrl']>
): ReturnType<ServerDirectoryService['getWebSocketUrl']> {
return this.service.getWebSocketUrl(...args);
}
searchServers(
...args: Parameters<ServerDirectoryService['searchServers']>
): ReturnType<ServerDirectoryService['searchServers']> {
return this.service.searchServers(...args);
}
getServers(
...args: Parameters<ServerDirectoryService['getServers']>
): ReturnType<ServerDirectoryService['getServers']> {
return this.service.getServers(...args);
}
getServer(
...args: Parameters<ServerDirectoryService['getServer']>
): ReturnType<ServerDirectoryService['getServer']> {
return this.service.getServer(...args);
}
findServerAcrossActiveEndpoints(
...args: Parameters<ServerDirectoryService['findServerAcrossActiveEndpoints']>
): ReturnType<ServerDirectoryService['findServerAcrossActiveEndpoints']> {
return this.service.findServerAcrossActiveEndpoints(...args);
}
registerServer(
...args: Parameters<ServerDirectoryService['registerServer']>
): ReturnType<ServerDirectoryService['registerServer']> {
return this.service.registerServer(...args);
}
updateServer(
...args: Parameters<ServerDirectoryService['updateServer']>
): ReturnType<ServerDirectoryService['updateServer']> {
return this.service.updateServer(...args);
}
unregisterServer(
...args: Parameters<ServerDirectoryService['unregisterServer']>
): ReturnType<ServerDirectoryService['unregisterServer']> {
return this.service.unregisterServer(...args);
}
getServerUsers(
...args: Parameters<ServerDirectoryService['getServerUsers']>
): ReturnType<ServerDirectoryService['getServerUsers']> {
return this.service.getServerUsers(...args);
}
requestJoin(
...args: Parameters<ServerDirectoryService['requestJoin']>
): ReturnType<ServerDirectoryService['requestJoin']> {
return this.service.requestJoin(...args);
}
createInvite(
...args: Parameters<ServerDirectoryService['createInvite']>
): ReturnType<ServerDirectoryService['createInvite']> {
return this.service.createInvite(...args);
}
getInvite(
...args: Parameters<ServerDirectoryService['getInvite']>
): ReturnType<ServerDirectoryService['getInvite']> {
return this.service.getInvite(...args);
}
kickServerMember(
...args: Parameters<ServerDirectoryService['kickServerMember']>
): ReturnType<ServerDirectoryService['kickServerMember']> {
return this.service.kickServerMember(...args);
}
banServerMember(
...args: Parameters<ServerDirectoryService['banServerMember']>
): ReturnType<ServerDirectoryService['banServerMember']> {
return this.service.banServerMember(...args);
}
unbanServerMember(
...args: Parameters<ServerDirectoryService['unbanServerMember']>
): ReturnType<ServerDirectoryService['unbanServerMember']> {
return this.service.unbanServerMember(...args);
}
notifyLeave(
...args: Parameters<ServerDirectoryService['notifyLeave']>
): ReturnType<ServerDirectoryService['notifyLeave']> {
return this.service.notifyLeave(...args);
}
updateUserCount(
...args: Parameters<ServerDirectoryService['updateUserCount']>
): ReturnType<ServerDirectoryService['updateUserCount']> {
return this.service.updateUserCount(...args);
}
sendHeartbeat(
...args: Parameters<ServerDirectoryService['sendHeartbeat']>
): ReturnType<ServerDirectoryService['sendHeartbeat']> {
return this.service.sendHeartbeat(...args);
}
}

View File

@@ -4,10 +4,9 @@ import {
type Signal
} from '@angular/core';
import { Observable } from 'rxjs';
import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../core/constants';
import { User } from '../../../shared-kernel';
import { CLIENT_UPDATE_REQUIRED_MESSAGE } from '../domain/server-directory.constants';
import { ServerDirectoryApiService } from '../infrastructure/server-directory-api.service';
import { STORAGE_KEY_CONNECTION_SETTINGS } from '../../../../core/constants';
import { User } from '../../../../shared-kernel';
import { ServerDirectoryApiService } from '../../infrastructure/server-directory-api.service';
import type {
BanServerMemberRequest,
CreateServerInviteRequest,
@@ -20,21 +19,19 @@ import type {
ServerJoinAccessResponse,
ServerSourceSelector,
UnbanServerMemberRequest
} from '../domain/server-directory.models';
} from '../../domain/server-directory.models';
import {
buildRoomSignalSelector,
buildRoomSignalSource,
type RoomSignalSource,
type RoomSignalSourceInput
} from '../domain/room-signal-source';
import { ServerEndpointCompatibilityService } from '../infrastructure/server-endpoint-compatibility.service';
import { ServerEndpointHealthService } from '../infrastructure/server-endpoint-health.service';
} from '../../domain/room-signal-source';
import { ServerEndpointCompatibilityService } from '../../infrastructure/server-endpoint-compatibility.service';
import { ServerEndpointHealthService } from '../../infrastructure/server-endpoint-health.service';
import { ServerEndpointStateService } from './server-endpoint-state.service';
export { CLIENT_UPDATE_REQUIRED_MESSAGE };
@Injectable({ providedIn: 'root' })
export class ServerDirectoryFacade {
export class ServerDirectoryService {
readonly servers: Signal<ServerEndpoint[]>;
readonly activeServers: Signal<ServerEndpoint[]>;
readonly hasMissingDefaultServers: Signal<boolean>;

View File

@@ -6,7 +6,7 @@ import {
type Signal
} from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
import { environment } from '../../../../environments/environment';
import { environment } from '../../../../../environments/environment';
import {
buildDefaultEndpointTemplates,
buildDefaultServerDefinitions,
@@ -16,14 +16,14 @@ import {
hasEndpointForDefault,
matchDefaultEndpointTemplate,
sanitiseServerBaseUrl
} from '../domain/server-endpoint-defaults';
import { ServerEndpointStorageService } from '../infrastructure/server-endpoint-storage.service';
} from '../../domain/server-endpoint-defaults';
import { ServerEndpointStorageService } from '../../infrastructure/server-endpoint-storage.service';
import type {
ConfiguredDefaultServerDefinition,
DefaultEndpointTemplate,
ServerEndpoint,
ServerEndpointVersions
} from '../domain/server-directory.models';
} from '../../domain/server-directory.models';
function resolveDefaultHttpProtocol(): 'http' | 'https' {
return typeof window !== 'undefined' && window.location?.protocol === 'https:'

View File

@@ -14,7 +14,7 @@ import { selectCurrentUser } from '../../../../store/users/users.selectors';
import type { ServerInviteInfo } from '../../domain/server-directory.models';
import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../application/server-directory.facade';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
import { User } from '../../../../shared-kernel';
@Component({

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ import {
RoomRoleAssignment,
User
} from '../../../shared-kernel';
import { ServerEndpointStateService } from '../application/server-endpoint-state.service';
import { ServerEndpointStateService } from '../application/services/server-endpoint-state.service';
import type {
BanServerMemberRequest,
CreateServerInviteRequest,