From 41a9e6d3f9219c7e8d4bea647943871b008b0b15 Mon Sep 17 00:00:00 2001 From: Saddhu Date: Thu, 26 Mar 2026 17:53:52 +0800 Subject: [PATCH] feat: expose update check state --- src/__tests__/client.test.ts | 67 ++++++++++++++++++++++++++++++++ src/client.ts | 14 +++++-- src/context.ts | 12 +++++- src/index.ts | 2 + src/provider.tsx | 75 +++++++++++++++++++++++++++++++++++- src/type.ts | 15 ++++++++ 6 files changed, 179 insertions(+), 6 deletions(-) diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 9b4a2664..dbffad88 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -135,4 +135,71 @@ describe('Pushy server config', () => { ]); expect(client.options.server?.queryUrls).toEqual(['https://q.example.com']); }); + + test('stores lastCheckError on failed check and clears it after a successful retry', async () => { + setupClientMocks(); + let shouldFail = true; + (globalThis as any).fetch = mock(async (url: string) => { + if (url.includes('/checkUpdate/')) { + if (shouldFail) { + throw new Error('network down'); + } + return createJsonResponse({ upToDate: true }); + } + return createJsonResponse([]); + }); + + const { Pushy } = await importFreshClient('check-error-state'); + const client = new Pushy({ + appKey: 'demo-app', + server: { + main: ['https://a.example.com'], + queryUrls: [], + }, + }); + + const failedResult = await client.checkUpdate(); + + expect(failedResult).toEqual({}); + expect(client.lastCheckError).toBeInstanceOf(Error); + expect(client.lastCheckError?.message).toContain('network down'); + + shouldFail = false; + + const successResult = await client.checkUpdate(); + + expect(successResult).toEqual({ upToDate: true }); + expect(client.lastCheckError).toBeUndefined(); + }); + + test('clears stale lastCheckError when check is skipped by beforeCheckUpdate', async () => { + setupClientMocks(); + (globalThis as any).fetch = mock(async (url: string) => { + if (url.includes('/checkUpdate/')) { + throw new Error('network down'); + } + return createJsonResponse([]); + }); + + const { Pushy } = await importFreshClient('clear-error-on-skip'); + const client = new Pushy({ + appKey: 'demo-app', + server: { + main: ['https://a.example.com'], + queryUrls: [], + }, + }); + + await client.checkUpdate(); + expect(client.lastCheckError).toBeInstanceOf(Error); + + client.setOptions({ + beforeCheckUpdate: () => false, + }); + + const skippedResult = await client.checkUpdate(); + + expect(skippedResult).toBeUndefined(); + expect(client.lastCheckError).toBeUndefined(); + }); }); diff --git a/src/client.ts b/src/client.ts index a07f4440..53db6d53 100644 --- a/src/client.ts +++ b/src/client.ts @@ -114,6 +114,7 @@ export class Pushy { clientType: 'Pushy' | 'Cresc' = 'Pushy'; lastChecking?: number; lastRespJson?: Promise; + lastCheckError?: Error; version = cInfo.rnu; loggerPromise = (() => { @@ -328,6 +329,8 @@ export class Pushy { } }; checkUpdate = async (extra?: Record) => { + // 新一轮检查开始前先清掉旧错误,避免跳过场景误复用上次失败状态。 + this.lastCheckError = undefined; if (!this.assertDebug('checkUpdate()')) { return; } @@ -384,19 +387,24 @@ export class Pushy { const respJsonPromise = this.fetchCheckResult(fetchPayload); this.lastRespJson = respJsonPromise; const result: CheckResult = await respJsonPromise; + this.lastCheckError = undefined; log('checking result:', result); return result; } catch (e: any) { this.lastRespJson = previousRespJson; - const errorMessage = - e?.message || this.t('error_cannot_connect_server'); + // 保持旧返回约定的同时,把真实失败记录给 Provider 判断状态。 + this.lastCheckError = + e instanceof Error + ? e + : Error(e?.message || this.t('error_cannot_connect_server')); + const errorMessage = this.lastCheckError.message; this.report({ type: 'errorChecking', message: errorMessage, }); - this.throwIfEnabled(e); + this.throwIfEnabled(this.lastCheckError); return previousRespJson ? await previousRespJson : emptyObj; } }; diff --git a/src/context.ts b/src/context.ts index 8fbf5e92..4ac325a2 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,10 @@ import { createContext, useContext } from 'react'; -import { CheckResult, ProgressData } from './type'; +import { + CheckResult, + ProgressData, + UpdateCheckState, + UPDATE_CHECK_STATUS, +} from './type'; import { Pushy, Cresc } from './client'; import i18n from './i18n'; @@ -20,6 +25,9 @@ export const defaultContext = { currentHash: '', packageVersion: '', currentVersionInfo: {}, + checkState: { + status: UPDATE_CHECK_STATUS.IDLE, + }, }; export const UpdateContext = createContext<{ @@ -49,6 +57,8 @@ export const UpdateContext = createContext<{ progress?: ProgressData; updateInfo?: CheckResult; lastError?: Error; + // 最近一次检查调用的完整快照,状态、结果和错误会一起更新。 + checkState: UpdateCheckState; }>(defaultContext); export const useUpdate = __DEV__ ? () => { diff --git a/src/index.ts b/src/index.ts index 2de9e853..3b49409a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,5 @@ export { Pushy, Cresc } from './client'; export { UpdateContext, usePushy, useUpdate } from './context'; export { PushyProvider, UpdateProvider } from './provider'; export { PushyModule, UpdateModule } from './core'; +export { UPDATE_CHECK_STATUS } from './type'; +export type { UpdateCheckState } from './type'; diff --git a/src/provider.tsx b/src/provider.tsx index c2919042..5159ffb1 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -22,6 +22,8 @@ import { import { CheckResult, ProgressData, + UpdateCheckState, + UPDATE_CHECK_STATUS, UpdateTestPayload, VersionInfo, } from './type'; @@ -45,8 +47,17 @@ export const UpdateProvider = ({ const updateInfoRef = useRef(updateInfo); const [progress, setProgress] = useState(); const [lastError, setLastError] = useState(); + const [checkState, setCheckStateState] = useState({ + status: UPDATE_CHECK_STATUS.IDLE, + }); + const checkStateRef = useRef(checkState); const lastChecking = useRef(0); + const setCheckState = useCallback((nextState: UpdateCheckState) => { + checkStateRef.current = nextState; + setCheckStateState(nextState); + }, []); + const throwErrorIfEnabled = useCallback( (e: Error) => { if (options.throwError) { @@ -58,7 +69,13 @@ export const UpdateProvider = ({ const dismissError = useCallback(() => { setLastError(undefined); - }, []); + if (checkStateRef.current.error) { + setCheckState({ + ...checkStateRef.current, + error: undefined, + }); + } + }, [setCheckState]); const alertUpdate = useCallback( (...args: Parameters) => { @@ -164,20 +181,56 @@ export const UpdateProvider = ({ const checkUpdate = useCallback( async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => { const now = Date.now(); + // 频繁重复触发时不再发起新检查,但需要把本次调用标记为跳过。 if (lastChecking.current && now - lastChecking.current < 1000) { + if (checkStateRef.current.status !== UPDATE_CHECK_STATUS.CHECKING) { + setCheckState({ + status: UPDATE_CHECK_STATUS.SKIPPED, + }); + } return; } lastChecking.current = now; + setCheckState({ + status: UPDATE_CHECK_STATUS.CHECKING, + }); + let result: CheckResult | void; let rootInfo: CheckResult | undefined; try { - rootInfo = { ...(await client.checkUpdate(extra)) }; + result = await client.checkUpdate(extra); + rootInfo = { ...result }; } catch (e: any) { setLastError(e); + setCheckState({ + status: UPDATE_CHECK_STATUS.ERROR, + error: e, + }); alertError(client.t('error_update_check_failed'), e.message); throwErrorIfEnabled(e); return; } + let nextCheckStatus: UpdateCheckState['status']; + let nextCheckError: Error | undefined; + // client.checkUpdate 默认会兜底返回旧结果或空对象,这里只额外标记状态,不截断原有流程。 + if (client.lastCheckError) { + nextCheckStatus = UPDATE_CHECK_STATUS.ERROR; + nextCheckError = client.lastCheckError; + setLastError(client.lastCheckError); + alertError( + client.t('error_update_check_failed'), + client.lastCheckError.message, + ); + } else if (!result) { + // undefined 表示本次调用被内部策略跳过,例如 beforeCheckUpdate 返回 false。 + nextCheckStatus = UPDATE_CHECK_STATUS.SKIPPED; + } else { + nextCheckStatus = UPDATE_CHECK_STATUS.COMPLETED; + } if (!rootInfo) { + setCheckState({ + status: nextCheckStatus, + error: nextCheckError, + }); return; } const versions = [rootInfo.expVersion, rootInfo].filter( @@ -202,6 +255,18 @@ export const UpdateProvider = ({ } updateInfoRef.current = info; setUpdateInfo(info); + if (nextCheckStatus === UPDATE_CHECK_STATUS.COMPLETED) { + setCheckState({ + status: nextCheckStatus, + result: info, + error: nextCheckError, + }); + } else { + setCheckState({ + status: nextCheckStatus, + error: nextCheckError, + }); + } if (info.expired) { if ( options.onPackageExpired && @@ -268,11 +333,16 @@ export const UpdateProvider = ({ } return info; } + setCheckState({ + status: nextCheckStatus, + error: nextCheckError, + }); }, [ client, alertError, throwErrorIfEnabled, + setCheckState, options, alertUpdate, downloadAndInstallApk, @@ -414,6 +484,7 @@ export const UpdateProvider = ({ currentVersionInfo, parseTestQrCode, restartApp, + checkState, }}> {children} diff --git a/src/type.ts b/src/type.ts index c222a31f..03861cca 100644 --- a/src/type.ts +++ b/src/type.ts @@ -29,6 +29,21 @@ export type CheckResult = RootResult & expVersion?: VersionInfo; }; +// 记录最近一次检查调用的状态,区分完成、跳过和出错。 +export const UPDATE_CHECK_STATUS = { + IDLE: 'idle', + CHECKING: 'checking', + COMPLETED: 'completed', + SKIPPED: 'skipped', + ERROR: 'error', +} as const; + +export interface UpdateCheckState { + status: (typeof UPDATE_CHECK_STATUS)[keyof typeof UPDATE_CHECK_STATUS]; + result?: CheckResult; + error?: Error; +} + export interface ProgressData { hash: string; received: number;