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
48 changes: 45 additions & 3 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ const createJsonResponse = (payload: unknown) =>
const setupClientMocks = ({
isFirstTime = false,
markSuccess = mock(() => {}),
downloadPatchFromPpk = mock(() => Promise.resolve()),
downloadPatchFromPackage = mock(() => Promise.resolve()),
downloadFullUpdate = mock(() => Promise.resolve()),
}: {
isFirstTime?: boolean;
markSuccess?: ReturnType<typeof mock>;
downloadPatchFromPpk?: ReturnType<typeof mock>;
downloadPatchFromPackage?: ReturnType<typeof mock>;
downloadFullUpdate?: ReturnType<typeof mock>;
} = {}) => {
(globalThis as any).__DEV__ = false;

Expand All @@ -38,9 +44,9 @@ const setupClientMocks = ({
markSuccess,
reloadUpdate: mock(() => Promise.resolve()),
setNeedUpdate: mock(() => Promise.resolve()),
downloadPatchFromPpk: mock(() => Promise.resolve()),
downloadPatchFromPackage: mock(() => Promise.resolve()),
downloadFullUpdate: mock(() => Promise.resolve()),
downloadPatchFromPpk,
downloadPatchFromPackage,
downloadFullUpdate,
downloadAndInstallApk: mock(() => Promise.resolve()),
restartApp: mock(() => Promise.resolve()),
},
Expand Down Expand Up @@ -206,6 +212,42 @@ describe('Pushy server config', () => {
});
});

test('skips downloading when update hash is already current', async () => {
const downloadPatchFromPpk = mock(() => Promise.resolve());
const downloadPatchFromPackage = mock(() => Promise.resolve());
const downloadFullUpdate = mock(() => Promise.resolve());
const logger = mock(() => {});
setupClientMocks({
downloadPatchFromPpk,
downloadPatchFromPackage,
downloadFullUpdate,
});

const { Pushy } = await importFreshClient('skip-current-hash-download');
const client = new Pushy({
appKey: 'demo-app',
logger,
});

await expect(
client.downloadUpdate({
update: true,
hash: 'hash',
full: 'hash',
paths: ['cdn.example.com'],
}),
).resolves.toBeUndefined();

expect(downloadPatchFromPpk).not.toHaveBeenCalled();
expect(downloadPatchFromPackage).not.toHaveBeenCalled();
expect(downloadFullUpdate).not.toHaveBeenCalled();
expect(logger).not.toHaveBeenCalledWith(
expect.objectContaining({
type: 'downloading',
}),
);
});

test('waits for native markSuccess before logging success', async () => {
let resolveNativeMarkSuccess = () => {};
const nativeMarkSuccess = mock(
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, test } from 'bun:test';
import { currentVersion, packageVersion } from '../core';
import { resolveCheckResult } from '../resolveCheckResult';
import type { CheckResult } from '../type';

const createRootResult = (overrides: Partial<CheckResult> = {}): CheckResult => ({
update: true,
hash: 'full-hash',
name: 'full-version',
description: 'full description',
metaInfo: 'full meta',
diff: 'current-full.hdiff',
pdiff: 'package-full.phdiff',
full: 'full-hash',
paths: ['cdn.example.com'],
...overrides,
});

describe('resolveCheckResult', () => {
test('returns upToDate when rollout target is already current', () => {
const result = resolveCheckResult(
createRootResult({
expVersion: {
name: 'gray-current',
hash: currentVersion,
description: 'gray description',
metaInfo: 'gray meta',
config: {
rollout: {
[packageVersion]: 100,
},
},
},
}),
);

expect(result).toEqual({ upToDate: true });
});

test('does not inherit root diff artifacts for rollout target', () => {
const result = resolveCheckResult(
createRootResult({
expVersion: {
name: 'gray-next',
hash: 'gray-hash',
description: 'gray description',
metaInfo: 'gray meta',
config: {
rollout: {
[packageVersion]: 100,
},
},
},
}),
);

expect(result).toEqual({
update: true,
hash: 'gray-hash',
name: 'gray-next',
description: 'gray description',
metaInfo: 'gray meta',
config: {
rollout: {
[packageVersion]: 100,
},
},
paths: ['cdn.example.com'],
});
});

test('falls back to root result when rollout target is not selected', () => {
const result = resolveCheckResult(
createRootResult({
expVersion: {
name: 'gray-next',
hash: 'gray-hash',
description: 'gray description',
metaInfo: 'gray meta',
config: {
rollout: {
[packageVersion]: 0,
},
},
},
}),
);

expect(result).toEqual(createRootResult());
});

test('returns upToDate when root target is already current', () => {
const result = resolveCheckResult(
createRootResult({ hash: currentVersion }),
);

expect(result).toEqual({ upToDate: true });
});
});
4 changes: 4 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ export class Pushy {
if (!updateInfo.update || !hash) {
return;
}
if (hash === currentVersion) {
log(`current hash ${currentVersion}, ignored`);
return;
}
if (rolledBackVersion === hash) {
log(`rolledback hash ${rolledBackVersion}, ignored`);
return;
Expand Down
132 changes: 57 additions & 75 deletions src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ import {
CheckResult,
ProgressData,
UpdateTestPayload,
VersionInfo,
} from './type';
import { UpdateContext } from './context';
import { URL } from 'react-native-url-polyfill';
import { isInRollout } from './isInRollout';
import { resolveCheckResult } from './resolveCheckResult';
import { assertWeb, log } from './utils';

export const UpdateProvider = ({
Expand Down Expand Up @@ -181,94 +180,77 @@ export const UpdateProvider = ({
if (!rootInfo) {
return;
}
const versions = [rootInfo.expVersion, rootInfo].filter(
Boolean,
) as VersionInfo[];
delete rootInfo.expVersion;
for (const versionInfo of versions) {
const info: CheckResult = {
...rootInfo,
...versionInfo,
};
const rollout = info.config?.rollout?.[packageVersion];
if (info.update && rollout) {
if (!isInRollout(rollout)) {
log(`${info.name} not in ${rollout}% rollout, ignored`);
continue;
}
log(`${info.name} in ${rollout}% rollout, continue`);
}
if (info.update) {
info.description = info.description ?? '';
const info = resolveCheckResult(rootInfo);
if (info.update) {
info.description = info.description ?? '';
}
updateInfoRef.current = info;
setUpdateInfo(info);
if (info.expired) {
if (
options.onPackageExpired &&
(await options.onPackageExpired(info)) === false
) {
log('onPackageExpired returned false, skipping');
return;
}
updateInfoRef.current = info;
setUpdateInfo(info);
if (info.expired) {
if (
options.onPackageExpired &&
(await options.onPackageExpired(info)) === false
) {
log('onPackageExpired returned false, skipping');
return;
}
const { downloadUrl } = info;
if (downloadUrl && sharedState.apkStatus === null) {
if (options.updateStrategy === 'silentAndNow') {
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
return info;
const { downloadUrl } = info;
if (downloadUrl && sharedState.apkStatus === null) {
if (options.updateStrategy === 'silentAndNow') {
if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
alertUpdate(
client.t('alert_title'),
client.t('alert_app_updated'),
[
{
text: client.t('alert_update_button'),
onPress: () => {
if (
Platform.OS === 'android' &&
downloadUrl.endsWith('.apk')
) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
},
},
],
);
}
} else if (info.update) {
if (
options.updateStrategy === 'silentAndNow' ||
options.updateStrategy === 'silentAndLater'
) {
downloadUpdate(info);
return info;
}
alertUpdate(
client.t('alert_title'),
client.t('alert_new_version_found', {
name: info.name!,
description: info.description!,
}),
client.t('alert_app_updated'),
[
{ text: client.t('alert_cancel'), style: 'cancel' },
{
text: client.t('alert_confirm'),
style: 'default',
text: client.t('alert_update_button'),
onPress: () => {
downloadUpdate();
if (
Platform.OS === 'android' &&
downloadUrl.endsWith('.apk')
) {
downloadAndInstallApk(downloadUrl);
} else {
Linking.openURL(downloadUrl);
}
},
},
],
);
}
return info;
} else if (info.update) {
if (
options.updateStrategy === 'silentAndNow' ||
options.updateStrategy === 'silentAndLater'
) {
downloadUpdate(info);
return info;
}
alertUpdate(
client.t('alert_title'),
client.t('alert_new_version_found', {
name: info.name!,
description: info.description!,
}),
[
{ text: client.t('alert_cancel'), style: 'cancel' },
{
text: client.t('alert_confirm'),
style: 'default',
onPress: () => {
downloadUpdate();
},
},
],
);
}
return info;
},
[
client,
Expand Down
30 changes: 30 additions & 0 deletions src/resolveCheckResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { currentVersion, packageVersion } from './core';
import { isInRollout } from './isInRollout';
import { CheckResult } from './type';
import { log } from './utils';

export function resolveCheckResult(rootInfo: CheckResult): CheckResult {
const { expVersion, ...rootResult } = rootInfo;
const rollout = expVersion?.config?.rollout?.[packageVersion];
if (rootResult.update && expVersion && typeof rollout === 'number') {
if (isInRollout(rollout)) {
log(`${expVersion.name} in ${rollout}% rollout, continue`);
if (expVersion.hash === currentVersion) {
return { upToDate: true };
}
const info: CheckResult = {
update: true,
...expVersion,
};
if (rootResult.paths) {
info.paths = rootResult.paths;
}
return info;
}
log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
}
if (rootResult.update && rootResult.hash === currentVersion) {
return { upToDate: true };
}
return rootResult;
}
Loading