/** * @module bootstrap * * Shared bootstrap entry point for ibgib-based web apps. * Collapses the metaspace, globalThis, and shell-init boilerplate. */ import { clone, extractErrorMsg, getTimestamp, getUUID, pickRandom_Letters, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { RCLIArgInfo, RCLIParamInfo } from "@ibgib/helper-gib/dist/rcli/rcli-types.mjs"; import { buildArgInfos } from "@ibgib/helper-gib/dist/rcli/rcli-helper.mjs"; import { IbGibAddr, TransformResult } from "@ibgib/ts-gib/dist/types.mjs"; import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { Factory_V1 as factory } from "@ibgib/ts-gib/dist/V1/factory.mjs"; import { Rel8n } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { IbGibAppAny, AppBase_V1 } from "@ibgib/core-gib/dist/witness/app/app-base-v1.mjs"; import { getFromSpace } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs"; import { createNewApp, getAppIb, getInfoFromAppIb } from "@ibgib/core-gib/dist/witness/app/app-helper.mjs"; import { MetaspaceService } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs"; import { IbGibSpaceAny } from "@ibgib/core-gib/dist/witness/space/space-base-v1.mjs"; import { APP_REL8N_NAME } from '@ibgib/core-gib/dist/witness/app/app-constants.mjs'; import { TAG_REL8N_NAME } from "@ibgib/core-gib/dist/common/tag/tag-constants.mjs"; import { TagIbGib_V1 } from "@ibgib/core-gib/dist/common/tag/tag-types.mjs"; import { SPACE_NAME_REGEXP } from "@ibgib/core-gib/dist/witness/space/space-constants.mjs"; import { SpecialIbGibType } from "@ibgib/core-gib/dist/common/other/other-types.mjs"; import { alertUser, promptForSecret, promptForText } from "../helpers.web.mjs"; import { Metaspace_Webspace } from "../witness/space/metaspace/metaspace-webspace/metaspace-webspace.mjs"; import { LiveProxyIbGib } from "../witness/live-proxy-ibgib/live-proxy-ibgib-one-file.mjs"; import { storageGet } from "../storage/storage-helpers.web.mjs"; import { getIbGibGlobalThis_IbGib } from "../helpers.mjs"; import { getContextModeService } from "../context/context-mode-service.mjs"; import { IbGibAmbientContextConfig, IbGibGlobalThis_IbGibApp, IbGibIdentityContext, RequestCommentData_V1, RequestCommentIbGib_V1, } from "./types.mjs"; // --------------------------------------------------------------------------- // Options // --------------------------------------------------------------------------- export interface TagConfig { text: string; icon: string; description: string; } export interface BootstrapIbGibAppOptions { /** Application context configuration (IndexedDB names, etc.) */ config: IbGibAmbientContextConfig; /** Accessor for the application's typed globalThis slot */ getGlobalThis: (config?: IbGibAmbientContextConfig) => IbGibGlobalThis_IbGibApp; /** The main application witness class */ AppClass: new (data?: any, rel8ns?: any) => TApp; /** * Default data used when generating the very first version of this app. * Only applied if `fnGetOrCreateApp` is omitted. */ defaultAppData: any; /** * Optional. Provide an array of parameters the application supports. */ paramInfos?: RCLIParamInfo[]; /** * Optional. Provide initial args for bootstrap. * Defaults to `['--interactive']`. */ getOpeningArgs?: () => Promise; /** * Optional. RCLI argument validation hook. */ validateArgInfos?: (opts: { argInfos: RCLIArgInfo[] }) => string | undefined; /** * Optional. Hook called before initializing the app, but after metaspace is ready. * Can be used to check environment, space state, etc. */ onBeforeCreate?: (metaspace: MetaspaceService, space: IbGibSpaceAny) => Promise; /** * Optional. Hook called immediately after the application witness is loaded or created. * A good place to execute migrations before returning the 'ready' app. */ onAppLoaded?: (app: TApp, isNew: boolean) => Promise; /** * Nuclear escape hatch. Provide this to replace the entire app get-or-create flow. * If provided, `onBeforeCreate`, `onAppLoaded`, and `defaultAppData` are ignored. */ fnGetOrCreateApp?: (metaspace: MetaspaceService, space: IbGibSpaceAny) => Promise; /** * Optional. Hook called to initialize identity context (e.g., keystone based auth). */ resolveIdentity?: (metaspace: MetaspaceService) => Promise; /** * Optional. The point at which the application logic dictates its readiness to UI components. * Typically this executes `getAppShellSvc().onEngineReady()`. */ onReady?: (app: TApp) => void | Promise; /** * Tags verified to exist in the global user space at startup. */ ensureTags?: TagConfig[]; /** * The key prefix used for allocating new spaces (e.g. `test_app_space`). */ localSpaceNamePrefix?: string; /** * Optional hook to register AI agent function tracking and deprecations. */ registerAgentFunctionInfos?: () => void; } // --------------------------------------------------------------------------- // Main Flow // --------------------------------------------------------------------------- export async function bootstrapIbGibApp( opts: BootstrapIbGibAppOptions ): Promise { const lc = `[${bootstrapIbGibApp.name}]`; const logalot = true; // Use a general log flag or derive from config try { if (logalot) { console.log(`${lc} starting...`); } const ibGibGlobalThis = opts.getGlobalThis(opts.config); if (ibGibGlobalThis.bootstrapStarted) { if (ibGibGlobalThis.bootstrapPromise) { console.warn(`${lc} already bootstrapping — awaiting that promise.`); } else { throw new Error(`(UNEXPECTED) bootstrapStarted but bootstrapPromise is falsy?`); } } else { ibGibGlobalThis.bootstrapStarted = true; ibGibGlobalThis.bootstrapPromise = new Promise(async (resolve, reject) => { try { const args = opts.getOpeningArgs ? await opts.getOpeningArgs() : ['--interactive']; const paramInfos = opts.paramInfos ?? [{ name: 'interactive', isFlag: true, argTypeName: 'boolean' }]; const paramInfosErrors = validateParamInfos({ paramInfos }); if (paramInfosErrors.length > 0) { throw new Error(`(UNEXPECTED) invalid PARAM_INFOS: ${paramInfosErrors.join('|')}`); } const argInfos = buildArgInfos({ args, paramInfos, logalot }); if (opts.validateArgInfos) { const validationErrors = opts.validateArgInfos({ argInfos }); if (validationErrors) { throw new Error(`Arg validation errors: ${validationErrors}`); } } await execFromArgs({ opts, args, argInfos, logalot }); resolve(); } catch (error) { reject(error); } }); } await ibGibGlobalThis.bootstrapPromise; // Execute onReady securely outside the promise resolution to prevent deadlock // when getComponentInstance is used by shell service. if (opts.onReady) { // Re-fetch the loaded app wrapper dynamically const getApp = () => ((ibGibGlobalThis as any).appInstance as TApp) || null; await opts.onReady(getApp()); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`\n${lc} complete.`); } } } async function execFromArgs({ opts, args, argInfos, logalot, }: { opts: BootstrapIbGibAppOptions, args: string[], argInfos: RCLIArgInfo[], logalot: boolean }): Promise { const lc = `[${execFromArgs.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const metaspace = await initMetaspace({ opts, logalot }); const ibgibGlobalThis_core = getIbGibGlobalThis_IbGib(); ibgibGlobalThis_core.metaspace = metaspace; const ibgibGlobalThis = opts.getGlobalThis(opts.config); ibgibGlobalThis.metaspace = metaspace; // Register with ContextModeService so Mode 1 never clobbers Mode 2 local storage getContextModeService().registerPersistentMetaspace(metaspace); if (opts.ensureTags) { await ensureTagsExist({ metaspace, tags: opts.ensureTags, logalot }); } if (opts.registerAgentFunctionInfos) { opts.registerAgentFunctionInfos(); } const resRequestCommentIbGib = await createRequestCommentIbGib_Common({ args, interpretedArgInfos: argInfos, }); await metaspace.persistTransformResult({ resTransform: resRequestCommentIbGib }); ibgibGlobalThis.initialCommentIbGib = resRequestCommentIbGib.newIbGib; const proxy = new LiveProxyIbGib(); await proxy.initialized; proxy.setWrappedIbGib({ ibGib: resRequestCommentIbGib.newIbGib }); ibgibGlobalThis.initialCommentIbGibProxy = proxy; if (opts.resolveIdentity) { ibgibGlobalThis.identity = await opts.resolveIdentity(metaspace); } await execInteractive({ opts, resRequestCommentIbGib, metaspace, logalot }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`\n${lc} complete.`); } } } async function initMetaspace({ opts, logalot }: { opts: BootstrapIbGibAppOptions, logalot: boolean }): Promise { const lc = `[${initMetaspace.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } let metaspace = new Metaspace_Webspace(/*cacheSvc*/undefined); const localSpaceNameKey = opts.localSpaceNamePrefix ? `${opts.localSpaceNamePrefix}_local_space_name` : `local_space_name`; const existingLocalSpaceName = await storageGet({ dbName: opts.config.dbName, storeName: opts.config.storeName, key: localSpaceNameKey, }); const prefix = opts.localSpaceNamePrefix ?? 'app_space'; const spaceName = existingLocalSpaceName ? undefined : await generateLocalSpaceName(prefix); await metaspace.initialize({ spaceName, metaspaceFactory: undefined, getFnAlert: () => ({ title, msg }) => alertUser({ title, msg }), getFnPrompt: () => ({ title, msg }) => promptForText({ title, msg, confirm: false }), getFnPromptPassword: () => () => promptForSecret({ confirm: true }), }); return metaspace; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } async function generateLocalSpaceName(prefix: string): Promise { const name = prefix + '_' + pickRandom_Letters({ count: 12 }); if (!SPACE_NAME_REGEXP.test(name)) { throw new Error(`(UNEXPECTED) generated space name (${name}) failed regexp`); } return name; } async function execInteractive({ opts, resRequestCommentIbGib, metaspace, logalot }: { opts: BootstrapIbGibAppOptions, resRequestCommentIbGib: TransformResult, metaspace: MetaspaceService, logalot: boolean }): Promise { const lc = `[${execInteractive.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const localUserSpace = await metaspace.getLocalUserSpace({ lock: false }); if (!localUserSpace) { throw new Error(`(UNEXPECTED) localUserSpace falsy?`); } const app = opts.fnGetOrCreateApp ? await opts.fnGetOrCreateApp(metaspace, localUserSpace) : await defaultGetOrCreateApp({ AppClass: opts.AppClass, defaultAppData: opts.defaultAppData, onBeforeCreate: opts.onBeforeCreate, onAppLoaded: opts.onAppLoaded, metaspace, space: localUserSpace, }); // Track the app so `onReady` can access it cleanly const ibgibGlobalThis = opts.getGlobalThis(opts.config); (ibgibGlobalThis as any).appInstance = app; app.metaspace = metaspace; await app.witness(resRequestCommentIbGib.newIbGib); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } async function defaultGetOrCreateApp({ AppClass, defaultAppData, onBeforeCreate, onAppLoaded, metaspace, space, }: { AppClass: new (data?: any, rel8ns?: any) => TApp, defaultAppData: any, onBeforeCreate?: (metaspace: MetaspaceService, space: IbGibSpaceAny) => Promise, onAppLoaded?: (app: TApp, isNew: boolean) => Promise, metaspace: MetaspaceService, space: IbGibSpaceAny, }): Promise { const lc = `[${defaultGetOrCreateApp.name}]`; const logalot = true; try { if (logalot) { console.log(`${lc} starting...`); } let appsIbGib = await metaspace.getSpecialIbGib({ type: SpecialIbGibType.apps, initialize: false, lock: false, space, }); if (!appsIbGib) { throw new Error(`(UNEXPECTED) no apps index ibgib`); } if (!appsIbGib.rel8ns) { throw new Error(`(UNEXPECTED) appsIbGib.rel8ns falsy`); } const appAddrs = (appsIbGib.rel8ns[APP_REL8N_NAME] ?? [] as IbGibAddr[]) .filter(addr => { const { ib } = getIbAndGib({ ibGibAddr: addr }); return getInfoFromAppIb({ appIb: ib }).appClassname === AppClass.name; }); let app: TApp; let appAddr: IbGibAddr; let isNew = false; if (appAddrs.length === 0) { isNew = true; if (onBeforeCreate) { await onBeforeCreate(metaspace, space); } app = await createAndSaveNewApp({ AppClass, defaultAppData, metaspace, space }); appAddr = getIbGibAddr({ ibGib: app }); } else if (appAddrs.length === 1) { appAddr = appAddrs[0]; } else { const list = appAddrs .map(a => getIbAndGib({ ibGibAddr: a }).ib) .map((ib, idx) => `\n${idx}) ${ib}`); let idx: number | undefined; let retry = false; do { const response = await promptForText({ title: 'Which App?', msg: `${retry ? 'huh? ' : ''}Multiple apps found. Which one?${list}`, }); const parsed = Number.parseInt(response); retry = Number.isNaN(parsed) || parsed >= appAddrs.length; if (!retry) { idx = parsed; } } while (retry); appAddr = appAddrs[idx!]; } const resGet = await getFromSpace({ addr: appAddr, space }); if (resGet.success && resGet.ibGibs?.length === 1) { app = resGet.ibGibs[0] as TApp; } else { throw new Error(`Couldn't get app (${appAddr}).`); } if (!app.witness) { const witness = new AppClass(undefined, undefined); await witness.loadIbGibDto(app); app = witness; } if (onAppLoaded) { app = await onAppLoaded(app, isNew); } return app; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } async function createAndSaveNewApp({ AppClass, defaultAppData, metaspace, space, }: { AppClass: new (data?: any, rel8ns?: any) => TApp, defaultAppData: any, metaspace: MetaspaceService, space: IbGibSpaceAny }): Promise { const lc = `[${createAndSaveNewApp.name}]`; const logalot = true; try { if (logalot) { console.log(`${lc} starting...`); } const fnPromptApp = (() => { return async () => { const name = `${AppClass.name}_${pickRandom_Letters({ count: 8 })}`; const data = clone(defaultAppData); data.name = name; data.uuid = await getUUID(); const rel8ns = undefined; // App templates usually have DEFAULT_APP_REL8NS as undefined const { classname } = data; const ib = getAppIb({ appData: data, classname }); return await factory.firstGen({ ib, parentIbGib: factory.primitive({ ib: `app ${classname}` }), data, rel8ns, dna: true, linkedRel8ns: [Rel8n.ancestor, Rel8n.past], nCounter: true, tjp: { timestamp: true }, }) as TransformResult; }; })(); const app = await createNewApp({ fnPromptApp, space, ibgibs: metaspace, }); if (!app) { throw new Error(`Failed to create app (falsy).`); } if (app?.data?.classname !== AppClass.name) { throw new Error(`(UNEXPECTED) wrong app classname.`); } return app as TApp; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } async function ensureTagsExist({ metaspace, tags, logalot }: { metaspace: MetaspaceService, tags: TagConfig[], logalot: boolean }): Promise { const lc = `[${ensureTagsExist.name}]`; try { if (logalot) { console.log(`${lc} starting...`); } const tagIbGibs = await metaspace.getSpecialRel8dIbGibs({ type: "tags", rel8nName: TAG_REL8N_NAME, }) as TagIbGib_V1[]; for (const tag of tags) { if (!tagIbGibs.some(x => x.data?.text === tag.text)) { await metaspace.createTagIbGib({ text: tag.text, icon: tag.icon, description: tag.description, space: undefined, }); } } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } export async function createRequestCommentIbGib_Common({ args, interpretedArgInfos, }: { args: string[], interpretedArgInfos: RCLIArgInfo[], }): Promise> { const text = args.join(' '); const data: RequestCommentData_V1 = { uuid: await getUUID(), text, args, interpretedArgInfos, textTimestamp: getTimestamp(), }; return await factory.firstGen({ parentIbGib: factory.primitive({ ib: 'comment' }), ib: `comment ${text.substring(0, 32).replace(/ /g, '_')}`, data, dna: true, tjp: { uuid: true, timestamp: true }, nCounter: true, }) as TransformResult; } export function validateParamInfos({ paramInfos }: { paramInfos: RCLIParamInfo[] }): string[] { const errors: string[] = []; const identifiers: string[] = []; paramInfos.forEach(p => { const names = [p.name, ...(p.synonyms ?? [])]; names.forEach(name => { if (identifiers.includes(name.toLowerCase())) { errors.push(`Duplicate identifier found: ${name}`); } identifiers.push(name.toLowerCase()); }); }); return errors; }