Skip to content

Commit 41a9e6d

Browse files
committed
feat: expose update check state
1 parent c2c66b0 commit 41a9e6d

6 files changed

Lines changed: 179 additions & 6 deletions

File tree

src/__tests__/client.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,71 @@ describe('Pushy server config', () => {
135135
]);
136136
expect(client.options.server?.queryUrls).toEqual(['https://q.example.com']);
137137
});
138+
139+
test('stores lastCheckError on failed check and clears it after a successful retry', async () => {
140+
setupClientMocks();
141+
let shouldFail = true;
142+
(globalThis as any).fetch = mock(async (url: string) => {
143+
if (url.includes('/checkUpdate/')) {
144+
if (shouldFail) {
145+
throw new Error('network down');
146+
}
147+
return createJsonResponse({ upToDate: true });
148+
}
149+
return createJsonResponse([]);
150+
});
151+
152+
const { Pushy } = await importFreshClient('check-error-state');
153+
const client = new Pushy({
154+
appKey: 'demo-app',
155+
server: {
156+
main: ['https://a.example.com'],
157+
queryUrls: [],
158+
},
159+
});
160+
161+
const failedResult = await client.checkUpdate();
162+
163+
expect(failedResult).toEqual({});
164+
expect(client.lastCheckError).toBeInstanceOf(Error);
165+
expect(client.lastCheckError?.message).toContain('network down');
166+
167+
shouldFail = false;
168+
169+
const successResult = await client.checkUpdate();
170+
171+
expect(successResult).toEqual({ upToDate: true });
172+
expect(client.lastCheckError).toBeUndefined();
173+
});
174+
175+
test('clears stale lastCheckError when check is skipped by beforeCheckUpdate', async () => {
176+
setupClientMocks();
177+
(globalThis as any).fetch = mock(async (url: string) => {
178+
if (url.includes('/checkUpdate/')) {
179+
throw new Error('network down');
180+
}
181+
return createJsonResponse([]);
182+
});
183+
184+
const { Pushy } = await importFreshClient('clear-error-on-skip');
185+
const client = new Pushy({
186+
appKey: 'demo-app',
187+
server: {
188+
main: ['https://a.example.com'],
189+
queryUrls: [],
190+
},
191+
});
192+
193+
await client.checkUpdate();
194+
expect(client.lastCheckError).toBeInstanceOf(Error);
195+
196+
client.setOptions({
197+
beforeCheckUpdate: () => false,
198+
});
199+
200+
const skippedResult = await client.checkUpdate();
201+
202+
expect(skippedResult).toBeUndefined();
203+
expect(client.lastCheckError).toBeUndefined();
204+
});
138205
});

src/client.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class Pushy {
114114
clientType: 'Pushy' | 'Cresc' = 'Pushy';
115115
lastChecking?: number;
116116
lastRespJson?: Promise<CheckResult>;
117+
lastCheckError?: Error;
117118

118119
version = cInfo.rnu;
119120
loggerPromise = (() => {
@@ -328,6 +329,8 @@ export class Pushy {
328329
}
329330
};
330331
checkUpdate = async (extra?: Record<string, any>) => {
332+
// 新一轮检查开始前先清掉旧错误,避免跳过场景误复用上次失败状态。
333+
this.lastCheckError = undefined;
331334
if (!this.assertDebug('checkUpdate()')) {
332335
return;
333336
}
@@ -384,19 +387,24 @@ export class Pushy {
384387
const respJsonPromise = this.fetchCheckResult(fetchPayload);
385388
this.lastRespJson = respJsonPromise;
386389
const result: CheckResult = await respJsonPromise;
390+
this.lastCheckError = undefined;
387391

388392
log('checking result:', result);
389393

390394
return result;
391395
} catch (e: any) {
392396
this.lastRespJson = previousRespJson;
393-
const errorMessage =
394-
e?.message || this.t('error_cannot_connect_server');
397+
// 保持旧返回约定的同时,把真实失败记录给 Provider 判断状态。
398+
this.lastCheckError =
399+
e instanceof Error
400+
? e
401+
: Error(e?.message || this.t('error_cannot_connect_server'));
402+
const errorMessage = this.lastCheckError.message;
395403
this.report({
396404
type: 'errorChecking',
397405
message: errorMessage,
398406
});
399-
this.throwIfEnabled(e);
407+
this.throwIfEnabled(this.lastCheckError);
400408
return previousRespJson ? await previousRespJson : emptyObj;
401409
}
402410
};

src/context.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { createContext, useContext } from 'react';
2-
import { CheckResult, ProgressData } from './type';
2+
import {
3+
CheckResult,
4+
ProgressData,
5+
UpdateCheckState,
6+
UPDATE_CHECK_STATUS,
7+
} from './type';
38
import { Pushy, Cresc } from './client';
49
import i18n from './i18n';
510

@@ -20,6 +25,9 @@ export const defaultContext = {
2025
currentHash: '',
2126
packageVersion: '',
2227
currentVersionInfo: {},
28+
checkState: {
29+
status: UPDATE_CHECK_STATUS.IDLE,
30+
},
2331
};
2432

2533
export const UpdateContext = createContext<{
@@ -49,6 +57,8 @@ export const UpdateContext = createContext<{
4957
progress?: ProgressData;
5058
updateInfo?: CheckResult;
5159
lastError?: Error;
60+
// 最近一次检查调用的完整快照,状态、结果和错误会一起更新。
61+
checkState: UpdateCheckState;
5262
}>(defaultContext);
5363

5464
export const useUpdate = __DEV__ ? () => {

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export { Pushy, Cresc } from './client';
22
export { UpdateContext, usePushy, useUpdate } from './context';
33
export { PushyProvider, UpdateProvider } from './provider';
44
export { PushyModule, UpdateModule } from './core';
5+
export { UPDATE_CHECK_STATUS } from './type';
6+
export type { UpdateCheckState } from './type';

src/provider.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
import {
2323
CheckResult,
2424
ProgressData,
25+
UpdateCheckState,
26+
UPDATE_CHECK_STATUS,
2527
UpdateTestPayload,
2628
VersionInfo,
2729
} from './type';
@@ -45,8 +47,17 @@ export const UpdateProvider = ({
4547
const updateInfoRef = useRef(updateInfo);
4648
const [progress, setProgress] = useState<ProgressData>();
4749
const [lastError, setLastError] = useState<Error>();
50+
const [checkState, setCheckStateState] = useState<UpdateCheckState>({
51+
status: UPDATE_CHECK_STATUS.IDLE,
52+
});
53+
const checkStateRef = useRef(checkState);
4854
const lastChecking = useRef(0);
4955

56+
const setCheckState = useCallback((nextState: UpdateCheckState) => {
57+
checkStateRef.current = nextState;
58+
setCheckStateState(nextState);
59+
}, []);
60+
5061
const throwErrorIfEnabled = useCallback(
5162
(e: Error) => {
5263
if (options.throwError) {
@@ -58,7 +69,13 @@ export const UpdateProvider = ({
5869

5970
const dismissError = useCallback(() => {
6071
setLastError(undefined);
61-
}, []);
72+
if (checkStateRef.current.error) {
73+
setCheckState({
74+
...checkStateRef.current,
75+
error: undefined,
76+
});
77+
}
78+
}, [setCheckState]);
6279

6380
const alertUpdate = useCallback(
6481
(...args: Parameters<typeof Alert.alert>) => {
@@ -164,20 +181,56 @@ export const UpdateProvider = ({
164181
const checkUpdate = useCallback(
165182
async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => {
166183
const now = Date.now();
184+
// 频繁重复触发时不再发起新检查,但需要把本次调用标记为跳过。
167185
if (lastChecking.current && now - lastChecking.current < 1000) {
186+
if (checkStateRef.current.status !== UPDATE_CHECK_STATUS.CHECKING) {
187+
setCheckState({
188+
status: UPDATE_CHECK_STATUS.SKIPPED,
189+
});
190+
}
168191
return;
169192
}
170193
lastChecking.current = now;
194+
setCheckState({
195+
status: UPDATE_CHECK_STATUS.CHECKING,
196+
});
197+
let result: CheckResult | void;
171198
let rootInfo: CheckResult | undefined;
172199
try {
173-
rootInfo = { ...(await client.checkUpdate(extra)) };
200+
result = await client.checkUpdate(extra);
201+
rootInfo = { ...result };
174202
} catch (e: any) {
175203
setLastError(e);
204+
setCheckState({
205+
status: UPDATE_CHECK_STATUS.ERROR,
206+
error: e,
207+
});
176208
alertError(client.t('error_update_check_failed'), e.message);
177209
throwErrorIfEnabled(e);
178210
return;
179211
}
212+
let nextCheckStatus: UpdateCheckState['status'];
213+
let nextCheckError: Error | undefined;
214+
// client.checkUpdate 默认会兜底返回旧结果或空对象,这里只额外标记状态,不截断原有流程。
215+
if (client.lastCheckError) {
216+
nextCheckStatus = UPDATE_CHECK_STATUS.ERROR;
217+
nextCheckError = client.lastCheckError;
218+
setLastError(client.lastCheckError);
219+
alertError(
220+
client.t('error_update_check_failed'),
221+
client.lastCheckError.message,
222+
);
223+
} else if (!result) {
224+
// undefined 表示本次调用被内部策略跳过,例如 beforeCheckUpdate 返回 false。
225+
nextCheckStatus = UPDATE_CHECK_STATUS.SKIPPED;
226+
} else {
227+
nextCheckStatus = UPDATE_CHECK_STATUS.COMPLETED;
228+
}
180229
if (!rootInfo) {
230+
setCheckState({
231+
status: nextCheckStatus,
232+
error: nextCheckError,
233+
});
181234
return;
182235
}
183236
const versions = [rootInfo.expVersion, rootInfo].filter(
@@ -202,6 +255,18 @@ export const UpdateProvider = ({
202255
}
203256
updateInfoRef.current = info;
204257
setUpdateInfo(info);
258+
if (nextCheckStatus === UPDATE_CHECK_STATUS.COMPLETED) {
259+
setCheckState({
260+
status: nextCheckStatus,
261+
result: info,
262+
error: nextCheckError,
263+
});
264+
} else {
265+
setCheckState({
266+
status: nextCheckStatus,
267+
error: nextCheckError,
268+
});
269+
}
205270
if (info.expired) {
206271
if (
207272
options.onPackageExpired &&
@@ -268,11 +333,16 @@ export const UpdateProvider = ({
268333
}
269334
return info;
270335
}
336+
setCheckState({
337+
status: nextCheckStatus,
338+
error: nextCheckError,
339+
});
271340
},
272341
[
273342
client,
274343
alertError,
275344
throwErrorIfEnabled,
345+
setCheckState,
276346
options,
277347
alertUpdate,
278348
downloadAndInstallApk,
@@ -414,6 +484,7 @@ export const UpdateProvider = ({
414484
currentVersionInfo,
415485
parseTestQrCode,
416486
restartApp,
487+
checkState,
417488
}}>
418489
{children}
419490
</UpdateContext.Provider>

src/type.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ export type CheckResult = RootResult &
2929
expVersion?: VersionInfo;
3030
};
3131

32+
// 记录最近一次检查调用的状态,区分完成、跳过和出错。
33+
export const UPDATE_CHECK_STATUS = {
34+
IDLE: 'idle',
35+
CHECKING: 'checking',
36+
COMPLETED: 'completed',
37+
SKIPPED: 'skipped',
38+
ERROR: 'error',
39+
} as const;
40+
41+
export interface UpdateCheckState {
42+
status: (typeof UPDATE_CHECK_STATUS)[keyof typeof UPDATE_CHECK_STATUS];
43+
result?: CheckResult;
44+
error?: Error;
45+
}
46+
3247
export interface ProgressData {
3348
hash: string;
3449
received: number;

0 commit comments

Comments
 (0)