refactor(gui): rebuild the client loading kernel as dsh-client-modules with a two-phase boot
The module system moves out of dsh-client-runtime (./loader retired) into its own package: a lazy CJS table where executing a bundle only registers its factory and materialization happens at first require, memoized, with recursive requires self-ordering. ClientModuleSystem is a class; index.ts keeps the types and a thin factory. Boot is two-phase: phase one prefetches the immediately tier in parallel (registration only, failures deferred to phase two's loud import); phase two mounts the vendored Loader with the module system as internal, creates one entry per graph row plus the app-shell pseudo-row the kernel appends itself, and settles on an all-ACTIVE sweep. The shell kernel is self-sufficient: hand-rolled loader-status stores, no plugin value imports, platform seed list single- sourced in platform.ts.
This commit is contained in:
@@ -1,39 +1,46 @@
|
||||
/**
|
||||
* Shell root: boot loading page → (loader settled) → real UI in one switch.
|
||||
* Pure shell component with zero plugin dependencies — before settled it may
|
||||
* only rely on itself; the real UI is produced by the boot assembly closure
|
||||
* (renderApp) once every plugin is active. A failed plugin keeps the loading
|
||||
* page and lists the failures (fail loud, no partial UI).
|
||||
* Shell root: boot loading page → (boot settled) → real UI in one switch.
|
||||
* Pure kernel component with zero plugin dependencies — before settled it may
|
||||
* only rely on itself (the fail-loud presentation must not depend on the
|
||||
* system whose failure it reports; the status/signal stores are kernel-own,
|
||||
* web2 shell self-sufficiency rule); the real UI is produced by the
|
||||
* app-shell entry once every entry is active. A failed boot keeps the
|
||||
* loading page, lists the per-entry fiber states and the sweep report (fail
|
||||
* loud, no partial UI).
|
||||
*/
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { LoaderStatus } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { KernelSignal, LoaderStatus } from './loader-status.ts'
|
||||
import css from './AppRoot.module.css'
|
||||
|
||||
/** AppRoot props: settled signal, loader status feed, deferred real-UI factory. */
|
||||
/** AppRoot props: settled signal, fiber-state projection feed, boot failure report, deferred real-UI factory. */
|
||||
export interface AppRootProps {
|
||||
/** True once loader.settled() resolved (the boot closure flips it; status-derived guesses race an incrementally filled table). */
|
||||
settled: ObservableSnapshot<boolean>
|
||||
/** Loader per-plugin status store (drives loading/failed rendering). */
|
||||
status: SnapshotStore<LoaderStatus>
|
||||
/** True once the boot chain settled (loader quiesced + all entries ACTIVE); the boot closure flips it. */
|
||||
settled: KernelSignal<boolean>
|
||||
/** Per-entry fiber-state projection store (drives loading/failed rendering). */
|
||||
status: KernelSignal<LoaderStatus>
|
||||
/** Boot failure report (the settle rejection message); undefined while loading or after success. */
|
||||
error: KernelSignal<string | undefined>
|
||||
/** Builds the real UI; called only after settled. */
|
||||
renderApp: () => ReactNode
|
||||
}
|
||||
|
||||
/** Boot gate: loading page until the loader settles; failures stay here. */
|
||||
/** Boot gate: loading page until the boot settles; failures stay here. */
|
||||
export function AppRoot(props: AppRootProps) {
|
||||
const settled = useSyncExternalStore(props.settled.subscribe, props.settled.getSnapshot)
|
||||
const status = useSyncExternalStore(props.status.subscribe, props.status.getSnapshot)
|
||||
const error = useSyncExternalStore(props.error.subscribe, props.error.getSnapshot)
|
||||
const failed = Object.entries(status).filter(([, s]) => s === 'failed')
|
||||
|
||||
if (settled) return <>{props.renderApp()}</>
|
||||
|
||||
const loud = error !== undefined || failed.length > 0
|
||||
|
||||
return (
|
||||
<div className={css.boot}>
|
||||
<div className={css.card}>
|
||||
<div className={css.wordmark}>HARNESS</div>
|
||||
{failed.length === 0
|
||||
{!loud
|
||||
? (
|
||||
<>
|
||||
<div className={css.spinner} />
|
||||
@@ -44,6 +51,7 @@ export function AppRoot(props: AppRootProps) {
|
||||
<div className={css.failed}>
|
||||
<div className={css.failedTitle}>Failed to load plugins</div>
|
||||
{failed.map(([id]) => <div key={id} className={css.failedItem}>{id}</div>)}
|
||||
{error !== undefined && <div className={css.failedItem}>{error}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user