refactor(api): code-review fixes (R7 S3b review round 1)

- barrel re-exports domains with `export type *` (not `export *`) so the
  internal downloadsApi/plexApi/messagingApi slices stay behind the façade;
  plex's runtime value exports (EMPTY_PLEX_FILTERS/plexFilterCount) re-exported
  explicitly
- App.tsx: drop the now-redundant `prefs as Record<string, unknown>` self-cast
  (Me.preferences is already that type) and its `prefsRec` alias
- plex.ts: comment why plexSetState keeps its status union inline (reusing
  VideoStatus would make an api.ts↔plex.ts type cycle)

Behaviour-identical. Gate green: typecheck, eslint 0, prettier, 74 vitest.
This commit is contained in:
2026-07-27 23:45:02 +02:00
parent 926ebe7ffa
commit 1223df6ccc
3 changed files with 15 additions and 10 deletions
+4 -5
View File
@@ -228,9 +228,8 @@ export default function App() {
useEffect(() => { useEffect(() => {
const prefs = meQuery.data?.preferences; const prefs = meQuery.data?.preferences;
if (!prefs) return; if (!prefs) return;
const prefsRec = prefs as Record<string, unknown>;
(["feed", "plex", "playlists"] as PanelId[]).forEach((p) => { (["feed", "plex", "playlists"] as PanelId[]).forEach((p) => {
const raw = prefsRec[PREF_KEY[p]]; const raw = prefs[PREF_KEY[p]];
if (raw) { if (raw) {
const l = normalizeLayout(p, raw); const l = normalizeLayout(p, raw);
setPanelLayouts((prev) => ({ ...prev, [p]: l })); setPanelLayouts((prev) => ({ ...prev, [p]: l }));
@@ -245,9 +244,9 @@ export default function App() {
setFilterCollapsedState(prefs.filterCollapsed); setFilterCollapsedState(prefs.filterCollapsed);
writeAccount(LS.filterCollapsed, prefs.filterCollapsed); writeAccount(LS.filterCollapsed, prefs.filterCollapsed);
} }
if (typeof prefsRec.playlistsCollapsed === "boolean") { if (typeof prefs.playlistsCollapsed === "boolean") {
setPlaylistsCollapsedState(prefsRec.playlistsCollapsed); setPlaylistsCollapsedState(prefs.playlistsCollapsed);
writeAccount(LS.playlistsCollapsed, prefsRec.playlistsCollapsed); writeAccount(LS.playlistsCollapsed, prefs.playlistsCollapsed);
} }
const lang = prefs.language; const lang = prefs.language;
if (typeof lang === "string" && isSupported(lang)) setLanguage(lang); if (typeof lang === "string" && isSupported(lang)) setLanguage(lang);
+9 -5
View File
@@ -18,14 +18,18 @@ export {
setUnauthorizedHandler, setUnauthorizedHandler,
}; };
// Domain modules split out of this file, re-exported so their types + the `api` façade stay // Domain modules split out of this file. Their TYPES are re-exported so consumers keep importing
// importable from "./api". Their method slices are spread into `api` below. // them from "./api" unchanged; their method slices stay internal (`export type *`, not `export *`)
// and reach consumers only through the `api` façade they're spread into below. Runtime value
// exports a domain genuinely owns (e.g. plex's EMPTY_PLEX_FILTERS/plexFilterCount) are re-exported
// explicitly.
import { downloadsApi } from "./api/downloads"; import { downloadsApi } from "./api/downloads";
export * from "./api/downloads"; export type * from "./api/downloads";
import { plexApi } from "./api/plex"; import { plexApi } from "./api/plex";
export * from "./api/plex"; export type * from "./api/plex";
export { EMPTY_PLEX_FILTERS, plexFilterCount } from "./api/plex";
import { messagingApi } from "./api/messaging"; import { messagingApi } from "./api/messaging";
export * from "./api/messaging"; export type * from "./api/messaging";
export interface Me { export interface Me {
id: number; id: number;
+2
View File
@@ -460,6 +460,8 @@ export const plexApi = {
duration_seconds, duration_seconds,
final, final,
}), }),
// Inline union (matches api.ts's VideoStatus by design) rather than importing VideoStatus — that
// lives in the api.ts barrel, which imports this module, so reusing it would make a type cycle.
plexSetState: (id: string, status: "new" | "watched" | "hidden"): Promise<unknown> => plexSetState: (id: string, status: "new" | "watched" | "hidden"): Promise<unknown> =>
req(`/api/plex/item/${encodeURIComponent(id)}/state`, { req(`/api/plex/item/${encodeURIComponent(id)}/state`, {
method: "POST", method: "POST",