Skip to content

Commit 6e46022

Browse files
committed
fix client rollout current-hash updates
1 parent 767841c commit 6e46022

5 files changed

Lines changed: 235 additions & 78 deletions

File tree

src/__tests__/client.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ const createJsonResponse = (payload: unknown) =>
1313
const setupClientMocks = ({
1414
isFirstTime = false,
1515
markSuccess = mock(() => {}),
16+
downloadPatchFromPpk = mock(() => Promise.resolve()),
17+
downloadPatchFromPackage = mock(() => Promise.resolve()),
18+
downloadFullUpdate = mock(() => Promise.resolve()),
1619
}: {
1720
isFirstTime?: boolean;
1821
markSuccess?: ReturnType<typeof mock>;
22+
downloadPatchFromPpk?: ReturnType<typeof mock>;
23+
downloadPatchFromPackage?: ReturnType<typeof mock>;
24+
downloadFullUpdate?: ReturnType<typeof mock>;
1925
} = {}) => {
2026
(globalThis as any).__DEV__ = false;
2127

@@ -38,9 +44,9 @@ const setupClientMocks = ({
3844
markSuccess,
3945
reloadUpdate: mock(() => Promise.resolve()),
4046
setNeedUpdate: mock(() => Promise.resolve()),
41-
downloadPatchFromPpk: mock(() => Promise.resolve()),
42-
downloadPatchFromPackage: mock(() => Promise.resolve()),
43-
downloadFullUpdate: mock(() => Promise.resolve()),
47+
downloadPatchFromPpk,
48+
downloadPatchFromPackage,
49+
downloadFullUpdate,
4450
downloadAndInstallApk: mock(() => Promise.resolve()),
4551
restartApp: mock(() => Promise.resolve()),
4652
},
@@ -206,6 +212,42 @@ describe('Pushy server config', () => {
206212
});
207213
});
208214

215+
test('skips downloading when update hash is already current', async () => {
216+
const downloadPatchFromPpk = mock(() => Promise.resolve());
217+
const downloadPatchFromPackage = mock(() => Promise.resolve());
218+
const downloadFullUpdate = mock(() => Promise.resolve());
219+
const logger = mock(() => {});
220+
setupClientMocks({
221+
downloadPatchFromPpk,
222+
downloadPatchFromPackage,
223+
downloadFullUpdate,
224+
});
225+
226+
const { Pushy } = await importFreshClient('skip-current-hash-download');
227+
const client = new Pushy({
228+
appKey: 'demo-app',
229+
logger,
230+
});
231+
232+
await expect(
233+
client.downloadUpdate({
234+
update: true,
235+
hash: 'hash',
236+
full: 'hash',
237+
paths: ['cdn.example.com'],
238+
}),
239+
).resolves.toBeUndefined();
240+
241+
expect(downloadPatchFromPpk).not.toHaveBeenCalled();
242+
expect(downloadPatchFromPackage).not.toHaveBeenCalled();
243+
expect(downloadFullUpdate).not.toHaveBeenCalled();
244+
expect(logger).not.toHaveBeenCalledWith(
245+
expect.objectContaining({
246+
type: 'downloading',
247+
}),
248+
);
249+
});
250+
209251
test('waits for native markSuccess before logging success', async () => {
210252
let resolveNativeMarkSuccess = () => {};
211253
const nativeMarkSuccess = mock(

src/__tests__/provider.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import { currentVersion, packageVersion } from '../core';
3+
import { resolveCheckResult } from '../resolveCheckResult';
4+
import type { CheckResult } from '../type';
5+
6+
const createRootResult = (overrides: Partial<CheckResult> = {}): CheckResult => ({
7+
update: true,
8+
hash: 'full-hash',
9+
name: 'full-version',
10+
description: 'full description',
11+
metaInfo: 'full meta',
12+
diff: 'current-full.hdiff',
13+
pdiff: 'package-full.phdiff',
14+
full: 'full-hash',
15+
paths: ['cdn.example.com'],
16+
...overrides,
17+
});
18+
19+
describe('resolveCheckResult', () => {
20+
test('returns upToDate when rollout target is already current', () => {
21+
const result = resolveCheckResult(
22+
createRootResult({
23+
expVersion: {
24+
name: 'gray-current',
25+
hash: currentVersion,
26+
description: 'gray description',
27+
metaInfo: 'gray meta',
28+
config: {
29+
rollout: {
30+
[packageVersion]: 100,
31+
},
32+
},
33+
},
34+
}),
35+
);
36+
37+
expect(result).toEqual({ upToDate: true });
38+
});
39+
40+
test('does not inherit root diff artifacts for rollout target', () => {
41+
const result = resolveCheckResult(
42+
createRootResult({
43+
expVersion: {
44+
name: 'gray-next',
45+
hash: 'gray-hash',
46+
description: 'gray description',
47+
metaInfo: 'gray meta',
48+
config: {
49+
rollout: {
50+
[packageVersion]: 100,
51+
},
52+
},
53+
},
54+
}),
55+
);
56+
57+
expect(result).toEqual({
58+
update: true,
59+
hash: 'gray-hash',
60+
name: 'gray-next',
61+
description: 'gray description',
62+
metaInfo: 'gray meta',
63+
config: {
64+
rollout: {
65+
[packageVersion]: 100,
66+
},
67+
},
68+
paths: ['cdn.example.com'],
69+
});
70+
});
71+
72+
test('falls back to root result when rollout target is not selected', () => {
73+
const result = resolveCheckResult(
74+
createRootResult({
75+
expVersion: {
76+
name: 'gray-next',
77+
hash: 'gray-hash',
78+
description: 'gray description',
79+
metaInfo: 'gray meta',
80+
config: {
81+
rollout: {
82+
[packageVersion]: 0,
83+
},
84+
},
85+
},
86+
}),
87+
);
88+
89+
expect(result).toEqual(createRootResult());
90+
});
91+
92+
test('returns upToDate when root target is already current', () => {
93+
const result = resolveCheckResult(
94+
createRootResult({ hash: currentVersion }),
95+
);
96+
97+
expect(result).toEqual({ upToDate: true });
98+
});
99+
});

src/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ export class Pushy {
452452
if (!updateInfo.update || !hash) {
453453
return;
454454
}
455+
if (hash === currentVersion) {
456+
log(`current hash ${currentVersion}, ignored`);
457+
return;
458+
}
455459
if (rolledBackVersion === hash) {
456460
log(`rolledback hash ${rolledBackVersion}, ignored`);
457461
return;

src/provider.tsx

Lines changed: 57 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ import {
2323
CheckResult,
2424
ProgressData,
2525
UpdateTestPayload,
26-
VersionInfo,
2726
} from './type';
2827
import { UpdateContext } from './context';
2928
import { URL } from 'react-native-url-polyfill';
30-
import { isInRollout } from './isInRollout';
29+
import { resolveCheckResult } from './resolveCheckResult';
3130
import { assertWeb, log } from './utils';
3231

3332
export const UpdateProvider = ({
@@ -181,94 +180,77 @@ export const UpdateProvider = ({
181180
if (!rootInfo) {
182181
return;
183182
}
184-
const versions = [rootInfo.expVersion, rootInfo].filter(
185-
Boolean,
186-
) as VersionInfo[];
187-
delete rootInfo.expVersion;
188-
for (const versionInfo of versions) {
189-
const info: CheckResult = {
190-
...rootInfo,
191-
...versionInfo,
192-
};
193-
const rollout = info.config?.rollout?.[packageVersion];
194-
if (info.update && rollout) {
195-
if (!isInRollout(rollout)) {
196-
log(`${info.name} not in ${rollout}% rollout, ignored`);
197-
continue;
198-
}
199-
log(`${info.name} in ${rollout}% rollout, continue`);
200-
}
201-
if (info.update) {
202-
info.description = info.description ?? '';
183+
const info = resolveCheckResult(rootInfo);
184+
if (info.update) {
185+
info.description = info.description ?? '';
186+
}
187+
updateInfoRef.current = info;
188+
setUpdateInfo(info);
189+
if (info.expired) {
190+
if (
191+
options.onPackageExpired &&
192+
(await options.onPackageExpired(info)) === false
193+
) {
194+
log('onPackageExpired returned false, skipping');
195+
return;
203196
}
204-
updateInfoRef.current = info;
205-
setUpdateInfo(info);
206-
if (info.expired) {
207-
if (
208-
options.onPackageExpired &&
209-
(await options.onPackageExpired(info)) === false
210-
) {
211-
log('onPackageExpired returned false, skipping');
212-
return;
213-
}
214-
const { downloadUrl } = info;
215-
if (downloadUrl && sharedState.apkStatus === null) {
216-
if (options.updateStrategy === 'silentAndNow') {
217-
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
218-
downloadAndInstallApk(downloadUrl);
219-
} else {
220-
Linking.openURL(downloadUrl);
221-
}
222-
return info;
197+
const { downloadUrl } = info;
198+
if (downloadUrl && sharedState.apkStatus === null) {
199+
if (options.updateStrategy === 'silentAndNow') {
200+
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
201+
downloadAndInstallApk(downloadUrl);
202+
} else {
203+
Linking.openURL(downloadUrl);
223204
}
224-
alertUpdate(
225-
client.t('alert_title'),
226-
client.t('alert_app_updated'),
227-
[
228-
{
229-
text: client.t('alert_update_button'),
230-
onPress: () => {
231-
if (
232-
Platform.OS === 'android' &&
233-
downloadUrl.endsWith('.apk')
234-
) {
235-
downloadAndInstallApk(downloadUrl);
236-
} else {
237-
Linking.openURL(downloadUrl);
238-
}
239-
},
240-
},
241-
],
242-
);
243-
}
244-
} else if (info.update) {
245-
if (
246-
options.updateStrategy === 'silentAndNow' ||
247-
options.updateStrategy === 'silentAndLater'
248-
) {
249-
downloadUpdate(info);
250205
return info;
251206
}
252207
alertUpdate(
253208
client.t('alert_title'),
254-
client.t('alert_new_version_found', {
255-
name: info.name!,
256-
description: info.description!,
257-
}),
209+
client.t('alert_app_updated'),
258210
[
259-
{ text: client.t('alert_cancel'), style: 'cancel' },
260211
{
261-
text: client.t('alert_confirm'),
262-
style: 'default',
212+
text: client.t('alert_update_button'),
263213
onPress: () => {
264-
downloadUpdate();
214+
if (
215+
Platform.OS === 'android' &&
216+
downloadUrl.endsWith('.apk')
217+
) {
218+
downloadAndInstallApk(downloadUrl);
219+
} else {
220+
Linking.openURL(downloadUrl);
221+
}
265222
},
266223
},
267224
],
268225
);
269226
}
270-
return info;
227+
} else if (info.update) {
228+
if (
229+
options.updateStrategy === 'silentAndNow' ||
230+
options.updateStrategy === 'silentAndLater'
231+
) {
232+
downloadUpdate(info);
233+
return info;
234+
}
235+
alertUpdate(
236+
client.t('alert_title'),
237+
client.t('alert_new_version_found', {
238+
name: info.name!,
239+
description: info.description!,
240+
}),
241+
[
242+
{ text: client.t('alert_cancel'), style: 'cancel' },
243+
{
244+
text: client.t('alert_confirm'),
245+
style: 'default',
246+
onPress: () => {
247+
downloadUpdate();
248+
},
249+
},
250+
],
251+
);
271252
}
253+
return info;
272254
},
273255
[
274256
client,

src/resolveCheckResult.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { currentVersion, packageVersion } from './core';
2+
import { isInRollout } from './isInRollout';
3+
import { CheckResult } from './type';
4+
import { log } from './utils';
5+
6+
export function resolveCheckResult(rootInfo: CheckResult): CheckResult {
7+
const { expVersion, ...rootResult } = rootInfo;
8+
const rollout = expVersion?.config?.rollout?.[packageVersion];
9+
if (rootResult.update && expVersion && typeof rollout === 'number') {
10+
if (isInRollout(rollout)) {
11+
log(`${expVersion.name} in ${rollout}% rollout, continue`);
12+
if (expVersion.hash === currentVersion) {
13+
return { upToDate: true };
14+
}
15+
const info: CheckResult = {
16+
update: true,
17+
...expVersion,
18+
};
19+
if (rootResult.paths) {
20+
info.paths = rootResult.paths;
21+
}
22+
return info;
23+
}
24+
log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
25+
}
26+
if (rootResult.update && rootResult.hash === currentVersion) {
27+
return { upToDate: true };
28+
}
29+
return rootResult;
30+
}

0 commit comments

Comments
 (0)