fix: multiple bug fixes
isolated users, db backup, weird disconnect issues for long voice sessions,
This commit is contained in:
@@ -79,7 +79,7 @@ graph TD
|
||||
|
||||
## 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.
|
||||
On startup, `ServerEndpointStateService` loads endpoints from localStorage, reconciles them with the configured defaults from the environment, and ensures at least one endpoint is active. Configured default endpoints are treated as active by default unless the user explicitly disabled or removed them.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
@@ -167,6 +167,7 @@ Default servers are configured in the environment file. The state service builds
|
||||
|
||||
- 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)
|
||||
- Default endpoints stay active by default unless the user explicitly disabled them (tracked separately from the endpoint payload)
|
||||
- `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
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Injector, runInInjectionContext } from '@angular/core';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import type { ServerEndpoint } from '../../domain/models/server-directory.model';
|
||||
import * as serverDirectoryStorageKeys from '../../infrastructure/constants/server-directory.infrastructure.constants';
|
||||
import { ServerEndpointStorageService } from '../../infrastructure/services/server-endpoint-storage.service';
|
||||
import { ServerEndpointStateService } from './server-endpoint-state.service';
|
||||
|
||||
function createLocalStorageMock(): Storage {
|
||||
const store = new Map<string, string>();
|
||||
|
||||
return {
|
||||
get length(): number {
|
||||
return store.size;
|
||||
},
|
||||
clear(): void {
|
||||
store.clear();
|
||||
},
|
||||
getItem(key: string): string | null {
|
||||
return store.get(key) ?? null;
|
||||
},
|
||||
key(index: number): string | null {
|
||||
return [...store.keys()][index] ?? null;
|
||||
},
|
||||
removeItem(key: string): void {
|
||||
store.delete(key);
|
||||
},
|
||||
setItem(key: string, value: string): void {
|
||||
store.set(key, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: createLocalStorageMock(),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
function getConfiguredDefaultServer(key: string): { key?: string; name?: string; url?: string } {
|
||||
const defaultServer = environment.defaultServers.find((server) => server.key === key);
|
||||
|
||||
if (!defaultServer) {
|
||||
throw new Error(`Missing configured default server for key: ${key}`);
|
||||
}
|
||||
|
||||
return defaultServer;
|
||||
}
|
||||
|
||||
function seedStoredEndpoints(endpoints: ServerEndpoint[]): void {
|
||||
localStorage.setItem(serverDirectoryStorageKeys.SERVER_ENDPOINTS_STORAGE_KEY, JSON.stringify(endpoints));
|
||||
}
|
||||
|
||||
function createService(): ServerEndpointStateService {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: ServerEndpointStorageService,
|
||||
useClass: ServerEndpointStorageService,
|
||||
deps: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return runInInjectionContext(injector, () => new ServerEndpointStateService());
|
||||
}
|
||||
|
||||
function getRequiredDefaultEndpoint(service: ServerEndpointStateService, defaultKey: string | undefined): ServerEndpoint {
|
||||
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultKey);
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error(`Expected default endpoint for key: ${defaultKey ?? 'unknown'}`);
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
describe('ServerEndpointStateService', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('reactivates configured default endpoints unless the user disabled them', () => {
|
||||
const defaultServer = getConfiguredDefaultServer('toju-primary');
|
||||
|
||||
seedStoredEndpoints([
|
||||
{
|
||||
id: 'default-server',
|
||||
name: 'Stored Default',
|
||||
url: defaultServer.url ?? '',
|
||||
isActive: false,
|
||||
isDefault: true,
|
||||
defaultKey: defaultServer.key,
|
||||
status: 'unknown'
|
||||
}
|
||||
]);
|
||||
|
||||
const service = createService();
|
||||
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
|
||||
|
||||
expect(endpoint?.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps a configured default endpoint inactive after the user turned it off', () => {
|
||||
const defaultServer = getConfiguredDefaultServer('toju-primary');
|
||||
|
||||
seedStoredEndpoints([
|
||||
{
|
||||
id: 'default-server',
|
||||
name: 'Stored Default',
|
||||
url: defaultServer.url ?? '',
|
||||
isActive: true,
|
||||
isDefault: true,
|
||||
defaultKey: defaultServer.key,
|
||||
status: 'unknown'
|
||||
}
|
||||
]);
|
||||
|
||||
localStorage.setItem(
|
||||
serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY,
|
||||
JSON.stringify([defaultServer.key])
|
||||
);
|
||||
|
||||
const service = createService();
|
||||
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
|
||||
|
||||
expect(endpoint?.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps configured default endpoints active even when stored as incompatible unless the user disabled them', () => {
|
||||
const defaultServer = getConfiguredDefaultServer('toju-primary');
|
||||
|
||||
seedStoredEndpoints([
|
||||
{
|
||||
id: 'default-server',
|
||||
name: 'Stored Default',
|
||||
url: defaultServer.url ?? '',
|
||||
isActive: false,
|
||||
isDefault: true,
|
||||
defaultKey: defaultServer.key,
|
||||
status: 'incompatible'
|
||||
}
|
||||
]);
|
||||
|
||||
const service = createService();
|
||||
const endpoint = service.servers().find((candidate) => candidate.defaultKey === defaultServer.key);
|
||||
|
||||
expect(endpoint?.isActive).toBe(true);
|
||||
expect(endpoint?.status).toBe('incompatible');
|
||||
});
|
||||
|
||||
it('does not deactivate configured default endpoints when compatibility checks fail', () => {
|
||||
const defaultServer = getConfiguredDefaultServer('toju-primary');
|
||||
const service = createService();
|
||||
const endpoint = getRequiredDefaultEndpoint(service, defaultServer.key);
|
||||
|
||||
service.updateServerStatus(endpoint.id, 'incompatible');
|
||||
|
||||
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('persists turning a configured default endpoint off and back on', () => {
|
||||
const defaultServer = getConfiguredDefaultServer('toju-primary');
|
||||
const service = createService();
|
||||
const endpoint = getRequiredDefaultEndpoint(service, defaultServer.key);
|
||||
|
||||
service.deactivateServer(endpoint.id);
|
||||
|
||||
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(false);
|
||||
expect(JSON.parse(
|
||||
localStorage.getItem(serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY) ?? '[]'
|
||||
)).toContain(defaultServer.key);
|
||||
|
||||
service.setActiveServer(endpoint.id);
|
||||
|
||||
expect(service.servers().find((candidate) => candidate.id === endpoint.id)?.isActive).toBe(true);
|
||||
expect(localStorage.getItem(serverDirectoryStorageKeys.DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -145,6 +145,7 @@ export class ServerEndpointStateService {
|
||||
|
||||
if (target.isDefault) {
|
||||
this.markDefaultEndpointRemoved(target);
|
||||
this.clearDefaultEndpointDisabled(target);
|
||||
}
|
||||
|
||||
const updatedEndpoints = ensureAnyActiveEndpoint(
|
||||
@@ -171,6 +172,7 @@ export class ServerEndpointStateService {
|
||||
|
||||
this._servers.update((endpoints) => ensureAnyActiveEndpoint([...endpoints, ...restoredEndpoints]));
|
||||
this.storage.clearRemovedDefaultEndpointKeys();
|
||||
this.clearDisabledDefaultEndpointKeys(restoredEndpoints);
|
||||
this.saveEndpoints();
|
||||
return restoredEndpoints;
|
||||
}
|
||||
@@ -190,6 +192,12 @@ export class ServerEndpointStateService {
|
||||
);
|
||||
});
|
||||
|
||||
const target = this._servers().find((endpoint) => endpoint.id === endpointId);
|
||||
|
||||
if (target?.isDefault) {
|
||||
this.clearDefaultEndpointDisabled(target);
|
||||
}
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
@@ -206,6 +214,12 @@ export class ServerEndpointStateService {
|
||||
)
|
||||
);
|
||||
|
||||
const target = this._servers().find((endpoint) => endpoint.id === endpointId);
|
||||
|
||||
if (target?.isDefault) {
|
||||
this.markDefaultEndpointDisabled(target);
|
||||
}
|
||||
|
||||
this.saveEndpoints();
|
||||
}
|
||||
|
||||
@@ -225,7 +239,7 @@ export class ServerEndpointStateService {
|
||||
instanceId: versions?.serverInstanceId ?? endpoint.instanceId,
|
||||
status,
|
||||
latency,
|
||||
isActive: status === 'incompatible' ? false : endpoint.isActive,
|
||||
isActive: status === 'incompatible' && !endpoint.isDefault ? false : endpoint.isActive,
|
||||
serverVersion: versions?.serverVersion ?? endpoint.serverVersion,
|
||||
clientVersion: versions?.clientVersion ?? endpoint.clientVersion
|
||||
};
|
||||
@@ -258,6 +272,7 @@ export class ServerEndpointStateService {
|
||||
private reconcileStoredEndpoints(storedEndpoints: ServerEndpoint[]): ServerEndpoint[] {
|
||||
const reconciled: ServerEndpoint[] = [];
|
||||
const claimedDefaultKeys = new Set<string>();
|
||||
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
|
||||
const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();
|
||||
|
||||
for (const endpoint of storedEndpoints) {
|
||||
@@ -279,6 +294,7 @@ export class ServerEndpointStateService {
|
||||
...endpoint,
|
||||
name: matchedDefault.name,
|
||||
url: matchedDefault.url,
|
||||
isActive: this.isDefaultEndpointActive(matchedDefault.defaultKey, disabledDefaultKeys),
|
||||
isDefault: true,
|
||||
defaultKey: matchedDefault.defaultKey,
|
||||
status: endpoint.status ?? 'unknown'
|
||||
@@ -303,7 +319,7 @@ export class ServerEndpointStateService {
|
||||
reconciled.push({
|
||||
...defaultEndpoint,
|
||||
id: uuidv4(),
|
||||
isActive: defaultEndpoint.isActive
|
||||
isActive: this.isDefaultEndpointActive(defaultEndpoint.defaultKey, disabledDefaultKeys)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -324,6 +340,64 @@ export class ServerEndpointStateService {
|
||||
this.storage.saveRemovedDefaultEndpointKeys(removedDefaultKeys);
|
||||
}
|
||||
|
||||
private markDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
|
||||
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
|
||||
|
||||
if (!defaultKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
|
||||
|
||||
disabledDefaultKeys.add(defaultKey);
|
||||
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
|
||||
}
|
||||
|
||||
private clearDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
|
||||
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
|
||||
|
||||
if (!defaultKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
|
||||
|
||||
if (!disabledDefaultKeys.delete(defaultKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
|
||||
}
|
||||
|
||||
private clearDisabledDefaultEndpointKeys(endpoints: ServerEndpoint[]): void {
|
||||
const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
|
||||
|
||||
let didChange = false;
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);
|
||||
|
||||
if (!defaultKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
didChange = disabledDefaultKeys.delete(defaultKey) || didChange;
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
|
||||
}
|
||||
|
||||
private isDefaultEndpointActive(
|
||||
defaultKey: string,
|
||||
disabledDefaultKeys: Set<string>
|
||||
): boolean {
|
||||
return !disabledDefaultKeys.has(defaultKey);
|
||||
}
|
||||
|
||||
private saveEndpoints(): void {
|
||||
this.storage.saveEndpoints(this._servers());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export const SERVER_ENDPOINTS_STORAGE_KEY = 'metoyou_server_endpoints';
|
||||
export const REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY = 'metoyou_removed_default_server_keys';
|
||||
export const DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY = 'metoyou_disabled_default_server_keys';
|
||||
export const SERVER_HEALTH_CHECK_TIMEOUT_MS = 5000;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY, SERVER_ENDPOINTS_STORAGE_KEY } from '../constants/server-directory.infrastructure.constants';
|
||||
import {
|
||||
DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY,
|
||||
REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY,
|
||||
SERVER_ENDPOINTS_STORAGE_KEY
|
||||
} from '../constants/server-directory.infrastructure.constants';
|
||||
import type { ServerEndpoint } from '../../domain/models/server-directory.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -26,8 +30,32 @@ export class ServerEndpointStorageService {
|
||||
localStorage.setItem(SERVER_ENDPOINTS_STORAGE_KEY, JSON.stringify(endpoints));
|
||||
}
|
||||
|
||||
loadDisabledDefaultEndpointKeys(): Set<string> {
|
||||
return this.loadStringSet(DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
}
|
||||
|
||||
saveDisabledDefaultEndpointKeys(keys: Set<string>): void {
|
||||
this.saveStringSet(DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY, keys);
|
||||
}
|
||||
|
||||
clearDisabledDefaultEndpointKeys(): void {
|
||||
localStorage.removeItem(DISABLED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
}
|
||||
|
||||
loadRemovedDefaultEndpointKeys(): Set<string> {
|
||||
const stored = localStorage.getItem(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
return this.loadStringSet(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
}
|
||||
|
||||
saveRemovedDefaultEndpointKeys(keys: Set<string>): void {
|
||||
this.saveStringSet(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY, keys);
|
||||
}
|
||||
|
||||
clearRemovedDefaultEndpointKeys(): void {
|
||||
localStorage.removeItem(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
}
|
||||
|
||||
private loadStringSet(storageKey: string): Set<string> {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
|
||||
if (!stored) {
|
||||
return new Set<string>();
|
||||
@@ -46,16 +74,12 @@ export class ServerEndpointStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
saveRemovedDefaultEndpointKeys(keys: Set<string>): void {
|
||||
private saveStringSet(storageKey: string, keys: Set<string>): void {
|
||||
if (keys.size === 0) {
|
||||
this.clearRemovedDefaultEndpointKeys();
|
||||
localStorage.removeItem(storageKey);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY, JSON.stringify([...keys]));
|
||||
}
|
||||
|
||||
clearRemovedDefaultEndpointKeys(): void {
|
||||
localStorage.removeItem(REMOVED_DEFAULT_SERVER_KEYS_STORAGE_KEY);
|
||||
localStorage.setItem(storageKey, JSON.stringify([...keys]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user