From 5d7297460bd3acb37fd020f27301c9bba2fa0cd1 Mon Sep 17 00:00:00 2001 From: Leidang Agent Date: Thu, 12 Nov 2026 10:26:01 +0000 Subject: [PATCH] fix(app-bridge): ignore event-less parent messages in wildcard subscriptions, cover init retries (STABLE360-13694) Co-Authored-By: Claude Sonnet 5.5 --- packages/app-bridge/src/app-bridge.test.ts | 21 +++++++++++++++++++++ packages/app-bridge/src/messages.test.ts | 12 ++++++++++++ packages/app-bridge/src/messages.ts | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/app-bridge/src/app-bridge.test.ts b/packages/app-bridge/src/app-bridge.test.ts index 6be4c7383..e8bf2a033 100644 --- a/packages/app-bridge/src/app-bridge.test.ts +++ b/packages/app-bridge/src/app-bridge.test.ts @@ -130,6 +130,27 @@ describe('app-bridge', () => { it('should timeout if no response received', async () => { await expect(initialize({ timeout: 50 })).rejects.toThrow(AppBridgeTimeoutError); }); + + it('should retry the handshake when the parent listener attaches late', async () => { + const initPromise = initialize({ timeout: 50 }); + + // Parent only becomes ready after the first attempt timed out + setTimeout(() => { + simulateParentMessage('app-bridge:init', { token: 'late-token', lang: 'de' }); + }, 80); + + await expect(initPromise).resolves.toEqual({ token: 'late-token', lang: 'de' }); + expect(postMessageMock).toHaveBeenCalledTimes(2); + }); + + it('should reject after exhausting retries and allow initializing again', async () => { + await expect(initialize({ timeout: 20, retries: 1 })).rejects.toThrow(AppBridgeTimeoutError); + expect(postMessageMock).toHaveBeenCalledTimes(2); + + const again = initialize({ timeout: 50 }); + setTimeout(() => simulateParentMessage('app-bridge:init', { token: 't', lang: 'en' }), 10); + await expect(again).resolves.toEqual({ token: 't', lang: 'en' }); + }); }); describe('getSession', () => { diff --git a/packages/app-bridge/src/messages.test.ts b/packages/app-bridge/src/messages.test.ts index 205eddf87..3b989403d 100644 --- a/packages/app-bridge/src/messages.test.ts +++ b/packages/app-bridge/src/messages.test.ts @@ -81,6 +81,18 @@ describe('messages', () => { expect(window.removeEventListener).toHaveBeenCalledWith('message', eventListener); }); + it('should ignore parent messages without an event name for wildcard suffix subscriptions', () => { + const handler = vi.fn(); + subscribeToParentMessages('prefix-*', handler); + + const eventListener = (window.addEventListener as Mock).mock.calls[0][1]; + + expect(() => + eventListener(new MessageEvent('message', { data: { foo: 'bar' }, source: window.parent })), + ).not.toThrow(); + expect(handler).not.toHaveBeenCalled(); + }); + it('should subscribe to all events with *', () => { const handler = vi.fn(); const unsubscribe = subscribeToParentMessages('*', handler); diff --git a/packages/app-bridge/src/messages.ts b/packages/app-bridge/src/messages.ts index e90adea48..949fcd530 100644 --- a/packages/app-bridge/src/messages.ts +++ b/packages/app-bridge/src/messages.ts @@ -14,7 +14,7 @@ export const subscribeToParentMessages = (event: string, handler: MessageHandler if ( event === '*' || incomingEvent === event || - (event.endsWith('*') && incomingEvent.startsWith(event.slice(0, -1))) + (event.endsWith('*') && typeof incomingEvent === 'string' && incomingEvent.startsWith(event.slice(0, -1))) ) { handler(msgEvent); }