@thoughtspot/visual-embed-sdk 1.52.0 → 1.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/cjs/package.json +9 -1
  2. package/cjs/src/contracts/contracts.spec.d.ts +2 -0
  3. package/cjs/src/contracts/contracts.spec.d.ts.map +1 -0
  4. package/cjs/src/contracts/contracts.spec.js +345 -0
  5. package/cjs/src/contracts/contracts.spec.js.map +1 -0
  6. package/cjs/src/contracts/embed-event-payloads.d.ts +73 -0
  7. package/cjs/src/contracts/embed-event-payloads.d.ts.map +1 -0
  8. package/cjs/src/contracts/embed-event-payloads.js +3 -0
  9. package/cjs/src/contracts/embed-event-payloads.js.map +1 -0
  10. package/cjs/src/contracts/host-event-contracts.d.ts +378 -0
  11. package/cjs/src/contracts/host-event-contracts.d.ts.map +1 -0
  12. package/cjs/src/contracts/host-event-contracts.js +3 -0
  13. package/cjs/src/contracts/host-event-contracts.js.map +1 -0
  14. package/cjs/src/contracts/index.d.ts +22 -0
  15. package/cjs/src/contracts/index.d.ts.map +1 -0
  16. package/cjs/src/contracts/index.js +33 -0
  17. package/cjs/src/contracts/index.js.map +1 -0
  18. package/cjs/src/contracts/ui-passthrough-contracts.d.ts +277 -0
  19. package/cjs/src/contracts/ui-passthrough-contracts.d.ts.map +1 -0
  20. package/cjs/src/{embed/hostEventClient/contracts.js → contracts/ui-passthrough-contracts.js} +6 -2
  21. package/cjs/src/contracts/ui-passthrough-contracts.js.map +1 -0
  22. package/cjs/src/embed/app.d.ts +17 -140
  23. package/cjs/src/embed/app.d.ts.map +1 -1
  24. package/cjs/src/embed/app.js +20 -120
  25. package/cjs/src/embed/app.js.map +1 -1
  26. package/cjs/src/embed/app.spec.js +70 -26
  27. package/cjs/src/embed/app.spec.js.map +1 -1
  28. package/cjs/src/embed/events.spec.js +12 -2
  29. package/cjs/src/embed/events.spec.js.map +1 -1
  30. package/cjs/src/embed/hostEventClient/host-event-client.d.ts +3 -2
  31. package/cjs/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  32. package/cjs/src/embed/hostEventClient/host-event-client.js +23 -18
  33. package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
  34. package/cjs/src/embed/hostEventClient/host-event-client.spec.js +36 -36
  35. package/cjs/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  36. package/cjs/src/embed/hostEventClient/utils.d.ts +1 -1
  37. package/cjs/src/embed/hostEventClient/utils.d.ts.map +1 -1
  38. package/cjs/src/embed/hostEventClient/utils.js +3 -3
  39. package/cjs/src/embed/hostEventClient/utils.js.map +1 -1
  40. package/cjs/src/embed/liveboard.d.ts +9 -161
  41. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  42. package/cjs/src/embed/liveboard.js +66 -134
  43. package/cjs/src/embed/liveboard.js.map +1 -1
  44. package/cjs/src/embed/liveboard.spec.js +211 -28
  45. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  46. package/cjs/src/embed/search.d.ts +14 -0
  47. package/cjs/src/embed/search.d.ts.map +1 -1
  48. package/cjs/src/embed/search.js +4 -1
  49. package/cjs/src/embed/search.js.map +1 -1
  50. package/cjs/src/embed/search.spec.js +65 -0
  51. package/cjs/src/embed/search.spec.js.map +1 -1
  52. package/cjs/src/embed/ts-embed.d.ts +17 -22
  53. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  54. package/cjs/src/embed/ts-embed.js +82 -56
  55. package/cjs/src/embed/ts-embed.js.map +1 -1
  56. package/cjs/src/embed/ts-embed.spec.js +325 -17
  57. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  58. package/cjs/src/full-height.d.ts +137 -0
  59. package/cjs/src/full-height.d.ts.map +1 -0
  60. package/cjs/src/full-height.js +272 -0
  61. package/cjs/src/full-height.js.map +1 -0
  62. package/cjs/src/full-height.spec.d.ts +2 -0
  63. package/cjs/src/full-height.spec.d.ts.map +1 -0
  64. package/cjs/src/full-height.spec.js +835 -0
  65. package/cjs/src/full-height.spec.js.map +1 -0
  66. package/cjs/src/index.d.ts +4 -3
  67. package/cjs/src/index.d.ts.map +1 -1
  68. package/cjs/src/index.js +3 -3
  69. package/cjs/src/index.js.map +1 -1
  70. package/cjs/src/react/all-types-export.d.ts +1 -1
  71. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  72. package/cjs/src/react/all-types-export.js.map +1 -1
  73. package/cjs/src/react/all-types-export.parity.spec.js +55 -3
  74. package/cjs/src/react/all-types-export.parity.spec.js.map +1 -1
  75. package/cjs/src/types.d.ts +540 -3
  76. package/cjs/src/types.d.ts.map +1 -1
  77. package/cjs/src/types.js +326 -1
  78. package/cjs/src/types.js.map +1 -1
  79. package/cjs/src/utils/processTrigger.d.ts +3 -1
  80. package/cjs/src/utils/processTrigger.d.ts.map +1 -1
  81. package/cjs/src/utils/processTrigger.js +19 -51
  82. package/cjs/src/utils/processTrigger.js.map +1 -1
  83. package/cjs/src/utils/transport/iframe-transport.d.ts +55 -0
  84. package/cjs/src/utils/transport/iframe-transport.d.ts.map +1 -0
  85. package/cjs/src/utils/transport/iframe-transport.js +108 -0
  86. package/cjs/src/utils/transport/iframe-transport.js.map +1 -0
  87. package/cjs/src/utils/transport/iframe-transport.spec.d.ts +2 -0
  88. package/cjs/src/utils/transport/iframe-transport.spec.d.ts.map +1 -0
  89. package/cjs/src/utils/transport/iframe-transport.spec.js +140 -0
  90. package/cjs/src/utils/transport/iframe-transport.spec.js.map +1 -0
  91. package/cjs/src/utils.d.ts +18 -0
  92. package/cjs/src/utils.d.ts.map +1 -1
  93. package/cjs/src/utils.js +39 -1
  94. package/cjs/src/utils.js.map +1 -1
  95. package/cjs/src/utils.spec.js +62 -0
  96. package/cjs/src/utils.spec.js.map +1 -1
  97. package/dist/{index-BMppCU63.js → index-C1pmYb5H.js} +1 -1
  98. package/dist/src/contracts/contracts.spec.d.ts +2 -0
  99. package/dist/src/contracts/contracts.spec.d.ts.map +1 -0
  100. package/dist/src/contracts/embed-event-payloads.d.ts +73 -0
  101. package/dist/src/contracts/embed-event-payloads.d.ts.map +1 -0
  102. package/dist/src/contracts/host-event-contracts.d.ts +378 -0
  103. package/dist/src/contracts/host-event-contracts.d.ts.map +1 -0
  104. package/dist/src/contracts/index.d.ts +22 -0
  105. package/dist/src/contracts/index.d.ts.map +1 -0
  106. package/dist/src/contracts/ui-passthrough-contracts.d.ts +277 -0
  107. package/dist/src/contracts/ui-passthrough-contracts.d.ts.map +1 -0
  108. package/dist/src/embed/app.d.ts +17 -140
  109. package/dist/src/embed/app.d.ts.map +1 -1
  110. package/dist/src/embed/hostEventClient/host-event-client.d.ts +3 -2
  111. package/dist/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  112. package/dist/src/embed/hostEventClient/utils.d.ts +1 -1
  113. package/dist/src/embed/hostEventClient/utils.d.ts.map +1 -1
  114. package/dist/src/embed/liveboard.d.ts +9 -161
  115. package/dist/src/embed/liveboard.d.ts.map +1 -1
  116. package/dist/src/embed/search.d.ts +14 -0
  117. package/dist/src/embed/search.d.ts.map +1 -1
  118. package/dist/src/embed/ts-embed.d.ts +17 -22
  119. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  120. package/dist/src/full-height.d.ts +137 -0
  121. package/dist/src/full-height.d.ts.map +1 -0
  122. package/dist/src/full-height.spec.d.ts +2 -0
  123. package/dist/src/full-height.spec.d.ts.map +1 -0
  124. package/dist/src/index.d.ts +4 -3
  125. package/dist/src/index.d.ts.map +1 -1
  126. package/dist/src/react/all-types-export.d.ts +1 -1
  127. package/dist/src/react/all-types-export.d.ts.map +1 -1
  128. package/dist/src/types.d.ts +540 -3
  129. package/dist/src/types.d.ts.map +1 -1
  130. package/dist/src/utils/processTrigger.d.ts +3 -1
  131. package/dist/src/utils/processTrigger.d.ts.map +1 -1
  132. package/dist/src/utils/transport/iframe-transport.d.ts +55 -0
  133. package/dist/src/utils/transport/iframe-transport.d.ts.map +1 -0
  134. package/dist/src/utils/transport/iframe-transport.spec.d.ts +2 -0
  135. package/dist/src/utils/transport/iframe-transport.spec.d.ts.map +1 -0
  136. package/dist/src/utils.d.ts +18 -0
  137. package/dist/src/utils.d.ts.map +1 -1
  138. package/dist/tsembed-react.es.js +925 -363
  139. package/dist/tsembed-react.js +924 -362
  140. package/dist/tsembed.es.js +925 -363
  141. package/dist/tsembed.js +924 -362
  142. package/dist/visual-embed-sdk-react-full.d.ts +1357 -656
  143. package/dist/visual-embed-sdk-react.d.ts +1257 -606
  144. package/dist/visual-embed-sdk.d.ts +1320 -642
  145. package/lib/package.json +9 -1
  146. package/lib/src/contracts/contracts.spec.d.ts +2 -0
  147. package/lib/src/contracts/contracts.spec.d.ts.map +1 -0
  148. package/lib/src/contracts/contracts.spec.js +342 -0
  149. package/lib/src/contracts/contracts.spec.js.map +1 -0
  150. package/lib/src/contracts/embed-event-payloads.d.ts +73 -0
  151. package/lib/src/contracts/embed-event-payloads.d.ts.map +1 -0
  152. package/lib/src/contracts/embed-event-payloads.js +2 -0
  153. package/lib/src/contracts/embed-event-payloads.js.map +1 -0
  154. package/lib/src/contracts/host-event-contracts.d.ts +378 -0
  155. package/lib/src/contracts/host-event-contracts.d.ts.map +1 -0
  156. package/lib/src/contracts/host-event-contracts.js +2 -0
  157. package/lib/src/contracts/host-event-contracts.js.map +1 -0
  158. package/lib/src/contracts/index.d.ts +22 -0
  159. package/lib/src/contracts/index.d.ts.map +1 -0
  160. package/lib/src/contracts/index.js +22 -0
  161. package/lib/src/contracts/index.js.map +1 -0
  162. package/lib/src/contracts/ui-passthrough-contracts.d.ts +277 -0
  163. package/lib/src/contracts/ui-passthrough-contracts.d.ts.map +1 -0
  164. package/lib/src/{embed/hostEventClient/contracts.js → contracts/ui-passthrough-contracts.js} +6 -2
  165. package/lib/src/contracts/ui-passthrough-contracts.js.map +1 -0
  166. package/lib/src/embed/app.d.ts +17 -140
  167. package/lib/src/embed/app.d.ts.map +1 -1
  168. package/lib/src/embed/app.js +22 -122
  169. package/lib/src/embed/app.js.map +1 -1
  170. package/lib/src/embed/app.spec.js +70 -26
  171. package/lib/src/embed/app.spec.js.map +1 -1
  172. package/lib/src/embed/events.spec.js +12 -2
  173. package/lib/src/embed/events.spec.js.map +1 -1
  174. package/lib/src/embed/hostEventClient/host-event-client.d.ts +3 -2
  175. package/lib/src/embed/hostEventClient/host-event-client.d.ts.map +1 -1
  176. package/lib/src/embed/hostEventClient/host-event-client.js +6 -1
  177. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  178. package/lib/src/embed/hostEventClient/host-event-client.spec.js +1 -1
  179. package/lib/src/embed/hostEventClient/host-event-client.spec.js.map +1 -1
  180. package/lib/src/embed/hostEventClient/utils.d.ts +1 -1
  181. package/lib/src/embed/hostEventClient/utils.d.ts.map +1 -1
  182. package/lib/src/embed/hostEventClient/utils.js +1 -1
  183. package/lib/src/embed/hostEventClient/utils.js.map +1 -1
  184. package/lib/src/embed/liveboard.d.ts +9 -161
  185. package/lib/src/embed/liveboard.d.ts.map +1 -1
  186. package/lib/src/embed/liveboard.js +67 -135
  187. package/lib/src/embed/liveboard.js.map +1 -1
  188. package/lib/src/embed/liveboard.spec.js +211 -28
  189. package/lib/src/embed/liveboard.spec.js.map +1 -1
  190. package/lib/src/embed/search.d.ts +14 -0
  191. package/lib/src/embed/search.d.ts.map +1 -1
  192. package/lib/src/embed/search.js +4 -1
  193. package/lib/src/embed/search.js.map +1 -1
  194. package/lib/src/embed/search.spec.js +65 -0
  195. package/lib/src/embed/search.spec.js.map +1 -1
  196. package/lib/src/embed/ts-embed.d.ts +17 -22
  197. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  198. package/lib/src/embed/ts-embed.js +82 -56
  199. package/lib/src/embed/ts-embed.js.map +1 -1
  200. package/lib/src/embed/ts-embed.spec.js +322 -14
  201. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  202. package/lib/src/full-height.d.ts +137 -0
  203. package/lib/src/full-height.d.ts.map +1 -0
  204. package/lib/src/full-height.js +268 -0
  205. package/lib/src/full-height.js.map +1 -0
  206. package/lib/src/full-height.spec.d.ts +2 -0
  207. package/lib/src/full-height.spec.d.ts.map +1 -0
  208. package/lib/src/full-height.spec.js +833 -0
  209. package/lib/src/full-height.spec.js.map +1 -0
  210. package/lib/src/index.d.ts +4 -3
  211. package/lib/src/index.d.ts.map +1 -1
  212. package/lib/src/index.js +1 -1
  213. package/lib/src/index.js.map +1 -1
  214. package/lib/src/react/all-types-export.d.ts +1 -1
  215. package/lib/src/react/all-types-export.d.ts.map +1 -1
  216. package/lib/src/react/all-types-export.js.map +1 -1
  217. package/lib/src/react/all-types-export.parity.spec.js +55 -3
  218. package/lib/src/react/all-types-export.parity.spec.js.map +1 -1
  219. package/lib/src/types.d.ts +540 -3
  220. package/lib/src/types.d.ts.map +1 -1
  221. package/lib/src/types.js +326 -1
  222. package/lib/src/types.js.map +1 -1
  223. package/lib/src/utils/processTrigger.d.ts +3 -1
  224. package/lib/src/utils/processTrigger.d.ts.map +1 -1
  225. package/lib/src/utils/processTrigger.js +19 -51
  226. package/lib/src/utils/processTrigger.js.map +1 -1
  227. package/lib/src/utils/transport/iframe-transport.d.ts +55 -0
  228. package/lib/src/utils/transport/iframe-transport.d.ts.map +1 -0
  229. package/lib/src/utils/transport/iframe-transport.js +101 -0
  230. package/lib/src/utils/transport/iframe-transport.js.map +1 -0
  231. package/lib/src/utils/transport/iframe-transport.spec.d.ts +2 -0
  232. package/lib/src/utils/transport/iframe-transport.spec.d.ts.map +1 -0
  233. package/lib/src/utils/transport/iframe-transport.spec.js +138 -0
  234. package/lib/src/utils/transport/iframe-transport.spec.js.map +1 -0
  235. package/lib/src/utils.d.ts +18 -0
  236. package/lib/src/utils.d.ts.map +1 -1
  237. package/lib/src/utils.js +37 -0
  238. package/lib/src/utils.js.map +1 -1
  239. package/lib/src/utils.spec.js +63 -1
  240. package/lib/src/utils.spec.js.map +1 -1
  241. package/lib/src/visual-embed-sdk.d.ts +1320 -642
  242. package/package.json +9 -1
  243. package/src/contracts/contracts.spec.ts +392 -0
  244. package/src/contracts/embed-event-payloads.ts +83 -0
  245. package/src/contracts/host-event-contracts.ts +456 -0
  246. package/src/contracts/index.ts +53 -0
  247. package/src/contracts/ui-passthrough-contracts.ts +301 -0
  248. package/src/embed/app.spec.ts +86 -34
  249. package/src/embed/app.ts +37 -264
  250. package/src/embed/events.spec.ts +14 -2
  251. package/src/embed/hostEventClient/host-event-client.spec.ts +2 -2
  252. package/src/embed/hostEventClient/host-event-client.ts +18 -5
  253. package/src/embed/hostEventClient/utils.ts +2 -1
  254. package/src/embed/liveboard.spec.ts +257 -38
  255. package/src/embed/liveboard.ts +75 -296
  256. package/src/embed/search.spec.ts +80 -0
  257. package/src/embed/search.ts +20 -0
  258. package/src/embed/ts-embed.spec.ts +448 -15
  259. package/src/embed/ts-embed.ts +109 -66
  260. package/src/full-height.spec.ts +1008 -0
  261. package/src/full-height.ts +361 -0
  262. package/src/index.ts +93 -5
  263. package/src/react/all-types-export.parity.spec.ts +63 -3
  264. package/src/react/all-types-export.ts +45 -0
  265. package/src/types.ts +540 -1
  266. package/src/utils/processTrigger.ts +28 -58
  267. package/src/utils/transport/iframe-transport.spec.ts +177 -0
  268. package/src/utils/transport/iframe-transport.ts +122 -0
  269. package/src/utils.spec.ts +71 -0
  270. package/src/utils.ts +38 -0
  271. package/cjs/src/embed/hostEventClient/contracts.d.ts +0 -231
  272. package/cjs/src/embed/hostEventClient/contracts.d.ts.map +0 -1
  273. package/cjs/src/embed/hostEventClient/contracts.js.map +0 -1
  274. package/dist/src/embed/hostEventClient/contracts.d.ts +0 -231
  275. package/dist/src/embed/hostEventClient/contracts.d.ts.map +0 -1
  276. package/lib/src/embed/hostEventClient/contracts.d.ts +0 -231
  277. package/lib/src/embed/hostEventClient/contracts.d.ts.map +0 -1
  278. package/lib/src/embed/hostEventClient/contracts.js.map +0 -1
  279. package/src/embed/hostEventClient/contracts.ts +0 -262
@@ -1,8 +1,11 @@
1
- import { ERROR_MESSAGE } from '../errors';
2
- import { ContextType, HostEvent, MessagePayload } from '../types';
1
+ import { ContextType, HostEvent } from '../types';
3
2
  import { logger } from '../utils/logger';
4
3
  import { handlePresentEvent } from '../utils';
5
4
  import { getEmbedConfig } from '../embed/embedConfig';
5
+ import {
6
+ MESSAGE_RESPONSE_TIMEOUT,
7
+ sendMessageWithResponse,
8
+ } from './transport/iframe-transport';
6
9
 
7
10
  /**
8
11
  * Reloads the ThoughtSpot iframe.
@@ -16,29 +19,12 @@ export const reload = (iFrame: HTMLIFrameElement) => {
16
19
  }, 100);
17
20
  };
18
21
 
19
- /**
20
- * Post iframe message.
21
- * @param iFrame
22
- * @param message
23
- * @param message.type
24
- * @param message.data
25
- * @param message.context
26
- * @param thoughtSpotHost
27
- * @param channel
28
- */
29
- function postIframeMessage(
30
- iFrame: HTMLIFrameElement,
31
- message: { type: HostEvent; data: any, context?: any },
32
- thoughtSpotHost: string,
33
- channel?: MessageChannel,
34
- ) {
35
- return iFrame.contentWindow?.postMessage(message, thoughtSpotHost, [channel?.port2]);
36
- }
37
-
38
- export const TRIGGER_TIMEOUT = 30000;
22
+ export const TRIGGER_TIMEOUT = MESSAGE_RESPONSE_TIMEOUT;
39
23
 
40
24
  /**
41
- *
25
+ * Processes a host event trigger: handles SDK-local events (Reload,
26
+ * Present) and forwards everything else to the embedded app over the
27
+ * iframe transport.
42
28
  * @param iFrame
43
29
  * @param messageType
44
30
  * @param thoughtSpotHost
@@ -52,42 +38,26 @@ export function processTrigger(
52
38
  data: any,
53
39
  context?: ContextType,
54
40
  ): Promise<any> {
55
- return new Promise<any>((res, rej) => {
56
- if (messageType === HostEvent.Reload) {
57
- reload(iFrame);
58
- return res(null);
59
- }
60
-
61
- if (messageType === HostEvent.Present) {
62
- const embedConfig = getEmbedConfig();
63
- const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
64
-
65
- if (!disableFullscreenPresentation) {
66
- handlePresentEvent(iFrame);
67
- } else {
68
- logger.warn('Fullscreen presentation mode is disabled. Set disableFullscreenPresentation: false to enable this feature.');
69
- }
70
- }
71
-
72
- const channel = new MessageChannel();
41
+ if (messageType === HostEvent.Reload) {
42
+ reload(iFrame);
43
+ return Promise.resolve(null);
44
+ }
73
45
 
74
- // Close the messageChannel and resolve the promise if timeout.
75
- const timeoutId = setTimeout(() => {
76
- channel.port1.close();
77
- res(new Error(ERROR_MESSAGE.TRIGGER_TIMED_OUT));
78
- }, TRIGGER_TIMEOUT);
46
+ if (messageType === HostEvent.Present) {
47
+ const embedConfig = getEmbedConfig();
48
+ const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
79
49
 
80
- channel.port1.onmessage = ({ data: responseData }) => {
81
- clearTimeout(timeoutId);
82
- channel.port1.close();
83
- const error = responseData?.error || responseData?.data?.error;
84
- if (error) {
85
- rej(error);
86
- } else {
87
- res(responseData);
88
- }
89
- };
50
+ if (!disableFullscreenPresentation) {
51
+ handlePresentEvent(iFrame);
52
+ } else {
53
+ logger.warn('Fullscreen presentation mode is disabled. Set disableFullscreenPresentation: false to enable this feature.');
54
+ }
55
+ }
90
56
 
91
- return postIframeMessage(iFrame, { type: messageType, data, context }, thoughtSpotHost, channel);
92
- });
57
+ return sendMessageWithResponse(
58
+ iFrame,
59
+ { type: messageType, data, context },
60
+ thoughtSpotHost,
61
+ TRIGGER_TIMEOUT,
62
+ );
93
63
  }
@@ -0,0 +1,177 @@
1
+ import {
2
+ MESSAGE_RESPONSE_TIMEOUT,
3
+ resolveMessageOrigin,
4
+ isMessageFromIframe,
5
+ postMessageToIframe,
6
+ sendMessageWithResponse,
7
+ } from './iframe-transport';
8
+ import { logger } from '../logger';
9
+ import { ERROR_MESSAGE } from '../../errors';
10
+
11
+ describe('iframe-transport', () => {
12
+ describe('resolveMessageOrigin', () => {
13
+ it('returns the origin (scheme://host[:port]) for a valid host URL', () => {
14
+ expect(resolveMessageOrigin('https://my.thoughtspot.com')).toBe(
15
+ 'https://my.thoughtspot.com',
16
+ );
17
+ expect(
18
+ resolveMessageOrigin('https://host.example.com:8443/some/path'),
19
+ ).toBe('https://host.example.com:8443');
20
+ });
21
+
22
+ it('returns null when the host cannot be parsed', () => {
23
+ expect(resolveMessageOrigin('not-a-url')).toBeNull();
24
+ expect(resolveMessageOrigin('')).toBeNull();
25
+ });
26
+ });
27
+
28
+ describe('isMessageFromIframe', () => {
29
+ const host = 'https://ts.example.com';
30
+ const contentWindow = {} as Window;
31
+ const iFrame = { contentWindow } as HTMLIFrameElement;
32
+
33
+ it('rejects a message whose source is not the iframe window', () => {
34
+ const event = { source: {}, origin: host } as unknown as MessageEvent;
35
+ expect(isMessageFromIframe(event, iFrame, host)).toBe(false);
36
+ });
37
+
38
+ it('accepts a message from the iframe window with a matching origin', () => {
39
+ const event = { source: contentWindow, origin: host } as unknown as MessageEvent;
40
+ expect(isMessageFromIframe(event, iFrame, host)).toBe(true);
41
+ });
42
+
43
+ it('rejects (and warns) when the origin does not match the expected host', () => {
44
+ const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
45
+ const event = {
46
+ source: contentWindow,
47
+ origin: 'https://evil.example.com',
48
+ } as unknown as MessageEvent;
49
+
50
+ expect(isMessageFromIframe(event, iFrame, host)).toBe(false);
51
+ expect(warnSpy).toHaveBeenCalledTimes(1);
52
+ warnSpy.mockRestore();
53
+ });
54
+
55
+ it('fails open (accepts) when the expected origin cannot be resolved', () => {
56
+ const event = {
57
+ source: contentWindow,
58
+ origin: 'https://anything.example.com',
59
+ } as unknown as MessageEvent;
60
+ // 'garbage-host' is unparseable → origin check is skipped.
61
+ expect(isMessageFromIframe(event, iFrame, 'garbage-host')).toBe(true);
62
+ });
63
+
64
+ it('accepts when the message origin is empty', () => {
65
+ const event = { source: contentWindow, origin: '' } as unknown as MessageEvent;
66
+ expect(isMessageFromIframe(event, iFrame, host)).toBe(true);
67
+ });
68
+
69
+ it('rejects when the iframe is not set', () => {
70
+ const event = { source: contentWindow, origin: host } as unknown as MessageEvent;
71
+ expect(
72
+ isMessageFromIframe(event, null as unknown as HTMLIFrameElement, host),
73
+ ).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('postMessageToIframe', () => {
78
+ const host = 'https://ts.example.com';
79
+ const message = { type: 'someEvent', data: { a: 1 } };
80
+
81
+ it('posts with no transferables when no channel is provided', () => {
82
+ const postMessage = jest.fn();
83
+ const iFrame = { contentWindow: { postMessage } } as unknown as HTMLIFrameElement;
84
+
85
+ postMessageToIframe(iFrame, message, host);
86
+
87
+ expect(postMessage).toHaveBeenCalledWith(message, host, []);
88
+ });
89
+
90
+ it('transfers the channel port2 when a channel is provided', () => {
91
+ const postMessage = jest.fn();
92
+ const iFrame = { contentWindow: { postMessage } } as unknown as HTMLIFrameElement;
93
+ const port2 = {} as MessagePort;
94
+ const channel = { port2 } as MessageChannel;
95
+
96
+ postMessageToIframe(iFrame, message, host, channel);
97
+
98
+ expect(postMessage).toHaveBeenCalledWith(message, host, [port2]);
99
+ });
100
+
101
+ it('is a no-op when the iframe has no contentWindow', () => {
102
+ const iFrame = { contentWindow: null } as unknown as HTMLIFrameElement;
103
+ expect(() => postMessageToIframe(iFrame, message, host)).not.toThrow();
104
+ });
105
+ });
106
+
107
+ describe('sendMessageWithResponse', () => {
108
+ const host = 'https://ts.example.com';
109
+ const message = { type: 'trigger', data: {} };
110
+ let port1: { onmessage: ((e: { data: any }) => void) | null; close: jest.Mock };
111
+ let port2: MessagePort;
112
+ let postMessage: jest.Mock;
113
+ let iFrame: HTMLIFrameElement;
114
+ let OriginalMessageChannel: typeof MessageChannel;
115
+
116
+ beforeEach(() => {
117
+ port1 = { onmessage: null, close: jest.fn() };
118
+ port2 = {} as MessagePort;
119
+ OriginalMessageChannel = global.MessageChannel;
120
+ (global as any).MessageChannel = jest.fn(() => ({ port1, port2 }));
121
+ postMessage = jest.fn();
122
+ iFrame = { contentWindow: { postMessage } } as unknown as HTMLIFrameElement;
123
+ });
124
+
125
+ afterEach(() => {
126
+ global.MessageChannel = OriginalMessageChannel;
127
+ });
128
+
129
+ it('exposes the default response timeout', () => {
130
+ expect(MESSAGE_RESPONSE_TIMEOUT).toBe(30000);
131
+ });
132
+
133
+ it('posts the message with the channel port and resolves with the response', async () => {
134
+ const promise = sendMessageWithResponse(iFrame, message, host);
135
+
136
+ expect(postMessage).toHaveBeenCalledWith(message, host, [port2]);
137
+
138
+ // Simulate the app posting a response back on the channel — the
139
+ // handler resolves with the event's `data` payload.
140
+ port1.onmessage?.({ data: { value: 'ok' } });
141
+
142
+ await expect(promise).resolves.toEqual({ value: 'ok' });
143
+ expect(port1.close).toHaveBeenCalled();
144
+ });
145
+
146
+ it('rejects when the response carries a top-level error', async () => {
147
+ const promise = sendMessageWithResponse(iFrame, message, host);
148
+ port1.onmessage?.({ data: { error: 'boom' } });
149
+ await expect(promise).rejects.toBe('boom');
150
+ });
151
+
152
+ it('rejects when the error is nested under data', async () => {
153
+ const promise = sendMessageWithResponse(iFrame, message, host);
154
+ port1.onmessage?.({ data: { data: { error: 'nested-boom' } } });
155
+ await expect(promise).rejects.toBe('nested-boom');
156
+ });
157
+
158
+ it('resolves with an empty response when no data is returned', async () => {
159
+ const promise = sendMessageWithResponse(iFrame, message, host);
160
+ port1.onmessage?.({ data: undefined });
161
+ await expect(promise).resolves.toBeUndefined();
162
+ });
163
+
164
+ it('resolves with a timeout Error when no response arrives in time', async () => {
165
+ jest.useFakeTimers();
166
+ const promise = sendMessageWithResponse(iFrame, message, host, 100);
167
+
168
+ jest.advanceTimersByTime(100);
169
+
170
+ const result = await promise;
171
+ expect(result).toBeInstanceOf(Error);
172
+ expect((result as Error).message).toBe(ERROR_MESSAGE.TRIGGER_TIMED_OUT);
173
+ expect(port1.close).toHaveBeenCalled();
174
+ jest.useRealTimers();
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Copyright (c) 2026
3
+ *
4
+ * Dedicated transport layer for SDK <-> ThoughtSpot iframe messaging.
5
+ *
6
+ * Owns the postMessage/MessageChannel mechanics and origin resolution so
7
+ * that embed/business logic (ts-embed.ts, hostEventClient) never touches
8
+ * the wire directly. The wire format is unchanged from the historical
9
+ * implementation — this layer is a pure structural extraction.
10
+ */
11
+ import { ERROR_MESSAGE } from '../../errors';
12
+ import { logger } from '../logger';
13
+
14
+ /**
15
+ * Default time to wait for the app to respond to a request-response
16
+ * message before giving up and reclaiming the channel.
17
+ */
18
+ export const MESSAGE_RESPONSE_TIMEOUT = 30000;
19
+
20
+ /**
21
+ * Single implementation of origin resolution for the embedded app.
22
+ * Returns the origin (scheme://host[:port]) for a ThoughtSpot host URL,
23
+ * or null when it cannot be parsed.
24
+ * @param thoughtSpotHost
25
+ */
26
+ export const resolveMessageOrigin = (thoughtSpotHost: string): string | null => {
27
+ try {
28
+ return new URL(thoughtSpotHost).origin;
29
+ } catch (e) {
30
+ return null;
31
+ }
32
+ };
33
+
34
+ /**
35
+ * Validates that an inbound window message came from the embedded
36
+ * ThoughtSpot app: the source window must be the embed iframe's window and,
37
+ * when the expected origin is resolvable, the message origin must match it.
38
+ *
39
+ * Fails open (returns true) on the origin check when the expected origin
40
+ * cannot be resolved, to avoid breaking unconventional-but-working setups;
41
+ * a warning is logged on mismatch either way.
42
+ * @param event The message event received on window.
43
+ * @param iFrame The embed iframe the message should originate from.
44
+ * @param thoughtSpotHost The configured ThoughtSpot host.
45
+ */
46
+ export const isMessageFromIframe = (
47
+ event: MessageEvent,
48
+ iFrame: HTMLIFrameElement,
49
+ thoughtSpotHost: string,
50
+ ): boolean => {
51
+ if (event.source !== iFrame?.contentWindow) {
52
+ return false;
53
+ }
54
+ const expectedOrigin = resolveMessageOrigin(thoughtSpotHost);
55
+ if (expectedOrigin && event.origin && event.origin !== expectedOrigin) {
56
+ logger.warn(
57
+ `Dropped message from unexpected origin ${event.origin}; expected ${expectedOrigin}`,
58
+ );
59
+ return false;
60
+ }
61
+ return true;
62
+ };
63
+
64
+ /**
65
+ * Posts a one-way message to the embedded app's iframe, optionally
66
+ * transferring a MessageChannel port for the response.
67
+ * @param iFrame
68
+ * @param message
69
+ * @param thoughtSpotHost Used as the postMessage targetOrigin.
70
+ * @param channel
71
+ */
72
+ export const postMessageToIframe = (
73
+ iFrame: HTMLIFrameElement,
74
+ message: { type: string; data: any; context?: any },
75
+ thoughtSpotHost: string,
76
+ channel?: MessageChannel,
77
+ ): void => iFrame.contentWindow?.postMessage(
78
+ message,
79
+ thoughtSpotHost,
80
+ channel ? [channel.port2] : [],
81
+ );
82
+
83
+ /**
84
+ * Sends a request-response message to the embedded app over a dedicated
85
+ * MessageChannel. Resolves with the response payload; rejects when the app
86
+ * responds with an error; resolves with a timeout Error object when no
87
+ * response arrives in time (historical behavior, preserved for backward
88
+ * compatibility).
89
+ * @param iFrame
90
+ * @param message
91
+ * @param thoughtSpotHost
92
+ * @param timeoutMs
93
+ */
94
+ export function sendMessageWithResponse(
95
+ iFrame: HTMLIFrameElement,
96
+ message: { type: string; data: any; context?: any },
97
+ thoughtSpotHost: string,
98
+ timeoutMs: number = MESSAGE_RESPONSE_TIMEOUT,
99
+ ): Promise<any> {
100
+ return new Promise<any>((res, rej) => {
101
+ const channel = new MessageChannel();
102
+
103
+ // Close the messageChannel and resolve the promise if timeout.
104
+ const timeoutId = setTimeout(() => {
105
+ channel.port1.close();
106
+ res(new Error(ERROR_MESSAGE.TRIGGER_TIMED_OUT));
107
+ }, timeoutMs);
108
+
109
+ channel.port1.onmessage = ({ data: responseData }) => {
110
+ clearTimeout(timeoutId);
111
+ channel.port1.close();
112
+ const error = responseData?.error || responseData?.data?.error;
113
+ if (error) {
114
+ rej(error);
115
+ } else {
116
+ res(responseData);
117
+ }
118
+ };
119
+
120
+ postMessageToIframe(iFrame, message, thoughtSpotHost, channel);
121
+ });
122
+ }
package/src/utils.spec.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  deepMerge,
33
33
  getHostEventsConfig,
34
34
  isWindowUndefined,
35
+ calculateElementCenter,
35
36
  getOffsetTop,
36
37
  getDOMNode,
37
38
  getOperationNameFromQuery,
@@ -1319,6 +1320,76 @@ describe('isWindowUndefined', () => {
1319
1320
  // ---------------------------------------------------------------------------
1320
1321
  // getOffsetTop
1321
1322
  // ---------------------------------------------------------------------------
1323
+ describe('calculateElementCenter', () => {
1324
+ const setViewport = (scrollY: number, innerHeight: number) => {
1325
+ Object.defineProperty(window, 'scrollY', { value: scrollY, configurable: true });
1326
+ Object.defineProperty(window, 'innerHeight', { value: innerHeight, configurable: true });
1327
+ };
1328
+
1329
+ const mockElement = (offsetTop: number, offsetHeight: number) => ({
1330
+ getBoundingClientRect: () => ({ top: offsetTop - window.scrollY }),
1331
+ offsetHeight,
1332
+ }) as unknown as HTMLElement;
1333
+
1334
+ test('centers on the element when it is shorter than the viewport', () => {
1335
+ setViewport(0, 1000);
1336
+ // Element sits at the top of the page and is 400px tall, fully visible.
1337
+ const result = calculateElementCenter(mockElement(0, 400));
1338
+ expect(result).toEqual({
1339
+ iframeCenter: 200,
1340
+ iframeScrolled: 0,
1341
+ iframeHeight: 400,
1342
+ viewPortHeight: 1000,
1343
+ iframeVisibleViewPort: 400,
1344
+ });
1345
+ });
1346
+
1347
+ test('centers on the visible slice when the element is taller than the viewport', () => {
1348
+ setViewport(0, 600);
1349
+ // 2000px element, only the top 600px is on screen.
1350
+ const result = calculateElementCenter(mockElement(0, 2000));
1351
+ expect(result.iframeVisibleViewPort).toBe(600);
1352
+ expect(result.iframeCenter).toBe(300);
1353
+ expect(result.iframeScrolled).toBe(0);
1354
+ });
1355
+
1356
+ test('tracks the center as the page scrolls into the element', () => {
1357
+ setViewport(500, 600);
1358
+ // Scrolled 500px into a 2000px element that starts at the page top.
1359
+ const result = calculateElementCenter(mockElement(0, 2000));
1360
+ expect(result.iframeScrolled).toBe(500);
1361
+ // 600px still visible, offset by the 500px already scrolled past.
1362
+ expect(result.iframeVisibleViewPort).toBe(600);
1363
+ expect(result.iframeCenter).toBe(800);
1364
+ });
1365
+
1366
+ test('clamps the visible slice to what is left of the element', () => {
1367
+ setViewport(1800, 600);
1368
+ // Only the last 200px of the 2000px element remains below the fold.
1369
+ const result = calculateElementCenter(mockElement(0, 2000));
1370
+ expect(result.iframeVisibleViewPort).toBe(200);
1371
+ expect(result.iframeCenter).toBe(1900);
1372
+ });
1373
+
1374
+ test('measures only the on-screen part when the element starts below the fold', () => {
1375
+ setViewport(0, 1000);
1376
+ // Element begins 800px down, so 200px of it is visible.
1377
+ const result = calculateElementCenter(mockElement(800, 400));
1378
+ expect(result.iframeScrolled).toBe(-800);
1379
+ expect(result.iframeVisibleViewPort).toBe(200);
1380
+ expect(result.iframeCenter).toBe(100);
1381
+ });
1382
+
1383
+ test('never reports more visible height than the element has', () => {
1384
+ setViewport(0, 1000);
1385
+ // Element is 100px tall and starts 200px down: the viewport has room to
1386
+ // spare, so the visible slice is the element itself, not the gap.
1387
+ const result = calculateElementCenter(mockElement(200, 100));
1388
+ expect(result.iframeVisibleViewPort).toBe(100);
1389
+ expect(result.iframeCenter).toBe(50);
1390
+ });
1391
+ });
1392
+
1322
1393
  describe('getOffsetTop', () => {
1323
1394
  test('returns rect.top + window.scrollY', () => {
1324
1395
  const mockElement = {
package/src/utils.ts CHANGED
@@ -776,3 +776,41 @@ export const setParamIfDefined = <T>(
776
776
  queryParams[param] = asBoolean ? !!value : value;
777
777
  }
778
778
  };
779
+
780
+ /**
781
+ * Calculates the element center for the currently visible viewport of the
782
+ * element, using the scroll position of the host app, the offsetTop of the
783
+ * element in the host app, and the viewport height of the tab.
784
+ *
785
+ * The returned keys are sent to the ThoughtSpot app over postMessage, so they
786
+ * are named after the iframe and must not be renamed.
787
+ * @param element The element to measure
788
+ * @returns The element center within the visible viewport, the element height,
789
+ * and the viewport height.
790
+ */
791
+ export const calculateElementCenter = (element: HTMLElement) => {
792
+ const offsetTopClient = getOffsetTop(element);
793
+ const scrollTopClient = window.scrollY;
794
+ const viewPortHeight = window.innerHeight;
795
+ const iframeHeight = element.offsetHeight;
796
+ const iframeScrolled = scrollTopClient - offsetTopClient;
797
+ let iframeVisibleViewPort;
798
+ let iframeOffset;
799
+
800
+ if (iframeScrolled < 0) {
801
+ iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
802
+ iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
803
+ iframeOffset = 0;
804
+ } else {
805
+ iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
806
+ iframeOffset = iframeScrolled;
807
+ }
808
+ const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
809
+ return {
810
+ iframeCenter,
811
+ iframeScrolled,
812
+ iframeHeight,
813
+ viewPortHeight,
814
+ iframeVisibleViewPort,
815
+ };
816
+ };