@@ -17,11 +17,18 @@ import {
1717 setLocalHashInfo ,
1818} from './core' ;
1919import { PermissionsAndroid } from './permissions' ;
20- import { CheckResult , ClientOptions , EventType , ProgressData } from './type' ;
20+ import {
21+ CheckResult ,
22+ ClientOptions ,
23+ EventType ,
24+ ProgressData ,
25+ UpdateServerConfig ,
26+ } from './type' ;
2127import {
2228 assertWeb ,
29+ DEFAULT_FETCH_TIMEOUT_MS ,
2330 emptyObj ,
24- enhancedFetch ,
31+ fetchWithTimeout ,
2532 info ,
2633 joinUrls ,
2734 log ,
@@ -30,6 +37,7 @@ import {
3037 testUrls ,
3138} from './utils' ;
3239import i18n from './i18n' ;
40+ import { dedupeEndpoints , executeEndpointFallback } from './endpoint' ;
3341
3442const SERVER_PRESETS = {
3543 // cn
@@ -63,6 +71,19 @@ const defaultClientOptions: ClientOptions = {
6371 throwError : false ,
6472} ;
6573
74+ const cloneServerConfig = (
75+ server ?: UpdateServerConfig ,
76+ ) : UpdateServerConfig | undefined => {
77+ if ( ! server ) {
78+ return undefined ;
79+ }
80+ return {
81+ main : server . main ,
82+ backups : server . backups ? [ ...server . backups ] : undefined ,
83+ queryUrls : server . queryUrls ? [ ...server . queryUrls ] : undefined ,
84+ } ;
85+ } ;
86+
6687export const sharedState : {
6788 progressHandlers : Record < string , EmitterSubscription > ;
6889 downloadedHash ?: string ;
@@ -110,7 +131,7 @@ export class Pushy {
110131
111132 constructor ( options : ClientOptions , clientType ?: 'Pushy' | 'Cresc' ) {
112133 this . clientType = clientType || 'Pushy' ;
113- this . options . server = SERVER_PRESETS [ this . clientType ] ;
134+ this . options . server = cloneServerConfig ( SERVER_PRESETS [ this . clientType ] ) ;
114135
115136 i18n . setLocale ( options . locale ?? this . clientType === 'Pushy' ? 'zh' : 'en' ) ;
116137
@@ -134,7 +155,10 @@ export class Pushy {
134155 setOptions = ( options : Partial < ClientOptions > ) => {
135156 for ( const [ key , value ] of Object . entries ( options ) ) {
136157 if ( value !== undefined ) {
137- ( this . options as any ) [ key ] = value ;
158+ ( this . options as any ) [ key ] =
159+ key === 'server'
160+ ? cloneServerConfig ( value as UpdateServerConfig )
161+ : value ;
138162 if ( key === 'logger' ) {
139163 this . loggerPromise . resolve ( ) ;
140164 }
@@ -188,6 +212,90 @@ export class Pushy {
188212 getCheckUrl = ( endpoint : string = this . options . server ! . main ) => {
189213 return `${ endpoint } /checkUpdate/${ this . options . appKey } ` ;
190214 } ;
215+ getConfiguredCheckEndpoints = ( ) => {
216+ const { server } = this . options ;
217+ if ( ! server ) {
218+ return [ ] ;
219+ }
220+ return dedupeEndpoints ( [ server . main , ...( server . backups || [ ] ) ] ) ;
221+ } ;
222+ getRemoteEndpoints = async ( ) => {
223+ const { server } = this . options ;
224+ if ( ! server ?. queryUrls ?. length ) {
225+ return [ ] ;
226+ }
227+ try {
228+ const resp = await promiseAny (
229+ server . queryUrls . map ( queryUrl =>
230+ fetchWithTimeout ( queryUrl , { } , DEFAULT_FETCH_TIMEOUT_MS ) ,
231+ ) ,
232+ ) ;
233+ const remoteEndpoints = await resp . json ( ) ;
234+ log ( 'fetch endpoints:' , remoteEndpoints ) ;
235+ if ( Array . isArray ( remoteEndpoints ) ) {
236+ const normalizedRemoteEndpoints = dedupeEndpoints (
237+ remoteEndpoints . filter (
238+ ( endpoint ) : endpoint is string => typeof endpoint === 'string' ,
239+ ) ,
240+ ) . filter ( endpoint => endpoint !== server . main ) ;
241+ server . backups = dedupeEndpoints ( [
242+ ...( server . backups || [ ] ) ,
243+ ...normalizedRemoteEndpoints ,
244+ ] ) . filter ( endpoint => endpoint !== server . main ) ;
245+ return normalizedRemoteEndpoints ;
246+ }
247+ } catch ( e ) {
248+ log ( 'failed to fetch endpoints from: ' , server . queryUrls , e ) ;
249+ }
250+ return [ ] ;
251+ } ;
252+ requestCheckResult = async (
253+ endpoint : string ,
254+ fetchPayload : Parameters < typeof fetch > [ 1 ] ,
255+ ) => {
256+ const resp = await fetchWithTimeout (
257+ this . getCheckUrl ( endpoint ) ,
258+ fetchPayload ,
259+ DEFAULT_FETCH_TIMEOUT_MS ,
260+ ) ;
261+
262+ if ( ! resp . ok ) {
263+ const respText = await resp . text ( ) ;
264+ throw Error (
265+ this . t ( 'error_http_status' , {
266+ status : resp . status ,
267+ statusText : respText ,
268+ } ) ,
269+ ) ;
270+ }
271+
272+ return ( await resp . json ( ) ) as CheckResult ;
273+ } ;
274+ fetchCheckResult = async ( fetchPayload : Parameters < typeof fetch > [ 1 ] ) => {
275+ const { endpoint, value } = await executeEndpointFallback < CheckResult > ( {
276+ configuredEndpoints : this . getConfiguredCheckEndpoints ( ) ,
277+ getRemoteEndpoints : this . getRemoteEndpoints ,
278+ tryEndpoint : async currentEndpoint => {
279+ try {
280+ return await this . requestCheckResult ( currentEndpoint , fetchPayload ) ;
281+ } catch ( e ) {
282+ log ( 'check endpoint failed' , currentEndpoint , e ) ;
283+ throw e ;
284+ }
285+ } ,
286+ onFirstFailure : ( { error } ) => {
287+ this . report ( {
288+ type : 'errorChecking' ,
289+ message : this . t ( 'error_cannot_connect_backup' , {
290+ message : error . message ,
291+ } ) ,
292+ } ) ;
293+ } ,
294+ } ) ;
295+
296+ log ( 'check endpoint success' , endpoint ) ;
297+ return value ;
298+ } ;
191299 assertDebug = ( matter : string ) => {
192300 if ( __DEV__ && ! this . options . debug ) {
193301 info ( this . t ( 'dev_debug_disabled' , { matter } ) ) ;
@@ -271,95 +379,40 @@ export class Pushy {
271379 } ,
272380 body,
273381 } ;
274- let resp ;
382+ const previousRespJson = this . lastRespJson ;
275383 try {
276384 this . report ( {
277385 type : 'checking' ,
278386 message : this . options . appKey + ': ' + stringifyBody ,
279387 } ) ;
280- resp = await enhancedFetch ( this . getCheckUrl ( ) , fetchPayload ) ;
281- } catch ( e : any ) {
282- this . report ( {
283- type : 'errorChecking' ,
284- message : this . t ( 'error_cannot_connect_backup' , { message : e . message } ) ,
285- } ) ;
286- const backupEndpoints = await this . getBackupEndpoints ( ) . catch ( ) ;
287- if ( backupEndpoints ) {
288- resp = await promiseAny (
289- backupEndpoints . map ( endpoint =>
290- enhancedFetch ( this . getCheckUrl ( endpoint ) , fetchPayload ) ,
291- ) ,
292- ) . catch ( ( ) => {
293- this . report ( {
294- type : 'errorChecking' ,
295- message : this . t ( 'errorCheckingUseBackup' ) ,
296- } ) ;
297- } ) ;
298- } else {
299- this . report ( {
300- type : 'errorChecking' ,
301- message : this . t ( 'errorCheckingGetBackup' ) ,
302- } ) ;
303- }
304- }
305- if ( ! resp ) {
306- this . report ( {
307- type : 'errorChecking' ,
308- message : this . t ( 'error_cannot_connect_server' ) ,
309- } ) ;
310- this . throwIfEnabled ( Error ( 'errorChecking' ) ) ;
311- return this . lastRespJson ? await this . lastRespJson : emptyObj ;
312- }
388+ const respJsonPromise = this . fetchCheckResult ( fetchPayload ) ;
389+ this . lastRespJson = respJsonPromise ;
390+ const result : CheckResult = await respJsonPromise ;
313391
314- if ( ! resp . ok ) {
315- const respText = await resp . text ( ) ;
316- const errorMessage = this . t ( 'error_http_status' , {
317- status : resp . status ,
318- statusText : respText ,
319- } ) ;
392+ log ( 'checking result:' , result ) ;
393+
394+ return result ;
395+ } catch ( e : any ) {
396+ this . lastRespJson = previousRespJson ;
397+ const errorMessage =
398+ e ?. message || this . t ( 'error_cannot_connect_server' ) ;
320399 this . report ( {
321400 type : 'errorChecking' ,
322401 message : errorMessage ,
323402 } ) ;
324- this . throwIfEnabled ( Error ( 'errorChecking: ' + errorMessage ) ) ;
325- return this . lastRespJson ? await this . lastRespJson : emptyObj ;
403+ this . throwIfEnabled ( e ) ;
404+ return previousRespJson ? await previousRespJson : emptyObj ;
326405 }
327- const respJsonPromise = resp . json ( ) as Promise < CheckResult > ;
328- this . lastRespJson = respJsonPromise ;
329-
330- const result : CheckResult = await respJsonPromise ;
331-
332- log ( 'checking result:' , result ) ;
333-
334- return result ;
335406 } ;
336407 getBackupEndpoints = async ( ) => {
337408 const { server } = this . options ;
338409 if ( ! server ) {
339410 return [ ] ;
340411 }
341- if ( server . queryUrls ) {
342- try {
343- const resp = await promiseAny (
344- server . queryUrls . map ( queryUrl => fetch ( queryUrl ) ) ,
345- ) ;
346- const remoteEndpoints = await resp . json ( ) ;
347- log ( 'fetch endpoints:' , remoteEndpoints ) ;
348- if ( Array . isArray ( remoteEndpoints ) ) {
349- const backups = server . backups || [ ] ;
350- const set = new Set ( backups ) ;
351- for ( const endpoint of remoteEndpoints ) {
352- set . add ( endpoint ) ;
353- }
354- if ( set . size !== backups . length ) {
355- server . backups = Array . from ( set ) ;
356- }
357- }
358- } catch ( e : any ) {
359- log ( 'failed to fetch endpoints from: ' , server . queryUrls ) ;
360- }
361- }
362- return server . backups ;
412+ const remoteEndpoints = await this . getRemoteEndpoints ( ) ;
413+ return dedupeEndpoints ( [ ...( server . backups || [ ] ) , ...remoteEndpoints ] ) . filter (
414+ endpoint => endpoint !== server . main ,
415+ ) ;
363416 } ;
364417 downloadUpdate = async (
365418 updateInfo : CheckResult ,
0 commit comments