LCOV - code coverage report
Current view: top level - dashql-app/src/app/loading - app_setup.ts Coverage Total Hit
Test: DashQL 915828b Lines: 89.2 % 139 124
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import { useLocation } from 'react-router-dom';
       3              : 
       4              : import { OPEN_EPHEMERAL_SESSION, OPEN_LINK_SESSION, sessionIdFromPathname, useRouterNavigate } from '../router/router.js';
       5              : import { useConnectionAllocator, useConnectionRegistrySetter } from '../../connections/connection_registry.js';
       6              : import { embeddedConnection, newSessionConnection } from '../../connections/embedded_connection.js';
       7              : import { useDashQLCoreSetup } from '../../runtime/core_provider.js';
       8              : import { useLogger } from '../../platform/logger/logger_provider.js';
       9              : import { createTrace } from '../../platform/logger/trace_context.js';
      10              : import { useEphemeralSession } from '../../session/session_creation.js';
      11              : import { AppLoadingProgress } from './app_loading_progress.js';
      12              : import { ProgressCounter } from '../../utils/progress.js';
      13              : import { deleteSurplusEphemeralSessions, loadApp, selectStartupSession, shouldFinishInitialNavigation } from './app_loading_logic.js';
      14              : import { useAppConfig } from '../../config/app_config.js';
      15              : import { useStorage, useStorageWriter } from '../../storage/storage_provider.js';
      16              : import { useSessionScriptsDeletion, useSessionScriptsRegistrySetter } from '../../session/scripts/session_scripts_registry.js';
      17              : import { useEmbeddedDatabaseSetup } from '../../platform/database/embedded_database_provider.js';
      18              : import { getAppHost } from '../../platform/native_globals.js';
      19              : import { restoreAppState } from '../../session/persistence/app_state_loader.js';
      20              : import { IndicatorStatus } from '../../ui/foundations/status_indicator.js';
      21              : import { useAppSettingsReady } from '../../config/app_settings_sync.js';
      22              : import { stringifyError } from '../../platform/logger/logger.js';
      23              : 
      24            1 : const LOG_CTX = "app_loader";
      25              : 
      26            1 : async function loadFonts(): Promise<void> {
      27              :     // The font families are theme tokens that the app flavor may override
      28            1 :     const style = getComputedStyle(document.documentElement);
      29            1 :     const family = style.getPropertyValue('--font_family').trim() || "'Instrument Sans'";
      30            1 :     const familyMono = style.getPropertyValue('--font_family_mono').trim() || "'JetBrains Mono'";
      31            1 :     await Promise.all([
      32            1 :         document.fonts.load(`300 16px ${family}`),
      33            1 :         document.fonts.load(`400 16px ${family}`),
      34            1 :         document.fonts.load(`500 16px ${family}`),
      35            1 :         document.fonts.load(`700 16px ${family}`),
      36            1 :         document.fonts.load(`400 16px ${familyMono}`),
      37            1 :         document.fonts.load(`500 16px ${familyMono}`),
      38            1 :         document.fonts.load(`700 16px ${familyMono}`),
      39            1 :     ]);
      40            1 : }
      41              : 
      42              : /// The setup of the app
      43              : export interface AppSetup {
      44              :     /// Resolves once the app state is restored
      45              :     done: Promise<void>;
      46              :     /// Resolves once the app opened the session it starts with
      47              :     opened: Promise<void>;
      48              :     /// The progress of the restoration
      49              :     progress: AppLoadingProgress;
      50              :     /// The status of the setup of the embedded database
      51              :     embeddedDatabaseStatus: IndicatorStatus;
      52              : }
      53              : 
      54              : /// Set up the app once its settings are ready.
      55              : ///
      56              : /// The setup initializes the core, the embedded database and the fonts, restores the connections and
      57              : /// sessions and makes sure that there is exactly one ephemeral session. It then opens the session of the path, otherwise
      58              : /// the greeter on the ephemeral session. A session that the app was started with is opened by its import instead,
      59              : /// `initialImport` resolves to true if it opened one.
      60            1 : export function useAppSetup(initialImport: React.RefObject<Promise<boolean> | null>): AppSetup {
      61            1 :     const config = useAppConfig();
      62            1 :     const appSettingsReady = useAppSettingsReady();
      63            1 :     const logger = useLogger();
      64            1 :     const navigate = useRouterNavigate();
      65            1 :     const setupCore = useDashQLCoreSetup();
      66            1 :     const ephemeralSession = useEphemeralSession();
      67            1 :     const deleteSessionScripts = useSessionScriptsDeletion();
      68            1 :     const storageWriter = useStorageWriter();
      69            1 :     const pathname = useLocation().pathname;
      70            1 :     const [storageReader] = useStorage();
      71            1 :     const allocateConnection = useConnectionAllocator();
      72            1 :     const setConnReg = useConnectionRegistrySetter();
      73            1 :     const setSessionScriptsRegistry = useSessionScriptsRegistrySetter();
      74            1 :     const setupEmbeddedDatabase = useEmbeddedDatabaseSetup();
      75              : 
      76            1 :     const [embeddedDatabaseStatus, setEmbeddedDatabaseStatus] = React.useState<IndicatorStatus>(IndicatorStatus.None);
      77            1 :     const [progress, setProgress] = React.useState<AppLoadingProgress>(() => ({
      78            1 :         restoreConnections: new ProgressCounter(),
      79            1 :         restoreCatalogs: new ProgressCounter(),
      80            1 :         restoreSessionScripts: new ProgressCounter(),
      81            1 :     }));
      82            1 :     const [done, resolveDone] = React.useMemo(() => {
      83            1 :         let resolve: () => void;
      84            1 :         const promise = new Promise<void>(a => { resolve = a; });
      85            1 :         return [promise, resolve!] as const;
      86            1 :     }, []);
      87            1 :     const [opened, resolveOpened] = React.useMemo(() => {
      88            1 :         let resolve: () => void;
      89            1 :         const promise = new Promise<void>(a => { resolve = a; });
      90            1 :         return [promise, resolve!] as const;
      91            1 :     }, []);
      92              : 
      93              :     // Run the setup once at the beginning. We guard against re-runs triggered by config identity
      94              :     // changes (e.g. AppSettingsSync hydrating persisted settings into AppConfig).
      95            1 :     const hasStartedSetup = React.useRef(false);
      96            1 :     React.useEffect(() => {
      97            1 :         if (config == null || !appSettingsReady || hasStartedSetup.current) {
      98            1 :             return;
      99              :         }
     100            1 :         hasStartedSetup.current = true;
     101            1 :         let failedComponent: string | null = null;
     102            1 :         const trackInitialization = async <T,>(name: string, promise: Promise<T>): Promise<T> => {
     103            1 :             try {
     104            1 :                 return await promise;
     105            1 :             } catch (error) {
     106            1 :                 failedComponent ??= name;
     107            1 :                 throw error;
     108            1 :             }
     109            1 :         };
     110              : 
     111            1 :         const run = async () => {
     112              :             // Start trace for app loading
     113            1 :             const traced = logger.withTrace(createTrace());
     114            1 :             traced.info("Initializing application", {}, LOG_CTX);
     115            1 :             const totalStartTime = performance.now();
     116              : 
     117              :             // Wait for Core and the platform's embedded database to be ready.
     118            1 :             traced.info("Initializing Core and embedded database", {}, LOG_CTX);
     119            1 :             const coreStartTime = performance.now();
     120              : 
     121            1 :             const corePromise = trackInitialization("Core", setupCore("app_setup"));
     122            1 :             setEmbeddedDatabaseStatus(IndicatorStatus.Running);
     123            1 :             const embeddedDatabasePromise = trackInitialization(
     124            1 :                 "HyperDB",
     125            1 :                 setupEmbeddedDatabase("app_setup").then(database => {
     126            1 :                     setEmbeddedDatabaseStatus(IndicatorStatus.Succeeded);
     127            1 :                     return database;
     128            1 :                 }).catch(error => {
     129            0 :                     setEmbeddedDatabaseStatus(IndicatorStatus.Failed);
     130            0 :                     throw error;
     131            1 :                 }),
     132            1 :             );
     133            1 :             const fontsPromise = trackInitialization("Fonts", loadFonts());
     134            1 :             const [core] = await Promise.all([corePromise, embeddedDatabasePromise, fontsPromise]);
     135              : 
     136            1 :             const coreDuration = performance.now() - coreStartTime;
     137            1 :             traced.info("Core and embedded database ready", {
     138            1 :                 durationMs: coreDuration.toFixed(2)
     139            1 :             }, LOG_CTX);
     140              : 
     141              :             // Load the app
     142            1 :             traced.info("Loading application state and sessions", {}, LOG_CTX);
     143            1 :             const loaded = await trackInitialization(
     144            1 :                 "Application state",
     145            1 :                 loadApp(traced, core, {
     146            1 :                     restoreAppState: (core, progressConsumer) => restoreAppState(core, storageReader.backend, logger, progressConsumer),
     147            1 :                 }, setConnReg, setSessionScriptsRegistry, setProgress),
     148            1 :             );
     149              : 
     150              :             // The scripts registry lists the sessions that were refused a load. This is just an
     151              :             // aggregate count for the log — each refused session already logs a WARN with its path
     152              :             // and reason, so a second WARN here would only duplicate the diagnostic. Keep it at INFO.
     153            1 :             if (loaded.invalidSessions.size > 0) {
     154            1 :                 traced.info("Some sessions were refused a load", {
     155            1 :                     count: loaded.invalidSessions.size.toString()
     156            1 :                 }, LOG_CTX);
     157              :             }
     158              : 
     159            1 :             const totalDuration = performance.now() - totalStartTime;
     160            1 :             traced.info("Application loaded successfully", {
     161            1 :                 totalDurationMs: totalDuration.toFixed(2)
     162            1 :             }, LOG_CTX);
     163              : 
     164              :             // Mark the setup as done
     165            1 :             traced.info("Marking setup as done", {}, LOG_CTX);
     166            1 :             resolveDone();
     167              : 
     168            1 :             traced.info("Finishing setup", {}, LOG_CTX);
     169              : 
     170              :             // Sessions fall back to the embedded Hyper and the user can't attach one, the app always holds it.
     171              :             // The restored state lacks it when no session uses it, e.g. when every session runs on the Hyper of a link.
     172            1 :             const connections = new Map(loaded.restoredConnections);
     173            1 :             const embedded = embeddedConnection(connections.values(), allocateConnection, core);
     174            1 :             connections.set(embedded.connectionId, embedded);
     175              : 
     176              :             // The app holds exactly one ephemeral session, whatever it opens below
     177            1 :             await deleteSurplusEphemeralSessions(loaded.restoredSessions.values(), async sessionId => {
     178            0 :                 await storageWriter.deleteSession(sessionId);
     179            0 :                 deleteSessionScripts(sessionId);
     180            0 :             }, (sessionId, error) => {
     181            0 :                 traced.warn("Failed to delete a surplus ephemeral session", { sessionId, error: stringifyError(error) }, LOG_CTX);
     182            1 :             });
     183            1 :             const greeter = ephemeralSession(newSessionConnection(connections, allocateConnection, core));
     184              : 
     185            1 :             if (await shouldFinishInitialNavigation(initialImport.current)) {
     186            1 :                 const sessionId = selectStartupSession(sessionIdFromPathname(pathname), loaded.restoredSessions);
     187            1 :                 navigate(sessionId != null
     188            1 :                     ? { type: OPEN_LINK_SESSION, value: sessionId }
     189            1 :                     : { type: OPEN_EPHEMERAL_SESSION, value: greeter.sessionId });
     190            1 :             }
     191            1 :             resolveOpened();
     192            1 :             globalThis.__DASHQL_STARTUP__ = {
     193            1 :                 embeddedDatabase: 'hyperdb-wasm',
     194            1 :                 host: getAppHost(),
     195            1 :                 status: 'ready',
     196            1 :             };
     197            1 :         };
     198            1 :         run().catch(error => {
     199            0 :             globalThis.__DASHQL_STARTUP__ = {
     200            0 :                 embeddedDatabase: null,
     201            0 :                 error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
     202            0 :                 host: getAppHost(),
     203            0 :                 status: 'failed',
     204            0 :             };
     205            0 :             logger.error("Application initialization failed", {
     206            0 :                 component: failedComponent ?? "Unknown",
     207            0 :             }, LOG_CTX);
     208            1 :         });
     209            1 :     }, [appSettingsReady, config]);
     210              : 
     211            1 :     return { done, opened, progress, embeddedDatabaseStatus };
     212            1 : }
        

Generated by: LCOV version 2.0-1