mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
160 lines
6.5 KiB
TypeScript
160 lines
6.5 KiB
TypeScript
/**
|
|
* Web boot kernel. It owns only the module system, Cordis loader, and a
|
|
* framework-free boot page. The dynamic UI renderer 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 type {
|
|
BootManifest, ClientModuleCreateOptions, ClientModuleSystem, DshWindow,
|
|
} from '@deepseek-ai/dsh-client-modules/client'
|
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/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<ClientModuleCreateOptions, 'loadBundle'>
|
|
|
|
/** 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
|
|
* UI renderer. Plugin failures remain visible on the boot page.
|
|
* @returns Resolves after application mount or failure rendering.
|
|
*/
|
|
async run(): Promise<void> {
|
|
try {
|
|
// Boot-readiness gate: whichever bootstrap applies the injection table
|
|
// settles this deferred once every row has taken effect — the served
|
|
// index resolves it in the rendered tail, so the await returns on the
|
|
// next microtask; an asynchronous bootstrap resolves it after its last
|
|
// row, or rejects it into the failure rendering below. An absent global
|
|
// means no bootstrap owns the document and there is nothing to wait for.
|
|
await (globalThis as { __DSH_BOOT_READY__?: { promise: Promise<void> } }).__DSH_BOOT_READY__?.promise
|
|
const win = globalThis as DshWindow
|
|
const moduleLoader = win.__ModuleLoader__
|
|
if (moduleLoader === undefined) {
|
|
throw new Error('web boot: window.__ModuleLoader__ bootstrap facade is missing')
|
|
}
|
|
// A pre-injected transport (the worker preview page) owns bundle bytes;
|
|
// its loadBundle is the default and explicit seams still win. The global
|
|
// is `ClientTransportHooks`, owned by @deepseek-ai/dsh-client-connection;
|
|
// this structural slice reads one optional member without adding a
|
|
// package edge.
|
|
const transport = (globalThis as {
|
|
__DSH_TRANSPORT__?: { loadBundle?: ClientModuleCreateOptions['loadBundle'] }
|
|
}).__DSH_TRANSPORT__
|
|
this.modules = moduleLoader.create({
|
|
boot: win.__DSH_BOOT__,
|
|
staticModules: getStaticModules(),
|
|
...transport?.loadBundle === undefined ? {} : { loadBundle: transport.loadBundle },
|
|
...this.seams,
|
|
})
|
|
this.manifest = this.modules.manifest
|
|
|
|
const prefetching = this.prefetchImmediateTier()
|
|
const ctx = new Context()
|
|
this.ctx = ctx
|
|
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<void> {
|
|
const ctx = this.ctx
|
|
this.ctx = undefined
|
|
if (ctx !== undefined) await ctx.fiber.dispose()
|
|
this.page.dispose()
|
|
}
|
|
|
|
/** Mount through a dependency fiber so replacing uiRenderer remounts the application. */
|
|
private async mountApp(ctx: Context): Promise<void> {
|
|
const mounted = ctx.inject(['uiRenderer'], (scope) => {
|
|
scope.effect(() => scope.uiRenderer.mount(this.container), 'web boot: application mount')
|
|
})
|
|
await mounted
|
|
}
|
|
|
|
/** Prefetch stage-one bundles and their dynamic requests before concurrent plugin imports. */
|
|
private async prefetchImmediateTier(): Promise<void> {
|
|
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<void>): Promise<void> {
|
|
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])
|
|
})
|
|
|
|
const rows = this.manifest.plugins.map(row => row.id)
|
|
this.page.setTotal(rows.length)
|
|
await prefetching
|
|
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')}`)
|
|
}
|
|
}
|
|
}
|