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

@@ -0,0 +1,46 @@
import {
areRoomSignalSourcesEqual,
buildRoomSignalSelector,
buildRoomSignalSource,
getSourceUrlFromSignalingUrl
} from './room-signal-source.logic';
describe('room-signal-source helpers', () => {
it('converts signaling urls back to normalized source urls', () => {
expect(getSourceUrlFromSignalingUrl('wss://signal.toju.app')).toBe('https://signal.toju.app');
expect(getSourceUrlFromSignalingUrl('ws://46.59.68.77:3001')).toBe('http://46.59.68.77:3001');
});
it('prefers the resolved endpoint when normalizing a room source', () => {
expect(buildRoomSignalSource({
sourceId: 'stale-id',
sourceName: 'Stale Source',
sourceUrl: 'https://old.example.com',
signalingUrl: 'wss://signal.toju.app'
}, {
id: 'primary-id',
name: 'Primary Signal',
url: 'https://signal.toju.app/'
})).toEqual({
sourceId: 'primary-id',
sourceName: 'Primary Signal',
sourceUrl: 'https://signal.toju.app'
});
});
it('builds selectors from signaling urls when no source url is persisted yet', () => {
expect(buildRoomSignalSelector({
signalingUrl: 'wss://signal-sweden.toju.app',
fallbackName: 'Toju Signal Sweden'
})).toEqual({
sourceUrl: 'https://signal-sweden.toju.app'
});
});
it('treats equivalent persisted and signaling-derived sources as equal', () => {
expect(areRoomSignalSourcesEqual(
{ sourceUrl: 'https://signal.toju.app/' },
{ signalingUrl: 'wss://signal.toju.app' }
)).toBeTrue();
});
});

View File

@@ -0,0 +1,95 @@
import type { ServerEndpoint, ServerSourceSelector } from '../models/server-directory.model';
import { normaliseConfiguredServerUrl, sanitiseServerBaseUrl } from './server-endpoint-defaults.logic';
export interface RoomSignalSource {
sourceId?: string;
sourceName?: string;
sourceUrl?: string;
}
export interface RoomSignalSourceInput extends RoomSignalSource {
fallbackName?: string;
signalingUrl?: string;
}
const DEFAULT_SIGNAL_SOURCE_NAME = 'Signal Server';
export function getSourceUrlFromSignalingUrl(signalingUrl?: string): string | undefined {
const normalizedUrl = normalizeString(signalingUrl);
if (!normalizedUrl) {
return undefined;
}
const resolvedUrl = normaliseConfiguredServerUrl(normalizedUrl, 'https');
return resolvedUrl || undefined;
}
export function buildRoomSignalSource(
source?: RoomSignalSourceInput | null,
endpoint?: Pick<ServerEndpoint, 'id' | 'name' | 'url'> | null
): RoomSignalSource {
const sourceId = normalizeString(endpoint?.id) ?? normalizeString(source?.sourceId);
const sourceUrl = endpoint
? sanitiseServerBaseUrl(endpoint.url)
: (normalizeUrl(source?.sourceUrl) ?? getSourceUrlFromSignalingUrl(source?.signalingUrl));
const sourceName = normalizeString(endpoint?.name)
?? normalizeString(source?.sourceName)
?? normalizeString(source?.fallbackName)
?? (sourceUrl ? DEFAULT_SIGNAL_SOURCE_NAME : undefined);
return {
sourceId,
sourceName,
sourceUrl
};
}
export function buildRoomSignalSelector(
source?: RoomSignalSourceInput | null
): ServerSourceSelector | undefined {
const normalizedSource = buildRoomSignalSource(source);
if (normalizedSource.sourceId) {
return { sourceId: normalizedSource.sourceId };
}
if (normalizedSource.sourceUrl) {
return { sourceUrl: normalizedSource.sourceUrl };
}
return undefined;
}
export function hasRoomSignalSource(source?: RoomSignalSourceInput | null): boolean {
return !!buildRoomSignalSelector(source);
}
export function areRoomSignalSourcesEqual(
left?: RoomSignalSourceInput | null,
right?: RoomSignalSourceInput | null
): boolean {
const normalizedLeft = buildRoomSignalSource(left);
const normalizedRight = buildRoomSignalSource(right);
return normalizedLeft.sourceId === normalizedRight.sourceId
&& normalizedLeft.sourceName === normalizedRight.sourceName
&& normalizedLeft.sourceUrl === normalizedRight.sourceUrl;
}
function normalizeString(value: string | undefined | null): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function normalizeUrl(value: string | undefined | null): string | undefined {
const normalizedValue = normalizeString(value);
return normalizedValue ? sanitiseServerBaseUrl(normalizedValue) : undefined;
}

View File

@@ -0,0 +1,187 @@
import type {
ConfiguredDefaultServerDefinition,
DefaultEndpointTemplate,
DefaultServerDefinition,
ServerEndpoint
} from '../models/server-directory.model';
export function sanitiseServerBaseUrl(rawUrl: string): string {
let cleaned = rawUrl.trim().replace(/\/+$/, '');
if (cleaned.toLowerCase().endsWith('/api')) {
cleaned = cleaned.slice(0, -4);
}
return cleaned;
}
export function normaliseConfiguredServerUrl(
rawUrl: string,
defaultProtocol: 'http' | 'https'
): string {
let cleaned = rawUrl.trim();
if (!cleaned) {
return '';
}
if (cleaned.toLowerCase().startsWith('ws://')) {
cleaned = `http://${cleaned.slice(5)}`;
} else if (cleaned.toLowerCase().startsWith('wss://')) {
cleaned = `https://${cleaned.slice(6)}`;
} else if (cleaned.startsWith('//')) {
cleaned = `${defaultProtocol}:${cleaned}`;
} else if (!/^[a-z][a-z\d+.-]*:\/\//i.test(cleaned)) {
cleaned = `${defaultProtocol}://${cleaned}`;
}
return sanitiseServerBaseUrl(cleaned);
}
export function buildFallbackDefaultServerUrl(
configuredUrl: string | undefined,
defaultProtocol: 'http' | 'https'
): string {
if (configuredUrl?.trim()) {
return normaliseConfiguredServerUrl(configuredUrl, defaultProtocol);
}
return `${defaultProtocol}://localhost:3001`;
}
export function buildDefaultServerDefinitions(
configuredDefaults: ConfiguredDefaultServerDefinition[] | undefined,
configuredUrl: string | undefined,
defaultProtocol: 'http' | 'https'
): DefaultServerDefinition[] {
const seenKeys = new Set<string>();
const seenUrls = new Set<string>();
const definitions = (configuredDefaults ?? [])
.map((server, index) => {
const key = server.key?.trim() || `default-${index + 1}`;
const url = normaliseConfiguredServerUrl(server.url ?? '', defaultProtocol);
if (!key || !url || seenKeys.has(key) || seenUrls.has(url)) {
return null;
}
seenKeys.add(key);
seenUrls.add(url);
return {
key,
name: server.name?.trim() || (index === 0 ? 'Default Server' : `Default Server ${index + 1}`),
url
} satisfies DefaultServerDefinition;
})
.filter((definition): definition is DefaultServerDefinition => definition !== null);
if (definitions.length > 0) {
return definitions;
}
return [
{
key: 'default',
name: 'Default Server',
url: buildFallbackDefaultServerUrl(configuredUrl, defaultProtocol)
}
];
}
export function buildDefaultEndpointTemplates(
definitions: DefaultServerDefinition[]
): DefaultEndpointTemplate[] {
return definitions.map((definition) => ({
name: definition.name,
url: definition.url,
isActive: true,
isDefault: true,
defaultKey: definition.key,
status: 'unknown'
}));
}
export function hasEndpointForDefault(
endpoints: ServerEndpoint[],
defaultEndpoint: DefaultEndpointTemplate
): boolean {
return endpoints.some((endpoint) =>
endpoint.defaultKey === defaultEndpoint.defaultKey
|| sanitiseServerBaseUrl(endpoint.url) === defaultEndpoint.url
);
}
export function matchDefaultEndpointTemplate(
defaultEndpoints: DefaultEndpointTemplate[],
endpoint: ServerEndpoint,
sanitisedUrl: string,
claimedDefaultKeys: Set<string>
): DefaultEndpointTemplate | null {
if (endpoint.defaultKey) {
return defaultEndpoints.find(
(candidate) => candidate.defaultKey === endpoint.defaultKey && !claimedDefaultKeys.has(candidate.defaultKey)
) ?? null;
}
if (!endpoint.isDefault) {
return null;
}
const matchingCurrentDefault = defaultEndpoints.find(
(candidate) => candidate.url === sanitisedUrl && !claimedDefaultKeys.has(candidate.defaultKey)
);
if (matchingCurrentDefault) {
return matchingCurrentDefault;
}
return defaultEndpoints.find(
(candidate) => !claimedDefaultKeys.has(candidate.defaultKey)
) ?? null;
}
export function findDefaultEndpointKeyByUrl(
defaultEndpoints: DefaultEndpointTemplate[],
url: string
): string | null {
const sanitisedUrl = sanitiseServerBaseUrl(url);
return defaultEndpoints.find((endpoint) => endpoint.url === sanitisedUrl)?.defaultKey ?? null;
}
export function ensureAnyActiveEndpoint(endpoints: ServerEndpoint[]): ServerEndpoint[] {
if (endpoints.length === 0 || endpoints.some((endpoint) => endpoint.isActive)) {
return endpoints;
}
const nextEndpoints = [...endpoints];
nextEndpoints[0] = {
...nextEndpoints[0],
isActive: true
};
return nextEndpoints;
}
export function ensureCompatibleActiveEndpoint(endpoints: ServerEndpoint[]): ServerEndpoint[] {
if (endpoints.length === 0 || endpoints.some((endpoint) => endpoint.isActive)) {
return endpoints;
}
const fallbackIndex = endpoints.findIndex((endpoint) => endpoint.status !== 'incompatible');
if (fallbackIndex < 0) {
return endpoints;
}
const nextEndpoints = [...endpoints];
nextEndpoints[fallbackIndex] = {
...nextEndpoints[fallbackIndex],
isActive: true
};
return nextEndpoints;
}