fix(settings): keep installSettingsSection quiet when its consumer unloads
The helper's cleanup ran the same fallback for two different events. A settings provider detaching leaves the consumer running, so falling back to the composition entry and re-judging derived facts is right. The consumer's own unload ran it too — re-registering routes and touching resources the teardown was releasing. The disposer now checks the consumer fiber's own state and returns when it is unloading or disposed.
This commit is contained in:
@@ -538,6 +538,20 @@ export abstract class Settings extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value mirror of the `FiberState` members {@link isUnloading} compares
|
||||
* against: a const enum has no runtime object to import, and the value is
|
||||
* needed at runtime (same rationale as the CLI boot driver's mirror).
|
||||
*/
|
||||
const FIBER_DISPOSED = 4
|
||||
const FIBER_UNLOADING = 5
|
||||
|
||||
/** Whether the consumer's own fiber is tearing down (not just losing the settings service). */
|
||||
function isUnloading(ctx: Context): boolean {
|
||||
const state: number = ctx.fiber.state
|
||||
return state === FIBER_UNLOADING || state === FIBER_DISPOSED
|
||||
}
|
||||
|
||||
/** Hooks a consumer hands to {@link installSettingsSection}. */
|
||||
export interface SettingsSectionHooks<T> {
|
||||
/**
|
||||
@@ -578,6 +592,13 @@ export function installSettingsSection<T>(
|
||||
const scope = sctx.settings.register(ns, schema, { base: entry })
|
||||
hooks.setSource(() => scope.get())
|
||||
sctx.effect(() => () => {
|
||||
// This disposer runs for two different reasons. A settings provider
|
||||
// detaching leaves the consumer running, so it must fall back to its
|
||||
// composition entry and re-judge what it derived. The consumer's own
|
||||
// unload runs it too — and there `onChange` would re-register routes
|
||||
// and touch resources the teardown is releasing, so the fallback is
|
||||
// pointless and the notification actively harmful.
|
||||
if (isUnloading(ctx)) return
|
||||
hooks.setSource(() => entry)
|
||||
hooks.onChange()
|
||||
})
|
||||
|
||||
@@ -694,4 +694,34 @@ describe('installSettingsSection', () => {
|
||||
})
|
||||
expect(current()).toEqual({ theme: 'entry' })
|
||||
})
|
||||
|
||||
it('stays silent when the consumer itself unloads', async () => {
|
||||
const { ctx } = await boot({ doc: { 'helper-ns': { theme: 'user' } } })
|
||||
const entry = { theme: 'entry' }
|
||||
let current: () => { theme: string } = () => entry
|
||||
const changes: string[] = []
|
||||
const consumer = ctx.plugin({
|
||||
inject: ['settings'],
|
||||
apply: (child: Context) => {
|
||||
installSettingsSection(child, settingsNamespace('helper-ns'), HelperSchema, entry, {
|
||||
setSource: (source) => {
|
||||
current = source
|
||||
},
|
||||
onChange: () => {
|
||||
changes.push(current().theme)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
await consumer
|
||||
await vi.waitFor(() => {
|
||||
expect(changes).toEqual(['user'])
|
||||
})
|
||||
|
||||
// The consumer's own teardown must not re-derive anything: an onChange
|
||||
// here would re-register routes and touch resources being released.
|
||||
await consumer.dispose()
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(changes).toEqual(['user'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user