@iblai/web-utils 1.11.11 → 1.12.0
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/dist/auth/web-utils/src/features/tracking/time-tracker.d.ts +55 -4
- package/dist/index.d.ts +55 -4
- package/dist/index.esm.js +216 -16
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +216 -16
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/web-utils/src/features/tracking/time-tracker.d.ts +55 -4
- package/dist/web-utils/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -3,27 +3,78 @@ export interface TimeTrackerConfig {
|
|
|
3
3
|
onTimeUpdate: (url: string, timeSpent: number) => void;
|
|
4
4
|
getCurrentUrl: () => string;
|
|
5
5
|
onRouteChange?: (callback: () => void) => () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Seconds of no user interaction (mouse, keyboard, touch, scroll) after
|
|
8
|
+
* which the user is considered idle and time tracking pauses. Set to 0 to
|
|
9
|
+
* disable idle detection. Defaults to 120 (2 minutes).
|
|
10
|
+
*/
|
|
11
|
+
idleTimeoutSeconds?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Pause tracking while the page/tab is hidden (Page Visibility API).
|
|
14
|
+
* Defaults to true.
|
|
15
|
+
*/
|
|
16
|
+
pauseOnHidden?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Pause tracking while the window loses focus (e.g. user switched to another
|
|
19
|
+
* application or window). Defaults to true.
|
|
20
|
+
*/
|
|
21
|
+
pauseOnBlur?: boolean;
|
|
6
22
|
}
|
|
7
23
|
export interface TimeTrackerState {
|
|
8
24
|
currentUrl: string;
|
|
9
|
-
startTime: number;
|
|
10
25
|
intervalId: number | null;
|
|
11
26
|
routeUnsubscribe: (() => void) | null;
|
|
27
|
+
/** Active time (ms) banked since the last reset. */
|
|
28
|
+
accumulatedMs: number;
|
|
29
|
+
/** Timestamp the current active window started, or null while inactive. */
|
|
30
|
+
activeSince: number | null;
|
|
31
|
+
isVisible: boolean;
|
|
32
|
+
isFocused: boolean;
|
|
33
|
+
isIdle: boolean;
|
|
34
|
+
idleTimerId: number | null;
|
|
12
35
|
}
|
|
13
36
|
export declare class TimeTracker {
|
|
14
37
|
private state;
|
|
15
38
|
private config;
|
|
39
|
+
private readonly idleTimeoutMs;
|
|
40
|
+
private readonly pauseOnHidden;
|
|
41
|
+
private readonly pauseOnBlur;
|
|
42
|
+
private readonly idleDetectionEnabled;
|
|
43
|
+
private lastActivityAt;
|
|
44
|
+
private readonly handleActivity;
|
|
45
|
+
private readonly handleVisibilityChange;
|
|
46
|
+
private readonly handleWindowFocus;
|
|
47
|
+
private readonly handleWindowBlur;
|
|
16
48
|
constructor(config: TimeTrackerConfig);
|
|
17
49
|
private initialize;
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
50
|
+
/** Whether time should currently be counted. */
|
|
51
|
+
private isActive;
|
|
52
|
+
/**
|
|
53
|
+
* Reconcile the active window with the current activity signals: open a new
|
|
54
|
+
* window when the user becomes active, or bank the elapsed time when they
|
|
55
|
+
* become inactive.
|
|
56
|
+
*/
|
|
57
|
+
private syncActiveWindow;
|
|
21
58
|
private getTimeSpent;
|
|
59
|
+
private resetTimer;
|
|
60
|
+
private startTracking;
|
|
61
|
+
private clearTrackingInterval;
|
|
22
62
|
private sendTimeUpdate;
|
|
23
63
|
private handleRouteChange;
|
|
64
|
+
private attachActivityListeners;
|
|
65
|
+
private detachActivityListeners;
|
|
66
|
+
private startIdleTimer;
|
|
67
|
+
private clearIdleTimer;
|
|
68
|
+
private markIdle;
|
|
69
|
+
private onActivity;
|
|
70
|
+
private onVisibilityChange;
|
|
71
|
+
private onWindowFocus;
|
|
72
|
+
private onWindowBlur;
|
|
24
73
|
pause(): void;
|
|
25
74
|
resume(): void;
|
|
26
75
|
destroy(): void;
|
|
27
76
|
getCurrentUrl(): string;
|
|
28
77
|
getTimeSpentSinceLastReset(): number;
|
|
78
|
+
/** Whether the user is currently considered active (present & interacting). */
|
|
79
|
+
getIsActive(): boolean;
|
|
29
80
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1294,29 +1294,80 @@ interface TimeTrackerConfig {
|
|
|
1294
1294
|
onTimeUpdate: (url: string, timeSpent: number) => void;
|
|
1295
1295
|
getCurrentUrl: () => string;
|
|
1296
1296
|
onRouteChange?: (callback: () => void) => () => void;
|
|
1297
|
+
/**
|
|
1298
|
+
* Seconds of no user interaction (mouse, keyboard, touch, scroll) after
|
|
1299
|
+
* which the user is considered idle and time tracking pauses. Set to 0 to
|
|
1300
|
+
* disable idle detection. Defaults to 120 (2 minutes).
|
|
1301
|
+
*/
|
|
1302
|
+
idleTimeoutSeconds?: number;
|
|
1303
|
+
/**
|
|
1304
|
+
* Pause tracking while the page/tab is hidden (Page Visibility API).
|
|
1305
|
+
* Defaults to true.
|
|
1306
|
+
*/
|
|
1307
|
+
pauseOnHidden?: boolean;
|
|
1308
|
+
/**
|
|
1309
|
+
* Pause tracking while the window loses focus (e.g. user switched to another
|
|
1310
|
+
* application or window). Defaults to true.
|
|
1311
|
+
*/
|
|
1312
|
+
pauseOnBlur?: boolean;
|
|
1297
1313
|
}
|
|
1298
1314
|
interface TimeTrackerState {
|
|
1299
1315
|
currentUrl: string;
|
|
1300
|
-
startTime: number;
|
|
1301
1316
|
intervalId: number | null;
|
|
1302
1317
|
routeUnsubscribe: (() => void) | null;
|
|
1318
|
+
/** Active time (ms) banked since the last reset. */
|
|
1319
|
+
accumulatedMs: number;
|
|
1320
|
+
/** Timestamp the current active window started, or null while inactive. */
|
|
1321
|
+
activeSince: number | null;
|
|
1322
|
+
isVisible: boolean;
|
|
1323
|
+
isFocused: boolean;
|
|
1324
|
+
isIdle: boolean;
|
|
1325
|
+
idleTimerId: number | null;
|
|
1303
1326
|
}
|
|
1304
1327
|
declare class TimeTracker {
|
|
1305
1328
|
private state;
|
|
1306
1329
|
private config;
|
|
1330
|
+
private readonly idleTimeoutMs;
|
|
1331
|
+
private readonly pauseOnHidden;
|
|
1332
|
+
private readonly pauseOnBlur;
|
|
1333
|
+
private readonly idleDetectionEnabled;
|
|
1334
|
+
private lastActivityAt;
|
|
1335
|
+
private readonly handleActivity;
|
|
1336
|
+
private readonly handleVisibilityChange;
|
|
1337
|
+
private readonly handleWindowFocus;
|
|
1338
|
+
private readonly handleWindowBlur;
|
|
1307
1339
|
constructor(config: TimeTrackerConfig);
|
|
1308
1340
|
private initialize;
|
|
1309
|
-
|
|
1310
|
-
private
|
|
1311
|
-
|
|
1341
|
+
/** Whether time should currently be counted. */
|
|
1342
|
+
private isActive;
|
|
1343
|
+
/**
|
|
1344
|
+
* Reconcile the active window with the current activity signals: open a new
|
|
1345
|
+
* window when the user becomes active, or bank the elapsed time when they
|
|
1346
|
+
* become inactive.
|
|
1347
|
+
*/
|
|
1348
|
+
private syncActiveWindow;
|
|
1312
1349
|
private getTimeSpent;
|
|
1350
|
+
private resetTimer;
|
|
1351
|
+
private startTracking;
|
|
1352
|
+
private clearTrackingInterval;
|
|
1313
1353
|
private sendTimeUpdate;
|
|
1314
1354
|
private handleRouteChange;
|
|
1355
|
+
private attachActivityListeners;
|
|
1356
|
+
private detachActivityListeners;
|
|
1357
|
+
private startIdleTimer;
|
|
1358
|
+
private clearIdleTimer;
|
|
1359
|
+
private markIdle;
|
|
1360
|
+
private onActivity;
|
|
1361
|
+
private onVisibilityChange;
|
|
1362
|
+
private onWindowFocus;
|
|
1363
|
+
private onWindowBlur;
|
|
1315
1364
|
pause(): void;
|
|
1316
1365
|
resume(): void;
|
|
1317
1366
|
destroy(): void;
|
|
1318
1367
|
getCurrentUrl(): string;
|
|
1319
1368
|
getTimeSpentSinceLastReset(): number;
|
|
1369
|
+
/** Whether the user is currently considered active (present & interacting). */
|
|
1370
|
+
getIsActive(): boolean;
|
|
1320
1371
|
}
|
|
1321
1372
|
|
|
1322
1373
|
interface UseTimeTrackerConfig {
|
package/dist/index.esm.js
CHANGED
|
@@ -9180,22 +9180,22 @@ async function syncAuthToCookies(storageService) {
|
|
|
9180
9180
|
function hasNonExpiredAuthToken() {
|
|
9181
9181
|
const token = window.localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
|
9182
9182
|
if (!token) {
|
|
9183
|
-
console.log(
|
|
9183
|
+
console.log("################### [hasNonExpiredAuthToken] axd token is not defined", token);
|
|
9184
9184
|
return false;
|
|
9185
9185
|
}
|
|
9186
9186
|
const tokenExpiry = window.localStorage.getItem(LOCAL_STORAGE_KEYS.TOKEN_EXPIRY);
|
|
9187
9187
|
if (!tokenExpiry) {
|
|
9188
|
-
console.log(
|
|
9188
|
+
console.log("################### [hasNonExpiredAuthToken] axd token expiry is not defined", tokenExpiry);
|
|
9189
9189
|
return true;
|
|
9190
9190
|
}
|
|
9191
9191
|
const expiryDate = new Date(tokenExpiry);
|
|
9192
9192
|
if (isNaN(expiryDate.getTime())) {
|
|
9193
|
-
console.log(
|
|
9193
|
+
console.log("################### [hasNonExpiredAuthToken] axd token expiry date", expiryDate);
|
|
9194
9194
|
return false;
|
|
9195
9195
|
}
|
|
9196
9196
|
const currentDate = new Date();
|
|
9197
9197
|
if (expiryDate <= currentDate) {
|
|
9198
|
-
console.log(
|
|
9198
|
+
console.log("################### [hasNonExpiredAuthToken] axd token expiry date is less than current date ", expiryDate, currentDate);
|
|
9199
9199
|
return false;
|
|
9200
9200
|
}
|
|
9201
9201
|
return true;
|
|
@@ -9458,6 +9458,20 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
|
|
|
9458
9458
|
console.log("[AuthProvider] Redirect already in progress, skipping");
|
|
9459
9459
|
return;
|
|
9460
9460
|
}
|
|
9461
|
+
// Defer every auth redirect while a tenant switch is in flight.
|
|
9462
|
+
// handleTenantSwitch() clears the SHARED (cross-tab) localStorage mid-switch,
|
|
9463
|
+
// so every open window transiently sees missing tokens. Without this guard a
|
|
9464
|
+
// bystander window force-logs-out on the absent DM token, which clears
|
|
9465
|
+
// storage + sets a logout-timestamp cookie that wipes the tokens the
|
|
9466
|
+
// switching tab just wrote and cascades the logout across tabs. The TTL lock
|
|
9467
|
+
// (writeTenantSwitchLock) survives the /login/complete -> /sso-login-complete
|
|
9468
|
+
// redirect and is kept fresh across tabs via useTenantSwitchSync, so it
|
|
9469
|
+
// brackets the switch window. The cross-SPA cookie-sync poll keeps running
|
|
9470
|
+
// and recovers once the new tenant's tokens land.
|
|
9471
|
+
if (isTenantSwitchInProgress()) {
|
|
9472
|
+
console.log("[AuthProvider] Tenant switch in progress, deferring auth redirect");
|
|
9473
|
+
return;
|
|
9474
|
+
}
|
|
9461
9475
|
isRedirectingRef.current = true;
|
|
9462
9476
|
redirectStartedAtRef.current = Date.now();
|
|
9463
9477
|
// NOTE: we intentionally do NOT clear the interval here.
|
|
@@ -9643,6 +9657,17 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
|
|
|
9643
9657
|
var _a;
|
|
9644
9658
|
try {
|
|
9645
9659
|
setIsAuthenticating(true);
|
|
9660
|
+
// During an in-flight tenant switch the shared, cross-tab localStorage is
|
|
9661
|
+
// transiently cleared (see handleTenantSwitch), so the auth/DM tokens are
|
|
9662
|
+
// legitimately absent. Bail out BEFORE onAuthFailure — some apps hard
|
|
9663
|
+
// navigate to /error/403 from that callback (e.g. skills does
|
|
9664
|
+
// `window.location.href = '/error/403'`), which would fire even though
|
|
9665
|
+
// safeRedirectToAuthSpa itself is guarded. The cookie-sync poll re-checks
|
|
9666
|
+
// once the new tenant's tokens land.
|
|
9667
|
+
if (isTenantSwitchInProgress()) {
|
|
9668
|
+
console.log("[AuthProvider] Tenant switch in progress, skipping auth check");
|
|
9669
|
+
return;
|
|
9670
|
+
}
|
|
9646
9671
|
const authToken = await (storageService === null || storageService === void 0 ? void 0 : storageService.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN));
|
|
9647
9672
|
if (authToken && !hasNonExpiredAuthToken()) {
|
|
9648
9673
|
const reason = "Auth token expired";
|
|
@@ -11869,46 +11894,117 @@ const selectIsReasoning = (state) => state.chatSliceShared.currentStreamingMessa
|
|
|
11869
11894
|
// NEW: Get streaming tool calls
|
|
11870
11895
|
const selectStreamingToolCalls = (state) => state.chatSliceShared.currentStreamingMessage.toolCalls;
|
|
11871
11896
|
|
|
11897
|
+
const DEFAULT_IDLE_TIMEOUT_SECONDS = 120;
|
|
11898
|
+
// Minimum gap between idle-timer resets. Interaction events like mousemove fire
|
|
11899
|
+
// rapidly; there's no need to restart the (minutes-long) idle countdown more
|
|
11900
|
+
// than once per second.
|
|
11901
|
+
const ACTIVITY_THROTTLE_MS = 1000;
|
|
11902
|
+
// User interactions that count as activity and keep the user "present".
|
|
11903
|
+
const ACTIVITY_EVENTS = [
|
|
11904
|
+
"mousemove",
|
|
11905
|
+
"mousedown",
|
|
11906
|
+
"keydown",
|
|
11907
|
+
"wheel",
|
|
11908
|
+
"scroll",
|
|
11909
|
+
"touchstart",
|
|
11910
|
+
"pointerdown",
|
|
11911
|
+
];
|
|
11872
11912
|
class TimeTracker {
|
|
11873
11913
|
constructor(config) {
|
|
11914
|
+
var _a, _b, _c;
|
|
11874
11915
|
this.state = {
|
|
11875
11916
|
currentUrl: "",
|
|
11876
|
-
startTime: 0,
|
|
11877
11917
|
intervalId: null,
|
|
11878
11918
|
routeUnsubscribe: null,
|
|
11919
|
+
accumulatedMs: 0,
|
|
11920
|
+
activeSince: null,
|
|
11921
|
+
isVisible: true,
|
|
11922
|
+
isFocused: true,
|
|
11923
|
+
isIdle: false,
|
|
11924
|
+
idleTimerId: null,
|
|
11879
11925
|
};
|
|
11926
|
+
// Timestamp of the last activity that reset the idle timer (for throttling).
|
|
11927
|
+
this.lastActivityAt = 0;
|
|
11928
|
+
// Bound listeners kept so they can be removed on destroy.
|
|
11929
|
+
this.handleActivity = () => this.onActivity();
|
|
11930
|
+
this.handleVisibilityChange = () => this.onVisibilityChange();
|
|
11931
|
+
this.handleWindowFocus = () => this.onWindowFocus();
|
|
11932
|
+
this.handleWindowBlur = () => this.onWindowBlur();
|
|
11880
11933
|
this.config = config;
|
|
11934
|
+
this.idleTimeoutMs =
|
|
11935
|
+
((_a = config.idleTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_IDLE_TIMEOUT_SECONDS) * 1000;
|
|
11936
|
+
this.pauseOnHidden = (_b = config.pauseOnHidden) !== null && _b !== void 0 ? _b : true;
|
|
11937
|
+
this.pauseOnBlur = (_c = config.pauseOnBlur) !== null && _c !== void 0 ? _c : true;
|
|
11938
|
+
this.idleDetectionEnabled =
|
|
11939
|
+
this.idleTimeoutMs > 0 &&
|
|
11940
|
+
typeof window !== "undefined" &&
|
|
11941
|
+
typeof document !== "undefined";
|
|
11881
11942
|
this.initialize();
|
|
11882
11943
|
}
|
|
11883
11944
|
initialize() {
|
|
11884
11945
|
this.state.currentUrl = this.config.getCurrentUrl();
|
|
11946
|
+
// Seed activity signals from the current environment.
|
|
11947
|
+
if (this.pauseOnHidden && typeof document !== "undefined") {
|
|
11948
|
+
this.state.isVisible = !document.hidden;
|
|
11949
|
+
}
|
|
11950
|
+
this.state.isFocused = true;
|
|
11951
|
+
this.state.isIdle = false;
|
|
11952
|
+
this.attachActivityListeners();
|
|
11885
11953
|
this.startTracking();
|
|
11954
|
+
this.startIdleTimer();
|
|
11886
11955
|
if (this.config.onRouteChange) {
|
|
11887
11956
|
this.state.routeUnsubscribe = this.config.onRouteChange(() => {
|
|
11888
11957
|
this.handleRouteChange();
|
|
11889
11958
|
});
|
|
11890
11959
|
}
|
|
11891
11960
|
}
|
|
11961
|
+
// --- Active time accounting -------------------------------------------------
|
|
11962
|
+
/** Whether time should currently be counted. */
|
|
11963
|
+
isActive() {
|
|
11964
|
+
return this.state.isVisible && this.state.isFocused && !this.state.isIdle;
|
|
11965
|
+
}
|
|
11966
|
+
/**
|
|
11967
|
+
* Reconcile the active window with the current activity signals: open a new
|
|
11968
|
+
* window when the user becomes active, or bank the elapsed time when they
|
|
11969
|
+
* become inactive.
|
|
11970
|
+
*/
|
|
11971
|
+
syncActiveWindow() {
|
|
11972
|
+
const active = this.isActive();
|
|
11973
|
+
if (active && this.state.activeSince === null) {
|
|
11974
|
+
this.state.activeSince = Date.now();
|
|
11975
|
+
}
|
|
11976
|
+
else if (!active && this.state.activeSince !== null) {
|
|
11977
|
+
this.state.accumulatedMs += Date.now() - this.state.activeSince;
|
|
11978
|
+
this.state.activeSince = null;
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
getTimeSpent() {
|
|
11982
|
+
let total = this.state.accumulatedMs;
|
|
11983
|
+
if (this.state.activeSince !== null) {
|
|
11984
|
+
total += Date.now() - this.state.activeSince;
|
|
11985
|
+
}
|
|
11986
|
+
return total;
|
|
11987
|
+
}
|
|
11988
|
+
resetTimer() {
|
|
11989
|
+
this.state.accumulatedMs = 0;
|
|
11990
|
+
this.state.activeSince = this.isActive() ? Date.now() : null;
|
|
11991
|
+
}
|
|
11892
11992
|
startTracking() {
|
|
11893
|
-
this.
|
|
11894
|
-
this.
|
|
11993
|
+
this.clearTrackingInterval();
|
|
11994
|
+
this.resetTimer();
|
|
11995
|
+
if (typeof window === "undefined")
|
|
11996
|
+
return;
|
|
11895
11997
|
this.state.intervalId = window.setInterval(() => {
|
|
11896
11998
|
this.sendTimeUpdate();
|
|
11897
11999
|
this.resetTimer();
|
|
11898
12000
|
}, this.config.intervalSeconds * 1000);
|
|
11899
12001
|
}
|
|
11900
|
-
|
|
12002
|
+
clearTrackingInterval() {
|
|
11901
12003
|
if (this.state.intervalId !== null) {
|
|
11902
12004
|
clearInterval(this.state.intervalId);
|
|
11903
12005
|
this.state.intervalId = null;
|
|
11904
12006
|
}
|
|
11905
12007
|
}
|
|
11906
|
-
resetTimer() {
|
|
11907
|
-
this.state.startTime = Date.now();
|
|
11908
|
-
}
|
|
11909
|
-
getTimeSpent() {
|
|
11910
|
-
return Date.now() - this.state.startTime;
|
|
11911
|
-
}
|
|
11912
12008
|
sendTimeUpdate() {
|
|
11913
12009
|
const timeSpent = this.getTimeSpent();
|
|
11914
12010
|
if (timeSpent > 0) {
|
|
@@ -11923,16 +12019,116 @@ class TimeTracker {
|
|
|
11923
12019
|
this.resetTimer();
|
|
11924
12020
|
}
|
|
11925
12021
|
}
|
|
12022
|
+
// --- Activity / inactivity detection ---------------------------------------
|
|
12023
|
+
attachActivityListeners() {
|
|
12024
|
+
if (typeof window !== "undefined" && window.addEventListener) {
|
|
12025
|
+
if (this.idleDetectionEnabled) {
|
|
12026
|
+
for (const event of ACTIVITY_EVENTS) {
|
|
12027
|
+
window.addEventListener(event, this.handleActivity, {
|
|
12028
|
+
passive: true,
|
|
12029
|
+
});
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
12032
|
+
if (this.pauseOnBlur) {
|
|
12033
|
+
window.addEventListener("focus", this.handleWindowFocus);
|
|
12034
|
+
window.addEventListener("blur", this.handleWindowBlur);
|
|
12035
|
+
}
|
|
12036
|
+
}
|
|
12037
|
+
if (this.pauseOnHidden &&
|
|
12038
|
+
typeof document !== "undefined" &&
|
|
12039
|
+
document.addEventListener) {
|
|
12040
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
12041
|
+
}
|
|
12042
|
+
}
|
|
12043
|
+
detachActivityListeners() {
|
|
12044
|
+
if (typeof window !== "undefined" && window.removeEventListener) {
|
|
12045
|
+
for (const event of ACTIVITY_EVENTS) {
|
|
12046
|
+
window.removeEventListener(event, this.handleActivity);
|
|
12047
|
+
}
|
|
12048
|
+
window.removeEventListener("focus", this.handleWindowFocus);
|
|
12049
|
+
window.removeEventListener("blur", this.handleWindowBlur);
|
|
12050
|
+
}
|
|
12051
|
+
if (typeof document !== "undefined" && document.removeEventListener) {
|
|
12052
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
startIdleTimer() {
|
|
12056
|
+
if (!this.idleDetectionEnabled)
|
|
12057
|
+
return;
|
|
12058
|
+
this.clearIdleTimer();
|
|
12059
|
+
this.lastActivityAt = Date.now();
|
|
12060
|
+
this.state.idleTimerId = window.setTimeout(() => {
|
|
12061
|
+
this.markIdle();
|
|
12062
|
+
}, this.idleTimeoutMs);
|
|
12063
|
+
}
|
|
12064
|
+
clearIdleTimer() {
|
|
12065
|
+
if (this.state.idleTimerId !== null) {
|
|
12066
|
+
clearTimeout(this.state.idleTimerId);
|
|
12067
|
+
this.state.idleTimerId = null;
|
|
12068
|
+
}
|
|
12069
|
+
}
|
|
12070
|
+
markIdle() {
|
|
12071
|
+
if (this.state.isIdle)
|
|
12072
|
+
return;
|
|
12073
|
+
this.state.isIdle = true;
|
|
12074
|
+
this.syncActiveWindow();
|
|
12075
|
+
}
|
|
12076
|
+
onActivity() {
|
|
12077
|
+
// Returning from idle must always restart the countdown immediately.
|
|
12078
|
+
if (this.state.isIdle) {
|
|
12079
|
+
this.state.isIdle = false;
|
|
12080
|
+
this.syncActiveWindow();
|
|
12081
|
+
this.startIdleTimer();
|
|
12082
|
+
return;
|
|
12083
|
+
}
|
|
12084
|
+
// Otherwise throttle: no need to reset a minutes-long timer on every
|
|
12085
|
+
// mousemove.
|
|
12086
|
+
if (Date.now() - this.lastActivityAt < ACTIVITY_THROTTLE_MS)
|
|
12087
|
+
return;
|
|
12088
|
+
this.startIdleTimer();
|
|
12089
|
+
}
|
|
12090
|
+
onVisibilityChange() {
|
|
12091
|
+
if (typeof document === "undefined")
|
|
12092
|
+
return;
|
|
12093
|
+
this.state.isVisible = !document.hidden;
|
|
12094
|
+
if (this.state.isVisible) {
|
|
12095
|
+
// Returning to the tab counts as being present again.
|
|
12096
|
+
this.state.isIdle = false;
|
|
12097
|
+
this.startIdleTimer();
|
|
12098
|
+
}
|
|
12099
|
+
this.syncActiveWindow();
|
|
12100
|
+
}
|
|
12101
|
+
onWindowFocus() {
|
|
12102
|
+
this.state.isFocused = true;
|
|
12103
|
+
this.state.isIdle = false;
|
|
12104
|
+
this.startIdleTimer();
|
|
12105
|
+
this.syncActiveWindow();
|
|
12106
|
+
}
|
|
12107
|
+
onWindowBlur() {
|
|
12108
|
+
this.state.isFocused = false;
|
|
12109
|
+
this.syncActiveWindow();
|
|
12110
|
+
}
|
|
12111
|
+
// --- Public API -------------------------------------------------------------
|
|
11926
12112
|
pause() {
|
|
11927
12113
|
this.sendTimeUpdate();
|
|
11928
|
-
this.
|
|
12114
|
+
this.clearTrackingInterval();
|
|
12115
|
+
this.clearIdleTimer();
|
|
12116
|
+
// Stop accounting until resumed.
|
|
12117
|
+
this.state.accumulatedMs = 0;
|
|
12118
|
+
this.state.activeSince = null;
|
|
11929
12119
|
}
|
|
11930
12120
|
resume() {
|
|
12121
|
+
// Treat resume as a fresh, present start so a stale idle flag can't keep
|
|
12122
|
+
// the tracker from counting.
|
|
12123
|
+
this.state.isIdle = false;
|
|
11931
12124
|
this.startTracking();
|
|
12125
|
+
this.startIdleTimer();
|
|
11932
12126
|
}
|
|
11933
12127
|
destroy() {
|
|
11934
12128
|
this.sendTimeUpdate();
|
|
11935
|
-
this.
|
|
12129
|
+
this.clearTrackingInterval();
|
|
12130
|
+
this.clearIdleTimer();
|
|
12131
|
+
this.detachActivityListeners();
|
|
11936
12132
|
if (this.state.routeUnsubscribe) {
|
|
11937
12133
|
this.state.routeUnsubscribe();
|
|
11938
12134
|
this.state.routeUnsubscribe = null;
|
|
@@ -11944,6 +12140,10 @@ class TimeTracker {
|
|
|
11944
12140
|
getTimeSpentSinceLastReset() {
|
|
11945
12141
|
return this.getTimeSpent();
|
|
11946
12142
|
}
|
|
12143
|
+
/** Whether the user is currently considered active (present & interacting). */
|
|
12144
|
+
getIsActive() {
|
|
12145
|
+
return this.isActive();
|
|
12146
|
+
}
|
|
11947
12147
|
}
|
|
11948
12148
|
|
|
11949
12149
|
function useTimeTracker(config) {
|