1
0
Эх сурвалжийг харах

docs(web): clarify client plugin reload failures

Yichen Jiang 2 долоо хоног өмнө
parent
commit
1fec9adb61

+ 2 - 2
packages/client/hmr/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/hmr/README.md
-README.md: e0e00cca9f3856ab68a22b1de9b93fe8abe249ee
-README.zh.md: 31bb5bfd103bb342d5d7af710afa7350f40c7c80
+README.md: e7d8244ac1151488570ddba3aaaa4633d38a2992
+README.zh.md: 11ad50b100fff6cf107f724286387328ff0b24c2

+ 3 - 3
packages/client/hmr/README.md

@@ -33,7 +33,7 @@ Run `pnpm run dev:web` (or any tsdown watch process that writes the plugin's `li
 
 ### What a reload does
 
-Each reload re-executes the plugin bundle and remounts the plugin with fresh state. Plugins that depend on the reloaded one reload with it automatically. A reload that fails is reported visibly and retried from scratch on the next rebuild.
+Each successful reload re-executes the plugin bundle and remounts the plugin with fresh state. Plugins that depend on the reloaded one reload with it automatically. Failures appear in the plugin list, where they can be retried without waiting for another rebuild.
 
 ### Configuration
 
@@ -71,7 +71,7 @@ A fiber's activation epoch strings its service providers' uids, so replacing a p
 
 ### Failure policy
 
-No rollback: failed imports and activation remain visible as page-local synchronization errors. Settings → Plugins → Plugin list retries the latest graph, even when its revision is unchanged; successful unrelated plugins remain active.
+Download failures leave the running plugin active. After the old fiber is torn down, import or activation failure does not restore the previous bundle. Failures appear as page-local synchronization errors. Settings → Plugins → Plugin list retries the latest graph, even when its revision is unchanged; a later rebuild also retries the affected plugin. Successful unrelated plugins remain active.
 
 ### Source map
 
@@ -114,7 +114,7 @@ None; this package neither assembles nor sends a provider request.
 These limits define what the reload driver does not preserve or restore. They are current package constraints, not a task backlog.
 
 - **Reload is coarse by design** — a fresh fiber and fresh components; React state inside the reloaded plugin is lost while the data layer (connection/runtime fibers, Session objects) is untouched. react-refresh-grade state preservation conflicts with re-executing the bundle and is deliberately out.
-- **No failure rollback** — a reload that fails leaves the entry FAILED and visible in the loader status projection; the previous bundle is not restored automatically.
+- **No failure rollback** — after the old fiber is torn down, a failed replacement does not restore the previous bundle.
 - **Web transport only** — Electron installation and backend restart handling do not use this SSE path. Entry reconciliation itself is transport-independent.
 
 <a id="dev-note"></a>

+ 3 - 3
packages/client/hmr/README.zh.md

@@ -33,7 +33,7 @@ kind: "package-reference"
 
 ### 一次重载做什么
 
-每次重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败的重载会以可见方式报告,并在下一次重建时从头重试。
+每次成功的重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败会显示在插件列表中,可直接重试,无需等待下一次重建。
 
 ### 配置
 
@@ -71,7 +71,7 @@ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方
 
 ### 失败策略
 
-不回滚:导入与激活失败会显示为当前页面的同步错误。「设置 → 插件 → 插件列表」会针对最新图重试,即使其 revision 未变化;无关且已成功运行的插件保持活动。
+下载失败时,正在运行的插件保持活动。旧 fiber 被卸载后,导入或激活失败不会恢复先前的 bundle。失败会显示为当前页面的同步错误。「设置 → 插件 → 插件列表」会针对最新图重试,即使其 revision 未变化;后续重建也会重试受影响的插件。无关且已成功运行的插件保持活动。
 
 ### 源码地图
 
@@ -114,7 +114,7 @@ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方
 这些限制说明重载驱动器不会保留或恢复什么。它们是当前包约束,不是任务积压。
 
 - **重载有意保持粗粒度**——全新 fiber 与全新组件;被重载插件内的 React 状态会丢失,而数据层(连接 fiber、运行时 fiber、Session 对象)不受影响。react-refresh 级状态保留与重新执行 bundle 冲突,因此有意排除。
-- **失败时不回滚**——失败的重载会让该 entry 保持 FAILED 并在 loader 状态投影中可见;系统不会自动恢复先前 bundle。
+- **失败时不回滚**——旧 fiber 被卸载后,替换失败不会恢复先前的 bundle。
 - **仅负责 Web 传输**——Electron 的安装和后端重启流程不使用此 SSE 路径。条目对账本身不依赖传输。
 
 <a id="dev-note"></a>

+ 2 - 2
packages/client/hmr/tests/transport.client.spec.ts

@@ -25,7 +25,7 @@ it('forwards full graphs and rebuilt frames, contains wire errors and closes its
     const graph = { rev: 'r', entries: [], batches: [] }
     receive({ data: JSON.stringify({ type: 'graph', graph }) })
     receive({ data: JSON.stringify({ type: 'rebuilt', id: 'a', rev: 'r1' }) })
-    await vi.waitFor(() =>{  expect(sync).toHaveBeenCalledWith(graph) })
+    await vi.waitFor(() => { expect(sync).toHaveBeenCalledWith(graph) })
     expect(reload).toHaveBeenCalledWith('a', 'r1')
     receive({ data: '{' })
     receive({ data: JSON.stringify({ type: 'graph', graph: null }) })
@@ -33,7 +33,7 @@ it('forwards full graphs and rebuilt frames, contains wire errors and closes its
     expect(warnings).toHaveBeenCalledTimes(2)
     sync.mockRejectedValueOnce(new Error('invalid graph'))
     receive({ data: JSON.stringify({ type: 'graph', graph: {} }) })
-    await vi.waitFor(() =>{  expect(errors).toHaveBeenCalledWith(expect.objectContaining({ message: 'invalid graph' })) })
+    await vi.waitFor(() => { expect(errors).toHaveBeenCalledWith(expect.objectContaining({ message: 'invalid graph' })) })
   } finally {
     await fiber.dispose()
     await ctx.fiber.dispose()

+ 2 - 2
packages/client/modules/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/modules/README.md
-README.md: 31dcffaefaffff4c40880873d95f0ce2fcb59db6
-README.zh.md: c7748a00e7a9ac5d80265d34158f3fb412771df4
+README.md: 8470935beabf8007850e26c8274c792ddf2adc1a
+README.zh.md: 06b1b0d7bc19465d34ed0d814162fcd7ed1dc2dc

+ 2 - 0
packages/client/modules/README.md

@@ -90,6 +90,8 @@ The host contributes structured index rows that inject, into `<head>`: the `wind
 | [`src/index.ts`](src/index.ts) | Node half: `ClientModuleRegistry`, scan, artifact snapshots, optional combo route, structured index rows |
 | [`src/client/index.ts`](src/client/index.ts) | Browser half: bootstrap export, `ctx.modules` enrollment |
 | [`src/client/system.ts`](src/client/system.ts) | `ClientModuleSystem`: load/materialize/invalidate machinery |
+| [`src/client/entries.ts`](src/client/entries.ts) | Page entry reconciliation, retries and code replacement |
+| [`src/client/entry-lifecycle.ts`](src/client/entry-lifecycle.ts) | Loader fiber teardown through the registry and owned-style cleanup |
 | [`src/client/manifest.ts`](src/client/manifest.ts) | Wire types, boot-manifest parsing, and the `dsh.client` declaration parser |
 
 </details>

+ 2 - 0
packages/client/modules/README.zh.md

@@ -90,6 +90,8 @@ bundle 路由随注入的 `webServer` 生命周期注册:服务就绪时注册
 | [`src/index.ts`](src/index.ts) | Node 半侧:`ClientModuleRegistry`、扫描、产物快照、可选 combo 路由、结构化 index 行 |
 | [`src/client/index.ts`](src/client/index.ts) | 浏览器半侧:bootstrap 导出、`ctx.modules` 登记 |
 | [`src/client/system.ts`](src/client/system.ts) | `ClientModuleSystem`:加载/物化/失效机制 |
+| [`src/client/entries.ts`](src/client/entries.ts) | 页面条目对账、重试与代码替换 |
+| [`src/client/entry-lifecycle.ts`](src/client/entry-lifecycle.ts) | 通过注册表清理 Loader fiber,回收模块自身样式 |
 | [`src/client/manifest.ts`](src/client/manifest.ts) | 协议类型、启动清单解析与 `dsh.client` 声明解析器 |
 
 </details>

+ 1 - 0
packages/client/modules/src/client/entries.ts

@@ -132,6 +132,7 @@ export class ClientEntries {
       try {
         listener()
       } catch (error) {
+        // The page controller has no owning plugin Context for a scoped logger.
         console.error('client-modules: synchronization subscriber failed', error)
       }
     }

+ 2 - 2
packages/client/modules/src/client/manifest.ts

@@ -372,7 +372,7 @@ export interface ClientModuleRecord {
 export interface ClientModuleLoader {
   /** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
   version: 'client'
-  /** Parsed Host boot graph shared with the web entry after module-system creation. */
+  /** Latest parsed Host graph, updated by live entry reconciliation. */
   manifest: BootManifest
   /** Page-owned entry reconciliation, shared by boot, graph updates and HMR. */
   entries: ClientEntries
@@ -413,7 +413,7 @@ export interface ClientModuleLoader {
 
 /** Internal construction inputs assembled by the modules bundle's bootstrap export. */
 export interface ClientModuleSystemOptions {
-  /** Parsed boot graph owned by the resulting module system. */
+  /** Boot graph validated by {@link parseBootManifest}, owned by the resulting module system. */
   manifest: BootManifest
   /** Module-table seed: platform-singleton specifier → shell instance. */
   staticModules: Record<string, unknown>

+ 4 - 4
packages/client/modules/tests/entries.client.spec.ts

@@ -11,7 +11,7 @@ afterEach(async () => {
     await ctx.fiber.dispose()
     await ctx.fiber.await()
   }
-  document.head.querySelectorAll('style').forEach((el) =>{  el.remove() })
+  document.head.querySelectorAll('style').forEach((el) => { el.remove() })
   document.body.replaceChildren()
   vi.restoreAllMocks()
 })
@@ -93,7 +93,7 @@ describe('client manifest entries', () => {
     window.dispatchEvent(new Event('live-test'))
     expect(effects.hits).toBe(1)
     const removing = b.modules.entries.sync(graph())
-    await vi.waitFor(() =>{  expect(document.querySelector('[data-live=pet]')).toBeNull() })
+    await vi.waitFor(() => { expect(document.querySelector('[data-live=pet]')).toBeNull() })
     const readding = b.modules.entries.sync(graph(row('pet')))
     expect(effects.disposed).toBe(0)
     expect(effects.mounted).toBe(1)
@@ -275,7 +275,7 @@ it('does not finish a stale code replacement after download or asynchronous tear
   await Promise.all([rebuilding, snapshot])
   expect(effects.mounted).toBe(1)
   const swapping = b.modules.entries.reload('a', 'r2')
-  await vi.waitFor(() =>{  expect(document.querySelector('[data-live=a]')).toBeNull() })
+  await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() })
   const disabling = b.modules.entries.sync(graph())
   cleanup.resolve()
   await Promise.all([swapping, disabling])
@@ -288,7 +288,7 @@ it('stops an obsolete multi-entry application after awaiting removal', async ()
   const effects = { mounted: 0, disposed: 0, hits: 0 }
   const b = await bench(graph(row('a')), { a: visible('a', effects, () => cleanup.promise), b: () => ({ apply() {} }) })
   const first = b.modules.entries.sync(graph(row('b')))
-  await vi.waitFor(() =>{  expect(document.querySelector('[data-live=a]')).toBeNull() })
+  await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() })
   const latest = b.modules.entries.sync(graph())
   cleanup.resolve()
   await Promise.all([first, latest])