@@ -17,7 +17,14 @@ import {
1717 useDebouncedCallback ,
1818 useWalletProviderState ,
1919 withTurnkeyErrorHandling ,
20- } from "../../utils" ;
20+ } from "../../utils/utils" ;
21+ import {
22+ type TimerMap ,
23+ clearKey ,
24+ clearAll ,
25+ setCappedTimeoutInMap ,
26+ setTimeoutInMap ,
27+ } from "../../utils/timers" ;
2128import {
2229 Chain ,
2330 CreateSubOrgParams ,
@@ -146,7 +153,7 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
146153 // this is so our useEffect that calls `initializeWalletProviderListeners()` only runs when it needs to
147154 const [ walletProviders , setWalletProviders ] = useWalletProviderState ( ) ;
148155
149- const expiryTimeoutsRef = useRef < Record < string , NodeJS . Timeout > > ( { } ) ;
156+ const expiryTimeoutsRef = useRef < TimerMap > ( { } ) ;
150157 const proxyAuthConfigRef = useRef < ProxyTGetWalletKitConfigResponse | null > (
151158 null ,
152159 ) ;
@@ -648,43 +655,42 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
648655
649656 /**
650657 * @internal
651- * Schedules a session expiration and warning timeout for the given session key.
658+ * Schedules a session expiration and warning timer for the given session key.
652659 *
653- * - This function sets up two timeouts: one for warning before the session expires and another to expire the session.
654- * - The warning timeout is set to trigger before the session expires, allowing for actions like refreshing the session.
655- * - The expiration timeout clears the session and triggers any necessary callbacks.
660+ * - Sets up two timers: one for warning before expiry and one for actual expiry.
661+ * - Uses capped timeouts under the hood so delays > 24.8 days are safe (see utils/timers.ts).
656662 *
657663 * @param params.sessionKey - The key of the session to schedule expiration for.
658- * @param params.expiry - The expiration time in seconds for the session .
664+ * @param params.expiry - The expiration time in seconds since epoch .
659665 * @throws {TurnkeyError } If an error occurs while scheduling the session expiration.
660666 */
661667 async function scheduleSessionExpiration ( params : {
662668 sessionKey : string ;
663- expiry : number ;
669+ expiry : number ; // seconds since epoch
664670 } ) {
665671 const { sessionKey, expiry } = params ;
666672
667673 try {
668- // Clear any existing timeout for this session key
669- if ( expiryTimeoutsRef . current [ sessionKey ] ) {
670- clearTimeout ( expiryTimeoutsRef . current [ sessionKey ] ) ;
671- delete expiryTimeoutsRef . current [ sessionKey ] ;
672- }
674+ const warnKey = `${ sessionKey } -warning` ;
673675
674- if ( expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] ) {
675- clearTimeout ( expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] ) ;
676- delete expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] ;
677- }
676+ // Replace any prior timers for this session
677+ clearKey ( expiryTimeoutsRef . current , sessionKey ) ;
678+ clearKey ( expiryTimeoutsRef . current , warnKey ) ;
678679
679- const timeUntilExpiry = expiry * 1000 - Date . now ( ) ;
680+ const now = Date . now ( ) ;
681+ const expiryMs = expiry * 1000 ;
682+ const timeUntilExpiry = expiryMs - now ;
680683
681684 const beforeExpiry = async ( ) => {
682685 const activeSession = await getSession ( ) ;
683- if ( ! activeSession && expiryTimeoutsRef . current [ sessionKey ] ) {
684- clearTimeout ( expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] ) ;
685- expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] = setTimeout (
686+
687+ if ( ! activeSession && expiryTimeoutsRef . current [ warnKey ] ) {
688+ // Keep nudging until session materializes (short 10s timer is fine)
689+ setTimeoutInMap (
690+ expiryTimeoutsRef . current ,
691+ warnKey ,
686692 beforeExpiry ,
687- 10000 ,
693+ 10_000 ,
688694 ) ;
689695 return ;
690696 }
@@ -693,6 +699,7 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
693699 if ( ! session ) return ;
694700
695701 callbacks ?. beforeSessionExpiry ?.( { sessionKey } ) ;
702+
696703 if ( masterConfig ?. auth ?. autoRefreshSession ) {
697704 await refreshSession ( {
698705 expirationSeconds : session . expirationSeconds ! ,
@@ -706,34 +713,50 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
706713 if ( ! expiredSession ) return ;
707714
708715 callbacks ?. onSessionExpired ?.( { sessionKey } ) ;
716+
709717 if ( ( await getActiveSessionKey ( ) ) === sessionKey ) {
710718 setSession ( undefined ) ;
711719 }
712- setAllSessions ( ( prevSessions ) => {
713- if ( ! prevSessions ) return prevSessions ;
714- const newSessions = { ...prevSessions } ;
715- delete newSessions [ sessionKey ] ;
716- return newSessions ;
720+
721+ setAllSessions ( ( prev ) => {
722+ if ( ! prev ) return prev ;
723+ const next = { ...prev } ;
724+ delete next [ sessionKey ] ;
725+ return next ;
717726 } ) ;
718727
719728 await clearSession ( { sessionKey } ) ;
720729
721- delete expiryTimeoutsRef . current [ sessionKey ] ;
722- delete expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] ;
730+ // Remove timers for this session
731+ clearKey ( expiryTimeoutsRef . current , sessionKey ) ;
732+ clearKey ( expiryTimeoutsRef . current , warnKey ) ;
723733
724734 await logout ( ) ;
725735 } ;
726736
727- if ( timeUntilExpiry <= SESSION_WARNING_THRESHOLD_MS ) {
728- beforeExpiry ( ) ;
737+ // Already expired → expire immediately
738+ if ( timeUntilExpiry <= 0 ) {
739+ await expireSession ( ) ;
740+ return ;
741+ }
742+
743+ // Warning timer (if threshold is in the future)
744+ const warnAt = expiryMs - SESSION_WARNING_THRESHOLD_MS ;
745+ if ( warnAt <= now ) {
746+ void beforeExpiry ( ) ; // fire-and-forget is fine
729747 } else {
730- expiryTimeoutsRef . current [ `${ sessionKey } -warning` ] = setTimeout (
748+ setCappedTimeoutInMap (
749+ expiryTimeoutsRef . current ,
750+ warnKey ,
731751 beforeExpiry ,
732- timeUntilExpiry - SESSION_WARNING_THRESHOLD_MS ,
752+ warnAt - now ,
733753 ) ;
734754 }
735755
736- expiryTimeoutsRef . current [ sessionKey ] = setTimeout (
756+ // Actual expiry timer (safe for long delays)
757+ setCappedTimeoutInMap (
758+ expiryTimeoutsRef . current ,
759+ sessionKey ,
737760 expireSession ,
738761 timeUntilExpiry ,
739762 ) ;
@@ -756,20 +779,16 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
756779 }
757780
758781 /**
759- * Clears all scheduled session expiration and warning timeouts for the client .
782+ * Clears all scheduled session timers ( warning + expiry) .
760783 *
761- * - This function removes all active session expiration and warning timeouts managed by the provider.
762- * - It is called automatically when sessions are re-initialized or on logout to prevent memory leaks and ensure no stale timeouts remain.
763- * - All timeouts stored in `expiryTimeoutsRef` are cleared and the reference is reset.
784+ * - Removes all active timers managed by this client.
785+ * - Useful on re-init or logout to avoid stale timers.
764786 *
765- * @throws {TurnkeyError } If an error occurs while clearing the timeouts .
787+ * @throws {TurnkeyError } If an error occurs while clearing the timers .
766788 */
767789 function clearSessionTimeouts ( ) {
768790 try {
769- Object . values ( expiryTimeoutsRef . current ) . forEach ( ( timeout ) => {
770- clearTimeout ( timeout ) ;
771- } ) ;
772- expiryTimeoutsRef . current = { } ;
791+ clearAll ( expiryTimeoutsRef . current ) ; // clears & deletes everything
773792 } catch ( error ) {
774793 if (
775794 error instanceof TurnkeyError ||
@@ -779,7 +798,7 @@ export const ClientProvider: React.FC<ClientProviderProps> = ({
779798 } else {
780799 callbacks ?. onError ?.(
781800 new TurnkeyError (
782- ` Failed to clear session timeouts` ,
801+ " Failed to clear session timeouts" ,
783802 TurnkeyErrorCodes . CLEAR_SESSION_TIMEOUTS_ERROR ,
784803 error ,
785804 ) ,
0 commit comments