fix(client): preload injected module factories

This commit is contained in:
imccyu 2026-08-24 02:32:10 +08:00
parent 91b545daf5
commit 5549b9add5
3 changed files with 41 additions and 13 deletions

View file

@ -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({

View file

@ -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)
}

View file

@ -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'] },
])
})