diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 9b4a2664..0f249768 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -135,4 +135,68 @@ describe('Pushy server config', () => { ]); expect(client.options.server?.queryUrls).toEqual(['https://q.example.com']); }); + + test('calls afterCheckUpdate with skipped when beforeCheckUpdate returns false', async () => { + setupClientMocks(); + const beforeCheckUpdate = mock(() => false); + const afterCheckUpdate = mock(() => {}); + + const { Pushy } = await importFreshClient('after-check-update-skipped'); + const client = new Pushy({ + appKey: 'demo-app', + beforeCheckUpdate, + afterCheckUpdate, + }); + + expect(await client.checkUpdate()).toBeUndefined(); + expect(afterCheckUpdate).toHaveBeenCalledWith({ + status: 'skipped', + }); + }); + + test('calls afterCheckUpdate with completed and result when check succeeds', async () => { + setupClientMocks(); + const afterCheckUpdate = mock(() => {}); + const checkResult = { + update: true as const, + name: '1.0.1', + hash: 'next-hash', + description: 'bugfix', + }; + (globalThis as any).fetch = mock(async () => createJsonResponse(checkResult)); + + const { Pushy } = await importFreshClient('after-check-update-completed'); + const client = new Pushy({ + appKey: 'demo-app', + afterCheckUpdate, + }); + + expect(await client.checkUpdate()).toEqual(checkResult); + expect(afterCheckUpdate).toHaveBeenCalledWith({ + status: 'completed', + result: checkResult, + }); + }); + + test('calls afterCheckUpdate with error before rethrowing when throwError is enabled', async () => { + setupClientMocks(); + const afterCheckUpdate = mock(() => {}); + const fetchError = new Error('boom'); + (globalThis as any).fetch = mock(async () => { + throw fetchError; + }); + + const { Pushy } = await importFreshClient('after-check-update-error'); + const client = new Pushy({ + appKey: 'demo-app', + throwError: true, + afterCheckUpdate, + }); + + await expect(client.checkUpdate()).rejects.toThrow('boom'); + expect(afterCheckUpdate).toHaveBeenCalledWith({ + status: 'error', + error: fetchError, + }); + }); }); diff --git a/src/client.ts b/src/client.ts index a07f4440..b1668372 100644 --- a/src/client.ts +++ b/src/client.ts @@ -22,6 +22,7 @@ import { ClientOptions, EventType, ProgressData, + UpdateCheckState, UpdateServerConfig, } from './type'; import { @@ -207,6 +208,16 @@ export class Pushy { throw e; } }; + notifyAfterCheckUpdate = (state: UpdateCheckState) => { + const { afterCheckUpdate } = this.options; + if (!afterCheckUpdate) { + return; + } + // 这里仅做状态通知,不阻塞原有检查流程 + Promise.resolve(afterCheckUpdate(state)).catch((error: any) => { + log('afterCheckUpdate failed:', error?.message || error); + }); + }; getCheckUrl = (endpoint: string) => { return `${endpoint}/checkUpdate/${this.options.appKey}`; }; @@ -329,9 +340,11 @@ export class Pushy { }; checkUpdate = async (extra?: Record) => { if (!this.assertDebug('checkUpdate()')) { + this.notifyAfterCheckUpdate({ status: 'skipped' }); return; } if (!assertWeb()) { + this.notifyAfterCheckUpdate({ status: 'skipped' }); return; } if ( @@ -339,6 +352,7 @@ export class Pushy { (await this.options.beforeCheckUpdate()) === false ) { log('beforeCheckUpdate returned false, skipping check'); + this.notifyAfterCheckUpdate({ status: 'skipped' }); return; } const now = Date.now(); @@ -347,7 +361,9 @@ export class Pushy { this.lastChecking && now - this.lastChecking < 1000 * 5 ) { - return await this.lastRespJson; + const result = await this.lastRespJson; + this.notifyAfterCheckUpdate({ status: 'completed', result }); + return result; } this.lastChecking = now; const fetchBody = { @@ -387,6 +403,7 @@ export class Pushy { log('checking result:', result); + this.notifyAfterCheckUpdate({ status: 'completed', result }); return result; } catch (e: any) { this.lastRespJson = previousRespJson; @@ -396,6 +413,7 @@ export class Pushy { type: 'errorChecking', message: errorMessage, }); + this.notifyAfterCheckUpdate({ status: 'error', error: e }); this.throwIfEnabled(e); return previousRespJson ? await previousRespJson : emptyObj; } diff --git a/src/provider.tsx b/src/provider.tsx index c2919042..08dec9ac 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -165,6 +165,7 @@ export const UpdateProvider = ({ async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => { const now = Date.now(); if (lastChecking.current && now - lastChecking.current < 1000) { + client.notifyAfterCheckUpdate({ status: 'skipped' }); return; } lastChecking.current = now; diff --git a/src/type.ts b/src/type.ts index c222a31f..12c4ea8c 100644 --- a/src/type.ts +++ b/src/type.ts @@ -35,6 +35,13 @@ export interface ProgressData { total: number; } +// 用于描述一次检查结束后的最终状态,便于业务侧感知成功、跳过或失败 +export interface UpdateCheckState { + status: 'completed' | 'skipped' | 'error'; + result?: CheckResult; + error?: Error; +} + export type EventType = | 'rollback' | 'errorChecking' @@ -98,6 +105,8 @@ export interface ClientOptions { debug?: boolean; throwError?: boolean; beforeCheckUpdate?: () => Promise | boolean; + // 每次检查结束后都会触发,不影响原有检查流程 + afterCheckUpdate?: (state: UpdateCheckState) => Promise | void; beforeDownloadUpdate?: (info: CheckResult) => Promise | boolean; afterDownloadUpdate?: (info: CheckResult) => Promise | boolean; onPackageExpired?: (info: CheckResult) => Promise | boolean;