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 : }
|