client-modules: a batch that already executed is never requested again, even when its first importer was a row it did register — every executed batch URL is remembered and a still-missing row goes straight to its one-resource URL. The in-process module system test in test-support is updated to the aggregated arrival message. describeError replaces three inline non-Error ternaries and is covered. subprocess-local: the spill path is published only after the exclusive open succeeds, so a failed open (EEXIST on a planted entry included) can no longer unlink a path this process did not create; a reporter that throws is contained and written to stderr; logSpillFailure builds the owner reporter for both the runtime and the SSH helper so the logging path is exercised once instead of duplicated. Tests cover the runtime logger, the bare-caller stderr default, EEXIST, and a throwing reporter. desktop: a fatal failure during shutdown still writes its report; the report phase follows the backend state so a failure during a restart is 'startup' again; writeReport is a required recovery operation and the dialog tests await the write; the shell pid is labelled; the withheld response-header list states why set-cookie is dropped. app-boot, READMEs, Agent Note: "never resumes" is restated as control never returning to the failed operation while the loop runs until release settles; inspect output is bounded; incident inferences (who removed the spill directory, which batch failure occurred) are written as inferences.
90 lines
4.0 KiB
TypeScript
90 lines
4.0 KiB
TypeScript
/**
|
|
* Production client composition without the page: mount the Loader over a
|
|
* module system, create every manifest row, wait for quiescence, and audit
|
|
* activation. `AppWebEntry` and the whole-client test carrier both call it.
|
|
* @module @deepseek-ai/dsh-client-web/src/boot-client
|
|
*/
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
import type { BootManifest, ClientModuleLoader } from '@deepseek-ai/dsh-client-modules/client'
|
|
import { STATE_LABELS } from './loader-status.ts'
|
|
|
|
/** Entry state label as the boot page renders it. */
|
|
export type EntryStateLabel = (typeof STATE_LABELS)[keyof typeof STATE_LABELS] | 'loading' | 'failed'
|
|
|
|
/** Inputs of {@link bootClient}. */
|
|
export interface ClientBootOptions {
|
|
/** Fresh root Context that will own the plugin tree. */
|
|
readonly ctx: Context
|
|
/** Module system installed as `loader.internal`. */
|
|
readonly modules: ClientModuleLoader
|
|
/** Parsed manifest whose `plugins` rows become Loader entries (entry name = row id). */
|
|
readonly manifest: BootManifest
|
|
/** Per-entry state reporting (the boot page); omitted when no one renders progress. */
|
|
readonly onEntryState?: (name: string, state: EntryStateLabel) => void
|
|
}
|
|
|
|
/**
|
|
* Compose the client: `ctx.plugin(Loader)`, `loader.internal = modules`, one
|
|
* `loader.create({ name })` per manifest row, `loader.await()`, then
|
|
* {@link assertEntriesActive}. A row whose module cannot be imported is marked
|
|
* failed; the Loader logs its import error, the module system records it, and
|
|
* the audit rejects startup with that error text per entry.
|
|
* @param options - context, module system, manifest, optional progress sink.
|
|
* @returns resolves after every entry is active; rejects with the audit report otherwise.
|
|
*/
|
|
export async function bootClient(options: ClientBootOptions): Promise<void> {
|
|
const { ctx, manifest, onEntryState } = options
|
|
await ctx.plugin(Loader)
|
|
const loader = ctx.loader
|
|
loader.internal = options.modules as never
|
|
|
|
ctx.on('internal/status', (fiber) => {
|
|
const entry = fiber.entry
|
|
if (entry === undefined || entry.fiber === undefined) return
|
|
onEntryState?.(entry.options.name, STATE_LABELS[entry.fiber.state])
|
|
})
|
|
|
|
const rows = manifest.plugins.map(row => row.id)
|
|
for (const name of rows) onEntryState?.(name, 'loading')
|
|
await options.modules.entries.start(loader, manifest)
|
|
for (const entry of loader.entries()) {
|
|
if (entry.fiber === undefined) onEntryState?.(entry.options.name, 'failed')
|
|
}
|
|
|
|
await loader.await()
|
|
assertEntriesActive(ctx, options.modules)
|
|
}
|
|
|
|
/**
|
|
* Reject entries that failed import/apply or still wait on missing services.
|
|
* @param ctx - root Context carrying the Loader.
|
|
* @param modules - the module system whose recorded import failures name why an entry
|
|
* has no fiber; a row with no record points at the console.
|
|
* @throws {Error} listing every non-active entry with its reason.
|
|
*/
|
|
export function assertEntriesActive(ctx: Context, modules: Pick<ClientModuleLoader, 'importError'>): void {
|
|
const failures: string[] = []
|
|
for (const entry of ctx.loader.entries()) {
|
|
const name = entry.options.name
|
|
if (entry.fiber === undefined) {
|
|
const importError = modules.importError(name)
|
|
failures.push(importError === undefined
|
|
? `${name}: import failed (see console for the import error)`
|
|
: `${name}: import failed: ${importError.message}`)
|
|
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')}`)
|
|
}
|
|
}
|