/** * @module init-orchestration * * Shared web orchestration functions for booting ibgib-based frontend apps. * * ## what belongs here * * Functions that define the startup sequence and are identical across all * ibgib apps. These depend only on the IbGibAmbientContextConfig seam. * App-specific logic (typed globalThis accessors, component helpers) belongs * in each app's own helpers.web.mts. */ import { delay, extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { storageGet } from "../storage/storage-helpers.web.mjs"; import { initAppStorage } from "../helpers.web.mjs"; import { getGlobalMetaspace_waitIfNeeded } from "../helpers.mjs"; import { IbGibDynamicComponentMetaCtorOpts } from "../ui/component/component-types.mjs"; import { IbGibGlobalThisInfo } from "../types.mjs"; import { IbGibAmbientContextConfig, IbGibGlobalThis_Common, IbGibGlobalThis_IbGibApp } from "./types.mjs"; // --------------------------------------------------------------------------- // Storage initialization // --------------------------------------------------------------------------- /** * Initializes the IndexedDB stores required by an ibgib app. * * Call once on DOMContentLoaded, before the bootstrap script is dynamically * loaded. This ensures storage is available when the metaspace initializes. */ export async function initIbGibStorage(config: IbGibAmbientContextConfig): Promise { const lc = `[${initIbGibStorage.name}]`; try { await initAppStorage({ infos: [ { dbName: config.dbName, storeNames: [config.storeName, ...(config.additionalStoreNames ?? [])], }, ], }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } // --------------------------------------------------------------------------- // globalThis initialization // --------------------------------------------------------------------------- /** * Sets up globalThis.ibgib.[globalThisKey] for the given app config. * * Idempotent — safe to call multiple times. The root `globalThis.ibgib` object * is created if it doesn't exist, then the per-app sub-key is populated. * * @param config The app's ambient context config. * @param globalThisKey The snake_case key for this app's namespace under * `globalThis.ibgib` (e.g. `'my_app'`, `'cool_new_thing'`). * @param version Optional semver string (from AUTO_GENERATED_VERSION). */ export function initIbGibGlobalThis( config: IbGibAmbientContextConfig, globalThisKey: string, version?: string, ): void { const lc = `[${initIbGibGlobalThis.name}]`; try { // Ensure the root ibgib object exists if (!(globalThis as any).ibgib) { (globalThis as any).ibgib = { dbName_hack: config.dbName, apiKeyName_hack: config.apiKeyName, storeName_hack: config.storeName, } satisfies IbGibGlobalThisInfo; } // Populate the per-app namespace if (!(globalThis as any).ibgib[globalThisKey]) { (globalThis as any).ibgib[globalThisKey] = { version, spaceShim: {}, fnDefaultGetAPIKey: async () => { const apiKey = await storageGet({ dbName: config.dbName, storeName: config.storeName, key: config.apiKeyName, }); return apiKey ?? ''; }, dbName_hack: config.dbName, apiKeyName_hack: config.apiKeyName, storeName_hack: config.storeName, } satisfies IbGibGlobalThis_IbGibApp; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } // --------------------------------------------------------------------------- // globalThis accessors // --------------------------------------------------------------------------- /** * Returns the raw IbGibGlobalThis_Common for the given key. * Typed as the Common base — use the app-specific typed accessor for * full-type access. */ export function getIbGibGlobalThis_Common(globalThisKey: string): IbGibGlobalThis_Common { const lc = `[${getIbGibGlobalThis_Common.name}]`; const g = (globalThis as any).ibgib?.[globalThisKey]; if (!g) { throw new Error(`${lc} (UNEXPECTED) globalThis.ibgib.${globalThisKey} not initialized. (E: 8f621732ibgib-commonfe25)`); } return g as IbGibGlobalThis_Common; } /** * Returns the IbGibGlobalThis_IbGibApp for the given key, initializing it * from config if not yet present. */ export function getIbGibGlobalThis_IbGibApp( globalThisKey: string, config?: IbGibAmbientContextConfig, version?: string, ): IbGibGlobalThis_IbGibApp { if (!(globalThis as any).ibgib?.[globalThisKey]) { if (!config) { throw new Error( `Global context not initialized and config not provided for key '${globalThisKey}'.` ); } initIbGibGlobalThis(config, globalThisKey, version); } return (globalThis as any).ibgib[globalThisKey] as IbGibGlobalThis_IbGibApp; } // --------------------------------------------------------------------------- // API key helper // --------------------------------------------------------------------------- /** * Returns a function that retrieves the AI API key from storage at call time. * The globalThisKey links back to the per-app namespace for DB/store/key names. */ export function getDefaultFnGetAPIKey( globalThisKey: string, ): () => Promise { return async () => { const gb = getIbGibGlobalThis_IbGibApp(globalThisKey); return (await storageGet({ dbName: gb.dbName_hack, storeName: gb.storeName_hack, key: gb.apiKeyName_hack, })) ?? ''; }; } // --------------------------------------------------------------------------- // Dynamic bootstrap loading // --------------------------------------------------------------------------- /** * Dynamically imports the bootstrap module at the given path and calls the * named export bootstrap function. * * Called after DOMContentLoaded so the heavy ibgib import graph does not block * first paint. * * @param path Path to bootstrap.mjs (relative to the app's index.mts). * @param bootstrapFnName The exported async function to call. Defaults to * `'bootstrapApp'` — override with the app-specific function name * (e.g. `'bootstrapMyAppApp'`). */ export async function dynamicallyLoadBootstrapScript( path: string, bootstrapFnName: string = 'bootstrapApp', ): Promise { const lc = `[${dynamicallyLoadBootstrapScript.name}]`; try { const module = await import(path); const fn = module[bootstrapFnName]; if (typeof fn !== 'function') { throw new Error( `${lc} Bootstrap function '${bootstrapFnName}' not found in '${path}'. ` + `Available exports: ${Object.keys(module).join(', ')} (E: 9c36085eibgib-common26)` ); } await fn(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } // --------------------------------------------------------------------------- // Component constructor args helper // --------------------------------------------------------------------------- /** * Returns the standard ctor options object for IbGib dynamic components. * * Provides two things components need at construction time: * 1. `fnGetMetaspace` — accesses the global metaspace once initialized * 2. `bootstrapPromise` — components can await this to defer their own init * until the App witness and metaspace are fully ready * * @param globalThisKey The per-app namespace key used to find the bootstrap * promise on globalThis. */ export function getComponentCtorArg(globalThisKey?: string): IbGibDynamicComponentMetaCtorOpts { const fnGetMetaspace = async () => getGlobalMetaspace_waitIfNeeded(); const bootstrapPromiseWrapper = new Promise(async (resolve, reject) => { try { let maxTries = 100_000; let counter = 0; let bootstrapPromise: Promise | undefined; do { if (globalThisKey) { bootstrapPromise = getIbGibGlobalThis_IbGibApp(globalThisKey)?.bootstrapPromise; } else { bootstrapPromise = (globalThis as any).ibgib?.bootstrapPromise; if (!bootstrapPromise && (globalThis as any).ibgib) { const keys = Object.keys((globalThis as any).ibgib); for (const k of keys) { if ((globalThis as any).ibgib[k]?.bootstrapPromise) { bootstrapPromise = (globalThis as any).ibgib[k].bootstrapPromise; break; } } } } counter++; if (counter > maxTries) { break; } await delay(10); } while (bootstrapPromise === undefined); if (!bootstrapPromise) { resolve(); return; } await bootstrapPromise; resolve(); } catch (error) { reject(error); } }); return { fnGetMetaspace, bootstrapPromise: bootstrapPromiseWrapper }; }