Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/embed/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,9 +1157,10 @@ describe('App embed tests', () => {
embedHeightCallback({ data: '100%' });

// Verify event handlers were registered
// Note: RouteChange is NOT registered for fullHeight (causes feedback loop)
await executeAfterWait(() => {
expect(onSpy).toHaveBeenCalledWith(EmbedEvent.EmbedHeight, expect.anything());
expect(onSpy).toHaveBeenCalledWith(EmbedEvent.RouteChange, expect.anything());
expect(onSpy).not.toHaveBeenCalledWith(EmbedEvent.RouteChange, expect.anything());
expect(onSpy).toHaveBeenCalledWith(EmbedEvent.EmbedIframeCenter, expect.anything());
expect(onSpy).toHaveBeenCalledWith(EmbedEvent.RequestVisibleEmbedCoordinates, expect.anything());
}, 100);
Expand Down
58 changes: 53 additions & 5 deletions src/embed/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,11 +658,27 @@ export class AppEmbed extends V1Embed {
constructor(domSelector: DOMSelector, viewConfig: AppViewConfig) {
viewConfig.embedComponentType = 'AppEmbed';
super(domSelector, viewConfig);
if (this.viewConfig.fullHeight === true) {
this.on(EmbedEvent.RouteChange, this.setIframeHeightForNonEmbedLiveboard);
this.on(EmbedEvent.EmbedHeight, this.updateIFrameHeight);
this.on(EmbedEvent.EmbedIframeCenter, this.embedIframeCenter);
this.on(EmbedEvent.RequestVisibleEmbedCoordinates, this.requestVisibleEmbedCoordinatesHandler);
}

/**
* Hook called before prerender becomes visible.
* Register event listeners here (AFTER isPreRendered is set)
* to ensure ownership check works correctly for prerender.
*/
protected beforePrerenderVisible(): void {
super.beforePrerenderVisible();

if (this.isPreRendered) {
if (this.viewConfig.fullHeight === true) {
// For fullHeight: only listen to height changes
// Don't register RouteChange - it causes feedback loop
this.on(EmbedEvent.EmbedHeight, this.updateIFrameHeight);
this.on(EmbedEvent.EmbedIframeCenter, this.embedIframeCenter);
this.on(EmbedEvent.RequestVisibleEmbedCoordinates, this.requestVisibleEmbedCoordinatesHandler);
} else {
// For fixed height: listen to RouteChange to reset height
this.on(EmbedEvent.RouteChange, this.setIframeHeightForNonEmbedLiveboard);
}
}
}

Expand Down Expand Up @@ -1046,6 +1062,19 @@ export class AppEmbed extends V1Embed {
}

private postRender() {
// For non-prerender cases, register event listeners here
if (!this.isPreRendered) {
if (this.viewConfig.fullHeight === true) {
// For fullHeight: only listen to height changes
// Don't register RouteChange - it causes feedback loop
this.on(EmbedEvent.EmbedHeight, this.updateIFrameHeight);
this.on(EmbedEvent.EmbedIframeCenter, this.embedIframeCenter);
this.on(EmbedEvent.RequestVisibleEmbedCoordinates, this.requestVisibleEmbedCoordinatesHandler);
} else {
// For fixed height: listen to RouteChange to reset height
this.on(EmbedEvent.RouteChange, this.setIframeHeightForNonEmbedLiveboard);
}
}
this.registerLazyLoadEvents();
}

Expand All @@ -1064,6 +1093,25 @@ export class AppEmbed extends V1Embed {
}
}

/**
* Hides the PreRender component and cleans up event listeners.
*/
public hidePreRender(): void {
// Clean up window scroll/resize listeners
this.unregisterLazyLoadEvents();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The unregisterLazyLoadEvents method has a bug that will cause a memory leak. The scroll event listener is registered with useCapture: true in registerLazyLoadEvents (line 1056), but unregisterLazyLoadEvents attempts to remove it without specifying useCapture: true (line 1063). For removeEventListener to work correctly, it must be called with the same arguments as addEventListener. Please update unregisterLazyLoadEvents to correctly remove the listener.


// Clean up event listeners based on height mode
if (this.viewConfig.fullHeight) {
this.off(EmbedEvent.EmbedHeight, this.updateIFrameHeight);
this.off(EmbedEvent.EmbedIframeCenter, this.embedIframeCenter);
this.off(EmbedEvent.RequestVisibleEmbedCoordinates, this.requestVisibleEmbedCoordinatesHandler);
} else {
this.off(EmbedEvent.RouteChange, this.setIframeHeightForNonEmbedLiveboard);
}

super.hidePreRender();
}

/**
* Renders the embedded application pages in the ThoughtSpot app.
* @param renderOptions An object containing the page ID
Expand Down
31 changes: 29 additions & 2 deletions src/embed/ts-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class TsEmbed {
/**
* A flag that is set to true post preRender.
*/
private isPreRendered: boolean;
protected isPreRendered: boolean;

/**
* Should we encode URL Query Params using base64 encoding which thoughtspot
Expand Down Expand Up @@ -362,7 +362,23 @@ export class TsEmbed {
const eventType = this.getEventType(event);
const eventPort = this.getEventPort(event);
const eventData = this.formatEventData(event, eventType);
if (event.source === this.iFrame.contentWindow) {
if (event.source === this.iFrame?.contentWindow) {
// CRITICAL FIX: When using prerender, check if THIS instance is the active owner
// This prevents multiple instances from processing the same iframe's messages
// when React creates a new instance before cleaning up the old one
if (this.isPreRendered && this.preRenderWrapper) {
const activeInstance = (this.preRenderWrapper as any)[this.embedNodeKey];
// Only skip if there IS an active instance AND it's not this one
if (activeInstance && activeInstance !== this) {
// This instance is not the active owner, skip processing
console.log(`[TsEmbed] Skipping message ${eventType} - not the active owner`);
return;
}
console.log(`[TsEmbed] Processing message ${eventType} - active owner`);
} else {
console.log(`[TsEmbed] Processing message ${eventType} - no ownership check (isPreRendered=${this.isPreRendered}, hasWrapper=${!!this.preRenderWrapper})`);
}

const processedEventData = processEventData(
eventType,
eventData,
Expand Down Expand Up @@ -848,6 +864,8 @@ export class TsEmbed {
this.insertIntoDOM(child);
}
if (this.insertedDomEl instanceof Node) {
// Store reference to this instance on the DOM element
// This is used to identify which instance "owns" the prerender wrapper
(this.insertedDomEl as any)[this.embedNodeKey] = this;
}
}
Expand Down Expand Up @@ -957,6 +975,9 @@ export class TsEmbed {
preRenderWrapper.id = preRenderIds.wrapper;
const initialPreRenderWrapperStyle = {
position: 'absolute',
// position: 'fixed',
// top: '0',
// left: '0',
width: '100vw',
height: '100vh',
};
Expand Down Expand Up @@ -1579,6 +1600,12 @@ export class TsEmbed {
});
}

// Transfer ownership to this instance when showing prerender
// This ensures the active instance processes iframe messages
if (this.preRenderWrapper) {
(this.preRenderWrapper as any)[this.embedNodeKey] = this;
}

this.beforePrerenderVisible();

if (this.el) {
Expand Down
Loading