diff --git a/packages/client/ui-permission-presets/src/client/catalog.ts b/packages/client/ui-permission-presets/src/client/catalog.ts index 009c47ad5c..4d8667ff72 100644 --- a/packages/client/ui-permission-presets/src/client/catalog.ts +++ b/packages/client/ui-permission-presets/src/client/catalog.ts @@ -55,8 +55,9 @@ export class PermissionCatalogDirectory { /** * Publish one invalidation tick for consumers holding displayed options. - * Both callers are disposal-guarded, and disposal unsubscribes them, so a - * disposed directory cannot reach this. + * Neither caller can run after disposal: `dispose()` unsubscribes the + * catalog-changed listener, and `syncGeneration()` returns early when the + * directory is disposed. */ private invalidate(): void { this.invalidations.set({ count: this.invalidations.getSnapshot().count + 1 }) diff --git a/packages/client/ui-permission-presets/tests/catalog.client.spec.ts b/packages/client/ui-permission-presets/tests/catalog.client.spec.ts index cab0825341..019d7926e8 100644 --- a/packages/client/ui-permission-presets/tests/catalog.client.spec.ts +++ b/packages/client/ui-permission-presets/tests/catalog.client.spec.ts @@ -243,11 +243,14 @@ describe('PermissionCatalogDirectory', () => { generation.setSilently(2) await expect(directory.load()).resolves.toEqual(SECOND) expect(calls).toBe(2) + // A generation change observed by load() withdraws displayed options too. + expect(directory.invalidations.getSnapshot()).toEqual({ count: 1 }) generation.setSilently(3) directory.refresh() await vi.waitFor(() => { expect(directory.store.getSnapshot().value).toEqual(FIRST) }) expect(calls).toBe(3) + expect(directory.invalidations.getSnapshot()).toEqual({ count: 2 }) directory.dispose() }) @@ -270,6 +273,9 @@ describe('PermissionCatalogDirectory', () => { directory.dispose() directory.dispose() directory.refresh() + // The pending generation change makes both assertions below depend on the + // disposed guard rather than on the same-generation early return. + generation.setSilently(2) lateGenerationListener() read.resolve({ ok: true, value: FIRST }) await read.promise