Skip to content

Commit 4455cbe

Browse files
SCAL-236431 Add embed initialization state management
1 parent 3e6d3d4 commit 4455cbe

File tree

5 files changed

+4463
-3603
lines changed

5 files changed

+4463
-3603
lines changed

src/embed/ts-embed.spec.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ContextMenuTriggerOptions,
2929
CustomActionTarget,
3030
CustomActionsPosition,
31+
InitState,
3132
} from '../types';
3233
import {
3334
executeAfterWait,
@@ -1908,14 +1909,67 @@ describe('Unit test case for ts embed', () => {
19081909
});
19091910
});
19101911

1911-
test('Error should be true', async () => {
1912+
test('Error should be false', async () => {
19121913
spyOn(logger, 'error');
19131914
const tsEmbed = new SearchEmbed(getRootEl(), {});
19141915
await tsEmbed.render();
1915-
expect(tsEmbed['isError']).toBe(true);
1916-
expect(logger.error).toHaveBeenCalledWith(
1917-
'You need to init the ThoughtSpot SDK module first',
1918-
);
1916+
expect(tsEmbed['isError']).toBe(false);
1917+
expect(tsEmbed['getInitState']()).toBe(InitState.Ready);
1918+
expect(logger.error).not.toHaveBeenCalled();
1919+
});
1920+
});
1921+
1922+
describe('Initialization State Management', () => {
1923+
let tsEmbed: SearchEmbed;
1924+
let mockInitPromise: Promise<any>;
1925+
let mockResolve: () => void;
1926+
let mockReject: (error: any) => void;
1927+
1928+
beforeEach(() => {
1929+
mockInitPromise = new Promise<void>((resolve) => {
1930+
mockResolve = () => resolve();
1931+
});
1932+
1933+
jest.spyOn(config, 'getThoughtSpotHost').mockImplementation(() => '');
1934+
init({
1935+
thoughtSpotHost: '',
1936+
authType: AuthType.None,
1937+
});
1938+
});
1939+
test('Should initialize with correct state progression (NotStarted -> Initializing -> Ready)', async () => {
1940+
const initStateChangeCallback = jest.fn();
1941+
1942+
tsEmbed = new SearchEmbed(getRootEl(), {});
1943+
1944+
// Check initial state immediately after construction
1945+
expect(tsEmbed.getInitState()).toBe(InitState.Initializing);
1946+
1947+
// Register callback for future state changes
1948+
tsEmbed.on(EmbedEvent.InitStateChange, initStateChangeCallback);
1949+
1950+
// Resolve the init promise to trigger Ready state
1951+
mockResolve();
1952+
await mockInitPromise;
1953+
1954+
// Wait a tick for the promise resolution to propagate
1955+
await new Promise(resolve => setTimeout(resolve, 0));
1956+
1957+
// Should now be Ready
1958+
expect(tsEmbed.getInitState()).toBe(InitState.Ready);
1959+
1960+
// Check that callback was called
1961+
expect(initStateChangeCallback).toHaveBeenCalledTimes(1);
1962+
1963+
// Extract just the first argument (the data)
1964+
const [callData] = initStateChangeCallback.mock.calls[0];
1965+
expect(callData).toEqual({
1966+
state: InitState.Ready,
1967+
previousState: InitState.Initializing,
1968+
timestamp: expect.any(Number),
1969+
});
1970+
1971+
// waitForInit should resolve
1972+
await expect(tsEmbed.waitForInit()).resolves.toBeUndefined();
19191973
});
19201974
});
19211975

src/embed/ts-embed.ts

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
ContextMenuTriggerOptions,
5858
DefaultAppInitData,
5959
AllEmbedViewConfig as ViewConfig,
60+
InitState,
6061
} from '../types';
6162
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
6263
import { processEventData, processAuthFailure } from '../utils/processData';
@@ -186,6 +187,63 @@ export class TsEmbed {
186187
*/
187188
private fullscreenChangeHandler: (() => void) | null = null;
188189

190+
/**
191+
* Current initialization state
192+
*/
193+
private initState: InitState = InitState.NotStarted;
194+
195+
/**
196+
* Promise that resolves when initialization is complete
197+
*/
198+
protected initPromise: Promise<void>;
199+
200+
/**
201+
* Resolve function for the init promise
202+
*/
203+
private initPromiseResolve: () => void;
204+
205+
/**
206+
* Sets the initialization state and emits events
207+
*/
208+
private setInitState(newState: InitState): void {
209+
const previousState = this.initState;
210+
this.initState = newState;
211+
212+
this.executeCallbacks(EmbedEvent.InitStateChange, {
213+
state: newState,
214+
previousState,
215+
timestamp: Date.now(),
216+
});
217+
218+
if (newState === InitState.Ready) {
219+
this.initPromiseResolve();
220+
}
221+
}
222+
223+
/**
224+
* Gets the current initialization state
225+
*/
226+
public getInitState(): InitState {
227+
return this.initState;
228+
}
229+
230+
/**
231+
* Returns a promise that resolves when initialization is complete
232+
*/
233+
public waitForInit(): Promise<void> {
234+
return this.initPromise;
235+
}
236+
237+
/**
238+
* Waits for initialization if needed, otherwise returns immediately
239+
*/
240+
private async ensureInitialized(): Promise<void> {
241+
if (this.initState === InitState.Ready) {
242+
return;
243+
}
244+
await this.waitForInit();
245+
}
246+
189247
constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
190248
this.el = getDOMNode(domSelector);
191249
this.eventHandlerMap = new Map();
@@ -203,23 +261,23 @@ export class TsEmbed {
203261
this.embedConfig = embedConfig;
204262

205263
this.hostEventClient = new HostEventClient(this.iFrame);
264+
265+
this.initPromise = new Promise((resolve) => {
266+
this.initPromiseResolve = resolve;
267+
});
268+
this.setInitState(InitState.Initializing);
269+
206270
this.isReadyForRenderPromise = getInitPromise().then(async () => {
207271
if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
208272
this.embedConfig.authTriggerContainer = domSelector;
209273
}
210274
this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
211275
this.thoughtSpotV2Base = getV2BasePath(embedConfig);
212276
this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
277+
this.setInitState(InitState.Ready);
213278
});
214279
}
215280

216-
/**
217-
* Throws error encountered during initialization.
218-
*/
219-
private throwInitError() {
220-
this.handleError('You need to init the ThoughtSpot SDK module first');
221-
}
222-
223281
/**
224282
* Handles errors within the SDK
225283
* @param error The error message or object
@@ -814,8 +872,12 @@ export class TsEmbed {
814872
if (this.isError) {
815873
return null;
816874
}
817-
if (!this.thoughtSpotHost) {
818-
this.throwInitError();
875+
// Wait for initialization instead of throwing error
876+
try {
877+
await this.ensureInitialized();
878+
} catch (error) {
879+
this.handleError('Cannot render: initialization failed');
880+
return null;
819881
}
820882
if (url.length > URL_MAX_LENGTH) {
821883
// warn: The URL is too long

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const ERROR_MESSAGE = {
2020
RENDER_CALLED_BEFORE_INIT: 'Looks like render was called before calling init, the render won\'t start until init is called.\nFor more info check\n1. https://developers.thoughtspot.com/docs/Function_init#_init\n2.https://developers.thoughtspot.com/docs/getting-started#initSdk',
2121
SPOTTER_AGENT_NOT_INITIALIZED: 'SpotterAgent not initialized',
2222
OFFLINE_WARNING : 'Network not Detected. Embed is offline. Please reconnect and refresh',
23+
EMBED_INITIALIZATION_FAILED: 'Embed initialization failed',
2324
};
2425

2526
export const CUSTOM_ACTIONS_ERROR_MESSAGE = {

src/types.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,6 +2905,25 @@ export enum EmbedEvent {
29052905
* @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl
29062906
*/
29072907
OrgSwitched = 'orgSwitched',
2908+
2909+
/**
2910+
* Embed initialization state has changed
2911+
* @returns state - The current initialization state
2912+
* @returns previousState - The previous initialization state
2913+
* @version SDK: 1.42.1 | ThoughtSpot: 10.15.0.cl
2914+
* @example
2915+
* ```js
2916+
* liveboardEmbed.on(EmbedEvent.InitStateChange, (payload) => {
2917+
* console.log(`Init state changed from ${payload.previousState} to ${payload.state}`);
2918+
* if (payload.state === 'initializing') {
2919+
* showLoader();
2920+
* } else if (payload.state === 'ready') {
2921+
* hideLoader();
2922+
* }
2923+
* });
2924+
* ```
2925+
*/
2926+
InitStateChange = 'initStateChange',
29082927
}
29092928

29102929
/**
@@ -5867,6 +5886,32 @@ export enum LogLevel {
58675886
TRACE = 'TRACE',
58685887
}
58695888

5889+
/**
5890+
* Represents the initialization state of an embed instance
5891+
*
5892+
* @version SDK: 1.42.1 | ThoughtSpot: 10.15.0.cl
5893+
* @example
5894+
* ``` js
5895+
* NotStarted = 'not-started'
5896+
* Initializing = 'initializing'
5897+
* Ready = 'ready'
5898+
* ```
5899+
*/
5900+
export enum InitState {
5901+
/**
5902+
* Embed instance created but initialization not started
5903+
*/
5904+
NotStarted = 'not-started',
5905+
/**
5906+
* SDK initialization in progress
5907+
*/
5908+
Initializing = 'initializing',
5909+
/**
5910+
* SDK initialization completed successfully
5911+
*/
5912+
Ready = 'ready',
5913+
}
5914+
58705915
export interface DefaultAppInitData {
58715916
customisations: CustomisationsInterface;
58725917
authToken: string;

0 commit comments

Comments
 (0)