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 <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-16 10:45:23 +01:00
parent a5017f7951
commit 4a1a3496f2
2 changed files with 41 additions and 0 deletions
@@ -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'),
@@ -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;
}