Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
20 changes: 19 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ClientOptions,
EventType,
ProgressData,
UpdateCheckState,
UpdateServerConfig,
} from './type';
import {
Expand Down Expand Up @@ -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}`;
};
Expand Down Expand Up @@ -329,16 +340,19 @@ export class Pushy {
};
checkUpdate = async (extra?: Record<string, any>) => {
if (!this.assertDebug('checkUpdate()')) {
this.notifyAfterCheckUpdate({ status: 'skipped' });
return;
}
if (!assertWeb()) {
this.notifyAfterCheckUpdate({ status: 'skipped' });
return;
}
if (
this.options.beforeCheckUpdate &&
(await this.options.beforeCheckUpdate()) === false
) {
log('beforeCheckUpdate returned false, skipping check');
this.notifyAfterCheckUpdate({ status: 'skipped' });
return;
}
const now = Date.now();
Expand All @@ -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 = {
Expand Down Expand Up @@ -387,6 +403,7 @@ export class Pushy {

log('checking result:', result);

this.notifyAfterCheckUpdate({ status: 'completed', result });
return result;
} catch (e: any) {
this.lastRespJson = previousRespJson;
Expand All @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -98,6 +105,8 @@ export interface ClientOptions {
debug?: boolean;
throwError?: boolean;
beforeCheckUpdate?: () => Promise<boolean> | boolean;
// 每次检查结束后都会触发,不影响原有检查流程
afterCheckUpdate?: (state: UpdateCheckState) => Promise<void> | void;
beforeDownloadUpdate?: (info: CheckResult) => Promise<boolean> | boolean;
afterDownloadUpdate?: (info: CheckResult) => Promise<boolean> | boolean;
onPackageExpired?: (info: CheckResult) => Promise<boolean> | boolean;
Expand Down