fix(gui): CI activation order + review-bot findings
The session-title snapshot exposed a real activation race: ui-trajectory and ui-question register into conversation-declared slots but only injected 'slots', so nothing ordered their applies after ui-conversation's — register() into the undeclared slot threw and the entry FAILED. Both now inject 'conversation' as an ordering edge (documented as such; specs stub the service where the bench declares the slot itself). Review-bot findings, all three applied: the module loader's load sink cross-checks the handoff id against the arriving row (a mis-stamped bundle can no longer register under another entry's identity); the default execute seam removes the inline script node right after its synchronous execution (repeated HMR rebuilds no longer accumulate dead nodes); a throwing onRebuilt subscriber is contained per-listener and routed to onError instead of escaping the fs.watchFile callback.
This commit is contained in:
@@ -29,6 +29,10 @@ const defaultExecuteBundle = (code: string, url: string): void => {
|
||||
// sourceURL comment keeps devtools stack frames attributed to the bundle.
|
||||
el.textContent = `${code}\n//# sourceURL=${url}`
|
||||
document.head.appendChild(el)
|
||||
// Execution is synchronous for inline scripts: the factory is registered by
|
||||
// now, so the node (and its source text) has no further job. Removing it
|
||||
// keeps repeated HMR rebuilds from accumulating dead script nodes.
|
||||
el.remove()
|
||||
}
|
||||
|
||||
const urlOf = (row: WebBootEntry): string => {
|
||||
@@ -84,6 +88,10 @@ export class ClientModuleLoaderImpl implements ClientModuleLoader {
|
||||
// Execution URL of the bundle currently being executed (bound into the
|
||||
// factory registration so diagnostics can name the source).
|
||||
private executingUrl = ''
|
||||
// Graph id of the row currently being executed ('' outside arrive):
|
||||
// the load sink cross-checks the handoff id against it so a mis-stamped
|
||||
// bundle cannot register under another entry's identity.
|
||||
private executingId = ''
|
||||
|
||||
private readonly fetchBundle: (url: string) => Promise<string>
|
||||
private readonly executeBundle: (code: string, url: string) => void
|
||||
@@ -109,6 +117,12 @@ export class ClientModuleLoaderImpl implements ClientModuleLoader {
|
||||
// Registration is keyed by the handoff id; a duplicate means a bundle
|
||||
// executed twice without an invalidate — always a bug, always loud.
|
||||
if (this.factories.has(handoff.id)) throw new Error(`client-modules: duplicate factory registration for "${handoff.id}" (bundle executed twice without invalidate?)`)
|
||||
// A fetched row's bundle must register the id its row names — a
|
||||
// mis-stamped bundle registering under another entry's identity
|
||||
// would let that entry silently materialize foreign exports.
|
||||
if (this.executingId !== '' && handoff.id !== this.executingId) {
|
||||
throw new Error(`client-modules: bundle ${this.executingUrl} registered "${handoff.id}" while arriving for "${this.executingId}" (mis-stamped bundle id)`)
|
||||
}
|
||||
this.factories.set(handoff.id, { factory: handoff.factory, url: this.executingUrl })
|
||||
},
|
||||
}
|
||||
@@ -124,10 +138,12 @@ export class ClientModuleLoaderImpl implements ClientModuleLoader {
|
||||
const url = urlOf(row)
|
||||
const code = await this.fetchBundle(url)
|
||||
this.executingUrl = url
|
||||
this.executingId = id
|
||||
try {
|
||||
this.executeBundle(code, url)
|
||||
} finally {
|
||||
this.executingUrl = ''
|
||||
this.executingId = ''
|
||||
}
|
||||
if (!this.factories.has(id)) {
|
||||
throw new Error(`client-modules: bundle ${url} executed without registering "${id}" via __ModuleLoader__.load`)
|
||||
|
||||
@@ -293,8 +293,9 @@ describe('default transport seams', () => {
|
||||
;(document as unknown as Record<string, unknown>).__realmBridge = win.__ModuleLoader__
|
||||
const surface = await loader.import('dee', '', {})
|
||||
expect((surface as { marker: string }).marker).toBe('via-script')
|
||||
const script = [...document.querySelectorAll('script')].at(-1)
|
||||
expect(script?.textContent).toContain('//# sourceURL=/plugins/dee/client.js?rev=0')
|
||||
// The script node is removed right after its synchronous execution —
|
||||
// repeated HMR rebuilds must not accumulate dead script nodes.
|
||||
expect([...document.querySelectorAll('script')]).toEqual([])
|
||||
})
|
||||
|
||||
it('a non-ok bundle response is loud with the status', async () => {
|
||||
|
||||
Reference in New Issue
Block a user