structured index injection table (webserver/index-inject)
结构化 index 注入表(webserver/index-inject 事件)
The web shell's boot HTML needs three kinds of injection: client-modules' boot protocol (the `__ModuleLoader__` registration queue inline script, the parser-blocking preload `<script src>` tags, the `__DSH_BOOT__` graph global) and ui-theme's first-paint theme script. The old mechanism was `webServer.tapIndex(html => html)` string transforms: each registrant regex-located `<head>`/`<body>` and spliced HTML on its own
English
Problem
The web shell's boot HTML needs three kinds of injection: client-modules' boot protocol (the __ModuleLoader__ registration queue inline script, the parser-blocking preload <script src> tags, the __DSH_BOOT__ graph global) and ui-theme's first-paint theme script. The old mechanism was webServer.tapIndex(html => html) string transforms: each registrant regex-located <head>/<body> and spliced HTML on its own. The static worker deployment (the page is a build artifact; the host tree runs in a Web Worker) has no serve-HTML step at all, so the worker side hand-copied the same data into its /__boot__ payload (graph + theme via ctx.get), and the page side re-implemented what the taps did (a facade installer, a theme applier, a preload loop) — one boot semantics, three implementations.
Decision
Make the injection surface an event over pure data: the webserver declares the webserver/index-inject event and the IndexInjection row union (global/script/script-src/style/html, head|body placement). A plugin that wants to inject subscribes and pushes rows; every collection (collectIndexInjections()) is a fresh emit, so subscribers read live state at emit time (module graph, theme preference — no re-registration staleness), and a subscription dies with its fiber.
One table, two renderers: the served form's webServer.renderIndex(html) renders rows into index.html deterministically (head rows after the opening head tag, body rows after the opening body tag; < JSON-escaped in global values, attribute-escaped src); the worker form's /__boot__ payload is { injections }, executed row by row by a small page-side interpreter (set global / create script element / load external through the tunnel's loadBundle / mount style and markup). Rows are pure JSON data — that is the both-ends-equivalent discipline.
tapIndex/applyIndexTaps survive as the raw-HTML escape hatch, applied after row rendering; every internal consumer moved to the event.
Consequences
- client-modules and ui-theme no longer regex-edit HTML; the worker's
readBootPayloadservice-poking (clientModules,settings, theme constants throughloader.load) is deleted; the page-sideinstallModuleLoaderFacade,applyBootTheme, andPARSER_PRELOAD_IDSre-implementations retire. - Ordering: across subscribers, subscription order (same as the old tap order); within one subscriber, push order — modules itself guarantees queue → preloads → global.
- The served rendering of the manifest global changed from
window.__DSH_BOOT__ =toglobalThis["__DSH_BOOT__"] =; no committed snapshot expectation carries that text, so none needed re-recording. - New model-visible or page-visible boot inputs extend the row union; no new tap consumers.
Alternatives considered
- Keep tap functions, add a worker-side renderer that re-runs them over a fake document — rejected: taps are opaque
html => htmlclosures, so the worker cannot serialize or replay them without shipping a DOM emulation into the boot path. - A registration-style table (
registerInjection(row): dispose) — rejected for the two problems the event dissolves: rows staled against live state (theme preference, module graph) unless every producer re-registered on change, and every producer owned one more disposer. The per-emit pull reads fresh state with fiber-scoped cleanup for free. - Deleting
tapIndexoutright — rejected: an escape hatch for raw HTML transforms costs nothing while the table is young, and external compositions may have transforms no row kind expresses yet.
中文
Problem
Web 壳的启动 HTML 需要三类注入:client-modules 的引导协议(__ModuleLoader__ 注册队列内联脚本、parser 阻塞的 preload <script src>、__DSH_BOOT__ 全局图)与 ui-theme 的首帧主题脚本。旧机制是 webServer.tapIndex(html => html) 字符串变换:每个注册方各自用正则找 <head>/<body> 改 HTML。静态 worker 部署(页面是构建产物、host 树在 Web Worker 里)没有「服 HTML」这一步,于是 worker 侧只能在 /__boot__ 载荷里手工重抄同一批数据(graph + theme,经 ctx.get 硬掏),页面侧再用手写代码(facade 安装、theme 应用、preload 循环)把 tap 干的事重演一遍——同一份启动语义存在三份实现。
Decision
注入面事件化、数据化:webserver 声明 webserver/index-inject 事件与纯数据行类型 IndexInjection(global/script/script-src/style/html,head|body 定位)。想注入的插件订阅事件、往表里 push 行;每次收集(collectIndexInjections())都是一次全新 emit,订阅方现读现填(模块图、主题偏好天然新鲜,无重注册问题),订阅随 fiber 销毁自动摘除。
一张表两个渲染器:served 形态 webServer.renderIndex(html) 确定性把行渲染进 index.html(head 行插 head 首、body 行插 body 首,全局值 JSON < 转义、src 属性转义);worker 形态 /__boot__ 载荷就是 { injections },页面侧小解释器逐行执行(设全局 / 建脚本元素 / 经 tunnel loadBundle 载外链 / 挂样式与 DOM)。行是纯 JSON 数据,这是双端等价的纪律。
tapIndex/applyIndexTaps 保留为原始 HTML 变换的逃生口,在行渲染之后执行;内部消费者全部迁走。
Consequences
- client-modules 与 ui-theme 不再各自正则改 HTML;worker 侧
readBootPayload的ctx.get手掏(clientModules、settings、theme 常量 loader.load)删除;页面侧installModuleLoaderFacade、applyBootTheme、PARSER_PRELOAD_IDS三份重抄退役。 - 顺序语义:跨订阅方按订阅注册顺序(与旧 tap 顺序一致),单订阅方内按 push 顺序;modules 自己保证 队列→preload→全局 三行有序。
__DSH_BOOT__的 served 渲染文本从window.__DSH_BOOT__ =变为globalThis["__DSH_BOOT__"] =;已核实无已提交快照期望含此文本,无需重录。- 新的模型可见/页面可见注入一律走行类型扩展,不再新增 tap 消费者。
Alternatives considered
- 保留 tap 函数、worker 侧对假 document 重放——否决:tap 是不透明的
html => html闭包,worker 无法序列化或重放,除非把 DOM 仿真塞进启动链。 - 注册表式(
registerInjection(row): dispose)——否决于事件天然化解的两个问题:行数据会相对活状态(主题偏好、模块图)过期,除非每个生产者变更时重注册;且每个生产者多背一个 disposer。按次 emit 的拉取免费获得新鲜读取与 fiber 级清理。 - 直接删除
tapIndex——否决:表还年轻,原始 HTML 变换的逃生口零成本,外部组合可能还有行类型暂不能表达的变换。