@tracelog/lib 0.0.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 (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/dist/browser/tracelog.js +4040 -0
  4. package/dist/browser/web-vitals-CCnqwnC8.mjs +198 -0
  5. package/dist/cjs/api.d.ts +46 -0
  6. package/dist/cjs/api.js +224 -0
  7. package/dist/cjs/app.constants.d.ts +1 -0
  8. package/dist/cjs/app.constants.js +5 -0
  9. package/dist/cjs/app.d.ts +59 -0
  10. package/dist/cjs/app.js +272 -0
  11. package/dist/cjs/app.types.d.ts +6 -0
  12. package/dist/cjs/app.types.js +22 -0
  13. package/dist/cjs/constants/api.constants.d.ts +4 -0
  14. package/dist/cjs/constants/api.constants.js +18 -0
  15. package/dist/cjs/constants/browser.constants.d.ts +3 -0
  16. package/dist/cjs/constants/browser.constants.js +41 -0
  17. package/dist/cjs/constants/index.d.ts +8 -0
  18. package/dist/cjs/constants/index.js +24 -0
  19. package/dist/cjs/constants/initialization.constants.d.ts +40 -0
  20. package/dist/cjs/constants/initialization.constants.js +48 -0
  21. package/dist/cjs/constants/limits.constants.d.ts +25 -0
  22. package/dist/cjs/constants/limits.constants.js +40 -0
  23. package/dist/cjs/constants/security.constants.d.ts +1 -0
  24. package/dist/cjs/constants/security.constants.js +12 -0
  25. package/dist/cjs/constants/storage.constants.d.ts +9 -0
  26. package/dist/cjs/constants/storage.constants.js +22 -0
  27. package/dist/cjs/constants/timing.constants.d.ts +22 -0
  28. package/dist/cjs/constants/timing.constants.js +34 -0
  29. package/dist/cjs/constants/validation.constants.d.ts +13 -0
  30. package/dist/cjs/constants/validation.constants.js +31 -0
  31. package/dist/cjs/handlers/click.handler.d.ts +17 -0
  32. package/dist/cjs/handlers/click.handler.js +199 -0
  33. package/dist/cjs/handlers/error.handler.d.ts +15 -0
  34. package/dist/cjs/handlers/error.handler.js +97 -0
  35. package/dist/cjs/handlers/network.handler.d.ts +16 -0
  36. package/dist/cjs/handlers/network.handler.js +136 -0
  37. package/dist/cjs/handlers/page-view.handler.d.ts +15 -0
  38. package/dist/cjs/handlers/page-view.handler.js +83 -0
  39. package/dist/cjs/handlers/performance.handler.d.ts +19 -0
  40. package/dist/cjs/handlers/performance.handler.js +255 -0
  41. package/dist/cjs/handlers/scroll.handler.d.ts +16 -0
  42. package/dist/cjs/handlers/scroll.handler.js +138 -0
  43. package/dist/cjs/handlers/session.handler.d.ts +29 -0
  44. package/dist/cjs/handlers/session.handler.js +357 -0
  45. package/dist/cjs/integrations/google-analytics.integration.d.ts +18 -0
  46. package/dist/cjs/integrations/google-analytics.integration.js +159 -0
  47. package/dist/cjs/listeners/activity-listener-manager.d.ts +8 -0
  48. package/dist/cjs/listeners/activity-listener-manager.js +32 -0
  49. package/dist/cjs/listeners/index.d.ts +6 -0
  50. package/dist/cjs/listeners/index.js +14 -0
  51. package/dist/cjs/listeners/input-listener-managers.d.ts +15 -0
  52. package/dist/cjs/listeners/input-listener-managers.js +58 -0
  53. package/dist/cjs/listeners/listeners.types.d.ts +4 -0
  54. package/dist/cjs/listeners/listeners.types.js +2 -0
  55. package/dist/cjs/listeners/touch-listener-manager.d.ts +10 -0
  56. package/dist/cjs/listeners/touch-listener-manager.js +56 -0
  57. package/dist/cjs/listeners/unload-listener-manager.d.ts +8 -0
  58. package/dist/cjs/listeners/unload-listener-manager.js +30 -0
  59. package/dist/cjs/listeners/visibility-listener-manager.d.ts +12 -0
  60. package/dist/cjs/listeners/visibility-listener-manager.js +83 -0
  61. package/dist/cjs/managers/api.manager.d.ts +3 -0
  62. package/dist/cjs/managers/api.manager.js +14 -0
  63. package/dist/cjs/managers/config.manager.d.ts +7 -0
  64. package/dist/cjs/managers/config.manager.js +94 -0
  65. package/dist/cjs/managers/cross-tab-session.manager.d.ts +170 -0
  66. package/dist/cjs/managers/cross-tab-session.manager.js +730 -0
  67. package/dist/cjs/managers/event.manager.d.ts +61 -0
  68. package/dist/cjs/managers/event.manager.js +508 -0
  69. package/dist/cjs/managers/sampling.manager.d.ts +8 -0
  70. package/dist/cjs/managers/sampling.manager.js +53 -0
  71. package/dist/cjs/managers/sender.manager.d.ts +46 -0
  72. package/dist/cjs/managers/sender.manager.js +304 -0
  73. package/dist/cjs/managers/session-recovery.manager.d.ts +65 -0
  74. package/dist/cjs/managers/session-recovery.manager.js +237 -0
  75. package/dist/cjs/managers/session.manager.d.ts +72 -0
  76. package/dist/cjs/managers/session.manager.js +587 -0
  77. package/dist/cjs/managers/state.manager.d.ts +5 -0
  78. package/dist/cjs/managers/state.manager.js +23 -0
  79. package/dist/cjs/managers/storage.manager.d.ts +10 -0
  80. package/dist/cjs/managers/storage.manager.js +81 -0
  81. package/dist/cjs/managers/tags.manager.d.ts +12 -0
  82. package/dist/cjs/managers/tags.manager.js +289 -0
  83. package/dist/cjs/managers/user.manager.d.ts +7 -0
  84. package/dist/cjs/managers/user.manager.js +22 -0
  85. package/dist/cjs/public-api.d.ts +1 -0
  86. package/dist/cjs/public-api.js +37 -0
  87. package/dist/cjs/types/api.types.d.ts +21 -0
  88. package/dist/cjs/types/api.types.js +25 -0
  89. package/dist/cjs/types/common.types.d.ts +1 -0
  90. package/dist/cjs/types/common.types.js +2 -0
  91. package/dist/cjs/types/config.types.d.ts +104 -0
  92. package/dist/cjs/types/config.types.js +2 -0
  93. package/dist/cjs/types/device.types.d.ts +6 -0
  94. package/dist/cjs/types/device.types.js +10 -0
  95. package/dist/cjs/types/event.types.d.ts +104 -0
  96. package/dist/cjs/types/event.types.js +25 -0
  97. package/dist/cjs/types/index.d.ts +13 -0
  98. package/dist/cjs/types/index.js +29 -0
  99. package/dist/cjs/types/log.types.d.ts +4 -0
  100. package/dist/cjs/types/log.types.js +2 -0
  101. package/dist/cjs/types/mode.types.d.ts +7 -0
  102. package/dist/cjs/types/mode.types.js +11 -0
  103. package/dist/cjs/types/queue.types.d.ts +23 -0
  104. package/dist/cjs/types/queue.types.js +2 -0
  105. package/dist/cjs/types/session.types.d.ts +65 -0
  106. package/dist/cjs/types/session.types.js +2 -0
  107. package/dist/cjs/types/state.types.d.ts +12 -0
  108. package/dist/cjs/types/state.types.js +2 -0
  109. package/dist/cjs/types/tag.types.d.ts +43 -0
  110. package/dist/cjs/types/tag.types.js +31 -0
  111. package/dist/cjs/types/validation-error.types.d.ts +42 -0
  112. package/dist/cjs/types/validation-error.types.js +68 -0
  113. package/dist/cjs/types/web-vitals.types.d.ts +6 -0
  114. package/dist/cjs/types/web-vitals.types.js +2 -0
  115. package/dist/cjs/types/window.types.d.ts +17 -0
  116. package/dist/cjs/types/window.types.js +2 -0
  117. package/dist/cjs/utils/browser/device-detector.utils.d.ts +6 -0
  118. package/dist/cjs/utils/browser/device-detector.utils.js +71 -0
  119. package/dist/cjs/utils/browser/index.d.ts +2 -0
  120. package/dist/cjs/utils/browser/index.js +18 -0
  121. package/dist/cjs/utils/browser/utm-params.utils.d.ts +6 -0
  122. package/dist/cjs/utils/browser/utm-params.utils.js +37 -0
  123. package/dist/cjs/utils/data/index.d.ts +1 -0
  124. package/dist/cjs/utils/data/index.js +17 -0
  125. package/dist/cjs/utils/data/uuid.utils.d.ts +5 -0
  126. package/dist/cjs/utils/data/uuid.utils.js +18 -0
  127. package/dist/cjs/utils/index.d.ts +6 -0
  128. package/dist/cjs/utils/index.js +22 -0
  129. package/dist/cjs/utils/logging/debug-logger.utils.d.ts +56 -0
  130. package/dist/cjs/utils/logging/debug-logger.utils.js +139 -0
  131. package/dist/cjs/utils/logging/index.d.ts +1 -0
  132. package/dist/cjs/utils/logging/index.js +5 -0
  133. package/dist/cjs/utils/network/index.d.ts +1 -0
  134. package/dist/cjs/utils/network/index.js +17 -0
  135. package/dist/cjs/utils/network/url.utils.d.ts +20 -0
  136. package/dist/cjs/utils/network/url.utils.js +172 -0
  137. package/dist/cjs/utils/security/index.d.ts +1 -0
  138. package/dist/cjs/utils/security/index.js +17 -0
  139. package/dist/cjs/utils/security/sanitize.utils.d.ts +32 -0
  140. package/dist/cjs/utils/security/sanitize.utils.js +319 -0
  141. package/dist/cjs/utils/validations/config-validations.utils.d.ts +42 -0
  142. package/dist/cjs/utils/validations/config-validations.utils.js +297 -0
  143. package/dist/cjs/utils/validations/event-validations.utils.d.ts +12 -0
  144. package/dist/cjs/utils/validations/event-validations.utils.js +30 -0
  145. package/dist/cjs/utils/validations/index.d.ts +5 -0
  146. package/dist/cjs/utils/validations/index.js +21 -0
  147. package/dist/cjs/utils/validations/metadata-validations.utils.d.ts +22 -0
  148. package/dist/cjs/utils/validations/metadata-validations.utils.js +115 -0
  149. package/dist/cjs/utils/validations/type-guards.utils.d.ts +6 -0
  150. package/dist/cjs/utils/validations/type-guards.utils.js +31 -0
  151. package/dist/cjs/utils/validations/url-validations.utils.d.ts +15 -0
  152. package/dist/cjs/utils/validations/url-validations.utils.js +47 -0
  153. package/dist/esm/api.d.ts +46 -0
  154. package/dist/esm/api.js +183 -0
  155. package/dist/esm/app.constants.d.ts +1 -0
  156. package/dist/esm/app.constants.js +1 -0
  157. package/dist/esm/app.d.ts +59 -0
  158. package/dist/esm/app.js +268 -0
  159. package/dist/esm/app.types.d.ts +6 -0
  160. package/dist/esm/app.types.js +6 -0
  161. package/dist/esm/constants/api.constants.d.ts +4 -0
  162. package/dist/esm/constants/api.constants.js +14 -0
  163. package/dist/esm/constants/browser.constants.d.ts +3 -0
  164. package/dist/esm/constants/browser.constants.js +38 -0
  165. package/dist/esm/constants/index.d.ts +8 -0
  166. package/dist/esm/constants/index.js +8 -0
  167. package/dist/esm/constants/initialization.constants.d.ts +40 -0
  168. package/dist/esm/constants/initialization.constants.js +45 -0
  169. package/dist/esm/constants/limits.constants.d.ts +25 -0
  170. package/dist/esm/constants/limits.constants.js +37 -0
  171. package/dist/esm/constants/security.constants.d.ts +1 -0
  172. package/dist/esm/constants/security.constants.js +9 -0
  173. package/dist/esm/constants/storage.constants.d.ts +9 -0
  174. package/dist/esm/constants/storage.constants.js +11 -0
  175. package/dist/esm/constants/timing.constants.d.ts +22 -0
  176. package/dist/esm/constants/timing.constants.js +31 -0
  177. package/dist/esm/constants/validation.constants.d.ts +13 -0
  178. package/dist/esm/constants/validation.constants.js +28 -0
  179. package/dist/esm/handlers/click.handler.d.ts +17 -0
  180. package/dist/esm/handlers/click.handler.js +195 -0
  181. package/dist/esm/handlers/error.handler.d.ts +15 -0
  182. package/dist/esm/handlers/error.handler.js +93 -0
  183. package/dist/esm/handlers/network.handler.d.ts +16 -0
  184. package/dist/esm/handlers/network.handler.js +132 -0
  185. package/dist/esm/handlers/page-view.handler.d.ts +15 -0
  186. package/dist/esm/handlers/page-view.handler.js +79 -0
  187. package/dist/esm/handlers/performance.handler.d.ts +19 -0
  188. package/dist/esm/handlers/performance.handler.js +218 -0
  189. package/dist/esm/handlers/scroll.handler.d.ts +16 -0
  190. package/dist/esm/handlers/scroll.handler.js +134 -0
  191. package/dist/esm/handlers/session.handler.d.ts +29 -0
  192. package/dist/esm/handlers/session.handler.js +353 -0
  193. package/dist/esm/integrations/google-analytics.integration.d.ts +18 -0
  194. package/dist/esm/integrations/google-analytics.integration.js +155 -0
  195. package/dist/esm/listeners/activity-listener-manager.d.ts +8 -0
  196. package/dist/esm/listeners/activity-listener-manager.js +28 -0
  197. package/dist/esm/listeners/index.d.ts +6 -0
  198. package/dist/esm/listeners/index.js +5 -0
  199. package/dist/esm/listeners/input-listener-managers.d.ts +15 -0
  200. package/dist/esm/listeners/input-listener-managers.js +53 -0
  201. package/dist/esm/listeners/listeners.types.d.ts +4 -0
  202. package/dist/esm/listeners/listeners.types.js +1 -0
  203. package/dist/esm/listeners/touch-listener-manager.d.ts +10 -0
  204. package/dist/esm/listeners/touch-listener-manager.js +52 -0
  205. package/dist/esm/listeners/unload-listener-manager.d.ts +8 -0
  206. package/dist/esm/listeners/unload-listener-manager.js +26 -0
  207. package/dist/esm/listeners/visibility-listener-manager.d.ts +12 -0
  208. package/dist/esm/listeners/visibility-listener-manager.js +79 -0
  209. package/dist/esm/managers/api.manager.d.ts +3 -0
  210. package/dist/esm/managers/api.manager.js +10 -0
  211. package/dist/esm/managers/config.manager.d.ts +7 -0
  212. package/dist/esm/managers/config.manager.js +90 -0
  213. package/dist/esm/managers/cross-tab-session.manager.d.ts +170 -0
  214. package/dist/esm/managers/cross-tab-session.manager.js +726 -0
  215. package/dist/esm/managers/event.manager.d.ts +61 -0
  216. package/dist/esm/managers/event.manager.js +504 -0
  217. package/dist/esm/managers/sampling.manager.d.ts +8 -0
  218. package/dist/esm/managers/sampling.manager.js +49 -0
  219. package/dist/esm/managers/sender.manager.d.ts +46 -0
  220. package/dist/esm/managers/sender.manager.js +300 -0
  221. package/dist/esm/managers/session-recovery.manager.d.ts +65 -0
  222. package/dist/esm/managers/session-recovery.manager.js +233 -0
  223. package/dist/esm/managers/session.manager.d.ts +72 -0
  224. package/dist/esm/managers/session.manager.js +583 -0
  225. package/dist/esm/managers/state.manager.d.ts +5 -0
  226. package/dist/esm/managers/state.manager.js +19 -0
  227. package/dist/esm/managers/storage.manager.d.ts +10 -0
  228. package/dist/esm/managers/storage.manager.js +77 -0
  229. package/dist/esm/managers/tags.manager.d.ts +12 -0
  230. package/dist/esm/managers/tags.manager.js +285 -0
  231. package/dist/esm/managers/user.manager.d.ts +7 -0
  232. package/dist/esm/managers/user.manager.js +18 -0
  233. package/dist/esm/public-api.d.ts +1 -0
  234. package/dist/esm/public-api.js +1 -0
  235. package/dist/esm/types/api.types.d.ts +21 -0
  236. package/dist/esm/types/api.types.js +22 -0
  237. package/dist/esm/types/common.types.d.ts +1 -0
  238. package/dist/esm/types/common.types.js +1 -0
  239. package/dist/esm/types/config.types.d.ts +104 -0
  240. package/dist/esm/types/config.types.js +1 -0
  241. package/dist/esm/types/device.types.d.ts +6 -0
  242. package/dist/esm/types/device.types.js +7 -0
  243. package/dist/esm/types/event.types.d.ts +104 -0
  244. package/dist/esm/types/event.types.js +22 -0
  245. package/dist/esm/types/index.d.ts +13 -0
  246. package/dist/esm/types/index.js +13 -0
  247. package/dist/esm/types/log.types.d.ts +4 -0
  248. package/dist/esm/types/log.types.js +1 -0
  249. package/dist/esm/types/mode.types.d.ts +7 -0
  250. package/dist/esm/types/mode.types.js +8 -0
  251. package/dist/esm/types/queue.types.d.ts +23 -0
  252. package/dist/esm/types/queue.types.js +1 -0
  253. package/dist/esm/types/session.types.d.ts +65 -0
  254. package/dist/esm/types/session.types.js +1 -0
  255. package/dist/esm/types/state.types.d.ts +12 -0
  256. package/dist/esm/types/state.types.js +1 -0
  257. package/dist/esm/types/tag.types.d.ts +43 -0
  258. package/dist/esm/types/tag.types.js +28 -0
  259. package/dist/esm/types/validation-error.types.d.ts +42 -0
  260. package/dist/esm/types/validation-error.types.js +59 -0
  261. package/dist/esm/types/web-vitals.types.d.ts +6 -0
  262. package/dist/esm/types/web-vitals.types.js +1 -0
  263. package/dist/esm/types/window.types.d.ts +17 -0
  264. package/dist/esm/types/window.types.js +1 -0
  265. package/dist/esm/utils/browser/device-detector.utils.d.ts +6 -0
  266. package/dist/esm/utils/browser/device-detector.utils.js +67 -0
  267. package/dist/esm/utils/browser/index.d.ts +2 -0
  268. package/dist/esm/utils/browser/index.js +2 -0
  269. package/dist/esm/utils/browser/utm-params.utils.d.ts +6 -0
  270. package/dist/esm/utils/browser/utm-params.utils.js +33 -0
  271. package/dist/esm/utils/data/index.d.ts +1 -0
  272. package/dist/esm/utils/data/index.js +1 -0
  273. package/dist/esm/utils/data/uuid.utils.d.ts +5 -0
  274. package/dist/esm/utils/data/uuid.utils.js +14 -0
  275. package/dist/esm/utils/index.d.ts +6 -0
  276. package/dist/esm/utils/index.js +6 -0
  277. package/dist/esm/utils/logging/debug-logger.utils.d.ts +56 -0
  278. package/dist/esm/utils/logging/debug-logger.utils.js +136 -0
  279. package/dist/esm/utils/logging/index.d.ts +1 -0
  280. package/dist/esm/utils/logging/index.js +1 -0
  281. package/dist/esm/utils/network/index.d.ts +1 -0
  282. package/dist/esm/utils/network/index.js +1 -0
  283. package/dist/esm/utils/network/url.utils.d.ts +20 -0
  284. package/dist/esm/utils/network/url.utils.js +166 -0
  285. package/dist/esm/utils/security/index.d.ts +1 -0
  286. package/dist/esm/utils/security/index.js +1 -0
  287. package/dist/esm/utils/security/sanitize.utils.d.ts +32 -0
  288. package/dist/esm/utils/security/sanitize.utils.js +311 -0
  289. package/dist/esm/utils/validations/config-validations.utils.d.ts +42 -0
  290. package/dist/esm/utils/validations/config-validations.utils.js +289 -0
  291. package/dist/esm/utils/validations/event-validations.utils.d.ts +12 -0
  292. package/dist/esm/utils/validations/event-validations.utils.js +26 -0
  293. package/dist/esm/utils/validations/index.d.ts +5 -0
  294. package/dist/esm/utils/validations/index.js +5 -0
  295. package/dist/esm/utils/validations/metadata-validations.utils.d.ts +22 -0
  296. package/dist/esm/utils/validations/metadata-validations.utils.js +110 -0
  297. package/dist/esm/utils/validations/type-guards.utils.d.ts +6 -0
  298. package/dist/esm/utils/validations/type-guards.utils.js +27 -0
  299. package/dist/esm/utils/validations/url-validations.utils.d.ts +15 -0
  300. package/dist/esm/utils/validations/url-validations.utils.js +42 -0
  301. package/package.json +80 -0
@@ -0,0 +1,726 @@
1
+ import { TAB_HEARTBEAT_INTERVAL_MS, TAB_ELECTION_TIMEOUT_MS, DEFAULT_SESSION_TIMEOUT_MS } from '../constants';
2
+ import { BROADCAST_CHANNEL_NAME, CROSS_TAB_SESSION_KEY, TAB_SPECIFIC_INFO_KEY } from '../constants/storage.constants';
3
+ import { generateUUID } from '../utils';
4
+ import { debugLog } from '../utils/logging';
5
+ import { StateManager } from './state.manager';
6
+ export class CrossTabSessionManager extends StateManager {
7
+ constructor(storageManager, projectId, config, callbacks) {
8
+ super();
9
+ this.callbacks = callbacks;
10
+ this.leaderTabId = null;
11
+ this.isTabLeader = false;
12
+ this.heartbeatInterval = null;
13
+ this.electionTimeout = null;
14
+ this.cleanupTimeout = null;
15
+ this.sessionEnded = false;
16
+ // Additional timeout tracking for proper cleanup
17
+ this.fallbackLeadershipTimeout = null;
18
+ this.electionDelayTimeout = null;
19
+ this.tabInfoCleanupTimeout = null;
20
+ this.closingAnnouncementTimeout = null;
21
+ this.leaderHealthCheckInterval = null;
22
+ this.lastHeartbeatSent = 0;
23
+ this.storageManager = storageManager;
24
+ this.projectId = projectId;
25
+ this.tabId = generateUUID();
26
+ this.config = {
27
+ tabHeartbeatIntervalMs: TAB_HEARTBEAT_INTERVAL_MS,
28
+ tabElectionTimeoutMs: TAB_ELECTION_TIMEOUT_MS,
29
+ debugMode: (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') ?? false,
30
+ ...config,
31
+ };
32
+ this.tabInfo = {
33
+ id: this.tabId,
34
+ lastHeartbeat: Date.now(),
35
+ isLeader: false,
36
+ sessionId: '',
37
+ startTime: Date.now(),
38
+ };
39
+ this.broadcastChannel = this.initializeBroadcastChannel();
40
+ this.initialize();
41
+ }
42
+ /**
43
+ * Initialize BroadcastChannel if supported
44
+ */
45
+ initializeBroadcastChannel() {
46
+ if (!this.isBroadcastChannelSupported()) {
47
+ return null;
48
+ }
49
+ try {
50
+ const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME(this.projectId));
51
+ this.setupBroadcastListeners(channel);
52
+ return channel;
53
+ }
54
+ catch (error) {
55
+ if (this.config.debugMode) {
56
+ debugLog.warn('CrossTabSession', 'Failed to initialize BroadcastChannel', { error });
57
+ }
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Initialize the cross-tab session manager
63
+ */
64
+ initialize() {
65
+ if (!this.broadcastChannel) {
66
+ // Fallback to single-tab behavior when BroadcastChannel is not available
67
+ this.becomeLeader(); // This will create a session and set up the tab as leader
68
+ return;
69
+ }
70
+ // Check for existing session
71
+ const existingSession = this.getStoredSessionContext();
72
+ if (existingSession) {
73
+ // Try to join existing session
74
+ this.tryJoinExistingSession(existingSession);
75
+ }
76
+ else {
77
+ // Start new session leadership election
78
+ this.startLeaderElection();
79
+ }
80
+ // Start heartbeat
81
+ this.startHeartbeat();
82
+ // Fallback mechanism: only in multi-tab scenarios (when BroadcastChannel is available)
83
+ if (this.broadcastChannel) {
84
+ this.setupLeadershipFallback();
85
+ }
86
+ }
87
+ /**
88
+ * Check if this tab should be the session leader
89
+ */
90
+ tryJoinExistingSession(sessionContext) {
91
+ if (this.config.debugMode) {
92
+ debugLog.debug('CrossTabSession', `Attempting to join existing session: ${sessionContext.sessionId}`);
93
+ }
94
+ // Set session info
95
+ this.tabInfo.sessionId = sessionContext.sessionId;
96
+ // Request leadership status from other tabs
97
+ this.requestLeadershipStatus();
98
+ // Update session context with new tab
99
+ sessionContext.tabCount += 1;
100
+ sessionContext.lastActivity = Date.now();
101
+ this.storeSessionContext(sessionContext);
102
+ // Notify activity callback
103
+ if (this.callbacks?.onTabActivity) {
104
+ this.callbacks.onTabActivity();
105
+ }
106
+ }
107
+ /**
108
+ * Request leadership status from other tabs
109
+ */
110
+ requestLeadershipStatus() {
111
+ if (!this.broadcastChannel)
112
+ return;
113
+ // Clear any existing election timeout
114
+ if (this.electionTimeout) {
115
+ clearTimeout(this.electionTimeout);
116
+ this.electionTimeout = null;
117
+ }
118
+ const message = {
119
+ type: 'election_request',
120
+ tabId: this.tabId,
121
+ sessionId: this.tabInfo.sessionId,
122
+ timestamp: Date.now(),
123
+ };
124
+ this.broadcastChannel.postMessage(message);
125
+ // Set timeout for election with additional random delay to prevent race conditions
126
+ const randomDelay = Math.floor(Math.random() * 500); // 0-500ms random delay
127
+ this.electionTimeout = window.setTimeout(() => {
128
+ // No response means we become the leader
129
+ if (!this.isTabLeader) {
130
+ this.becomeLeader();
131
+ }
132
+ }, this.config.tabElectionTimeoutMs + randomDelay);
133
+ }
134
+ /**
135
+ * Start leader election process with debouncing to prevent excessive elections
136
+ */
137
+ startLeaderElection() {
138
+ // Prevent multiple concurrent elections
139
+ if (this.electionTimeout) {
140
+ if (this.config.debugMode) {
141
+ debugLog.debug('CrossTabSession', 'Leader election already in progress, skipping');
142
+ }
143
+ return;
144
+ }
145
+ if (this.config.debugMode) {
146
+ debugLog.debug('CrossTabSession', 'Starting leader election');
147
+ }
148
+ // Add randomized delay to prevent thundering herd (optimized for performance tests)
149
+ const randomDelay = Math.floor(Math.random() * 50) + 10; // 10-60ms delay (reduced for better performance)
150
+ this.electionTimeout = window.setTimeout(() => {
151
+ this.electionTimeout = null;
152
+ this.requestLeadershipStatus();
153
+ }, randomDelay);
154
+ }
155
+ /**
156
+ * Become the session leader
157
+ */
158
+ becomeLeader() {
159
+ // Double-check we're not already a leader (race condition protection)
160
+ if (this.isTabLeader) {
161
+ return;
162
+ }
163
+ this.isTabLeader = true;
164
+ this.tabInfo.isLeader = true;
165
+ this.leaderTabId = this.tabId;
166
+ if (this.config.debugMode) {
167
+ debugLog.debug('CrossTabSession', `Tab ${this.tabId} became session leader`);
168
+ }
169
+ // Clear any existing election timeout
170
+ if (this.electionTimeout) {
171
+ clearTimeout(this.electionTimeout);
172
+ this.electionTimeout = null;
173
+ }
174
+ // Start new session if we don't have one
175
+ if (!this.tabInfo.sessionId) {
176
+ const sessionId = generateUUID();
177
+ this.tabInfo.sessionId = sessionId;
178
+ const sessionContext = {
179
+ sessionId,
180
+ startTime: Date.now(),
181
+ lastActivity: Date.now(),
182
+ tabCount: 1,
183
+ recoveryAttempts: 0,
184
+ };
185
+ this.storeSessionContext(sessionContext);
186
+ // Notify session start
187
+ if (this.callbacks?.onSessionStart) {
188
+ this.callbacks.onSessionStart(sessionId);
189
+ }
190
+ // Announce new session to other tabs
191
+ this.announceSessionStart(sessionId);
192
+ }
193
+ else {
194
+ // Update existing session context
195
+ const sessionContext = this.getStoredSessionContext();
196
+ if (sessionContext) {
197
+ sessionContext.lastActivity = Date.now();
198
+ this.storeSessionContext(sessionContext);
199
+ }
200
+ }
201
+ // Store tab info
202
+ this.storeTabInfo();
203
+ // Send immediate leadership announcement to ensure other tabs know
204
+ this.announceLeadership();
205
+ }
206
+ /**
207
+ * Announce session start to other tabs
208
+ */
209
+ announceSessionStart(sessionId) {
210
+ if (!this.broadcastChannel)
211
+ return;
212
+ const message = {
213
+ type: 'session_start',
214
+ tabId: this.tabId,
215
+ sessionId,
216
+ timestamp: Date.now(),
217
+ };
218
+ this.broadcastChannel.postMessage(message);
219
+ }
220
+ /**
221
+ * Announce leadership to other tabs
222
+ */
223
+ announceLeadership() {
224
+ if (!this.broadcastChannel || !this.tabInfo.sessionId)
225
+ return;
226
+ const message = {
227
+ type: 'election_response',
228
+ tabId: this.tabId,
229
+ sessionId: this.tabInfo.sessionId,
230
+ timestamp: Date.now(),
231
+ data: { isLeader: true },
232
+ };
233
+ this.broadcastChannel.postMessage(message);
234
+ }
235
+ /**
236
+ * Setup fallback mechanism to ensure a leader is always elected
237
+ */
238
+ setupLeadershipFallback() {
239
+ // Shorter fallback delay to ensure it works within test timeouts
240
+ const fallbackDelay = this.config.tabElectionTimeoutMs + 1500; // Election timeout + 1.5s buffer
241
+ this.fallbackLeadershipTimeout = window.setTimeout(() => {
242
+ // Check if we need to force leadership
243
+ if (!this.isTabLeader && !this.leaderTabId) {
244
+ // If we have a session but no leader, become leader
245
+ if (this.tabInfo.sessionId) {
246
+ if (this.config.debugMode) {
247
+ debugLog.warn('CrossTabSession', `No leader detected after ${fallbackDelay}ms, forcing leadership for tab ${this.tabId}`);
248
+ }
249
+ this.becomeLeader();
250
+ }
251
+ else {
252
+ // If we don't even have a session, start a new one
253
+ if (this.config.debugMode) {
254
+ debugLog.warn('CrossTabSession', `No session or leader detected after ${fallbackDelay}ms, starting new session for tab ${this.tabId}`);
255
+ }
256
+ this.becomeLeader();
257
+ }
258
+ }
259
+ this.fallbackLeadershipTimeout = null;
260
+ }, fallbackDelay);
261
+ // Additional periodic check for leader health
262
+ this.leaderHealthCheckInterval = window.setInterval(() => {
263
+ if (!this.sessionEnded && this.leaderTabId && !this.isTabLeader) {
264
+ // Check if leader is still responsive by checking last activity
265
+ const sessionContext = this.getStoredSessionContext();
266
+ if (sessionContext) {
267
+ const timeSinceLastActivity = Date.now() - sessionContext.lastActivity;
268
+ const maxInactiveTime = this.config.tabHeartbeatIntervalMs * 3; // 3 heartbeat intervals
269
+ if (timeSinceLastActivity > maxInactiveTime) {
270
+ if (this.config.debugMode) {
271
+ debugLog.warn('CrossTabSession', `Leader tab appears inactive (${timeSinceLastActivity}ms), attempting to become leader`);
272
+ }
273
+ this.leaderTabId = null;
274
+ this.startLeaderElection();
275
+ }
276
+ }
277
+ }
278
+ }, this.config.tabHeartbeatIntervalMs * 2); // Check every 2 heartbeat intervals
279
+ // Clean up the health check interval when session ends
280
+ const originalEndSession = this.endSession.bind(this);
281
+ this.endSession = (reason) => {
282
+ if (this.leaderHealthCheckInterval) {
283
+ clearInterval(this.leaderHealthCheckInterval);
284
+ this.leaderHealthCheckInterval = null;
285
+ }
286
+ originalEndSession(reason);
287
+ };
288
+ }
289
+ /**
290
+ * Setup BroadcastChannel event listeners
291
+ */
292
+ setupBroadcastListeners(channel) {
293
+ channel.addEventListener('message', (event) => {
294
+ const message = event.data;
295
+ if (message.tabId === this.tabId) {
296
+ return;
297
+ }
298
+ this.handleCrossTabMessage(message);
299
+ });
300
+ }
301
+ /**
302
+ * Handle cross-tab messages
303
+ */
304
+ handleCrossTabMessage(message) {
305
+ if (this.config.debugMode) {
306
+ debugLog.debug('CrossTabSession', `Received cross-tab message: ${message.type} from ${message.tabId}`);
307
+ }
308
+ switch (message.type) {
309
+ case 'heartbeat':
310
+ this.handleHeartbeatMessage(message);
311
+ break;
312
+ case 'session_start':
313
+ this.handleSessionStartMessage(message);
314
+ break;
315
+ case 'session_end':
316
+ this.handleSessionEndMessage(message);
317
+ break;
318
+ case 'tab_closing':
319
+ this.handleTabClosingMessage(message);
320
+ break;
321
+ case 'election_request':
322
+ this.handleElectionRequest(message);
323
+ break;
324
+ case 'election_response':
325
+ this.handleElectionResponse(message);
326
+ break;
327
+ }
328
+ }
329
+ /**
330
+ * Handle heartbeat message from another tab
331
+ */
332
+ handleHeartbeatMessage(message) {
333
+ // Update session activity if this tab has the same session
334
+ if (message.sessionId === this.tabInfo.sessionId) {
335
+ const sessionContext = this.getStoredSessionContext();
336
+ if (sessionContext) {
337
+ sessionContext.lastActivity = Date.now();
338
+ this.storeSessionContext(sessionContext);
339
+ // Notify activity callback
340
+ if (this.callbacks?.onTabActivity) {
341
+ this.callbacks.onTabActivity();
342
+ }
343
+ }
344
+ }
345
+ }
346
+ /**
347
+ * Handle session start message from another tab
348
+ */
349
+ handleSessionStartMessage(message) {
350
+ if (!message.sessionId)
351
+ return;
352
+ // Join the session if we don't have one
353
+ if (!this.tabInfo.sessionId) {
354
+ this.tabInfo.sessionId = message.sessionId;
355
+ this.storeTabInfo();
356
+ // Update session context
357
+ const sessionContext = this.getStoredSessionContext();
358
+ if (sessionContext) {
359
+ sessionContext.tabCount += 1;
360
+ this.storeSessionContext(sessionContext);
361
+ }
362
+ }
363
+ }
364
+ /**
365
+ * Handle session end message from another tab
366
+ */
367
+ handleSessionEndMessage(message) {
368
+ // Ignore if this tab is the leader
369
+ if (this.isTabLeader) {
370
+ if (this.config.debugMode) {
371
+ debugLog.debug('CrossTabSession', `Ignoring session end message from ${message.tabId} (this tab is leader)`);
372
+ }
373
+ return;
374
+ }
375
+ // Verify the message is from the current leader
376
+ if (!this.leaderTabId || message.tabId !== this.leaderTabId) {
377
+ if (this.config.debugMode) {
378
+ const extra = this.leaderTabId ? `; leader is ${this.leaderTabId}` : '';
379
+ debugLog.debug('CrossTabSession', `Ignoring session end message from ${message.tabId}${extra}`);
380
+ }
381
+ return;
382
+ }
383
+ this.tabInfo.sessionId = '';
384
+ this.storeTabInfo();
385
+ this.leaderTabId = null;
386
+ const sessionContext = this.getStoredSessionContext();
387
+ // Start a new session if none exists
388
+ if (!sessionContext) {
389
+ if (this.broadcastChannel) {
390
+ this.startLeaderElection();
391
+ }
392
+ else {
393
+ this.becomeLeader();
394
+ }
395
+ }
396
+ }
397
+ /**
398
+ * Handle tab closing message from another tab
399
+ */
400
+ handleTabClosingMessage(message) {
401
+ const sessionContext = this.getStoredSessionContext();
402
+ if (sessionContext && message.sessionId === sessionContext.sessionId) {
403
+ // Decrease tab count with minimum of 1 (current tab)
404
+ const oldCount = sessionContext.tabCount;
405
+ sessionContext.tabCount = Math.max(1, sessionContext.tabCount - 1);
406
+ sessionContext.lastActivity = Date.now();
407
+ this.storeSessionContext(sessionContext);
408
+ if (this.config.debugMode) {
409
+ debugLog.debug('CrossTabSession', `Tab count updated from ${oldCount} to ${sessionContext.tabCount} after tab ${message.tabId} closed`);
410
+ }
411
+ // If the closing tab was the leader, handle leadership transition
412
+ const wasLeader = message.data?.isLeader ?? message.tabId === this.leaderTabId;
413
+ if (wasLeader && !this.isTabLeader) {
414
+ if (this.config.debugMode) {
415
+ debugLog.debug('CrossTabSession', `Leader tab ${message.tabId} closed, starting leader election`);
416
+ }
417
+ this.leaderTabId = null;
418
+ // Add a small delay to ensure other tabs have processed the closing message
419
+ this.electionDelayTimeout = window.setTimeout(() => {
420
+ this.startLeaderElection();
421
+ this.electionDelayTimeout = null;
422
+ }, 200);
423
+ }
424
+ }
425
+ }
426
+ /**
427
+ * Handle election request from another tab
428
+ */
429
+ handleElectionRequest(_message) {
430
+ if (this.isTabLeader) {
431
+ // Respond that we're the leader
432
+ const response = {
433
+ type: 'election_response',
434
+ tabId: this.tabId,
435
+ sessionId: this.tabInfo.sessionId,
436
+ timestamp: Date.now(),
437
+ data: { isLeader: true },
438
+ };
439
+ if (this.broadcastChannel) {
440
+ this.broadcastChannel.postMessage(response);
441
+ }
442
+ }
443
+ }
444
+ /**
445
+ * Handle election response from another tab
446
+ */
447
+ handleElectionResponse(message) {
448
+ if (message.data?.isLeader) {
449
+ // Another tab is already the leader - only accept if we're not already a leader
450
+ if (!this.isTabLeader) {
451
+ this.isTabLeader = false;
452
+ this.tabInfo.isLeader = false;
453
+ this.leaderTabId = message.tabId;
454
+ if (this.config.debugMode) {
455
+ debugLog.debug('CrossTabSession', `Acknowledging tab ${message.tabId} as leader`);
456
+ }
457
+ // Clear election timeout
458
+ if (this.electionTimeout) {
459
+ clearTimeout(this.electionTimeout);
460
+ this.electionTimeout = null;
461
+ }
462
+ // Join their session
463
+ if (message.sessionId) {
464
+ this.tabInfo.sessionId = message.sessionId;
465
+ this.storeTabInfo();
466
+ }
467
+ }
468
+ else if (this.config.debugMode) {
469
+ // We're already a leader, log potential conflict
470
+ debugLog.warn('CrossTabSession', `Received leadership claim from ${message.tabId} but this tab is already leader`);
471
+ // Notify conflict callback
472
+ if (this.callbacks?.onCrossTabConflict) {
473
+ this.callbacks.onCrossTabConflict();
474
+ }
475
+ }
476
+ }
477
+ }
478
+ /**
479
+ * Start heartbeat to keep session active
480
+ */
481
+ startHeartbeat() {
482
+ if (this.heartbeatInterval) {
483
+ clearInterval(this.heartbeatInterval);
484
+ }
485
+ this.heartbeatInterval = window.setInterval(() => {
486
+ this.sendHeartbeat();
487
+ this.updateTabInfo();
488
+ }, this.config.tabHeartbeatIntervalMs);
489
+ }
490
+ /**
491
+ * Send heartbeat to other tabs with rate limiting to prevent flooding
492
+ */
493
+ sendHeartbeat() {
494
+ if (!this.broadcastChannel || !this.tabInfo.sessionId)
495
+ return;
496
+ // Rate limit heartbeats - only send if we're the leader or haven't sent recently
497
+ const now = Date.now();
498
+ const lastHeartbeat = this.lastHeartbeatSent ?? 0;
499
+ const minHeartbeatInterval = this.config.tabHeartbeatIntervalMs * 0.8; // 80% of interval
500
+ if (!this.isTabLeader && now - lastHeartbeat < minHeartbeatInterval) {
501
+ return; // Skip heartbeat to reduce noise
502
+ }
503
+ const message = {
504
+ type: 'heartbeat',
505
+ tabId: this.tabId,
506
+ sessionId: this.tabInfo.sessionId,
507
+ timestamp: now,
508
+ };
509
+ this.broadcastChannel.postMessage(message);
510
+ this.lastHeartbeatSent = now;
511
+ }
512
+ /**
513
+ * Update tab info with current timestamp
514
+ */
515
+ updateTabInfo() {
516
+ this.tabInfo.lastHeartbeat = Date.now();
517
+ this.storeTabInfo();
518
+ }
519
+ /**
520
+ * End session and notify other tabs
521
+ */
522
+ endSession(reason) {
523
+ if (this.sessionEnded) {
524
+ return;
525
+ }
526
+ this.sessionEnded = true;
527
+ if (this.config.debugMode) {
528
+ debugLog.debug('CrossTabSession', `Ending cross-tab session: ${reason} (tab: ${this.tabId}, isLeader: ${this.isTabLeader})`);
529
+ }
530
+ // Announce tab closing with current state
531
+ this.announceTabClosing();
532
+ // If this is the leader, announce session end to remaining tabs
533
+ if (this.isTabLeader && reason !== 'manual_stop') {
534
+ this.announceSessionEnd(reason);
535
+ }
536
+ // Give time for messages to be sent before cleanup
537
+ this.tabInfoCleanupTimeout = window.setTimeout(() => {
538
+ this.clearTabInfo();
539
+ this.tabInfoCleanupTimeout = null;
540
+ }, 150);
541
+ }
542
+ /**
543
+ * Announce tab is closing to other tabs
544
+ */
545
+ announceTabClosing() {
546
+ if (!this.broadcastChannel || !this.tabInfo.sessionId)
547
+ return;
548
+ const message = {
549
+ type: 'tab_closing',
550
+ tabId: this.tabId,
551
+ sessionId: this.tabInfo.sessionId,
552
+ timestamp: Date.now(),
553
+ data: { isLeader: this.isTabLeader },
554
+ };
555
+ this.broadcastChannel.postMessage(message);
556
+ // Give other tabs time to process the message before we close
557
+ this.closingAnnouncementTimeout = window.setTimeout(() => {
558
+ if (this.config.debugMode) {
559
+ debugLog.debug('CrossTabSession', `Tab ${this.tabId} closing announcement sent`);
560
+ }
561
+ this.closingAnnouncementTimeout = null;
562
+ }, 100);
563
+ }
564
+ /**
565
+ * Announce session end to other tabs
566
+ */
567
+ announceSessionEnd(reason) {
568
+ if (!this.broadcastChannel)
569
+ return;
570
+ const message = {
571
+ type: 'session_end',
572
+ tabId: this.tabId,
573
+ sessionId: this.tabInfo.sessionId,
574
+ timestamp: Date.now(),
575
+ data: { reason },
576
+ };
577
+ this.broadcastChannel.postMessage(message);
578
+ }
579
+ /**
580
+ * Get current session ID
581
+ */
582
+ getSessionId() {
583
+ return this.tabInfo.sessionId;
584
+ }
585
+ /**
586
+ * Get current tab ID
587
+ */
588
+ getTabId() {
589
+ return this.tabId;
590
+ }
591
+ /**
592
+ * Check if this tab is the session leader
593
+ */
594
+ isLeader() {
595
+ return this.isTabLeader;
596
+ }
597
+ /**
598
+ * Get current session context from storage
599
+ */
600
+ getStoredSessionContext() {
601
+ try {
602
+ const stored = this.storageManager.getItem(CROSS_TAB_SESSION_KEY(this.projectId));
603
+ return stored ? JSON.parse(stored) : null;
604
+ }
605
+ catch (error) {
606
+ if (this.config.debugMode) {
607
+ debugLog.warn('CrossTabSession', 'Failed to parse stored session context', { error });
608
+ }
609
+ return null;
610
+ }
611
+ }
612
+ /**
613
+ * Store session context to localStorage
614
+ */
615
+ storeSessionContext(context) {
616
+ try {
617
+ this.storageManager.setItem(CROSS_TAB_SESSION_KEY(this.projectId), JSON.stringify(context));
618
+ }
619
+ catch (error) {
620
+ if (this.config.debugMode) {
621
+ debugLog.warn('CrossTabSession', 'Failed to store session context', { error });
622
+ }
623
+ }
624
+ }
625
+ /**
626
+ * Clear stored session context
627
+ */
628
+ clearStoredSessionContext() {
629
+ this.storageManager.removeItem(CROSS_TAB_SESSION_KEY(this.projectId));
630
+ }
631
+ /**
632
+ * Store tab info to localStorage
633
+ */
634
+ storeTabInfo() {
635
+ try {
636
+ this.storageManager.setItem(TAB_SPECIFIC_INFO_KEY(this.projectId, this.tabId), JSON.stringify(this.tabInfo));
637
+ }
638
+ catch (error) {
639
+ if (this.config.debugMode) {
640
+ debugLog.warn('CrossTabSession', 'Failed to store tab info', { error });
641
+ }
642
+ }
643
+ }
644
+ /**
645
+ * Clear tab info from localStorage
646
+ */
647
+ clearTabInfo() {
648
+ this.storageManager.removeItem(TAB_SPECIFIC_INFO_KEY(this.projectId, this.tabId));
649
+ }
650
+ /**
651
+ * Check if BroadcastChannel is supported
652
+ */
653
+ isBroadcastChannelSupported() {
654
+ return typeof window !== 'undefined' && 'BroadcastChannel' in window;
655
+ }
656
+ /**
657
+ * Get session timeout considering cross-tab activity
658
+ */
659
+ getEffectiveSessionTimeout() {
660
+ const sessionContext = this.getStoredSessionContext();
661
+ if (!sessionContext) {
662
+ return this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT_MS;
663
+ }
664
+ const now = Date.now();
665
+ const timeSinceLastActivity = now - sessionContext.lastActivity;
666
+ const sessionTimeout = this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT_MS;
667
+ return Math.max(0, sessionTimeout - timeSinceLastActivity);
668
+ }
669
+ /**
670
+ * Update session activity from any tab
671
+ */
672
+ updateSessionActivity() {
673
+ const sessionContext = this.getStoredSessionContext();
674
+ if (sessionContext) {
675
+ sessionContext.lastActivity = Date.now();
676
+ this.storeSessionContext(sessionContext);
677
+ }
678
+ // Send heartbeat to notify other tabs
679
+ this.sendHeartbeat();
680
+ }
681
+ /**
682
+ * Cleanup resources
683
+ */
684
+ destroy() {
685
+ // Clear intervals and timeouts
686
+ if (this.heartbeatInterval) {
687
+ clearInterval(this.heartbeatInterval);
688
+ this.heartbeatInterval = null;
689
+ }
690
+ if (this.electionTimeout) {
691
+ clearTimeout(this.electionTimeout);
692
+ this.electionTimeout = null;
693
+ }
694
+ if (this.cleanupTimeout) {
695
+ clearTimeout(this.cleanupTimeout);
696
+ this.cleanupTimeout = null;
697
+ }
698
+ // Clear additional timeouts
699
+ if (this.fallbackLeadershipTimeout) {
700
+ clearTimeout(this.fallbackLeadershipTimeout);
701
+ this.fallbackLeadershipTimeout = null;
702
+ }
703
+ if (this.electionDelayTimeout) {
704
+ clearTimeout(this.electionDelayTimeout);
705
+ this.electionDelayTimeout = null;
706
+ }
707
+ if (this.tabInfoCleanupTimeout) {
708
+ clearTimeout(this.tabInfoCleanupTimeout);
709
+ this.tabInfoCleanupTimeout = null;
710
+ }
711
+ if (this.closingAnnouncementTimeout) {
712
+ clearTimeout(this.closingAnnouncementTimeout);
713
+ this.closingAnnouncementTimeout = null;
714
+ }
715
+ if (this.leaderHealthCheckInterval) {
716
+ clearInterval(this.leaderHealthCheckInterval);
717
+ this.leaderHealthCheckInterval = null;
718
+ }
719
+ // End session and cleanup
720
+ this.endSession('manual_stop');
721
+ // Close BroadcastChannel
722
+ if (this.broadcastChannel) {
723
+ this.broadcastChannel.close();
724
+ }
725
+ }
726
+ }