From 4a1a3496f248bb6bbfd4bdfa958d8a33b452f3ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 10:45:23 +0100 Subject: [PATCH] frontend-app-api: separate prepared app finalization modes Make prepared apps choose either onFinalized or finalize so the async and direct finalization flows can no longer be mixed on the same instance. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/wiring/createSpecializedApp.test.tsx | 26 +++++++++++++++++++ .../src/wiring/prepareSpecializedApp.tsx | 15 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index cd1356d4cb..3d5cedbf30 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -941,6 +941,32 @@ describe('createSpecializedApp', () => { ); }); + it('should reject finalize after selecting onFinalized', () => { + const preparedApp = prepareSpecializedApp({ + features: [makeAppPlugin()], + }); + + const unsubscribe = preparedApp.onFinalized(() => {}); + + expect(() => preparedApp.finalize()).toThrow( + 'prepareSpecializedApp only supports using either onFinalized() or finalize(), not both', + ); + + unsubscribe(); + }); + + it('should reject onFinalized after selecting finalize', () => { + const preparedApp = prepareSpecializedApp({ + features: [makeAppPlugin()], + }); + + preparedApp.finalize(); + + expect(() => preparedApp.onFinalized(() => {})).toThrow( + 'prepareSpecializedApp only supports using either onFinalized() or finalize(), not both', + ); + }); + it('should synchronously finalize feature flag predicates without sign-in', async () => { const featureFlagsApi = { isActive: jest.fn((name: string) => name === 'test-flag'), diff --git a/packages/frontend-app-api/src/wiring/prepareSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/prepareSpecializedApp.tsx index 0e0027e200..50a116f80a 100644 --- a/packages/frontend-app-api/src/wiring/prepareSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/prepareSpecializedApp.tsx @@ -151,6 +151,8 @@ type FinalizationState = { reject(error: unknown): void; }; +type FinalizationMode = 'onFinalized' | 'finalize'; + type InternalSpecializedAppSessionState = { apis: ApiHolder; identityApi?: IdentityApi; @@ -374,6 +376,7 @@ export function prepareSpecializedApp( let bootstrapApp: BootstrapSpecializedApp | undefined; let bootstrapError: Error | undefined; let finalizationState: FinalizationState | undefined; + let finalizationMode: FinalizationMode | undefined; function updateIdentityApiTarget(identityApi?: IdentityApi) { if (!identityApi) { @@ -541,6 +544,16 @@ export function prepareSpecializedApp( return finalization.promise; } + function selectFinalizationMode(mode: FinalizationMode) { + if (finalizationMode && finalizationMode !== mode) { + throw new Error( + `prepareSpecializedApp only supports using either onFinalized() or finalize(), not both`, + ); + } + + finalizationMode = mode; + } + function getBootstrapApp() { if (bootstrapApp) { return bootstrapApp; @@ -596,6 +609,7 @@ export function prepareSpecializedApp( return { getBootstrapApp, onFinalized(callback) { + selectFinalizationMode('onFinalized'); getBootstrapApp(); let subscribed = true; @@ -628,6 +642,7 @@ export function prepareSpecializedApp( }; }, finalize(finalizeOptions?: { sessionState?: SpecializedAppSessionState }) { + selectFinalizationMode('finalize'); if (finalized) { return finalized; }