Move toju-app into own its folder
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
Injectable,
|
||||
inject,
|
||||
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 type {
|
||||
BanServerMemberRequest,
|
||||
CreateServerInviteRequest,
|
||||
KickServerMemberRequest,
|
||||
ServerEndpoint,
|
||||
ServerEndpointVersions,
|
||||
ServerInfo,
|
||||
ServerInviteInfo,
|
||||
ServerJoinAccessRequest,
|
||||
ServerJoinAccessResponse,
|
||||
ServerSourceSelector,
|
||||
UnbanServerMemberRequest
|
||||
} from '../domain/server-directory.models';
|
||||
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 {
|
||||
readonly servers: Signal<ServerEndpoint[]>;
|
||||
readonly activeServers: Signal<ServerEndpoint[]>;
|
||||
readonly hasMissingDefaultServers: Signal<boolean>;
|
||||
readonly activeServer: Signal<ServerEndpoint | null>;
|
||||
|
||||
private readonly endpointState = inject(ServerEndpointStateService);
|
||||
private readonly endpointCompatibility = inject(ServerEndpointCompatibilityService);
|
||||
private readonly endpointHealth = inject(ServerEndpointHealthService);
|
||||
private readonly api = inject(ServerDirectoryApiService);
|
||||
private shouldSearchAllServers = true;
|
||||
|
||||
constructor() {
|
||||
this.servers = this.endpointState.servers;
|
||||
this.activeServers = this.endpointState.activeServers;
|
||||
this.hasMissingDefaultServers = this.endpointState.hasMissingDefaultServers;
|
||||
this.activeServer = this.endpointState.activeServer;
|
||||
|
||||
this.loadConnectionSettings();
|
||||
void this.testAllServers();
|
||||
}
|
||||
|
||||
addServer(server: { name: string; url: string }): ServerEndpoint {
|
||||
return this.endpointState.addServer(server);
|
||||
}
|
||||
|
||||
ensureServerEndpoint(
|
||||
server: { name: string; url: string },
|
||||
options?: { setActive?: boolean }
|
||||
): ServerEndpoint {
|
||||
return this.endpointState.ensureServerEndpoint(server, options);
|
||||
}
|
||||
|
||||
findServerByUrl(url: string): ServerEndpoint | undefined {
|
||||
return this.endpointState.findServerByUrl(url);
|
||||
}
|
||||
|
||||
removeServer(endpointId: string): void {
|
||||
this.endpointState.removeServer(endpointId);
|
||||
}
|
||||
|
||||
restoreDefaultServers(): ServerEndpoint[] {
|
||||
return this.endpointState.restoreDefaultServers();
|
||||
}
|
||||
|
||||
setActiveServer(endpointId: string): void {
|
||||
this.endpointState.setActiveServer(endpointId);
|
||||
}
|
||||
|
||||
deactivateServer(endpointId: string): void {
|
||||
this.endpointState.deactivateServer(endpointId);
|
||||
}
|
||||
|
||||
updateServerStatus(
|
||||
endpointId: string,
|
||||
status: ServerEndpoint['status'],
|
||||
latency?: number,
|
||||
versions?: ServerEndpointVersions
|
||||
): void {
|
||||
this.endpointState.updateServerStatus(endpointId, status, latency, versions);
|
||||
}
|
||||
|
||||
async ensureEndpointVersionCompatibility(selector?: ServerSourceSelector): Promise<boolean> {
|
||||
const endpoint = this.api.resolveEndpoint(selector);
|
||||
|
||||
if (!endpoint || endpoint.status === 'incompatible') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const clientVersion = await this.endpointCompatibility.getClientVersion();
|
||||
|
||||
if (!clientVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.testServer(endpoint.id);
|
||||
|
||||
const refreshedEndpoint = this.servers().find((candidate) => candidate.id === endpoint.id);
|
||||
|
||||
return !!refreshedEndpoint && refreshedEndpoint.status !== 'incompatible';
|
||||
}
|
||||
|
||||
setSearchAllServers(enabled: boolean): void {
|
||||
this.shouldSearchAllServers = enabled;
|
||||
}
|
||||
|
||||
async testServer(endpointId: string): Promise<boolean> {
|
||||
const endpoint = this.servers().find((entry) => entry.id === endpointId);
|
||||
|
||||
if (!endpoint) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.updateServerStatus(endpointId, 'checking');
|
||||
const clientVersion = await this.endpointCompatibility.getClientVersion();
|
||||
const healthResult = await this.endpointHealth.probeEndpoint(endpoint, clientVersion);
|
||||
|
||||
this.updateServerStatus(
|
||||
endpointId,
|
||||
healthResult.status,
|
||||
healthResult.latency,
|
||||
healthResult.versions
|
||||
);
|
||||
|
||||
return healthResult.status === 'online';
|
||||
}
|
||||
|
||||
async testAllServers(): Promise<void> {
|
||||
await Promise.all(this.servers().map((endpoint) => this.testServer(endpoint.id)));
|
||||
}
|
||||
|
||||
getApiBaseUrl(selector?: ServerSourceSelector): string {
|
||||
return this.api.getApiBaseUrl(selector);
|
||||
}
|
||||
|
||||
getWebSocketUrl(selector?: ServerSourceSelector): string {
|
||||
return this.api.getWebSocketUrl(selector);
|
||||
}
|
||||
|
||||
searchServers(query: string): Observable<ServerInfo[]> {
|
||||
return this.api.searchServers(query, this.shouldSearchAllServers);
|
||||
}
|
||||
|
||||
getServers(): Observable<ServerInfo[]> {
|
||||
return this.api.getServers(this.shouldSearchAllServers);
|
||||
}
|
||||
|
||||
getServer(serverId: string, selector?: ServerSourceSelector): Observable<ServerInfo | null> {
|
||||
return this.api.getServer(serverId, selector);
|
||||
}
|
||||
|
||||
registerServer(
|
||||
server: Omit<ServerInfo, 'createdAt'> & { id?: string; password?: string | null },
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<ServerInfo> {
|
||||
return this.api.registerServer(server, selector);
|
||||
}
|
||||
|
||||
updateServer(
|
||||
serverId: string,
|
||||
updates: Partial<ServerInfo> & {
|
||||
currentOwnerId: string;
|
||||
actingRole?: string;
|
||||
password?: string | null;
|
||||
},
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<ServerInfo> {
|
||||
return this.api.updateServer(serverId, updates, selector);
|
||||
}
|
||||
|
||||
unregisterServer(serverId: string, selector?: ServerSourceSelector): Observable<void> {
|
||||
return this.api.unregisterServer(serverId, selector);
|
||||
}
|
||||
|
||||
getServerUsers(serverId: string, selector?: ServerSourceSelector): Observable<User[]> {
|
||||
return this.api.getServerUsers(serverId, selector);
|
||||
}
|
||||
|
||||
requestJoin(
|
||||
request: ServerJoinAccessRequest,
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<ServerJoinAccessResponse> {
|
||||
return this.api.requestJoin(request, selector);
|
||||
}
|
||||
|
||||
createInvite(
|
||||
serverId: string,
|
||||
request: CreateServerInviteRequest,
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<ServerInviteInfo> {
|
||||
return this.api.createInvite(serverId, request, selector);
|
||||
}
|
||||
|
||||
getInvite(inviteId: string, selector?: ServerSourceSelector): Observable<ServerInviteInfo> {
|
||||
return this.api.getInvite(inviteId, selector);
|
||||
}
|
||||
|
||||
kickServerMember(
|
||||
serverId: string,
|
||||
request: KickServerMemberRequest,
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<void> {
|
||||
return this.api.kickServerMember(serverId, request, selector);
|
||||
}
|
||||
|
||||
banServerMember(
|
||||
serverId: string,
|
||||
request: BanServerMemberRequest,
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<void> {
|
||||
return this.api.banServerMember(serverId, request, selector);
|
||||
}
|
||||
|
||||
unbanServerMember(
|
||||
serverId: string,
|
||||
request: UnbanServerMemberRequest,
|
||||
selector?: ServerSourceSelector
|
||||
): Observable<void> {
|
||||
return this.api.unbanServerMember(serverId, request, selector);
|
||||
}
|
||||
|
||||
notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable<void> {
|
||||
return this.api.notifyLeave(serverId, userId, selector);
|
||||
}
|
||||
|
||||
updateUserCount(serverId: string, count: number): Observable<void> {
|
||||
return this.api.updateUserCount(serverId, count);
|
||||
}
|
||||
|
||||
sendHeartbeat(serverId: string): Observable<void> {
|
||||
return this.api.sendHeartbeat(serverId);
|
||||
}
|
||||
|
||||
private loadConnectionSettings(): void {
|
||||
const stored = localStorage.getItem(STORAGE_KEY_CONNECTION_SETTINGS);
|
||||
|
||||
if (!stored) {
|
||||
this.shouldSearchAllServers = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as { searchAllServers?: boolean };
|
||||
|
||||
this.shouldSearchAllServers = parsed.searchAllServers ?? true;
|
||||
} catch {
|
||||
this.shouldSearchAllServers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
Injectable,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
type Signal
|
||||
} from '@angular/core';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import {
|
||||
buildDefaultEndpointTemplates,
|
||||
buildDefaultServerDefinitions,
|
||||
ensureAnyActiveEndpoint,
|
||||
ensureCompatibleActiveEndpoint,
|
||||
findDefaultEndpointKeyByUrl,
|
||||
hasEndpointForDefault,
|
||||
matchDefaultEndpointTemplate,
|
||||
sanitiseServerBaseUrl
|
||||
} from '../domain/server-endpoint-defaults';
|
||||
import { ServerEndpointStorageService } from '../infrastructure/server-endpoint-storage.service';
|
||||
import type {
|
||||
ConfiguredDefaultServerDefinition,
|
||||
DefaultEndpointTemplate,
|
||||
ServerEndpoint,
|
||||
ServerEndpointVersions
|
||||
} from '../domain/server-directory.models';
|
||||
|
||||
function resolveDefaultHttpProtocol(): 'http' | 'https' {
|
||||
return typeof window !== 'undefined' && window.location?.protocol === 'https:'
|
||||
? 'https'
|
||||
: 'http';
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ServerEndpointStateService {
|
||||
readonly servers: Signal<ServerEndpoint[]>;
|
||||
readonly activeServers: Signal<ServerEndpoint[]>;
|
||||
readonly hasMissingDefaultServers: Signal<boolean>;
|
||||
readonly activeServer: Signal<ServerEndpoint | null>;
|
||||
|
||||
private readonly storage = inject(ServerEndpointStorageService);
|
||||
private readonly _servers = signal<ServerEndpoint[]>([]);
|
||||
private readonly defaultEndpoints: DefaultEndpointTemplate[];
|
||||
private readonly primaryDefaultServerUrl: string;
|
||||
|
||||
constructor() {
|
||||
const defaultServerDefinitions = buildDefaultServerDefinitions(
|
||||
Array.isArray(environment.defaultServers)
|
||||
? environment.defaultServers as ConfiguredDefaultServerDefinition[]
|
||||
: [],
|
||||
environment.defaultServerUrl,
|
||||
resolveDefaultHttpProtocol()
|
||||
);
|
||||
|
||||
this.defaultEndpoints = buildDefaultEndpointTemplates(defaultServerDefinitions);
|
||||
this.primaryDefaultServerUrl = this.defaultEndpoints[0]?.url ?? 'http://localhost:3001';
|
||||
|
||||
this.servers = computed(() => this._servers());
|
||||
this.activeServers = computed(() =>
|
||||
this._servers().filter((endpoint) => endpoint.isActive && endpoint.status !== 'incompatible')
|
||||
);
|
||||
|
||||
this.hasMissingDefaultServers = computed(() =>
|
||||
this.defaultEndpoints.some((endpoint) => !hasEndpointForDefault(this._servers(), endpoint))
|
||||
);
|
||||
|
||||
this.activeServer = computed(() => this.activeServers()[0] ?? null);
|
||||
|
||||
this.loadEndpoints();
|
||||
}
|
||||
|
||||
getPrimaryDefaultServerUrl(): string {
|
||||
return this.primaryDefaultServerUrl;
|
||||
}
|
||||
|
||||
sanitiseUrl(rawUrl: string): string {
|
||||
return sanitiseServerBaseUrl(rawUrl);
|
||||
}
|
||||
|
||||
addServer(server: { name: string; url: string }): ServerEndpoint {
|
||||
const newEndpoint: ServerEndpoint = {
|
||||
id: uuidv4(),
|
||||
name: server.name,
|
||||
url: this.sanitiseUrl(server.url),
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
status: 'unknown'
|
||||
};
|
||||
|
||||
this._servers.update((endpoints) => [...endpoints, newEndpoint]);
|
||||
this.saveEndpoints();
|
||||
return newEndpoint;
|
||||
}
|
||||
|
||||
ensureServerEndpoint(
|
||||
server: { name: string; url: string },
|
||||
options?: { setActive?: boolean }
|
||||
): ServerEndpoint {
|
||||
const existing = this.findServerByUrl(server.url);
|
||||
|
||||
if (existing) {
|
||||
if (options?.setActive) {
|
||||
this.setActiveServer(existing.id);
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
const created = this.addServer(server);
|
||||
|
||||
if (options?.setActive) {
|
||||
this.setActiveServer(created.id);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
findServerByUrl(url: string): ServerEndpoint | undefined {
|
||||
const sanitisedUrl = this.sanitiseUrl(url);
|
||||
|
||||
return this._servers().find((endpoint) => this.sanitiseUrl(endpoint.url) === sanitisedUrl);
|
||||
}
|
||||
|
||||
removeServer(endpointId: string): void {
|
||||
const endpoints = this._servers();
|
||||
const target = endpoints.find((endpoint) => endpoint.id === endpointId);
|
||||
|
||||
if (!target || endpoints.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.isDefault) {
|
||||
this.markDefaultEndpointRemoved(target);
|
||||
}
|
||||
|
||||
const updatedEndpoints = ensureAnyActiveEndpoint(
|
||||
endpoints.filter((endpoint) => endpoint.id !== endpointId)
|
||||
);
|
||||
|
||||
this._servers.set(updatedEndpoints);
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
restoreDefaultServers(): ServerEndpoint[] {
|
||||
const restoredEndpoints = this.defaultEndpoints
|
||||
.filter((defaultEndpoint) => !hasEndpointForDefault(this._servers(), defaultEndpoint))
|
||||
.map((defaultEndpoint) => ({
|
||||
...defaultEndpoint,
|
||||
id: uuidv4(),
|
||||
isActive: true
|
||||
}));
|
||||
|
||||
if (restoredEndpoints.length === 0) {
|
||||
this.storage.clearRemovedDefaultEndpointKeys();
|
||||
return [];
|
||||
}
|
||||
|
||||
this._servers.update((endpoints) => ensureAnyActiveEndpoint([...endpoints, ...restoredEndpoints]));
|
||||
this.storage.clearRemovedDefaultEndpointKeys();
|
||||
this.saveEndpoints();
|
||||
return restoredEndpoints;
|
||||
}
|
||||
|
||||
setActiveServer(endpointId: string): void {
|
||||
this._servers.update((endpoints) => {
|
||||
const target = endpoints.find((endpoint) => endpoint.id === endpointId);
|
||||
|
||||
if (!target || target.status === 'incompatible') {
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
return endpoints.map((endpoint) =>
|
||||
endpoint.id === endpointId
|
||||
? { ...endpoint, isActive: true }
|
||||
: endpoint
|
||||
);
|
||||
});
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
deactivateServer(endpointId: string): void {
|
||||
if (this.activeServers().length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._servers.update((endpoints) =>
|
||||
endpoints.map((endpoint) =>
|
||||
endpoint.id === endpointId
|
||||
? { ...endpoint, isActive: false }
|
||||
: endpoint
|
||||
)
|
||||
);
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
updateServerStatus(
|
||||
endpointId: string,
|
||||
status: ServerEndpoint['status'],
|
||||
latency?: number,
|
||||
versions?: ServerEndpointVersions
|
||||
): void {
|
||||
this._servers.update((endpoints) => ensureCompatibleActiveEndpoint(endpoints.map((endpoint) => {
|
||||
if (endpoint.id !== endpointId) {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
return {
|
||||
...endpoint,
|
||||
status,
|
||||
latency,
|
||||
isActive: status === 'incompatible' ? false : endpoint.isActive,
|
||||
serverVersion: versions?.serverVersion ?? endpoint.serverVersion,
|
||||
clientVersion: versions?.clientVersion ?? endpoint.clientVersion
|
||||
};
|
||||
})));
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
private loadEndpoints(): void {
|
||||
const storedEndpoints = this.storage.loadEndpoints();
|
||||
|
||||
if (!storedEndpoints) {
|
||||
this.initialiseDefaultEndpoints();
|
||||
return;
|
||||
}
|
||||
|
||||
this._servers.set(this.reconcileStoredEndpoints(storedEndpoints));
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
private initialiseDefaultEndpoints(): void {
|
||||
this._servers.set(this.defaultEndpoints.map((endpoint) => ({
|
||||
...endpoint,
|
||||
id: uuidv4()
|
||||
})));
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
private reconcileStoredEndpoints(storedEndpoints: ServerEndpoint[]): ServerEndpoint[] {
|
||||
const reconciled: ServerEndpoint[] = [];
|
||||
const claimedDefaultKeys = new Set<string>();
|
||||
const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();
|
||||
|
||||
for (const endpoint of storedEndpoints) {
|
||||
if (!endpoint || typeof endpoint.id !== 'string' || typeof endpoint.url !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sanitisedUrl = this.sanitiseUrl(endpoint.url);
|
||||
const matchedDefault = matchDefaultEndpointTemplate(
|
||||
this.defaultEndpoints,
|
||||
endpoint,
|
||||
sanitisedUrl,
|
||||
claimedDefaultKeys
|
||||
);
|
||||
|
||||
if (matchedDefault) {
|
||||
claimedDefaultKeys.add(matchedDefault.defaultKey);
|
||||
reconciled.push({
|
||||
...endpoint,
|
||||
name: matchedDefault.name,
|
||||
url: matchedDefault.url,
|
||||
isDefault: true,
|
||||
defaultKey: matchedDefault.defaultKey,
|
||||
status: endpoint.status ?? 'unknown'
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
reconciled.push({
|
||||
...endpoint,
|
||||
url: sanitisedUrl,
|
||||
status: endpoint.status ?? 'unknown'
|
||||
});
|
||||
}
|
||||
|
||||
for (const defaultEndpoint of this.defaultEndpoints) {
|
||||
if (
|
||||
!claimedDefaultKeys.has(defaultEndpoint.defaultKey)
|
||||
&& !removedDefaultKeys.has(defaultEndpoint.defaultKey)
|
||||
&& !hasEndpointForDefault(reconciled, defaultEndpoint)
|
||||
) {
|
||||
reconciled.push({
|
||||
...defaultEndpoint,
|
||||
id: uuidv4(),
|
||||
isActive: defaultEndpoint.isActive
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ensureAnyActiveEndpoint(reconciled);
|
||||
}
|
||||
|
||||
private markDefaultEndpointRemoved(endpoint: ServerEndpoint): void {
|
||||
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
|
||||
|
||||
if (!defaultKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();
|
||||
|
||||
removedDefaultKeys.add(defaultKey);
|
||||
this.storage.saveRemovedDefaultEndpointKeys(removedDefaultKeys);
|
||||
}
|
||||
|
||||
private saveEndpoints(): void {
|
||||
this.storage.saveEndpoints(this._servers());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user