11import axios from 'axios' ;
22import { getUserStore , globalErrorStore , loaderStore } from '$lib/helpers/store.js' ;
3+ import { renewToken } from '$lib/services/auth-service' ;
4+
5+ // Refresh handling state
6+ let isRefreshing = false ;
7+
8+ /** @type {{config: import('axios').InternalAxiosRequestConfig, resolve: (value:any)=>void, reject: (reason?: any)=>void}[] } */
9+ const failedQueue = [ ] ;
10+
11+ /** @type {{config: import('axios').InternalAxiosRequestConfig, resolve: (value:any)=>void, reject: (reason?: any)=>void}[] } */
12+ const pendingRequestQueue = [ ] ;
13+
14+ /**
15+ * Wrap renewToken into a Promise that resolves with the new access token string
16+ * @param {string } token
17+ * @returns {Promise<string> }
18+ */
19+ function refreshAccessToken ( token ) {
20+ return new Promise ( ( resolve , reject ) => {
21+ renewToken ( token , ( newToken ) => resolve ( newToken ) , ( ) => reject ( new Error ( 'Failed to refresh token' ) ) ) ;
22+ } ) ;
23+ }
24+
25+ /**
26+ * Retry queued requests sequentially with the provided token
27+ * @param {string } newToken
28+ * @returns {Promise<void> }
29+ */
30+ function processQueueSequentially ( newToken ) {
31+ let chain = Promise . resolve ( ) ;
32+ while ( failedQueue . length ) {
33+ const item = failedQueue . shift ( ) ;
34+ if ( ! item ) continue ;
35+ const { config, resolve, reject } = item ;
36+ // Ensure headers exists; Axios may use AxiosHeaders type
37+ // @ts -ignore
38+ config . headers = config . headers || { } ;
39+ // @ts -ignore
40+ config . headers . Authorization = `Bearer ${ newToken } ` ;
41+ chain = chain . then ( ( ) => axios ( config ) . then ( resolve ) . catch ( reject ) ) ;
42+ }
43+ return chain ;
44+ }
345
446// Add a request interceptor to attach authentication tokens or headers
547axios . interceptors . request . use (
648 ( config ) => {
7- // Add your authentication logic here
849 const user = getUserStore ( ) ;
950 if ( ! skipLoader ( config ) ) {
1051 loaderStore . set ( true ) ;
1152 }
12- // For example, attach an authentication token to the request headers
13- if ( user . token )
53+
54+ // Proactive token refresh: if expired or a refresh is in progress,
55+ // queue this request until a new token is available
56+ if ( isTokenExired ( ) || isRefreshing ) {
57+ return new Promise ( ( resolve , reject ) => {
58+ pendingRequestQueue . push ( { config, resolve, reject } ) ;
59+
60+ if ( ! isRefreshing ) {
61+ isRefreshing = true ;
62+ refreshAccessToken ( user ?. token || '' )
63+ . then ( ( newToken ) => {
64+ isRefreshing = false ;
65+ // Release queued requests with the new token
66+ while ( pendingRequestQueue . length ) {
67+ const item = pendingRequestQueue . shift ( ) ;
68+ if ( ! item ) continue ;
69+ const { config : cfg , resolve : res } = item ;
70+ // @ts -ignore
71+ cfg . headers = cfg . headers || { } ;
72+ // @ts -ignore
73+ cfg . headers . Authorization = `Bearer ${ newToken } ` ;
74+ res ( cfg ) ;
75+ }
76+ } )
77+ . catch ( ( err ) => {
78+ isRefreshing = false ;
79+ // Reject queued requests and redirect to login
80+ while ( pendingRequestQueue . length ) {
81+ const item = pendingRequestQueue . shift ( ) ;
82+ if ( item ) item . reject ( err ) ;
83+ }
84+ redirectToLogin ( ) ;
85+ } ) ;
86+ }
87+ } ) ;
88+ }
89+
90+ // Attach current token if present
91+ if ( user . token ) {
92+ // @ts -ignore
93+ config . headers = config . headers || { } ;
94+ // @ts -ignore
1495 config . headers . Authorization = `Bearer ${ user . token } ` ;
96+ }
1597 return config ;
1698 } ,
1799 ( error ) => {
@@ -25,23 +107,46 @@ axios.interceptors.response.use(
25107 ( response ) => {
26108 // If the request was successful, return the response
27109 loaderStore . set ( false ) ;
28- const user = getUserStore ( ) ;
29- const isExpired = Date . now ( ) / 1000 > user . expires ;
30- if ( isExpired || response ?. status === 401 ) {
31- redirectToLogin ( ) ;
32- return Promise . reject ( 'user token expired!' ) ;
33- }
34110 return response ;
35111 } ,
36112 ( error ) => {
37113 loaderStore . set ( false ) ;
38- const user = getUserStore ( ) ;
39-
40- const isExpired = Date . now ( ) / 1000 > user . expires ;
41- if ( isExpired || error ?. response ?. status === 401 ) {
42- redirectToLogin ( ) ;
43- return Promise . reject ( error ) ;
44- } else if ( ! skipGlobalError ( error . config ) ) {
114+ const originalRequest = error ?. config ;
115+
116+ // If token expired or 401 returned, attempt a single token refresh and
117+ // retry failed requests in sequence.
118+ if ( ( error ?. response ?. status === 401 || isTokenExired ( ) ) && originalRequest && ! originalRequest . _retry ) {
119+ originalRequest . _retry = true ;
120+
121+ return new Promise ( ( resolve , reject ) => {
122+ // Push the current request into the queue
123+ failedQueue . push ( { config : originalRequest , resolve, reject } ) ;
124+
125+ // Start refresh if not already in progress
126+ if ( ! isRefreshing ) {
127+ isRefreshing = true ;
128+ const user = getUserStore ( ) ;
129+
130+ refreshAccessToken ( user ?. token || '' )
131+ . then ( ( newToken ) => {
132+ isRefreshing = false ;
133+ return processQueueSequentially ( newToken ) ;
134+ } )
135+ . catch ( ( err ) => {
136+ isRefreshing = false ;
137+
138+ // Reject all queued requests
139+ while ( failedQueue . length ) {
140+ const item = failedQueue . shift ( ) ;
141+ if ( item ) item . reject ( err ) ;
142+ }
143+
144+ redirectToLogin ( ) ;
145+ throw err ;
146+ } ) ;
147+ }
148+ } ) ;
149+ } else if ( ! skipGlobalError ( originalRequest ) ) {
45150 globalErrorStore . set ( true ) ;
46151 setTimeout ( ( ) => {
47152 globalErrorStore . set ( false ) ;
@@ -53,6 +158,10 @@ axios.interceptors.response.use(
53158 }
54159) ;
55160
161+ function isTokenExired ( ) {
162+ const user = getUserStore ( ) ;
163+ return Date . now ( ) / 1000 > user . expires ;
164+ }
56165
57166function redirectToLogin ( ) {
58167 const curUrl = window . location . pathname + window . location . search ;
@@ -155,7 +264,7 @@ function skipGlobalError(config) {
155264 new RegExp ( 'http(s*)://(.*?)/conversation/(.*?)/update-message' , 'g' ) ,
156265 new RegExp ( 'http(s*)://(.*?)/conversation/(.*?)/update-tags' , 'g' )
157266 ] ;
158-
267+
159268 /** @type {RegExp[] } */
160269 const deleteRegexes = [
161270 new RegExp ( 'http(s*)://(.*?)/knowledge/vector/(.*?)/delete-collection' , 'g' ) ,
@@ -201,7 +310,7 @@ export function replaceUrl(url, args) {
201310
202311/**
203312 * Replace new line as <br>
204- * @param {string } text
313+ * @param {string } text
205314 * @returns string
206315 */
207316export function replaceNewLine ( text ) {
@@ -210,7 +319,7 @@ export function replaceNewLine(text) {
210319
211320/**
212321 * Replace unnecessary markdown
213- * @param {string } text
322+ * @param {string } text
214323 * @returns {string }
215324 */
216325export function replaceMarkdown ( text ) {
0 commit comments