@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.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/dist/browser/tracelog.js +4040 -0
- package/dist/browser/web-vitals-CCnqwnC8.mjs +198 -0
- package/dist/cjs/api.d.ts +46 -0
- package/dist/cjs/api.js +224 -0
- package/dist/cjs/app.constants.d.ts +1 -0
- package/dist/cjs/app.constants.js +5 -0
- package/dist/cjs/app.d.ts +59 -0
- package/dist/cjs/app.js +272 -0
- package/dist/cjs/app.types.d.ts +6 -0
- package/dist/cjs/app.types.js +22 -0
- package/dist/cjs/constants/api.constants.d.ts +4 -0
- package/dist/cjs/constants/api.constants.js +18 -0
- package/dist/cjs/constants/browser.constants.d.ts +3 -0
- package/dist/cjs/constants/browser.constants.js +41 -0
- package/dist/cjs/constants/index.d.ts +8 -0
- package/dist/cjs/constants/index.js +24 -0
- package/dist/cjs/constants/initialization.constants.d.ts +40 -0
- package/dist/cjs/constants/initialization.constants.js +48 -0
- package/dist/cjs/constants/limits.constants.d.ts +25 -0
- package/dist/cjs/constants/limits.constants.js +40 -0
- package/dist/cjs/constants/security.constants.d.ts +1 -0
- package/dist/cjs/constants/security.constants.js +12 -0
- package/dist/cjs/constants/storage.constants.d.ts +9 -0
- package/dist/cjs/constants/storage.constants.js +22 -0
- package/dist/cjs/constants/timing.constants.d.ts +22 -0
- package/dist/cjs/constants/timing.constants.js +34 -0
- package/dist/cjs/constants/validation.constants.d.ts +13 -0
- package/dist/cjs/constants/validation.constants.js +31 -0
- package/dist/cjs/handlers/click.handler.d.ts +17 -0
- package/dist/cjs/handlers/click.handler.js +199 -0
- package/dist/cjs/handlers/error.handler.d.ts +15 -0
- package/dist/cjs/handlers/error.handler.js +97 -0
- package/dist/cjs/handlers/network.handler.d.ts +16 -0
- package/dist/cjs/handlers/network.handler.js +136 -0
- package/dist/cjs/handlers/page-view.handler.d.ts +15 -0
- package/dist/cjs/handlers/page-view.handler.js +83 -0
- package/dist/cjs/handlers/performance.handler.d.ts +19 -0
- package/dist/cjs/handlers/performance.handler.js +255 -0
- package/dist/cjs/handlers/scroll.handler.d.ts +16 -0
- package/dist/cjs/handlers/scroll.handler.js +138 -0
- package/dist/cjs/handlers/session.handler.d.ts +29 -0
- package/dist/cjs/handlers/session.handler.js +357 -0
- package/dist/cjs/integrations/google-analytics.integration.d.ts +18 -0
- package/dist/cjs/integrations/google-analytics.integration.js +159 -0
- package/dist/cjs/listeners/activity-listener-manager.d.ts +8 -0
- package/dist/cjs/listeners/activity-listener-manager.js +32 -0
- package/dist/cjs/listeners/index.d.ts +6 -0
- package/dist/cjs/listeners/index.js +14 -0
- package/dist/cjs/listeners/input-listener-managers.d.ts +15 -0
- package/dist/cjs/listeners/input-listener-managers.js +58 -0
- package/dist/cjs/listeners/listeners.types.d.ts +4 -0
- package/dist/cjs/listeners/listeners.types.js +2 -0
- package/dist/cjs/listeners/touch-listener-manager.d.ts +10 -0
- package/dist/cjs/listeners/touch-listener-manager.js +56 -0
- package/dist/cjs/listeners/unload-listener-manager.d.ts +8 -0
- package/dist/cjs/listeners/unload-listener-manager.js +30 -0
- package/dist/cjs/listeners/visibility-listener-manager.d.ts +12 -0
- package/dist/cjs/listeners/visibility-listener-manager.js +83 -0
- package/dist/cjs/managers/api.manager.d.ts +3 -0
- package/dist/cjs/managers/api.manager.js +14 -0
- package/dist/cjs/managers/config.manager.d.ts +7 -0
- package/dist/cjs/managers/config.manager.js +94 -0
- package/dist/cjs/managers/cross-tab-session.manager.d.ts +170 -0
- package/dist/cjs/managers/cross-tab-session.manager.js +730 -0
- package/dist/cjs/managers/event.manager.d.ts +61 -0
- package/dist/cjs/managers/event.manager.js +508 -0
- package/dist/cjs/managers/sampling.manager.d.ts +8 -0
- package/dist/cjs/managers/sampling.manager.js +53 -0
- package/dist/cjs/managers/sender.manager.d.ts +46 -0
- package/dist/cjs/managers/sender.manager.js +304 -0
- package/dist/cjs/managers/session-recovery.manager.d.ts +65 -0
- package/dist/cjs/managers/session-recovery.manager.js +237 -0
- package/dist/cjs/managers/session.manager.d.ts +72 -0
- package/dist/cjs/managers/session.manager.js +587 -0
- package/dist/cjs/managers/state.manager.d.ts +5 -0
- package/dist/cjs/managers/state.manager.js +23 -0
- package/dist/cjs/managers/storage.manager.d.ts +10 -0
- package/dist/cjs/managers/storage.manager.js +81 -0
- package/dist/cjs/managers/tags.manager.d.ts +12 -0
- package/dist/cjs/managers/tags.manager.js +289 -0
- package/dist/cjs/managers/user.manager.d.ts +7 -0
- package/dist/cjs/managers/user.manager.js +22 -0
- package/dist/cjs/public-api.d.ts +1 -0
- package/dist/cjs/public-api.js +37 -0
- package/dist/cjs/types/api.types.d.ts +21 -0
- package/dist/cjs/types/api.types.js +25 -0
- package/dist/cjs/types/common.types.d.ts +1 -0
- package/dist/cjs/types/common.types.js +2 -0
- package/dist/cjs/types/config.types.d.ts +104 -0
- package/dist/cjs/types/config.types.js +2 -0
- package/dist/cjs/types/device.types.d.ts +6 -0
- package/dist/cjs/types/device.types.js +10 -0
- package/dist/cjs/types/event.types.d.ts +104 -0
- package/dist/cjs/types/event.types.js +25 -0
- package/dist/cjs/types/index.d.ts +13 -0
- package/dist/cjs/types/index.js +29 -0
- package/dist/cjs/types/log.types.d.ts +4 -0
- package/dist/cjs/types/log.types.js +2 -0
- package/dist/cjs/types/mode.types.d.ts +7 -0
- package/dist/cjs/types/mode.types.js +11 -0
- package/dist/cjs/types/queue.types.d.ts +23 -0
- package/dist/cjs/types/queue.types.js +2 -0
- package/dist/cjs/types/session.types.d.ts +65 -0
- package/dist/cjs/types/session.types.js +2 -0
- package/dist/cjs/types/state.types.d.ts +12 -0
- package/dist/cjs/types/state.types.js +2 -0
- package/dist/cjs/types/tag.types.d.ts +43 -0
- package/dist/cjs/types/tag.types.js +31 -0
- package/dist/cjs/types/validation-error.types.d.ts +42 -0
- package/dist/cjs/types/validation-error.types.js +68 -0
- package/dist/cjs/types/web-vitals.types.d.ts +6 -0
- package/dist/cjs/types/web-vitals.types.js +2 -0
- package/dist/cjs/types/window.types.d.ts +17 -0
- package/dist/cjs/types/window.types.js +2 -0
- package/dist/cjs/utils/browser/device-detector.utils.d.ts +6 -0
- package/dist/cjs/utils/browser/device-detector.utils.js +71 -0
- package/dist/cjs/utils/browser/index.d.ts +2 -0
- package/dist/cjs/utils/browser/index.js +18 -0
- package/dist/cjs/utils/browser/utm-params.utils.d.ts +6 -0
- package/dist/cjs/utils/browser/utm-params.utils.js +37 -0
- package/dist/cjs/utils/data/index.d.ts +1 -0
- package/dist/cjs/utils/data/index.js +17 -0
- package/dist/cjs/utils/data/uuid.utils.d.ts +5 -0
- package/dist/cjs/utils/data/uuid.utils.js +18 -0
- package/dist/cjs/utils/index.d.ts +6 -0
- package/dist/cjs/utils/index.js +22 -0
- package/dist/cjs/utils/logging/debug-logger.utils.d.ts +56 -0
- package/dist/cjs/utils/logging/debug-logger.utils.js +139 -0
- package/dist/cjs/utils/logging/index.d.ts +1 -0
- package/dist/cjs/utils/logging/index.js +5 -0
- package/dist/cjs/utils/network/index.d.ts +1 -0
- package/dist/cjs/utils/network/index.js +17 -0
- package/dist/cjs/utils/network/url.utils.d.ts +20 -0
- package/dist/cjs/utils/network/url.utils.js +172 -0
- package/dist/cjs/utils/security/index.d.ts +1 -0
- package/dist/cjs/utils/security/index.js +17 -0
- package/dist/cjs/utils/security/sanitize.utils.d.ts +32 -0
- package/dist/cjs/utils/security/sanitize.utils.js +319 -0
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +42 -0
- package/dist/cjs/utils/validations/config-validations.utils.js +297 -0
- package/dist/cjs/utils/validations/event-validations.utils.d.ts +12 -0
- package/dist/cjs/utils/validations/event-validations.utils.js +30 -0
- package/dist/cjs/utils/validations/index.d.ts +5 -0
- package/dist/cjs/utils/validations/index.js +21 -0
- package/dist/cjs/utils/validations/metadata-validations.utils.d.ts +22 -0
- package/dist/cjs/utils/validations/metadata-validations.utils.js +115 -0
- package/dist/cjs/utils/validations/type-guards.utils.d.ts +6 -0
- package/dist/cjs/utils/validations/type-guards.utils.js +31 -0
- package/dist/cjs/utils/validations/url-validations.utils.d.ts +15 -0
- package/dist/cjs/utils/validations/url-validations.utils.js +47 -0
- package/dist/esm/api.d.ts +46 -0
- package/dist/esm/api.js +183 -0
- package/dist/esm/app.constants.d.ts +1 -0
- package/dist/esm/app.constants.js +1 -0
- package/dist/esm/app.d.ts +59 -0
- package/dist/esm/app.js +268 -0
- package/dist/esm/app.types.d.ts +6 -0
- package/dist/esm/app.types.js +6 -0
- package/dist/esm/constants/api.constants.d.ts +4 -0
- package/dist/esm/constants/api.constants.js +14 -0
- package/dist/esm/constants/browser.constants.d.ts +3 -0
- package/dist/esm/constants/browser.constants.js +38 -0
- package/dist/esm/constants/index.d.ts +8 -0
- package/dist/esm/constants/index.js +8 -0
- package/dist/esm/constants/initialization.constants.d.ts +40 -0
- package/dist/esm/constants/initialization.constants.js +45 -0
- package/dist/esm/constants/limits.constants.d.ts +25 -0
- package/dist/esm/constants/limits.constants.js +37 -0
- package/dist/esm/constants/security.constants.d.ts +1 -0
- package/dist/esm/constants/security.constants.js +9 -0
- package/dist/esm/constants/storage.constants.d.ts +9 -0
- package/dist/esm/constants/storage.constants.js +11 -0
- package/dist/esm/constants/timing.constants.d.ts +22 -0
- package/dist/esm/constants/timing.constants.js +31 -0
- package/dist/esm/constants/validation.constants.d.ts +13 -0
- package/dist/esm/constants/validation.constants.js +28 -0
- package/dist/esm/handlers/click.handler.d.ts +17 -0
- package/dist/esm/handlers/click.handler.js +195 -0
- package/dist/esm/handlers/error.handler.d.ts +15 -0
- package/dist/esm/handlers/error.handler.js +93 -0
- package/dist/esm/handlers/network.handler.d.ts +16 -0
- package/dist/esm/handlers/network.handler.js +132 -0
- package/dist/esm/handlers/page-view.handler.d.ts +15 -0
- package/dist/esm/handlers/page-view.handler.js +79 -0
- package/dist/esm/handlers/performance.handler.d.ts +19 -0
- package/dist/esm/handlers/performance.handler.js +218 -0
- package/dist/esm/handlers/scroll.handler.d.ts +16 -0
- package/dist/esm/handlers/scroll.handler.js +134 -0
- package/dist/esm/handlers/session.handler.d.ts +29 -0
- package/dist/esm/handlers/session.handler.js +353 -0
- package/dist/esm/integrations/google-analytics.integration.d.ts +18 -0
- package/dist/esm/integrations/google-analytics.integration.js +155 -0
- package/dist/esm/listeners/activity-listener-manager.d.ts +8 -0
- package/dist/esm/listeners/activity-listener-manager.js +28 -0
- package/dist/esm/listeners/index.d.ts +6 -0
- package/dist/esm/listeners/index.js +5 -0
- package/dist/esm/listeners/input-listener-managers.d.ts +15 -0
- package/dist/esm/listeners/input-listener-managers.js +53 -0
- package/dist/esm/listeners/listeners.types.d.ts +4 -0
- package/dist/esm/listeners/listeners.types.js +1 -0
- package/dist/esm/listeners/touch-listener-manager.d.ts +10 -0
- package/dist/esm/listeners/touch-listener-manager.js +52 -0
- package/dist/esm/listeners/unload-listener-manager.d.ts +8 -0
- package/dist/esm/listeners/unload-listener-manager.js +26 -0
- package/dist/esm/listeners/visibility-listener-manager.d.ts +12 -0
- package/dist/esm/listeners/visibility-listener-manager.js +79 -0
- package/dist/esm/managers/api.manager.d.ts +3 -0
- package/dist/esm/managers/api.manager.js +10 -0
- package/dist/esm/managers/config.manager.d.ts +7 -0
- package/dist/esm/managers/config.manager.js +90 -0
- package/dist/esm/managers/cross-tab-session.manager.d.ts +170 -0
- package/dist/esm/managers/cross-tab-session.manager.js +726 -0
- package/dist/esm/managers/event.manager.d.ts +61 -0
- package/dist/esm/managers/event.manager.js +504 -0
- package/dist/esm/managers/sampling.manager.d.ts +8 -0
- package/dist/esm/managers/sampling.manager.js +49 -0
- package/dist/esm/managers/sender.manager.d.ts +46 -0
- package/dist/esm/managers/sender.manager.js +300 -0
- package/dist/esm/managers/session-recovery.manager.d.ts +65 -0
- package/dist/esm/managers/session-recovery.manager.js +233 -0
- package/dist/esm/managers/session.manager.d.ts +72 -0
- package/dist/esm/managers/session.manager.js +583 -0
- package/dist/esm/managers/state.manager.d.ts +5 -0
- package/dist/esm/managers/state.manager.js +19 -0
- package/dist/esm/managers/storage.manager.d.ts +10 -0
- package/dist/esm/managers/storage.manager.js +77 -0
- package/dist/esm/managers/tags.manager.d.ts +12 -0
- package/dist/esm/managers/tags.manager.js +285 -0
- package/dist/esm/managers/user.manager.d.ts +7 -0
- package/dist/esm/managers/user.manager.js +18 -0
- package/dist/esm/public-api.d.ts +1 -0
- package/dist/esm/public-api.js +1 -0
- package/dist/esm/types/api.types.d.ts +21 -0
- package/dist/esm/types/api.types.js +22 -0
- package/dist/esm/types/common.types.d.ts +1 -0
- package/dist/esm/types/common.types.js +1 -0
- package/dist/esm/types/config.types.d.ts +104 -0
- package/dist/esm/types/config.types.js +1 -0
- package/dist/esm/types/device.types.d.ts +6 -0
- package/dist/esm/types/device.types.js +7 -0
- package/dist/esm/types/event.types.d.ts +104 -0
- package/dist/esm/types/event.types.js +22 -0
- package/dist/esm/types/index.d.ts +13 -0
- package/dist/esm/types/index.js +13 -0
- package/dist/esm/types/log.types.d.ts +4 -0
- package/dist/esm/types/log.types.js +1 -0
- package/dist/esm/types/mode.types.d.ts +7 -0
- package/dist/esm/types/mode.types.js +8 -0
- package/dist/esm/types/queue.types.d.ts +23 -0
- package/dist/esm/types/queue.types.js +1 -0
- package/dist/esm/types/session.types.d.ts +65 -0
- package/dist/esm/types/session.types.js +1 -0
- package/dist/esm/types/state.types.d.ts +12 -0
- package/dist/esm/types/state.types.js +1 -0
- package/dist/esm/types/tag.types.d.ts +43 -0
- package/dist/esm/types/tag.types.js +28 -0
- package/dist/esm/types/validation-error.types.d.ts +42 -0
- package/dist/esm/types/validation-error.types.js +59 -0
- package/dist/esm/types/web-vitals.types.d.ts +6 -0
- package/dist/esm/types/web-vitals.types.js +1 -0
- package/dist/esm/types/window.types.d.ts +17 -0
- package/dist/esm/types/window.types.js +1 -0
- package/dist/esm/utils/browser/device-detector.utils.d.ts +6 -0
- package/dist/esm/utils/browser/device-detector.utils.js +67 -0
- package/dist/esm/utils/browser/index.d.ts +2 -0
- package/dist/esm/utils/browser/index.js +2 -0
- package/dist/esm/utils/browser/utm-params.utils.d.ts +6 -0
- package/dist/esm/utils/browser/utm-params.utils.js +33 -0
- package/dist/esm/utils/data/index.d.ts +1 -0
- package/dist/esm/utils/data/index.js +1 -0
- package/dist/esm/utils/data/uuid.utils.d.ts +5 -0
- package/dist/esm/utils/data/uuid.utils.js +14 -0
- package/dist/esm/utils/index.d.ts +6 -0
- package/dist/esm/utils/index.js +6 -0
- package/dist/esm/utils/logging/debug-logger.utils.d.ts +56 -0
- package/dist/esm/utils/logging/debug-logger.utils.js +136 -0
- package/dist/esm/utils/logging/index.d.ts +1 -0
- package/dist/esm/utils/logging/index.js +1 -0
- package/dist/esm/utils/network/index.d.ts +1 -0
- package/dist/esm/utils/network/index.js +1 -0
- package/dist/esm/utils/network/url.utils.d.ts +20 -0
- package/dist/esm/utils/network/url.utils.js +166 -0
- package/dist/esm/utils/security/index.d.ts +1 -0
- package/dist/esm/utils/security/index.js +1 -0
- package/dist/esm/utils/security/sanitize.utils.d.ts +32 -0
- package/dist/esm/utils/security/sanitize.utils.js +311 -0
- package/dist/esm/utils/validations/config-validations.utils.d.ts +42 -0
- package/dist/esm/utils/validations/config-validations.utils.js +289 -0
- package/dist/esm/utils/validations/event-validations.utils.d.ts +12 -0
- package/dist/esm/utils/validations/event-validations.utils.js +26 -0
- package/dist/esm/utils/validations/index.d.ts +5 -0
- package/dist/esm/utils/validations/index.js +5 -0
- package/dist/esm/utils/validations/metadata-validations.utils.d.ts +22 -0
- package/dist/esm/utils/validations/metadata-validations.utils.js +110 -0
- package/dist/esm/utils/validations/type-guards.utils.d.ts +6 -0
- package/dist/esm/utils/validations/type-guards.utils.js +27 -0
- package/dist/esm/utils/validations/url-validations.utils.d.ts +15 -0
- package/dist/esm/utils/validations/url-validations.utils.js +42 -0
- 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
|
+
}
|