feat(web): unify served and preview startup behind a boot-ready seam

The webserver renders a boot-readiness tail after the injection rows and
AppWebEntry.run awaits the __DSH_BOOT_READY__ deferred before reading any
injected state. Whichever bootstrap applies the injection table settles
the deferred - the served renderer resolves it inline, an asynchronous
bootstrap installs it ahead of the entry module and settles it with the
handshake - so both deployments run one startup chain and a failed
handshake surfaces on the boot page instead of proceeding on missing
globals.
This commit is contained in:
imccyu
2026-08-21 20:35:32 +08:00
parent 4779ec9af9
commit fd3112a23f
3 changed files with 23 additions and 3 deletions
+7
View File
@@ -45,6 +45,13 @@ export class AppWebEntry {
*/
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) {
+12 -1
View File
@@ -71,10 +71,20 @@ function splice(html: string, at: number, markup: string): string {
return `${html.slice(0, at)}${markup}${html.slice(at)}`
}
/**
* Tail script settling the boot-readiness deferred (`__DSH_BOOT_READY__`):
* the client entry awaits its `.promise` before reading any injected state.
* Whichever side runs first creates the deferred (`??=`), so a bootstrap that
* applies the table asynchronously installs it ahead of the entry module and
* settles it after the last row; the served form below creates and resolves
* it in one statement, because every row is already in the document text.
*/
const READY_MARKUP = '<script>(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()</script>'
/**
* Render rows into an index.html body: head rows immediately after the
* opening head tag, body rows immediately after the opening body tag, each
* group in table order.
* group in table order, and the boot-readiness tail after the last body row.
* @param html - the raw index.html body.
* @param rows - the collected injection table.
* @returns the html with every row rendered.
@@ -87,6 +97,7 @@ export function renderIndexInjections(html: string, rows: readonly IndexInjectio
if (rendered.placement === 'head') head += rendered.markup
else body += rendered.markup
}
body += READY_MARKUP
let out = html
if (head !== '') {
const open = /<head(?:\s[^>]*)?>/i.exec(out)
@@ -241,11 +241,13 @@ describe('real Loader composition', () => {
expect(server.renderIndex('<head></head><body></body>')).toContain('window.__Q__=2')
untap()
// Tag-less fragments: head rows prepend, body rows append.
// Tag-less fragments: head rows prepend, body rows append, and the
// boot-readiness tail lands after the last body row.
expect(renderIndexInjections('<main>x</main>', [
{ kind: 'script', placement: 'head', text: 'H' },
{ kind: 'script', placement: 'body', text: 'B' },
])).toBe('<script>H</script><main>x</main><script>B</script>')
])).toBe('<script>H</script><main>x</main><script>B</script>'
+ '<script>(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()</script>')
})
it('fails the fiber when the port is already taken (fail-loud at activation)', { timeout: 60_000 }, async () => {