Skip to content

Commit 8d20eee

Browse files
committed
fix(download): normalize retries and cover progress callback
1 parent 975f258 commit 8d20eee

3 files changed

Lines changed: 66 additions & 4 deletions

File tree

src/__tests__/client.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const setupClientMocks = ({
1818
downloadPatchFromPpk = mock(() => Promise.resolve()),
1919
downloadPatchFromPackage = mock(() => Promise.resolve()),
2020
downloadFullUpdate = mock(() => Promise.resolve()),
21+
addProgressListener = mock(() => ({ remove: mock(() => {}) })),
2122
restartApp = mock(() => Promise.resolve()),
2223
}: {
2324
isFirstTime?: boolean;
@@ -27,6 +28,7 @@ const setupClientMocks = ({
2728
downloadPatchFromPpk?: ReturnType<typeof mock>;
2829
downloadPatchFromPackage?: ReturnType<typeof mock>;
2930
downloadFullUpdate?: ReturnType<typeof mock>;
31+
addProgressListener?: ReturnType<typeof mock>;
3032
restartApp?: ReturnType<typeof mock>;
3133
} = {}) => {
3234
(globalThis as any).__DEV__ = false;
@@ -69,7 +71,7 @@ const setupClientMocks = ({
6971
isRolledBack: false,
7072
packageVersion: '1.0.0',
7173
pushyNativeEventEmitter: {
72-
addListener: mock(() => ({ remove: mock(() => {}) })),
74+
addListener: addProgressListener,
7375
},
7476
rolledBackVersion: '',
7577
setLocalHashInfo: mock(() => {}),
@@ -376,15 +378,18 @@ describe('downloadUpdate fallback chain', () => {
376378
downloadPatchFromPpk = mock(() => Promise.resolve()),
377379
downloadPatchFromPackage = mock(() => Promise.resolve()),
378380
downloadFullUpdate = mock(() => Promise.resolve()),
381+
addProgressListener = mock(() => ({ remove: mock(() => {}) })),
379382
}: {
380383
downloadPatchFromPpk?: ReturnType<typeof mock>;
381384
downloadPatchFromPackage?: ReturnType<typeof mock>;
382385
downloadFullUpdate?: ReturnType<typeof mock>;
386+
addProgressListener?: ReturnType<typeof mock>;
383387
} = {}) => {
384388
setupClientMocks({
385389
downloadPatchFromPpk,
386390
downloadPatchFromPackage,
387391
downloadFullUpdate,
392+
addProgressListener,
388393
});
389394

390395
// Override setTimeout to skip real backoff delays in retry tests
@@ -396,7 +401,9 @@ describe('downloadUpdate fallback chain', () => {
396401
__esModule: true,
397402
assertWeb: () => true,
398403
computeProgress: (received: number, total: number) =>
399-
total > 0 ? Math.floor((received / total) * 100) : 0,
404+
total > 0
405+
? Math.min(100, Math.max(0, Math.floor((received / total) * 100)))
406+
: 0,
400407
DEFAULT_FETCH_TIMEOUT_MS: 5000,
401408
emptyObj: {},
402409
fetchWithTimeout: mock(() => Promise.resolve()),
@@ -436,6 +443,45 @@ describe('downloadUpdate fallback chain', () => {
436443
expect(downloadPatchFromPpk).toHaveBeenCalledTimes(1);
437444
});
438445

446+
test('adds computed progress to download progress callbacks', async () => {
447+
let progressListener:
448+
| ((data: {
449+
hash: string;
450+
received: number;
451+
total: number;
452+
}) => void)
453+
| undefined;
454+
const addProgressListener = mock(
455+
(_event: string, listener: typeof progressListener) => {
456+
progressListener = listener;
457+
return { remove: mock(() => {}) };
458+
},
459+
);
460+
const onDownloadProgress = mock(() => {});
461+
setupDownloadMocks({
462+
addProgressListener,
463+
downloadPatchFromPpk: mock(async () => {
464+
progressListener?.({
465+
hash: 'new-hash',
466+
received: 1200,
467+
total: 1000,
468+
});
469+
}),
470+
});
471+
const { Pushy, sharedState } = await importFreshClient('dl-progress');
472+
sharedState.downloadedHash = undefined;
473+
const client = new Pushy({ appKey: 'demo-app' });
474+
475+
await client.downloadUpdate(updateInfo, onDownloadProgress);
476+
477+
expect(onDownloadProgress).toHaveBeenCalledWith({
478+
hash: 'new-hash',
479+
received: 1200,
480+
total: 1000,
481+
progress: 100,
482+
});
483+
});
484+
439485
test('falls back to pdiff when diff fails', async () => {
440486
const { downloadPatchFromPpk, downloadPatchFromPackage } =
441487
setupDownloadMocks({
@@ -487,6 +533,22 @@ describe('downloadUpdate fallback chain', () => {
487533
);
488534
});
489535

536+
test('treats negative maxRetries as zero retries', async () => {
537+
const { downloadFullUpdate } = setupDownloadMocks({
538+
downloadPatchFromPpk: mock(() => Promise.reject(Error('diff fail'))),
539+
downloadPatchFromPackage: mock(() => Promise.reject(Error('pdiff fail'))),
540+
downloadFullUpdate: mock(() => Promise.reject(Error('full fail'))),
541+
});
542+
const { Pushy, sharedState } = await importFreshClient('dl-negative-retries');
543+
sharedState.downloadedHash = undefined;
544+
const client = new Pushy({ appKey: 'demo-app', maxRetries: -1 });
545+
546+
await expect(client.downloadUpdate(updateInfo)).rejects.toThrow(
547+
'error_full_patch_failed',
548+
);
549+
expect(downloadFullUpdate).toHaveBeenCalledTimes(1);
550+
});
551+
490552
test('retries download when maxRetries is set', async () => {
491553
let callCount = 0;
492554
const { downloadFullUpdate } = setupDownloadMocks({

src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export class Pushy {
520520
);
521521
}
522522
}
523-
const maxRetries = this.options.maxRetries ?? 3;
523+
const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
524524
let succeeded = '';
525525
let lastError: any;
526526
const errorMessages: string[] = [];

src/type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface ProgressData {
3333
hash: string;
3434
received: number;
3535
total: number;
36-
/** Download progress percentage (0-100), computed as Math.floor(received / total * 100). Only populated in downloadUpdate callbacks. */
36+
/** Download progress percentage (0-100), computed from received / total and clamped to range. Only populated in downloadUpdate callbacks. */
3737
progress?: number;
3838
}
3939

0 commit comments

Comments
 (0)