diff --git a/.gitignore b/.gitignore index 7c639035..80ec3be0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ Example/**/.pushy Example/testHotUpdate/artifacts yarn-error.log +.cursor/mcp.json diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 850ef324..f78629b0 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -349,8 +349,7 @@ - (instancetype)init } } -RCT_EXPORT_METHOD(markSuccess: - resolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/lib/endpoint.ts b/lib/endpoint.ts index 98a14c9e..9c0bea22 100644 --- a/lib/endpoint.ts +++ b/lib/endpoint.ts @@ -1,13 +1,21 @@ -import { logger } from './utils'; +import { logger, promiseAny } from './utils'; let currentEndpoint = 'https://update.react-native.cn/api'; -let backupEndpoints: string[] = ['https://update.reactnative.cn/api']; -let backupEndpointsQueryUrl: string | null = null; +let backupEndpoints: string[] = [ + 'https://pushy-koa-qgbgqmcpis.cn-beijing.fcapp.run', + 'https://update.reactnative.cn/api', +]; +let backupEndpointsQueryUrls = [ + 'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json', + 'https://cdn.jsdelivr.net/gh/reactnativecn/react-native-pushy@master/endpoints.json', +]; export async function updateBackupEndpoints() { - if (backupEndpointsQueryUrl) { + if (backupEndpointsQueryUrls) { try { - const resp = await fetch(backupEndpointsQueryUrl); + const resp = await promiseAny( + backupEndpointsQueryUrls.map(queryUrl => fetch(queryUrl)), + ); const remoteEndpoints = await resp.json(); if (Array.isArray(remoteEndpoints)) { backupEndpoints = Array.from( @@ -36,18 +44,17 @@ export function getCheckUrl(APPKEY, endpoint = currentEndpoint) { export function setCustomEndpoints({ main, backups, - backupQueryUrl, + backupQueryUrls, }: { main: string; backups?: string[]; - backupQueryUrl?: string; + backupQueryUrls?: string[]; }) { currentEndpoint = main; - backupEndpointsQueryUrl = null; if (Array.isArray(backups) && backups.length> 0) { backupEndpoints = backups; } - if (typeof backupQueryUrl === 'string') { - backupEndpointsQueryUrl = backupQueryUrl; + if (Array.isArray(backupQueryUrls) && backupQueryUrls.length> 0) { + backupEndpointsQueryUrls = backupQueryUrls; } } diff --git a/lib/main.ts b/lib/main.ts index b7fa3b74..0f3bb6b2 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -16,7 +16,7 @@ import { UpdateAvailableResult, UpdateEventsListener, } from './type'; -import { assertRelease, logger } from './utils'; +import { assertRelease, enhancedFetch, logger, promiseAny, testUrls } from './utils'; export { setCustomEndpoints }; const { version: v, @@ -158,7 +158,7 @@ export async function checkUpdate(APPKEY: string) { }; let resp; try { - resp = await fetch(getCheckUrl(APPKEY), fetchPayload); + resp = await enhancedFetch(getCheckUrl(APPKEY), fetchPayload); } catch (e) { report({ type: 'errorChecking', @@ -167,9 +167,9 @@ export async function checkUpdate(APPKEY: string) { const backupEndpoints = await updateBackupEndpoints(); if (backupEndpoints) { try { - resp = await Promise.race( - backupEndpoints.map((endpoint) => - fetch(getCheckUrl(APPKEY, endpoint), fetchPayload), + resp = await promiseAny( + backupEndpoints.map(endpoint => + enhancedFetch(getCheckUrl(APPKEY, endpoint), fetchPayload), ), ); } catch {} @@ -205,7 +205,7 @@ function checkOperation( if (!Array.isArray(op)) { return; } - op.forEach((action) => { + op.forEach(action => { if (action.type === 'block') { blockUpdate = { reason: action.reason, @@ -250,7 +250,7 @@ export async function downloadUpdate( const downloadCallback = eventListeners.onDownloadProgress; progressHandler = eventEmitter.addListener( 'RCTPushyDownloadProgress', - (progressData) => { + progressData => { if (progressData.hash === options.hash) { downloadCallback(progressData); } @@ -260,11 +260,12 @@ export async function downloadUpdate( } let succeeded = false; report({ type: 'downloading' }); - if (options.diffUrl) { + const diffUrl = (await testUrls(options.diffUrls)) || options.diffUrl; + if (diffUrl) { logger('downloading diff'); try { await PushyModule.downloadPatchFromPpk({ - updateUrl: options.diffUrl, + updateUrl: diffUrl, hash: options.hash, originHash: currentVersion, }); @@ -273,28 +274,34 @@ export async function downloadUpdate( logger(`diff error: ${e.message}, try pdiff`); } } - if (!succeeded && options.pdiffUrl) { - logger('downloading pdiff'); - try { - await PushyModule.downloadPatchFromPackage({ - updateUrl: options.pdiffUrl, - hash: options.hash, - }); - succeeded = true; - } catch (e) { - logger(`pdiff error: ${e.message}, try full patch`); + if (!succeeded) { + const pdiffUrl = (await testUrls(options.pdiffUrls)) || options.pdiffUrl; + if (pdiffUrl) { + logger('downloading pdiff'); + try { + await PushyModule.downloadPatchFromPackage({ + updateUrl: pdiffUrl, + hash: options.hash, + }); + succeeded = true; + } catch (e) { + logger(`pdiff error: ${e.message}, try full patch`); + } } } - if (!succeeded && options.updateUrl) { - logger('downloading full patch'); - try { - await PushyModule.downloadFullUpdate({ - updateUrl: options.updateUrl, - hash: options.hash, - }); - succeeded = true; - } catch (e) { - logger(`full patch error: ${e.message}`); + if (!succeeded) { + const updateUrl = (await testUrls(options.updateUrls)) || options.updateUrl; + if (updateUrl) { + logger('downloading full patch'); + try { + await PushyModule.downloadFullUpdate({ + updateUrl: updateUrl, + hash: options.hash, + }); + succeeded = true; + } catch (e) { + logger(`full patch error: ${e.message}`); + } } } progressHandler && progressHandler.remove(); @@ -322,12 +329,10 @@ function assertHash(hash: string) { return true; } -let applyingUpdate = false; export function switchVersion(hash: string) { assertRelease(); - if (assertHash(hash) && !applyingUpdate) { + if (assertHash(hash)) { logger('switchVersion: ' + hash); - applyingUpdate = true; PushyModule.reloadUpdate({ hash }); } } diff --git a/lib/type.ts b/lib/type.ts index 00ec1978..d8c8ad09 100644 --- a/lib/type.ts +++ b/lib/type.ts @@ -19,8 +19,11 @@ export interface UpdateAvailableResult { description: string; metaInfo: string; pdiffUrl: string; + pdiffUrls?: string[]; diffUrl?: string; + diffUrls?: string[]; updateUrl?: string; + updateUrls?: string[]; } export type CheckResult = diff --git a/lib/utils.ts b/lib/utils.ts index 9b024de4..06768e65 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,9 +1,100 @@ +import { Platform } from 'react-native'; + export function logger(...args: any[]) { console.log('Pushy: ', ...args); } +export function promiseAny(promises: Promise[]) { + return new Promise((resolve, reject) => { + let count = 0; + + promises.forEach((promise) => { + Promise.resolve(promise) + .then(resolve) + .catch(() => { + count++; + if (count === promises.length) { + reject(new Error('All promises were rejected')); + } + }); + }); + }); +} + export function assertRelease() { if (__DEV__) { throw new Error('react-native-update 只能在 RELEASE 版本中运行.'); } } + +const ping = + Platform.OS === 'web' + ? Promise.resolve + : async (url: string) => { + let pingFinished = false; + return Promise.race([ + enhancedFetch(url, { + method: 'HEAD', + }) + .then(({ status, statusText }) => { + pingFinished = true; + if (status === 200) { + return url; + } + logger('ping failed', url, status, statusText); + throw new Error('Ping failed'); + }) + .catch((e) => { + pingFinished = true; + logger('ping error', url, e); + throw e; + }), + new Promise((_, reject) => + setTimeout(() => { + reject(new Error('Ping timeout')); + if (!pingFinished) { + logger('ping timeout', url); + } + }, 2000), + ), + ]); + }; + +export const testUrls = async (urls?: string[]) => { + if (!urls?.length) { + return null; + } + try { + const ret = await promiseAny(urls.map(ping)); + if (ret) { + return ret; + } + } catch {} + logger('all ping failed, use first url:', urls[0]); + return urls[0]; +}; + + + +// export const isAndroid70AndBelow = () => { +// // android 7.0 and below devices do not support letsencrypt cert +// // https://letsencrypt.org/2023/07/10/cross-sign-expiration/ +// return Platform.OS === 'android' && Platform.Version <= 24; +// }; + +export const enhancedFetch = async ( + url: string, + params: Parameters[1], + isRetry = false, +) => { + return fetch(url, params) + .catch((e) => { + logger('fetch error', url, e); + if (isRetry) { + throw e; + } + logger('trying fallback to http'); + return enhancedFetch(url.replace('https', 'http'), params, true); + }); +}; + diff --git a/package.json b/package.json index a85c0354..ffce3e04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-update", - "version": "9.1.6", + "version": "9.2.8", "description": "react-native hot update", "main": "lib/index.ts", "scripts": { @@ -67,5 +67,6 @@ "react-native": "^0.72.6", "ts-jest": "^29.0.3", "typescript": "^5.2.2" - } + }, + "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" }

AltStyle によって変換されたページ (->オリジナル) /