11import axios from 'axios' ;
22import { getUserStore , globalErrorStore , loaderStore } from '$lib/helpers/store.js' ;
3+ import { renewToken } from '$lib/services/auth-service' ;
4+ import { delay } from './utils/common' ;
5+
6+
7+ const retryQueue = {
8+ /** @type {{config: import('axios').InternalAxiosRequestConfig, resolve: (value: any) => void, reject: (reason?: any) => void}[] } */
9+ queue : [ ] ,
10+
11+ /** @type {boolean } */
12+ isRefreshingToken : false ,
13+
14+ /** @type {number } */
15+ timeout : 20 ,
16+
17+ /**
18+ * refresh access token
19+ * @param {string } token
20+ * @returns {Promise<string> }
21+ */
22+ refreshAccessToken ( token ) {
23+ return new Promise ( ( resolve , reject ) => {
24+ renewToken ( token , ( newToken ) => resolve ( newToken ) , ( ) => reject ( new Error ( 'Failed to refresh token' ) ) ) ;
25+ } ) ;
26+ } ,
27+
28+ /** @param {{config: import('axios').InternalAxiosRequestConfig, resolve: (value: any) => void, reject: (reason?: any) => void} } item */
29+ enqueue ( item ) {
30+ this . queue . push ( item ) ;
31+
32+ if ( ! this . isRefreshingToken ) {
33+ const user = getUserStore ( ) ;
34+ if ( ! isTokenExired ( user . expires ) ) {
35+ this . dequeue ( user . token ) ;
36+ } else {
37+ this . isRefreshingToken = true ;
38+ this . refreshAccessToken ( user ?. token || '' )
39+ . then ( ( newToken ) => {
40+ this . isRefreshingToken = false ;
41+ const promise = this . dequeue ( newToken ) ;
42+ return promise ;
43+ } )
44+ . catch ( ( err ) => {
45+ this . isRefreshingToken = false ;
46+ // Reject all queued requests
47+ while ( this . queue . length > 0 ) {
48+ const item = this . queue . shift ( ) ;
49+ if ( item ) {
50+ item . reject ( err ) ;
51+ }
52+ }
53+ redirectToLogin ( ) ;
54+ } ) ;
55+ }
56+ }
57+ } ,
58+
59+ /**
60+ * @param {string } newToken
61+ * @returns {Promise<void> }
62+ */
63+ dequeue ( newToken ) {
64+ let chain = Promise . resolve ( ) ;
65+ while ( this . queue . length > 0 ) {
66+ const item = this . queue . shift ( ) ;
67+ if ( ! item ?. config ) {
68+ continue ;
69+ }
70+
71+ const { config } = item ;
72+ // @ts -ignore
73+ config . headers = config . headers || { } ;
74+ // @ts -ignore
75+ config . headers . Authorization = `Bearer ${ newToken } ` ;
76+
77+ chain = chain . then ( ( ) => delay ( this . timeout ) )
78+ . then ( ( ) => {
79+ return new Promise ( ( resolve ) => {
80+ axios ( config ) . then ( ( response ) => {
81+ resolve ( ) ;
82+ item . resolve ( response ) ;
83+ } ) . catch ( ( err ) => {
84+ resolve ( ) ;
85+ item . reject ( err ) ;
86+ } ) ;
87+ } ) ;
88+ } ) ;
89+ }
90+ return chain ;
91+ }
92+ } ;
393
494// Add a request interceptor to attach authentication tokens or headers
595axios . interceptors . request . use (
@@ -9,9 +99,10 @@ axios.interceptors.request.use(
999 if ( ! skipLoader ( config ) ) {
10100 loaderStore . set ( true ) ;
11101 }
12- // For example, attach an authentication token to the request headers
13- if ( user . token )
102+ // Attach an authentication token to the request headers
103+ if ( user . token ) {
14104 config . headers . Authorization = `Bearer ${ user . token } ` ;
105+ }
15106 return config ;
16107 } ,
17108 ( error ) => {
@@ -23,25 +114,24 @@ axios.interceptors.request.use(
23114// Add a response interceptor to handle 401 errors globally
24115axios . interceptors . response . use (
25116 ( response ) => {
26- // If the request was successful, return the response
27117 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- }
34118 return response ;
35119 } ,
36120 ( error ) => {
37121 loaderStore . set ( false ) ;
122+ const originalRequest = error ?. config || { } ;
38123 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 ) ) {
124+
125+ // If token expired or 401 returned, attempt a single token refresh and retry requests in queue.
126+ if ( ( error ?. response ?. status === 401 || isTokenExired ( user . expires ) )
127+ && originalRequest
128+ && ! originalRequest . _retried
129+ && ! originalRequest . url . includes ( 'renew-token' ) ) {
130+ originalRequest . _retried = true ;
131+ return new Promise ( ( resolve , reject ) => {
132+ retryQueue . enqueue ( { config : originalRequest , resolve, reject } ) ;
133+ } ) ;
134+ } else if ( ! skipGlobalError ( originalRequest ) ) {
45135 globalErrorStore . set ( true ) ;
46136 setTimeout ( ( ) => {
47137 globalErrorStore . set ( false ) ;
@@ -53,6 +143,12 @@ axios.interceptors.response.use(
53143 }
54144) ;
55145
146+ /**
147+ * @param {number } expires
148+ */
149+ function isTokenExired ( expires ) {
150+ return Date . now ( ) / 1000 > expires ;
151+ }
56152
57153function redirectToLogin ( ) {
58154 const curUrl = window . location . pathname + window . location . search ;
@@ -75,7 +171,8 @@ function skipLoader(config) {
75171 new RegExp ( 'http(s*)://(.*?)/knowledge/document/(.*?)/page' , 'g' ) ,
76172 new RegExp ( 'http(s*)://(.*?)/users' , 'g' ) ,
77173 new RegExp ( 'http(s*)://(.*?)/instruct/chat-completion' , 'g' ) ,
78- new RegExp ( 'http(s*)://(.*?)/agent/(.*?)/code-scripts' , 'g' )
174+ new RegExp ( 'http(s*)://(.*?)/agent/(.*?)/code-scripts' , 'g' ) ,
175+ new RegExp ( 'http(s*)://(.*?)/agent/(.*?)/code-script/generate' , 'g' )
79176 ] ;
80177
81178 /** @type {RegExp[] } */
@@ -111,7 +208,8 @@ function skipLoader(config) {
111208 new RegExp ( 'http(s*)://(.*?)/logger/instruction/log/keys' , 'g' ) ,
112209 new RegExp ( 'http(s*)://(.*?)/logger/conversation/(.*?)/content-log' , 'g' ) ,
113210 new RegExp ( 'http(s*)://(.*?)/logger/conversation/(.*?)/state-log' , 'g' ) ,
114- new RegExp ( 'http(s*)://(.*?)/mcp/server-configs' , 'g' )
211+ new RegExp ( 'http(s*)://(.*?)/mcp/server-configs' , 'g' ) ,
212+ new RegExp ( 'http(s*)://(.*?)/agent/(.*?)/code-scripts' , 'g' )
115213 ] ;
116214
117215 if ( config . method === 'post' && postRegexes . some ( regex => regex . test ( config . url || '' ) ) ) {
@@ -153,7 +251,7 @@ function skipGlobalError(config) {
153251 new RegExp ( 'http(s*)://(.*?)/conversation/(.*?)/update-message' , 'g' ) ,
154252 new RegExp ( 'http(s*)://(.*?)/conversation/(.*?)/update-tags' , 'g' )
155253 ] ;
156-
254+
157255 /** @type {RegExp[] } */
158256 const deleteRegexes = [
159257 new RegExp ( 'http(s*)://(.*?)/knowledge/vector/(.*?)/delete-collection' , 'g' ) ,
@@ -199,7 +297,7 @@ export function replaceUrl(url, args) {
199297
200298/**
201299 * Replace new line as <br>
202- * @param {string } text
300+ * @param {string } text
203301 * @returns string
204302 */
205303export function replaceNewLine ( text ) {
@@ -208,7 +306,7 @@ export function replaceNewLine(text) {
208306
209307/**
210308 * Replace unnecessary markdown
211- * @param {string } text
309+ * @param {string } text
212310 * @returns {string }
213311 */
214312export function replaceMarkdown ( text ) {
0 commit comments