refactor: extract persisted ui state

This commit is contained in:
MythEclipse
2026-05-19 14:11:09 +07:00
parent f48f62893d
commit a1b85d8ac3
5 changed files with 164 additions and 119 deletions
+7 -19
View File
@@ -1,26 +1,14 @@
import type { Router } from "express";
import express from "express";
import { createChildLogger } from "../logger";
import type { SharedUIState, SharedUIStatePatch } from "../state/uiState";
const logger = createChildLogger("ui-state-routes");
export interface SharedUIState {
selectedVoiceGuild: string;
selectedVoiceChannel: string;
selectedTextGuild: string;
selectedTextChannel: string;
activeTab: "voice" | "messages" | "media" | "review" | "recordings";
isListening: boolean;
isStreaming: boolean;
}
export type SharedUIStatePatch = Partial<SharedUIState> & {
selectedGuild?: string;
};
export { SharedUIState, SharedUIStatePatch };
export interface UIStateRouteOptions {
getSharedUIState: () => SharedUIState;
patchSharedUIState: (patch: SharedUIStatePatch) => SharedUIState;
patchSharedUIState: (
patch: SharedUIStatePatch,
) => Promise<SharedUIState> | SharedUIState;
}
export function createUIStateRoutes(options: UIStateRouteOptions): Router {
@@ -38,10 +26,10 @@ export function createUIStateRoutes(options: UIStateRouteOptions): Router {
});
// POST /api/ui-state - Update UI state
router.post("/ui-state", (req, res, next) => {
router.post("/ui-state", async (req, res, next) => {
try {
const patch = req.body as SharedUIStatePatch;
const updated = patchSharedUIState(patch);
const updated = await patchSharedUIState(patch);
res.json(updated);
} catch (error) {
next(error);
+8 -4
View File
@@ -10,7 +10,9 @@ const logger = createChildLogger("voice-routes");
export interface VoiceRouteOptions {
voiceController: VoiceController;
patchSharedUIState: (patch: Partial<SharedUIState>) => SharedUIState;
patchSharedUIState: (
patch: Partial<SharedUIState>,
) => Promise<SharedUIState> | SharedUIState;
broadcaster: ModerationBroadcaster;
adminPassword?: string;
}
@@ -23,7 +25,9 @@ export function createVoiceRoutes(
// Support both old signature (VoiceController) and new signature (options object)
let voiceController: VoiceController;
let patchSharedUIState:
| ((patch: Partial<SharedUIState>) => SharedUIState)
| ((
patch: Partial<SharedUIState>,
) => Promise<SharedUIState> | SharedUIState)
| undefined;
let broadcaster: ModerationBroadcaster | undefined;
let adminPassword: string | undefined;
@@ -137,7 +141,7 @@ export function createVoiceRoutes(
// Update UI state and broadcast to connected clients
if (patchSharedUIState && broadcaster) {
const updatedState = patchSharedUIState({
const updatedState = await patchSharedUIState({
selectedVoiceGuild: guildId,
selectedVoiceChannel: channelId,
});
@@ -163,7 +167,7 @@ export function createVoiceRoutes(
// Update UI state and broadcast to connected clients
if (patchSharedUIState && broadcaster) {
const updatedState = patchSharedUIState({
const updatedState = await patchSharedUIState({
selectedVoiceGuild: "",
selectedVoiceChannel: "",
});
+102
View File
@@ -0,0 +1,102 @@
import { getPersistedValue, setPersistedValue } from "../muxer-queue";
export type ActiveTab =
| "voice"
| "messages"
| "media"
| "review"
| "recordings";
export interface SharedUIState {
selectedVoiceGuild: string;
selectedVoiceChannel: string;
selectedTextGuild: string;
selectedTextChannel: string;
activeTab: ActiveTab;
isListening: boolean;
isStreaming: boolean;
}
export type SharedUIStatePatch = Partial<SharedUIState> & {
selectedGuild?: string;
};
const activeTabs: ActiveTab[] = [
"voice",
"messages",
"media",
"review",
"recordings",
];
export const defaultSharedUIState: SharedUIState = {
selectedVoiceGuild: "",
selectedVoiceChannel: "",
selectedTextGuild: "",
selectedTextChannel: "",
activeTab: "voice",
isListening: false,
isStreaming: false,
};
export function normalizeSharedUIState(
value: SharedUIStatePatch,
): SharedUIState {
const guild = value.selectedGuild ?? "";
return {
selectedVoiceGuild: value.selectedVoiceGuild ?? guild,
selectedVoiceChannel: value.selectedVoiceChannel ?? "",
selectedTextGuild: value.selectedTextGuild ?? guild,
selectedTextChannel: value.selectedTextChannel ?? "",
activeTab: activeTabs.includes(value.activeTab as ActiveTab)
? (value.activeTab as ActiveTab)
: "voice",
isListening: value.isListening ?? false,
isStreaming: value.isStreaming ?? false,
};
}
export async function createSharedUIStateStore() {
let sharedUIState = normalizeSharedUIState(
await getPersistedValue("web-ui-state", defaultSharedUIState),
);
function getSharedUIState(): SharedUIState {
return { ...sharedUIState };
}
async function patchSharedUIState(
patch: SharedUIStatePatch,
): Promise<SharedUIState> {
if (typeof patch.selectedGuild === "string") {
sharedUIState.selectedVoiceGuild = patch.selectedGuild;
sharedUIState.selectedTextGuild = patch.selectedGuild;
}
if (typeof patch.selectedVoiceGuild === "string") {
sharedUIState.selectedVoiceGuild = patch.selectedVoiceGuild;
}
if (typeof patch.selectedVoiceChannel === "string") {
sharedUIState.selectedVoiceChannel = patch.selectedVoiceChannel;
}
if (typeof patch.selectedTextGuild === "string") {
sharedUIState.selectedTextGuild = patch.selectedTextGuild;
}
if (typeof patch.selectedTextChannel === "string") {
sharedUIState.selectedTextChannel = patch.selectedTextChannel;
}
if (activeTabs.includes(patch.activeTab as ActiveTab)) {
sharedUIState.activeTab = patch.activeTab as ActiveTab;
}
if (typeof patch.isListening === "boolean") {
sharedUIState.isListening = patch.isListening;
}
if (typeof patch.isStreaming === "boolean") {
sharedUIState.isStreaming = patch.isStreaming;
}
await setPersistedValue("web-ui-state", sharedUIState);
return getSharedUIState();
}
return { getSharedUIState, patchSharedUIState };
}
+3 -96
View File
@@ -30,6 +30,7 @@ import { createRecordingsRoutes } from "./routes/recordingsRoutes";
import { createSyncRoutes } from "./routes/syncRoutes";
import { createUIStateRoutes } from "./routes/uiStateRoutes";
import { createVoiceRoutes } from "./routes/voiceRoutes";
import { createSharedUIStateStore } from "./state/uiState";
import { Streamer } from "./streaming";
import type { VoiceController } from "./voiceController";
@@ -53,65 +54,14 @@ type VoiceGlobals = typeof globalThis & {
) => void;
};
interface SharedUIState {
selectedVoiceGuild: string;
selectedVoiceChannel: string;
selectedTextGuild: string;
selectedTextChannel: string;
activeTab: "voice" | "messages" | "media" | "review" | "recordings";
isListening: boolean;
isStreaming: boolean;
}
interface MediaSettings {
musicVolume: number;
}
type SharedUIStatePatch = Partial<SharedUIState> & {
selectedGuild?: string;
};
const defaultSharedUIState: SharedUIState = {
selectedVoiceGuild: "",
selectedVoiceChannel: "",
selectedTextGuild: "",
selectedTextChannel: "",
activeTab: "voice",
isListening: false,
isStreaming: false,
};
const defaultMediaSettings: MediaSettings = {
musicVolume: 1,
};
let sharedUIState: SharedUIState = { ...defaultSharedUIState };
export function normalizeSharedUIState(
value: SharedUIStatePatch,
): SharedUIState {
const guild = value.selectedGuild ?? "";
return {
selectedVoiceGuild: value.selectedVoiceGuild ?? guild,
selectedVoiceChannel: value.selectedVoiceChannel ?? "",
selectedTextGuild: value.selectedTextGuild ?? guild,
selectedTextChannel: value.selectedTextChannel ?? "",
activeTab: (["voice", "messages", "media", "review", "recordings"].includes(
value.activeTab ?? "",
)
? value.activeTab
: "voice") as "voice" | "messages" | "media" | "review" | "recordings",
isListening: value.isListening ?? false,
isStreaming: value.isStreaming ?? false,
};
}
async function initializeSharedUIState() {
sharedUIState = normalizeSharedUIState(
await getPersistedValue("web-ui-state", defaultSharedUIState),
);
}
async function initializeMediaSettings(): Promise<MediaSettings> {
const stored = await getPersistedValue(
"media-settings",
@@ -123,56 +73,13 @@ async function initializeMediaSettings(): Promise<MediaSettings> {
};
}
function getSharedUIState(): SharedUIState {
return { ...sharedUIState };
}
function patchSharedUIState(patch: SharedUIStatePatch) {
if (typeof patch.selectedGuild === "string") {
sharedUIState.selectedVoiceGuild = patch.selectedGuild;
sharedUIState.selectedTextGuild = patch.selectedGuild;
}
if (typeof patch.selectedVoiceGuild === "string") {
sharedUIState.selectedVoiceGuild = patch.selectedVoiceGuild;
}
if (typeof patch.selectedVoiceChannel === "string") {
sharedUIState.selectedVoiceChannel = patch.selectedVoiceChannel;
}
if (typeof patch.selectedTextGuild === "string") {
sharedUIState.selectedTextGuild = patch.selectedTextGuild;
}
if (typeof patch.selectedTextChannel === "string") {
sharedUIState.selectedTextChannel = patch.selectedTextChannel;
}
if (
["voice", "messages", "media", "review", "recordings"].includes(
patch.activeTab ?? "",
)
) {
sharedUIState.activeTab = patch.activeTab as
| "voice"
| "messages"
| "media"
| "review"
| "recordings";
}
if (typeof patch.isListening === "boolean") {
sharedUIState.isListening = patch.isListening;
}
if (typeof patch.isStreaming === "boolean") {
sharedUIState.isStreaming = patch.isStreaming;
}
setPersistedValue("web-ui-state", sharedUIState);
return getSharedUIState();
}
export async function startWebserver(
port: number = 3000,
_client: Client,
voiceController: VoiceController,
) {
await initializeSharedUIState();
const { getSharedUIState, patchSharedUIState } = await createSharedUIStateStore();
let mediaSettings = await initializeMediaSettings();
const app = express();
@@ -206,7 +113,7 @@ export async function startWebserver(
const mediaController = new MediaController({
isVoiceConnected: () => voiceController.getStatus().connected,
isBrowserStreaming: () => sharedUIState.isStreaming,
isBrowserStreaming: () => getSharedUIState().isStreaming,
screenController,
onStateChange: (state) => broadcaster.mediaState(state),
initialMusicVolume: mediaSettings.musicVolume,
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { normalizeSharedUIState } from "../../src/state/uiState";
describe("normalizeSharedUIState", () => {
it("maps legacy selectedGuild into voice and text guilds", () => {
expect(normalizeSharedUIState({ selectedGuild: "guild-1" })).toEqual({
selectedVoiceGuild: "guild-1",
selectedVoiceChannel: "",
selectedTextGuild: "guild-1",
selectedTextChannel: "",
activeTab: "voice",
isListening: false,
isStreaming: false,
});
});
it("keeps valid explicit values", () => {
expect(
normalizeSharedUIState({
selectedVoiceGuild: "voice-guild",
selectedVoiceChannel: "voice-channel",
selectedTextGuild: "text-guild",
selectedTextChannel: "text-channel",
activeTab: "media",
isListening: true,
isStreaming: true,
}),
).toEqual({
selectedVoiceGuild: "voice-guild",
selectedVoiceChannel: "voice-channel",
selectedTextGuild: "text-guild",
selectedTextChannel: "text-channel",
activeTab: "media",
isListening: true,
isStreaming: true,
});
});
it("falls back to voice tab for invalid activeTab", () => {
expect(normalizeSharedUIState({ activeTab: "bad" as never })).toMatchObject({
activeTab: "voice",
});
});
});