Răsfoiți Sursa

fix(client): preload injected module factories

imccyu 1 lună în urmă
părinte
comite
5549b9add5

+ 8 - 6
packages/client/modules/src/client/manifest.ts

@@ -42,11 +42,10 @@ declare module '@deepseek-ai/cordis' {
 /**
  * One composed client entry pushed by the host (a graph row). Wire
  * single source: the host node half (package root) produces this same shape.
- * `immediately` marks stage-one prefetch; `inject` is informational graph
- * metadata (the authoritative edges live in each package's `dsh.client`
- * declaration and reach fibers through entry creation). `external` carries
- * module-graph edges: unlike `inject`, they constrain code arrival because
- * `require` is synchronous (see {@link WebBootGraph.entries}).
+ * `immediately` marks stage-one prefetch. `inject` names package rows whose
+ * factories must arrive before this row materializes, while Cordis separately
+ * uses the same package edges to compose entries. `external` carries exact
+ * non-inject module requests (see {@link WebBootGraph.entries}).
  */
 export interface WebBootEntry {
   /** Entry name == package name. */
@@ -55,7 +54,7 @@ export interface WebBootEntry {
   url: string
   /** Bundle content hash (cache-busting consistency anchor). */
   rev: string
-  /** Package-name dependency edges, informational (preflight display / HMR diffing). */
+  /** Package-name dependency edges used for factory arrival and plugin composition. */
   inject?: string[]
   /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
   immediately?: boolean
@@ -83,6 +82,8 @@ export interface BootModuleRow {
   url: string
   /** Bundle content hash. */
   rev: string
+  /** Injected package rows whose factories arrive before this row materializes. */
+  inject: string[]
   /** Module specifiers this row requests from the module table ([] when the wire omits them). */
   external: string[]
 }
@@ -176,6 +177,7 @@ export function parseBootManifest(wire: unknown): BootManifest {
       id: row.id,
       url: row.url,
       rev: row.rev,
+      inject: inject === undefined ? [] : [...inject],
       external: external === undefined ? [] : [...external],
     })
     plugins.push({

+ 13 - 3
packages/client/modules/src/client/system.ts

@@ -124,8 +124,12 @@ export class ClientModuleSystem implements ClientModuleLoader {
     return task
   }
 
-  /** Register each unresolved dynamic request before registering its consumer. */
-  private async arriveGraphRow(row: BootModuleRow, open: readonly string[] = []): Promise<void> {
+  /** Register each injected package and unresolved dynamic request before its consumer. */
+  private async arriveGraphRow(
+    row: BootModuleRow,
+    open: readonly string[] = [],
+    visited = new Set<string>(),
+  ): Promise<void> {
     const cycleStart = open.indexOf(row.id)
     if (cycleStart !== -1) {
       throw new Error(
@@ -133,12 +137,18 @@ export class ClientModuleSystem implements ClientModuleLoader {
         + '(the host must reject this graph before serving it)',
       )
     }
+    if (visited.has(row.id)) return
+    visited.add(row.id)
     const next = [...open, row.id]
     for (const request of row.external) {
       const id = stripClientSuffix(request)
       if (this.seed.has(request) || this.loadCache.has(id)) continue
       const dependency = this.graphRows.get(id)
-      if (dependency !== undefined) await this.arriveGraphRow(dependency, next)
+      if (dependency !== undefined) await this.arriveGraphRow(dependency, next, visited)
+    }
+    for (const packageName of row.inject) {
+      const dependency = this.graphRows.get(packageName)
+      if (dependency !== undefined) await this.arriveGraphRow(dependency, [], visited)
     }
     await this.arrive(row)
   }

+ 20 - 4
packages/client/modules/tests/loader.client.spec.ts

@@ -20,7 +20,7 @@ afterEach(() => {
 })
 
 const row = (id: string, fields: Partial<BootModuleRow> = {}): BootModuleRow =>
-  ({ id, url: `/plugins/${id}/client.js?rev=0`, rev: '0', external: [], ...fields })
+  ({ id, url: `/plugins/${id}/client.js?rev=0`, rev: '0', inject: [], external: [], ...fields })
 
 interface Bench {
   loader: ClientModuleLoader
@@ -147,6 +147,22 @@ describe('lazy CJS arrival', () => {
     expect(exports.react.marker).toBe('react')
   })
 
+  it('registers injected package factories before materializing a consumer', async () => {
+    const b = bench([
+      row('consumer', { inject: ['provider'] }),
+      row('provider', { inject: ['consumer'] }),
+    ], {
+      consumer: req => ({ provider: req('provider/client') }),
+      provider: () => ({ marker: 'provider' }),
+    })
+    const exports = await b.loader.import('consumer', '', {}) as { provider: { marker: string } }
+    expect(b.fetched).toEqual([
+      '/plugins/provider/client.js?rev=0',
+      '/plugins/consumer/client.js?rev=0',
+    ])
+    expect(exports.provider.marker).toBe('provider')
+  })
+
   it('concurrent callers share one in-flight arrival and materialize once', async () => {
     const ran: string[] = []
     const url = '/plugins/a/client.js?rev=0'
@@ -306,13 +322,13 @@ describe('boot manifest wire', () => {
     const manifest = parseBootManifest({
       rev: 'graph',
       entries: [
-        { id: 'a', url: '/plugins/a/client.js', rev: '1' },
+        { id: 'a', url: '/plugins/a/client.js', rev: '1', inject: ['b'] },
         { id: 'b', url: '/plugins/b/client.js', rev: '2', external: ['react'] },
       ],
     })
     expect(manifest.modules).toEqual([
-      { id: 'a', url: '/plugins/a/client.js', rev: '1', external: [] },
-      { id: 'b', url: '/plugins/b/client.js', rev: '2', external: ['react'] },
+      { id: 'a', url: '/plugins/a/client.js', rev: '1', inject: ['b'], external: [] },
+      { id: 'b', url: '/plugins/b/client.js', rev: '2', inject: [], external: ['react'] },
     ])
   })