Просмотр исходного кода

fix(boot): expose profile recomposition settlement to observers

Yichen Jiang 1 месяц назад
Родитель
Сommit
8c33df5d8e

+ 2 - 2
docs/subsystems/core.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 docs/subsystems/core.md
-core.md: 5b1a249d4d0b2df05750e62044ee539a24b32b0e
-core.zh.md: 2c38ffe6ec9956fc436d4a4f7e2fcc3d1f7420f8
+core.md: a5f61dddd033ad5332fdc2f940b48029c1eedd27
+core.zh.md: 1b0599e9121702dae5163fe93438044491574072

+ 8 - 0
docs/subsystems/core.md

@@ -948,6 +948,14 @@ userDisables(entry: Entry): boolean
  * @throws when preparation fails or the root Include cannot accept the update.
  */
 async recompose(options: { reloadBundles?: boolean } = {}): Promise<readonly EntryIssue[]>
+
+/**
+ * Wait for recompositions already queued when called, including removed-fiber cleanup.
+ * Observers may read accepted composition facts afterwards; a failed operation
+ * still reports its error to its caller and does not reject this observation.
+ * @returns after the current recomposition queue settles.
+ */
+whenIdle(): Promise<void>
 ```
 
 Source: [`packages/boot/app-boot/src/profile-runtime.ts`](../../packages/boot/app-boot/src/profile-runtime.ts)

+ 8 - 0
docs/subsystems/core.zh.md

@@ -958,6 +958,14 @@ userDisables(entry: Entry): boolean
  * @throws when preparation fails or the root Include cannot accept the update.
  */
 async recompose(options: { reloadBundles?: boolean } = {}): Promise<readonly EntryIssue[]>
+
+/**
+ * Wait for recompositions already queued when called, including removed-fiber cleanup.
+ * Observers may read accepted composition facts afterwards; a failed operation
+ * still reports its error to its caller and does not reject this observation.
+ * @returns after the current recomposition queue settles.
+ */
+whenIdle(): Promise<void>
 ```
 
 Source: [`packages/boot/app-boot/src/profile-runtime.ts`](../../packages/boot/app-boot/src/profile-runtime.ts)

+ 2 - 2
packages/boot/app-boot/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/boot/app-boot/README.md
-README.md: 2bcdeff9ca96f8ee54e0a8ef2474496e8cf0776e
-README.zh.md: 2cbb7e7e022abe6cebc6b4f5ae468598040476ae
+README.md: 455b55910f8089a8833b46d99673fd55928f5f01
+README.zh.md: c941328f77e663b5bfd75fde20fa7ca3f213c219

+ 1 - 1
packages/boot/app-boot/README.md

@@ -60,7 +60,7 @@ Inserted plugin names may be absolute filesystem paths, file URLs, or package sp
 
 Installed dependencies default to `external`; template bundles and packages in `dsh.profile.firstParty` are `builtin`. Effective stage is `dsh.profile.stages[package]`, then the package’s `dsh.bundle.stage`, then `runtime`. Stage selects startup failure policy, not a later execution phase. Bundle patches retain their declared ids, parents, and ordering. Built-in and boot-staged layers claim ids first; an optional bundle with a duplicate id is omitted whole and reported, while a conflicting user insert is omitted per row. `dependencies` records installation; `dsh.profile.bundles` selects enabled layers, including all their inserts and overrides.
 
-The launcher provides `ctx.profileRuntime` before any configuration entry mounts. It owns row provenance, accepted composition, conflicts, and user-disabled rows. Watchers and management operations share its serial recomposition queue. Recomposition waits for current entries and removed fibers, then publishes accepted options and reports per-entry issues; a failed update can leave a fiber running its previous valid config. `installFailLoud` remains installed until shutdown for process-level unhandled rejections.
+The launcher provides `ctx.profileRuntime` before any configuration entry mounts. It owns row provenance, accepted composition, conflicts, and user-disabled rows. Watchers and management operations share its serial recomposition queue. Recomposition waits for current entries and removed fibers, then publishes accepted options and reports per-entry issues; a failed update can leave a fiber running its previous valid config. `installFailLoud` remains installed until shutdown for process-level unhandled rejections. Observers can await `whenIdle()` before publishing a refreshed view of composition ownership.
 
 ### Previewing the effective configuration
 

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -60,7 +60,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 已安装依赖默认为 `external`;模板组合包和 `dsh.profile.firstParty` 中的包为 `builtin`。有效 stage 依次取 `dsh.profile.stages[package]`、包的 `dsh.bundle.stage`,最后默认为 `runtime`。stage 决定启动失败策略,不表示稍后执行。组合包 patch 保留声明的 id、父组和顺序。内置与 boot 阶段层优先占有 id;可选组合包有重复 id 时整层被排除并报告,冲突的用户插入则逐行排除。`dependencies` 记录安装;`dsh.profile.bundles` 选择启用的层,包括它们的全部插入和覆盖。
 
-launcher 在任何配置行挂载前提供 `ctx.profileRuntime`。它拥有行来源、已接受的组合、冲突和用户禁用行信息。文件监听与管理操作共用它的串行重组队列。重组等待当前条目和已移除 fiber 完成后,发布已接受的选项并报告逐行问题;更新失败时,fiber 可能仍使用先前的有效配置运行。`installFailLoud` 保持到应用关闭,处理进程级未处理 rejection。
+launcher 在任何配置行挂载前提供 `ctx.profileRuntime`。它拥有行来源、已接受的组合、冲突和用户禁用行信息。文件监听与管理操作共用它的串行重组队列。重组等待当前条目和已移除 fiber 完成后,发布已接受的选项并报告逐行问题;更新失败时,fiber 可能仍使用先前的有效配置运行。`installFailLoud` 保持到应用关闭,处理进程级未处理 rejection。 观测方可等待 `whenIdle()` 后,再发布包含组合归属的刷新视图。
 
 ### 预览生效配置
 

+ 10 - 0
packages/boot/app-boot/src/profile-runtime.ts

@@ -183,6 +183,16 @@ export class ProfileRuntime extends Service {
     this.committed = { profile, stack }
     return inspectEntryIssues(this.ctx)
   }
+
+  /**
+   * Wait for recompositions already queued when called, including removed-fiber cleanup.
+   * Observers may read accepted composition facts afterwards; a failed operation
+   * still reports its error to its caller and does not reject this observation.
+   * @returns after the current recomposition queue settles.
+   */
+  whenIdle(): Promise<void> {
+    return this.queue
+  }
 }
 
 /** The walk behind {@link ProfileRuntime.userDisables}: the entry, then each group holding it, outward. */

+ 6 - 1
packages/boot/app-boot/tests/profile-runtime.spec.ts

@@ -147,10 +147,14 @@ describe('ProfileRuntime', () => {
     // Enabling a bundle re-reads the profile and waits on the tree; a watcher fires meanwhile.
     const enabling = runtime.recompose({ reloadBundles: true })
     const watching = runtime.recompose()
+    const observed = vi.fn()
+    const idle = runtime.whenIdle().then(observed)
     await Promise.resolve()
     expect(applied).toEqual(['composed-for-2'])
+    expect(observed).not.toHaveBeenCalled()
     release()
-    await Promise.all([enabling, watching])
+    await Promise.all([enabling, watching, idle])
+    expect(observed).toHaveBeenCalledOnce()
     // The watcher composed from the profile the enable committed, not the one before it.
     expect(applied).toEqual(['composed-for-2', 'composed-for-2'])
     expect(compose.mock.calls.at(-1)?.[0]).toBe(reloaded)
@@ -169,6 +173,7 @@ describe('ProfileRuntime', () => {
     const second = runtime.recompose()
     await expect(first).rejects.toThrow('rejected')
     await expect(second).resolves.toEqual([])
+    await expect(runtime.whenIdle()).resolves.toBeUndefined()
   })
 
   it('keeps the committed profile, provenance, and conflicts when the root include rejects the update', async () => {

+ 6 - 0
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1434,6 +1434,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'current entry issues after application, without rolling back successful siblings.',
         throws: ['when preparation fails or the root Include cannot accept the update.'],
       },
+      {
+        signature: 'whenIdle(): Promise<void>',
+        description: 'Wait for recompositions already queued when called, including removed-fiber cleanup. Observers may read accepted composition facts afterwards; a failed operation still reports its error to its caller and does not reject this observation.',
+        parameters: [],
+        returns: 'after the current recomposition queue settles.',
+      },
     ],
   },
   {