From 3094464f62361700316d804f3f061f7b329fe060 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 15 May 2026 19:53:10 +0300 Subject: [PATCH] perf: optimize launcher boot and media loading --- server/dev-server.mjs | 14 +- src/app/LauncherApp.tsx | 246 +++++++++------------ src/styles/globals.css | 12 +- src/widgets/service-rail/ServiceRail.tsx | 14 +- src/widgets/service-stage/ServiceStage.tsx | 18 +- 5 files changed, 143 insertions(+), 161 deletions(-) diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 21b5e65..102f2ad 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -1543,7 +1543,19 @@ if (process.env.NODE_ENV === "production") { throw new Error("Launcher production build is missing. Run npm run build before starting the server."); } - app.use(express.static(distRoot, { index: false })); + app.use(express.static(distRoot, { + index: false, + immutable: true, + maxAge: "1y", + setHeaders(res, assetPath) { + if (assetPath === indexHtmlPath) { + res.setHeader("Cache-Control", noStoreCacheControl); + return; + } + + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + }, + })); app.use((req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") { next(); diff --git a/src/app/LauncherApp.tsx b/src/app/LauncherApp.tsx index b74f3e3..9168bc6 100644 --- a/src/app/LauncherApp.tsx +++ b/src/app/LauncherApp.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { Client } from "../entities/client/types"; import type { Invite } from "../entities/invite/types"; import { syncServiceLaunchLink } from "../entities/service/links"; @@ -21,7 +21,6 @@ import { ensureAdminTaskManagerProjectMembership, ensureAdminTaskManagerWorkspaceMembership, fetchAdminTaskManagerWorkspaces, - fetchControlPlaneSnapshot, reorderAdminServices, retryAdminSync, rejectAdminAccessRequest, @@ -64,21 +63,24 @@ import { acceptInvite, fetchPublicInvite, registerInvite, type PublicInviteRespo import type { AccessRequest, CreateAccessRequestCommand } from "../entities/access-request/types"; import { subscribeToNodeDCLogoutEvents } from "../shared/session/sessionSync"; import { loadPersistedLauncherData } from "../shared/api/storageApi"; -import { - AdminOverlay, - type AccessAssignmentValue, - type CreateUserCommand, - type EnsureTaskManagerProjectMemberCommand, - type SetServiceModuleEntitlementCommand, - type SetUserServiceAccessCommand, +import type { + AccessAssignmentValue, + CreateUserCommand, + EnsureTaskManagerProjectMemberCommand, + SetServiceModuleEntitlementCommand, + SetUserServiceAccessCommand, } from "../widgets/admin-overlay/AdminOverlay"; -import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel"; import { ServiceRail } from "../widgets/service-rail/ServiceRail"; import { ServiceStage } from "../widgets/service-stage/ServiceStage"; import { TopBar, type LauncherAdminMode } from "../widgets/top-bar/TopBar"; let lastAuthRedirect: { url: string; startedAt: number } | null = null; +const AdminOverlay = lazy(() => import("../widgets/admin-overlay/AdminOverlay").then((module) => ({ default: module.AdminOverlay }))); +const ProfileSettingsPanel = lazy(() => + import("../widgets/profile-settings-panel/ProfileSettingsPanel").then((module) => ({ default: module.ProfileSettingsPanel })) +); + type InviteFlowState = | { status: "loading" } | { status: "ready"; payload: PublicInviteResponse } @@ -101,6 +103,7 @@ export function LauncherApp() { const [adminMode, setAdminMode] = useState("admin"); const [authSession, setAuthSession] = useState(null); const [authApps, setAuthApps] = useState(null); + const [runtimeReady, setRuntimeReady] = useState(false); const [profileSettingsOpen, setProfileSettingsOpen] = useState(false); const [pendingAccessAssignments, setPendingAccessAssignments] = useState>({}); const [pendingServiceModuleEntitlements, setPendingServiceModuleEntitlements] = useState>({}); @@ -113,6 +116,7 @@ export function LauncherApp() { const runtimeDataRef = useRef(data); const runtimeProfileIdRef = useRef(activeProfileId); const runtimeClientIdRef = useRef(activeClientId); + const runtimeRefreshInFlightRef = useRef | null>(null); const resolvedProfileId = useMemo( () => resolveRuntimeProfileId(data, authSession, activeProfileId), [activeProfileId, authSession, data] @@ -219,38 +223,63 @@ export function LauncherApp() { const selectedService = launcherServices.find((service) => service.id === selectedServiceId); - useEffect(() => { - let isMounted = true; + const refreshRuntimeState = useCallback(async () => { + if (runtimeRefreshInFlightRef.current) { + return runtimeRefreshInFlightRef.current; + } - fetchAuthSession() - .then(async (session) => { - if (!isMounted) return; + const request = (async () => { + try { + const nextSession = await fetchAuthSession(); - setAuthSession(session); + setAuthSession(nextSession); - if (!session.authenticated) { + if (!nextSession.authenticated) { setAuthApps([]); + setRuntimeReady(true); return; } - const apps = await fetchAvailableApps(); + const [persistedData, apps] = await Promise.all([ + loadPersistedLauncherData(), + fetchAvailableApps(), + ]); - if (isMounted) { - setAuthApps(apps); + if (persistedData) { + setData(syncLauncherServiceLinks(persistedData)); } - }) + + setAuthApps(apps); + setRuntimeReady(true); + } catch (error: unknown) { + setRuntimeReady(true); + console.warn(error instanceof Error ? error.message : "Не удалось обновить runtime состояние Launcher"); + } + })().finally(() => { + runtimeRefreshInFlightRef.current = null; + }); + + runtimeRefreshInFlightRef.current = request; + return request; + }, []); + + useEffect(() => { + let isMounted = true; + + refreshRuntimeState() .catch((error: unknown) => { if (!isMounted) return; setAuthSession({ authenticated: false, loginUrl: "/auth/login" }); setAuthApps([]); + setRuntimeReady(true); console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы"); }); return () => { isMounted = false; }; - }, []); + }, [refreshRuntimeState]); useEffect(() => { if (!authSession || authSession.authenticated) return; @@ -338,41 +367,6 @@ export function LauncherApp() { } }, [activeClientId, activeProfileId, authSession, data]); - useEffect(() => { - let isMounted = true; - - loadPersistedLauncherData() - .then((persistedData) => { - if (isMounted && persistedData) { - setData(syncLauncherServiceLinks(persistedData)); - } - }); - - return () => { - isMounted = false; - }; - }, []); - - useEffect(() => { - if (!canOpenAdminApi) return; - - let isMounted = true; - - fetchControlPlaneSnapshot() - .then((snapshot) => { - if (isMounted) { - setData(syncLauncherServiceLinks(snapshot.data)); - } - }) - .catch((error: unknown) => { - console.warn(error instanceof Error ? error.message : "Не удалось загрузить control-plane snapshot"); - }); - - return () => { - isMounted = false; - }; - }, [canOpenAdminApi]); - useEffect(() => { if (!adminOpen || !canOpenAdminApi) return; void refreshTaskManagerWorkspaces(); @@ -385,32 +379,6 @@ export function LauncherApp() { setAdminMode("admin"); }, [runtimeMe.permissions.canOpenAdmin]); - const refreshRuntimeState = useCallback(async () => { - try { - const nextSession = await fetchAuthSession(); - - setAuthSession(nextSession); - - if (!nextSession.authenticated) { - setAuthApps([]); - return; - } - - const [persistedData, apps] = await Promise.all([ - loadPersistedLauncherData(), - fetchAvailableApps(), - ]); - - if (persistedData) { - setData(syncLauncherServiceLinks(persistedData)); - } - - setAuthApps(apps); - } catch (error: unknown) { - console.warn(error instanceof Error ? error.message : "Не удалось обновить runtime состояние Launcher"); - } - }, []); - useEffect(() => { if (!authSession?.authenticated) return; @@ -423,10 +391,6 @@ export function LauncherApp() { const eventSource = new EventSource("/api/events"); - eventSource.addEventListener("nodedc-ready", () => { - void refreshMountedRuntimeState(); - }); - eventSource.addEventListener("nodedc-runtime", () => { void refreshMountedRuntimeState(); }); @@ -466,7 +430,7 @@ export function LauncherApp() { if (document.visibilityState === "visible") { void refreshRuntimeState(); } - }, 5000); + }, 60000); return () => { window.clearInterval(intervalId); @@ -871,7 +835,7 @@ export function LauncherApp() { ); } - if (!authSession) { + if (!authSession || (authSession.authenticated && !runtimeReady)) { return null; } @@ -923,58 +887,62 @@ export function LauncherApp() { onSelectNext={() => handleStageStep("next")} /> {adminOpen && me.permissions.canOpenAdmin ? ( - setAdminOpen(false)} - onSetUserServiceAccess={handleSetUserServiceAccess} - onCreateInvite={handleCreateInvite} - onUpdateInvite={handleUpdateInvite} - onDeleteInvite={handleDeleteInvite} - onUpdateAccessRequest={handleUpdateAccessRequest} - onApproveAccessRequest={handleApproveAccessRequest} - onRejectAccessRequest={handleRejectAccessRequest} - onApproveTaskerInviteRequest={handleApproveTaskerInviteRequest} - onRejectTaskerInviteRequest={handleRejectTaskerInviteRequest} - onRetrySync={handleRetrySync} - onCreateClient={handleCreateClient} - onUpdateClient={handleUpdateClient} - onDeleteClient={handleDeleteClient} - onCreateUser={handleCreateUser} - onUpdateUser={handleUpdateUser} - onDeleteUser={handleDeleteUser} - onUpdateMembership={handleUpdateMembership} - onDeleteMembership={handleDeleteMembership} - pendingAccessAssignments={pendingAccessAssignments} - pendingServiceModuleEntitlements={pendingServiceModuleEntitlements} - onCreateGroup={handleCreateGroup} - onUpdateGroup={handleUpdateGroup} - onDeleteGroup={handleDeleteGroup} - onUpdateService={handleUpdateService} - onReorderServices={handleReorderServices} - onCreateService={handleCreateService} - onDeleteService={handleDeleteService} - onUpdateSettings={handleUpdateSettings} - taskManagerWorkspaces={taskManagerWorkspaces} - taskManagerWorkspacesLoading={taskManagerWorkspacesLoading} - taskManagerWorkspacesError={taskManagerWorkspacesError} - pendingTaskManagerMemberships={pendingTaskManagerMemberships} - pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships} - onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()} - onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole} - onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole} - onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement} - /> + + setAdminOpen(false)} + onSetUserServiceAccess={handleSetUserServiceAccess} + onCreateInvite={handleCreateInvite} + onUpdateInvite={handleUpdateInvite} + onDeleteInvite={handleDeleteInvite} + onUpdateAccessRequest={handleUpdateAccessRequest} + onApproveAccessRequest={handleApproveAccessRequest} + onRejectAccessRequest={handleRejectAccessRequest} + onApproveTaskerInviteRequest={handleApproveTaskerInviteRequest} + onRejectTaskerInviteRequest={handleRejectTaskerInviteRequest} + onRetrySync={handleRetrySync} + onCreateClient={handleCreateClient} + onUpdateClient={handleUpdateClient} + onDeleteClient={handleDeleteClient} + onCreateUser={handleCreateUser} + onUpdateUser={handleUpdateUser} + onDeleteUser={handleDeleteUser} + onUpdateMembership={handleUpdateMembership} + onDeleteMembership={handleDeleteMembership} + pendingAccessAssignments={pendingAccessAssignments} + pendingServiceModuleEntitlements={pendingServiceModuleEntitlements} + onCreateGroup={handleCreateGroup} + onUpdateGroup={handleUpdateGroup} + onDeleteGroup={handleDeleteGroup} + onUpdateService={handleUpdateService} + onReorderServices={handleReorderServices} + onCreateService={handleCreateService} + onDeleteService={handleDeleteService} + onUpdateSettings={handleUpdateSettings} + taskManagerWorkspaces={taskManagerWorkspaces} + taskManagerWorkspacesLoading={taskManagerWorkspacesLoading} + taskManagerWorkspacesError={taskManagerWorkspacesError} + pendingTaskManagerMemberships={pendingTaskManagerMemberships} + pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships} + onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()} + onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole} + onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole} + onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement} + /> + ) : null} {profileSettingsOpen && activeProfileUser ? ( - setProfileSettingsOpen(false)} - onSaveProfile={handleUpdateOwnProfile} - onChangePassword={handleUpdateOwnPassword} - /> + + setProfileSettingsOpen(false)} + onSaveProfile={handleUpdateOwnProfile} + onChangePassword={handleUpdateOwnPassword} + /> + ) : null} diff --git a/src/styles/globals.css b/src/styles/globals.css index f61cae2..e72d9d6 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -754,7 +754,6 @@ code { content: ""; opacity: 0.34; transform: rotate(-2deg); - animation: stageVideoDrift 16s linear infinite; mix-blend-mode: screen; } @@ -1045,15 +1044,6 @@ code { background: color-mix(in srgb, var(--service-accent) 45%, rgba(255, 255, 255, 0.16)); } -@keyframes stageVideoDrift { - from { - transform: translate3d(0, 0, 0) rotate(-2deg); - } - to { - transform: translate3d(-7rem, -7rem, 0) rotate(-2deg); - } -} - @keyframes stageFigureFloat { from { transform: translate3d(-0.8rem, 0, 0) rotate(-9deg) scale(1); @@ -1201,7 +1191,7 @@ code { flex: 0 0 var(--service-rail-card-size); overflow: hidden; border-radius: 1rem; - background: color-mix(in srgb, var(--tile-accent) 36%, rgba(255, 255, 255, 0.12)); + background: #050506; } .service-tile__media-asset { diff --git a/src/widgets/service-rail/ServiceRail.tsx b/src/widgets/service-rail/ServiceRail.tsx index 109c699..82c1a60 100644 --- a/src/widgets/service-rail/ServiceRail.tsx +++ b/src/widgets/service-rail/ServiceRail.tsx @@ -1,5 +1,5 @@ import { ChevronRight } from "lucide-react"; -import { DEFAULT_AMBIENT_MEDIA, resolveAmbientMedia } from "../../entities/service/media"; +import { resolveAmbientMedia } from "../../entities/service/media"; import type { LauncherServiceView } from "../../entities/service/types"; import { cn } from "../../shared/lib/cn"; @@ -34,8 +34,8 @@ export function ServiceRail({ > @@ -57,15 +57,17 @@ function RailMedia({ kind, className, }: { - src: string; + src?: string | null; kind?: LauncherServiceView["media"]["coverKind"]; className: string; }) { + if (!src) return null; + if (kind === "video" || isVideoSource(src)) { - return