/** * Web boot kernel. It owns only the module system, Cordis loader, and a * framework-free boot page. The dynamic render service receives the mount * point after every client entry activates. * @module @deepseek-ai/dsh-client-web/src/boot */ import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import * as ModulesClient from '@deepseek-ai/dsh-client-modules/client' import { ClientModuleSystem, parseBootManifest, type BootManifest, type ClientModuleSystemOptions, type DshWindow, } from '@deepseek-ai/dsh-client-modules/client' import type {} from '@deepseek-ai/dsh-client-render-service/client' import { BootPage } from './boot-page.ts' import { getStaticModules } from './seed.ts' import { STATE_LABELS } from './loader-status.ts' import './base.css' /** Module transport hook replaced by jsdom tests. */ export type BootSeams = Pick /** Statically adopted bootstrap package that constructs the client module system. */ const MODULES_ID = '@deepseek-ai/dsh-client-modules' /** Browser boot entry consumed by `apps/web`. */ export class AppWebEntry { private readonly container: HTMLElement private readonly seams: BootSeams | undefined private readonly page: BootPage private ctx: Context | undefined private modules!: ClientModuleSystem private manifest!: BootManifest /** * Draw the boot page; {@link run} starts the loader. * @param container - Application mount point. * @param seams - Optional module transport replacement. */ constructor(container: HTMLElement, seams?: BootSeams) { this.container = container this.seams = seams this.page = new BootPage(container) } /** * Load and activate every client entry, then hand the mount point to the * render service. Plugin failures remain visible on the boot page. * @returns Resolves after application mount or failure rendering. */ async run(): Promise { this.manifest = parseBootManifest((globalThis as DshWindow).__DSH_BOOT__) this.modules = new ClientModuleSystem({ modules: this.manifest.modules, staticModules: getStaticModules(), ...this.seams, }) this.modules.registerStatic(MODULES_ID, ModulesClient) ;(globalThis as DshWindow).__DSH_MODULES__ = this.modules const prefetching = this.prefetchImmediateTier() const ctx = new Context() this.ctx = ctx try { await this.runPluginBoot(ctx, prefetching) await this.mountApp(ctx) } catch (reason) { console.error(reason) this.page.fail(reason instanceof Error ? reason.message : String(reason)) } } /** Dispose the client plugin tree and whichever page owns the mount point. */ async dispose(): Promise { const ctx = this.ctx this.ctx = undefined if (ctx !== undefined) await ctx.fiber.dispose() this.page.dispose() } /** Mount through a dependency fiber so replacing appShell remounts the application. */ private async mountApp(ctx: Context): Promise { const mounted = ctx.inject(['appShell'], (scope) => { const shell = scope.get('appShell') if (shell === undefined) throw new Error('web boot: appShell service missing after settled') scope.effect(() => shell.mount(this.container), 'web boot: application mount') this.page.dispose() }) await mounted } /** Prefetch stage-one bundles; their import path owns any eventual failure. */ private async prefetchImmediateTier(): Promise { await Promise.all(this.manifest.plugins .filter(row => row.immediately) .map(row => this.modules.prefetch(row.id).catch((_prefetchError: unknown) => { // Prefetch only starts transport early; the Loader import retries and reports this bundle failure. }))) } /** Mount the Loader, create all graph entries, await quiescence, and audit activation. */ private async runPluginBoot(ctx: Context, prefetching: Promise): Promise { await ctx.plugin(Loader) const loader = ctx.loader loader.internal = this.modules as never ctx.on('internal/status', (fiber) => { const entry = fiber.entry if (entry === undefined || entry.fiber === undefined) return this.page.setState(entry.options.name, STATE_LABELS[entry.fiber.state]) }) await prefetching const rows = [MODULES_ID, ...this.manifest.plugins.map(row => row.id).filter(id => id !== MODULES_ID)] await Promise.all(rows.map(async (name) => { this.page.setState(name, 'loading') const id = await loader.create({ name }) if (loader.resolve(id).fiber === undefined) this.page.setState(name, 'failed') })) await loader.await() this.assertEntriesActive(ctx) } /** Reject entries that failed import/apply or still wait on missing services. */ private assertEntriesActive(ctx: Context): void { const failures: string[] = [] for (const entry of ctx.loader.entries()) { const name = entry.options.name if (entry.fiber === undefined) { failures.push(`${name}: import failed (see console for the import error)`) continue } const state = STATE_LABELS[entry.fiber.state] if (state === 'active') continue if (state === 'pending') { const missing = Object.keys(entry.fiber.inject).filter(service => ctx.get(service) === undefined) failures.push(`${name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`) } else { failures.push(`${name}: ${state}`) } } if (failures.length > 0) { throw new Error(`web boot: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`) } } }