@iblai/web-utils 1.11.12 → 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.
@@ -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
- private startTracking;
19
- private clearInterval;
20
- private resetTimer;
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
@@ -1101,7 +1101,7 @@ declare const isNode: () => string | false;
1101
1101
  declare const isExpo: () => any;
1102
1102
  declare const isTauri: () => boolean;
1103
1103
  declare function isSafariBrowser(): boolean;
1104
- declare const getPlatform: () => "web" | "react-native" | "node" | "unknown";
1104
+ declare const getPlatform: () => "react-native" | "web" | "node" | "unknown";
1105
1105
  declare const safeRequire: (moduleName: string) => any;
1106
1106
  declare const getNextNavigation: () => any;
1107
1107
 
@@ -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
- private startTracking;
1310
- private clearInterval;
1311
- private resetTimer;
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
@@ -11894,46 +11894,117 @@ const selectIsReasoning = (state) => state.chatSliceShared.currentStreamingMessa
11894
11894
  // NEW: Get streaming tool calls
11895
11895
  const selectStreamingToolCalls = (state) => state.chatSliceShared.currentStreamingMessage.toolCalls;
11896
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
+ ];
11897
11912
  class TimeTracker {
11898
11913
  constructor(config) {
11914
+ var _a, _b, _c;
11899
11915
  this.state = {
11900
11916
  currentUrl: "",
11901
- startTime: 0,
11902
11917
  intervalId: null,
11903
11918
  routeUnsubscribe: null,
11919
+ accumulatedMs: 0,
11920
+ activeSince: null,
11921
+ isVisible: true,
11922
+ isFocused: true,
11923
+ isIdle: false,
11924
+ idleTimerId: null,
11904
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();
11905
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";
11906
11942
  this.initialize();
11907
11943
  }
11908
11944
  initialize() {
11909
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();
11910
11953
  this.startTracking();
11954
+ this.startIdleTimer();
11911
11955
  if (this.config.onRouteChange) {
11912
11956
  this.state.routeUnsubscribe = this.config.onRouteChange(() => {
11913
11957
  this.handleRouteChange();
11914
11958
  });
11915
11959
  }
11916
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
+ }
11917
11992
  startTracking() {
11918
- this.state.startTime = Date.now();
11919
- this.clearInterval();
11993
+ this.clearTrackingInterval();
11994
+ this.resetTimer();
11995
+ if (typeof window === "undefined")
11996
+ return;
11920
11997
  this.state.intervalId = window.setInterval(() => {
11921
11998
  this.sendTimeUpdate();
11922
11999
  this.resetTimer();
11923
12000
  }, this.config.intervalSeconds * 1000);
11924
12001
  }
11925
- clearInterval() {
12002
+ clearTrackingInterval() {
11926
12003
  if (this.state.intervalId !== null) {
11927
12004
  clearInterval(this.state.intervalId);
11928
12005
  this.state.intervalId = null;
11929
12006
  }
11930
12007
  }
11931
- resetTimer() {
11932
- this.state.startTime = Date.now();
11933
- }
11934
- getTimeSpent() {
11935
- return Date.now() - this.state.startTime;
11936
- }
11937
12008
  sendTimeUpdate() {
11938
12009
  const timeSpent = this.getTimeSpent();
11939
12010
  if (timeSpent > 0) {
@@ -11948,16 +12019,116 @@ class TimeTracker {
11948
12019
  this.resetTimer();
11949
12020
  }
11950
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 -------------------------------------------------------------
11951
12112
  pause() {
11952
12113
  this.sendTimeUpdate();
11953
- this.clearInterval();
12114
+ this.clearTrackingInterval();
12115
+ this.clearIdleTimer();
12116
+ // Stop accounting until resumed.
12117
+ this.state.accumulatedMs = 0;
12118
+ this.state.activeSince = null;
11954
12119
  }
11955
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;
11956
12124
  this.startTracking();
12125
+ this.startIdleTimer();
11957
12126
  }
11958
12127
  destroy() {
11959
12128
  this.sendTimeUpdate();
11960
- this.clearInterval();
12129
+ this.clearTrackingInterval();
12130
+ this.clearIdleTimer();
12131
+ this.detachActivityListeners();
11961
12132
  if (this.state.routeUnsubscribe) {
11962
12133
  this.state.routeUnsubscribe();
11963
12134
  this.state.routeUnsubscribe = null;
@@ -11969,6 +12140,10 @@ class TimeTracker {
11969
12140
  getTimeSpentSinceLastReset() {
11970
12141
  return this.getTimeSpent();
11971
12142
  }
12143
+ /** Whether the user is currently considered active (present & interacting). */
12144
+ getIsActive() {
12145
+ return this.isActive();
12146
+ }
11972
12147
  }
11973
12148
 
11974
12149
  function useTimeTracker(config) {