@bravemobile/react-native-code-push 13.1.0-beta.6 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.6",
3
+ "version": "13.1.0",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
package/src/CodePush.js CHANGED
@@ -2,6 +2,7 @@ import { Alert, AppState, Platform } from "react-native";
2
2
  import log from "./logging";
3
3
  import hoistStatics from 'hoist-non-react-statics';
4
4
  import { SemverVersioning } from './versioning/SemverVersioning'
5
+ import invokeTelemetryCallback from './invokeTelemetryCallback';
5
6
 
6
7
  const NativeCodePushModule = require("./native/NativeCodePush");
7
8
  let NativeCodePush = NativeCodePushModule.getNativeCodePush?.() ?? NativeCodePushModule.default;
@@ -83,7 +84,7 @@ async function decideLatestReleaseIsInRollout(versioning, clientId, onRolloutSki
83
84
 
84
85
  if (!inRollout) {
85
86
  log(`Skipping update due to rollout. Bucket ${bucket} is not smaller than rollout range ${latestReleaseInfo.rollout}.`);
86
- onRolloutSkipped?.(latestVersion);
87
+ invokeTelemetryCallback('onRolloutSkipped', onRolloutSkipped, latestVersion);
87
88
  }
88
89
 
89
90
  return inRollout;
@@ -378,11 +379,11 @@ async function tryReportStatus(statusReport, retryOnAppResume) {
378
379
  const label = statusReport.package.label;
379
380
  if (statusReport.status === "DeploymentSucceeded") {
380
381
  log(`Reporting CodePush update success (${label})`);
381
- sharedCodePushOptions?.onUpdateSuccess?.(label);
382
+ invokeTelemetryCallback('onUpdateSuccess', sharedCodePushOptions?.onUpdateSuccess, label);
382
383
  } else {
383
384
  log(`Reporting CodePush update rollback (${label})`);
384
385
  await nativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash);
385
- sharedCodePushOptions?.onUpdateRollback?.(label);
386
+ invokeTelemetryCallback('onUpdateRollback', sharedCodePushOptions?.onUpdateRollback, label);
386
387
  }
387
388
  }
388
389
 
@@ -565,20 +566,8 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
565
566
  ...options,
566
567
  };
567
568
 
568
- /*
569
- * A callback an app registered to observe how the update archives went must not be able
570
- * to cost it an update, so it is isolated the way the other sync callbacks are: whatever
571
- * it throws is logged and the install carries on. An app that registered none leaves the
572
- * download exactly as it was before this option existed.
573
- */
574
569
  const onUpdateArchiveResult = typeof syncOptions.onUpdateArchiveResult === "function"
575
- ? (label, result) => {
576
- try {
577
- syncOptions.onUpdateArchiveResult(label, result);
578
- } catch (error) {
579
- log(`An error has occurred : ${error.stack}`);
580
- }
581
- }
570
+ ? (label, result) => invokeTelemetryCallback('onUpdateArchiveResult', syncOptions.onUpdateArchiveResult, label, result)
582
571
  : null;
583
572
 
584
573
  syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
@@ -630,14 +619,14 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
630
619
 
631
620
  const doDownloadAndInstall = async () => {
632
621
  syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
633
- sharedCodePushOptions.onDownloadStart?.(remotePackageLabel);
622
+ invokeTelemetryCallback('onDownloadStart', sharedCodePushOptions.onDownloadStart, remotePackageLabel);
634
623
 
635
624
  const localPackage = await remotePackage.download(
636
625
  downloadProgressCallback,
637
626
  onUpdateArchiveResult && ((result) => onUpdateArchiveResult(remotePackageLabel, result)),
638
627
  );
639
628
 
640
- sharedCodePushOptions.onDownloadSuccess?.(remotePackageLabel);
629
+ invokeTelemetryCallback('onDownloadSuccess', sharedCodePushOptions.onDownloadSuccess, remotePackageLabel);
641
630
 
642
631
  // Determine the correct install mode based on whether the update is mandatory or not.
643
632
  resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
@@ -721,7 +710,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
721
710
  }
722
711
  } catch (error) {
723
712
  syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
724
- sharedCodePushOptions?.onSyncError?.(remotePackageLabel ?? 'unknown', error);
713
+ invokeTelemetryCallback('onSyncError', sharedCodePushOptions?.onSyncError, remotePackageLabel ?? 'unknown', error);
725
714
  log(error.message);
726
715
  throw error;
727
716
  }
@@ -752,26 +741,26 @@ let CodePush;
752
741
  * updateChecker: updateChecker | undefined,
753
742
  * setUpdateChecker(updateCheckerFunction: updateChecker | undefined): void,
754
743
  *
755
- * onUpdateSuccess: (label: string) => void | undefined,
756
- * setOnUpdateSuccess(onUpdateSuccessFunction: (label: string) => void | undefined): void,
744
+ * onUpdateSuccess: (label: string) => void | Promise<void> | undefined,
745
+ * setOnUpdateSuccess(onUpdateSuccessFunction: (label: string) => void | Promise<void> | undefined): void,
757
746
  *
758
- * onUpdateRollback: (label: string) => void | undefined,
759
- * setOnUpdateRollback(onUpdateRollbackFunction: (label: string) => void | undefined): void,
747
+ * onUpdateRollback: (label: string) => void | Promise<void> | undefined,
748
+ * setOnUpdateRollback(onUpdateRollbackFunction: (label: string) => void | Promise<void> | undefined): void,
760
749
  *
761
- * onDownloadStart: (label: string) => void | undefined,
762
- * setOnDownloadStart(onDownloadStartFunction: (label: string) => void | undefined): void,
750
+ * onDownloadStart: (label: string) => void | Promise<void> | undefined,
751
+ * setOnDownloadStart(onDownloadStartFunction: (label: string) => void | Promise<void> | undefined): void,
763
752
  *
764
- * onDownloadSuccess: (label: string) => void | undefined,
765
- * setOnDownloadSuccess(onDownloadSuccessFunction: (label: string) => void | undefined): void,
753
+ * onDownloadSuccess: (label: string) => void | Promise<void> | undefined,
754
+ * setOnDownloadSuccess(onDownloadSuccessFunction: (label: string) => void | Promise<void> | undefined): void,
766
755
  *
767
- * onSyncError: (label: string, error: Error) => void | undefined,
768
- * setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
756
+ * onSyncError: (label: string, error: Error) => void | Promise<void> | undefined,
757
+ * setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | Promise<void> | undefined): void,
769
758
  *
770
- * onRolloutSkipped: (label: string, error: Error) => void | undefined,
771
- * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
759
+ * onRolloutSkipped: (label: string, error: Error) => void | Promise<void> | undefined,
760
+ * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | Promise<void> | undefined): void,
772
761
  *
773
- * onUpdateArchiveResult: (label: string, result: object) => void | undefined,
774
- * setOnUpdateArchiveResult(onUpdateArchiveResultFunction: (label: string, result: object) => void | undefined): void,
762
+ * onUpdateArchiveResult: (label: string, result: object) => void | Promise<void> | undefined,
763
+ * setOnUpdateArchiveResult(onUpdateArchiveResultFunction: (label: string, result: object) => void | Promise<void> | undefined): void,
775
764
  * }}
776
765
  */
777
766
  const sharedCodePushOptions = {
@@ -64,7 +64,7 @@ function diffRelease() {
64
64
  * which only a download that had an archive to try comes back with.
65
65
  * @param installedPackage the CodePush update the app is running, if any.
66
66
  */
67
- function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
67
+ function createNativeBridge({ updateArchiveResult, installedPackage, statusReport } = {}) {
68
68
  return {
69
69
  addDownloadProgressListener: jest.fn(() => ({ remove: jest.fn() })),
70
70
  downloadUpdate: jest.fn(async (updatePackage) => ({
@@ -78,9 +78,12 @@ function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
78
78
  // The rest of what a full `sync()` reaches for.
79
79
  InstallMode,
80
80
  clearPendingRestart: jest.fn(),
81
- getNewStatusReport: jest.fn(async () => null),
81
+ getNewStatusReport: jest.fn(async () => statusReport ?? null),
82
82
  installUpdate: jest.fn(async () => {}),
83
83
  notifyApplicationReady: jest.fn(async () => {}),
84
+ recordStatusReported: jest.fn(),
85
+ saveStatusReportForRetry: jest.fn(),
86
+ setLatestRollbackInfo: jest.fn(async () => {}),
84
87
  };
85
88
  }
86
89
 
@@ -89,13 +92,13 @@ function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
89
92
  * native bridge live on the module itself - configured the way an app configures it: the
90
93
  * decorator registers the app-wide options, and every sync below is one the app asks for.
91
94
  *
92
- * @param onUpdateArchiveResult a callback the app registers on the decorator, as opposed to
93
- * one it passes to a single `sync()` call.
95
+ * Callback options left after the bridge setup values are removed are registered on the
96
+ * decorator, the same way an app registers them.
94
97
  */
95
- function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult, onUpdateArchiveResult, installedPackage } = {}) {
98
+ function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult, installedPackage, statusReport, ...codePushOptions } = {}) {
96
99
  jest.resetModules();
97
100
  const CodePush = require('./CodePush');
98
- const nativeBridge = createNativeBridge({ updateArchiveResult, installedPackage });
101
+ const nativeBridge = createNativeBridge({ updateArchiveResult, installedPackage, statusReport });
99
102
 
100
103
  CodePush.setUpTestDependencies(
101
104
  null,
@@ -106,7 +109,7 @@ function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult,
106
109
  checkFrequency: CodePush.CheckFrequency.MANUAL,
107
110
  releaseHistoryFetcher: async () => releaseHistory,
108
111
  updateChecker,
109
- onUpdateArchiveResult,
112
+ ...codePushOptions,
110
113
  });
111
114
 
112
115
  return { CodePush, nativeBridge };
@@ -492,6 +495,40 @@ describe('the update archive result of a sync', () => {
492
495
  expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
493
496
  });
494
497
 
498
+ it('installs the update when the callback returns a rejected Promise', async () => {
499
+ const { CodePush, nativeBridge } = loadCodePush({
500
+ releaseHistory: patchedRelease(),
501
+ updateArchiveResult: APPLIED,
502
+ });
503
+ const onUpdateArchiveResult = jest.fn(async () => {
504
+ throw new Error('the telemetry request failed');
505
+ });
506
+
507
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
508
+ await Promise.resolve();
509
+
510
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
511
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, APPLIED);
512
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
513
+ });
514
+
515
+ it('resolves a direct download when the callback returns a rejected Promise', async () => {
516
+ const { CodePush } = loadCodePush({
517
+ releaseHistory: patchedRelease(),
518
+ updateArchiveResult: APPLIED,
519
+ });
520
+ const onUpdateArchiveResult = jest.fn(async () => {
521
+ throw new Error('the telemetry request failed');
522
+ });
523
+
524
+ const remotePackage = await CodePush.checkForUpdate();
525
+ const localPackage = await remotePackage.download(undefined, onUpdateArchiveResult);
526
+ await Promise.resolve();
527
+
528
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(APPLIED);
529
+ expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
530
+ });
531
+
495
532
  it('leaves the result off the package a download resolves with', async () => {
496
533
  const { CodePush } = loadCodePush({
497
534
  releaseHistory: patchedRelease(),
@@ -505,3 +542,110 @@ describe('the update archive result of a sync', () => {
505
542
  expect(localPackage).not.toHaveProperty('updateArchiveResult');
506
543
  });
507
544
  });
545
+
546
+ describe('telemetry callback errors', () => {
547
+ it('installs the update when onDownloadStart throws', async () => {
548
+ const onDownloadStart = jest.fn(() => {
549
+ throw new Error('the telemetry client is unavailable');
550
+ });
551
+ const { CodePush, nativeBridge } = loadCodePush({
552
+ releaseHistory: fullOnlyRelease(),
553
+ onDownloadStart,
554
+ });
555
+
556
+ const syncStatus = await CodePush.sync();
557
+
558
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
559
+ expect(onDownloadStart).toHaveBeenCalledWith(LABEL);
560
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
561
+ });
562
+
563
+ it('installs the update when onDownloadSuccess returns a rejected Promise', async () => {
564
+ const onDownloadSuccess = jest.fn(async () => {
565
+ throw new Error('the telemetry request failed');
566
+ });
567
+ const { CodePush, nativeBridge } = loadCodePush({
568
+ releaseHistory: fullOnlyRelease(),
569
+ onDownloadSuccess,
570
+ });
571
+
572
+ const syncStatus = await CodePush.sync();
573
+ await Promise.resolve();
574
+
575
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
576
+ expect(onDownloadSuccess).toHaveBeenCalledWith(LABEL);
577
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
578
+ });
579
+
580
+ it('uses the next release when onRolloutSkipped throws', async () => {
581
+ const onRolloutSkipped = jest.fn(() => {
582
+ throw new Error('the telemetry client is unavailable');
583
+ });
584
+ const { CodePush } = loadCodePush({
585
+ releaseHistory: {
586
+ ...fullOnlyRelease(),
587
+ '1.0.2': {
588
+ ...fullOnlyRelease()[LABEL],
589
+ rollout: 0,
590
+ packageHash: 'b'.repeat(64),
591
+ },
592
+ },
593
+ onRolloutSkipped,
594
+ });
595
+
596
+ const remotePackage = await CodePush.checkForUpdate();
597
+
598
+ expect(onRolloutSkipped).toHaveBeenCalledWith('1.0.2');
599
+ expect(remotePackage.label).toBe(LABEL);
600
+ });
601
+
602
+ it('rejects with the original error when onSyncError throws', async () => {
603
+ const downloadError = new Error('the update download failed');
604
+ const onSyncError = jest.fn(() => {
605
+ throw new Error('the telemetry client is unavailable');
606
+ });
607
+ const { CodePush, nativeBridge } = loadCodePush({
608
+ releaseHistory: fullOnlyRelease(),
609
+ onSyncError,
610
+ });
611
+ nativeBridge.downloadUpdate.mockRejectedValueOnce(downloadError);
612
+
613
+ await expect(CodePush.sync()).rejects.toBe(downloadError);
614
+ expect(onSyncError).toHaveBeenCalledWith(LABEL, downloadError);
615
+ });
616
+
617
+ it('records a successful update when onUpdateSuccess throws', async () => {
618
+ const statusReport = {
619
+ status: 'DeploymentSucceeded',
620
+ package: { label: LABEL, packageHash: PACKAGE_HASH },
621
+ };
622
+ const onUpdateSuccess = jest.fn(() => {
623
+ throw new Error('the telemetry client is unavailable');
624
+ });
625
+ const { CodePush, nativeBridge } = loadCodePush({ statusReport, onUpdateSuccess });
626
+
627
+ await CodePush.notifyAppReady();
628
+
629
+ expect(onUpdateSuccess).toHaveBeenCalledWith(LABEL);
630
+ expect(nativeBridge.recordStatusReported).toHaveBeenCalledWith(statusReport);
631
+ expect(nativeBridge.saveStatusReportForRetry).not.toHaveBeenCalled();
632
+ });
633
+
634
+ it('records a rollback when onUpdateRollback returns a rejected Promise', async () => {
635
+ const statusReport = {
636
+ status: 'DeploymentFailed',
637
+ package: { label: LABEL, packageHash: PACKAGE_HASH },
638
+ };
639
+ const onUpdateRollback = jest.fn(async () => {
640
+ throw new Error('the telemetry request failed');
641
+ });
642
+ const { CodePush, nativeBridge } = loadCodePush({ statusReport, onUpdateRollback });
643
+
644
+ await CodePush.notifyAppReady();
645
+ await Promise.resolve();
646
+
647
+ expect(onUpdateRollback).toHaveBeenCalledWith(LABEL);
648
+ expect(nativeBridge.recordStatusReported).toHaveBeenCalledWith(statusReport);
649
+ expect(nativeBridge.saveStatusReportForRetry).not.toHaveBeenCalled();
650
+ });
651
+ });
@@ -0,0 +1,18 @@
1
+ const log = require("./logging");
2
+
3
+ module.exports = function invokeTelemetryCallback(name, callback, ...args) {
4
+ if (typeof callback !== "function") return;
5
+
6
+ const logError = (error) => {
7
+ log(`The ${name} callback failed: ${error?.message ?? error}`);
8
+ };
9
+
10
+ try {
11
+ const result = callback(...args);
12
+ if (result && typeof result.then === "function") {
13
+ Promise.resolve(result).catch(logError);
14
+ }
15
+ } catch (error) {
16
+ logError(error);
17
+ }
18
+ };
@@ -1,4 +1,4 @@
1
- const log = require("./logging");
1
+ const invokeTelemetryCallback = require("./invokeTelemetryCallback");
2
2
 
3
3
  // This function is used to augment remote and local
4
4
  // package objects with additional functionality/properties
@@ -42,14 +42,10 @@ module.exports = (NativeCodePush) => {
42
42
  // taken off the package and reported on its own, leaving the package exactly what
43
43
  // the native side saved.
44
44
  const { updateArchiveResult, ...downloadedPackage } = downloadResult ?? {};
45
- if (updateArchiveResult && updateArchiveResultCallback) {
45
+ if (updateArchiveResult) {
46
46
  // The result is an observation, so an observer that throws must not turn a
47
47
  // downloaded update into a failed one.
48
- try {
49
- updateArchiveResultCallback(updateArchiveResult);
50
- } catch (error) {
51
- log(`The update archive result callback threw: ${error?.message ?? error}`);
52
- }
48
+ invokeTelemetryCallback('onUpdateArchiveResult', updateArchiveResultCallback, updateArchiveResult);
53
49
  }
54
50
 
55
51
  return { ...downloadedPackage, ...local };
@@ -211,27 +211,27 @@ export interface CodePushOptions extends SyncOptions {
211
211
  /**
212
212
  * Callback function that is called when the update installation succeeds.
213
213
  */
214
- onUpdateSuccess?: (label: string) => void;
214
+ onUpdateSuccess?: (label: string) => void | Promise<void>;
215
215
  /**
216
216
  * Callback function that is called when the update rolled back.
217
217
  */
218
- onUpdateRollback?: (label: string) => void;
218
+ onUpdateRollback?: (label: string) => void | Promise<void>;
219
219
  /**
220
220
  * Callback function that is called when download starts.
221
221
  */
222
- onDownloadStart?: (label: string) => void;
222
+ onDownloadStart?: (label: string) => void | Promise<void>;
223
223
  /**
224
224
  * Callback function that is called when download finished successfully.
225
225
  */
226
- onDownloadSuccess?: (label: string) => void;
226
+ onDownloadSuccess?: (label: string) => void | Promise<void>;
227
227
  /**
228
228
  * Callback function that is called when sync process failed.
229
229
  */
230
- onSyncError?: (label: string, error: Error) => void;
230
+ onSyncError?: (label: string, error: Error) => void | Promise<void>;
231
231
  /**
232
232
  * Callback function that is called when rollout is skipped.
233
233
  */
234
- onRolloutSkipped?: (label: string, error: Error) => void;
234
+ onRolloutSkipped?: (label: string, error: Error) => void | Promise<void>;
235
235
  }
236
236
 
237
237
  export interface DownloadProgress {
@@ -321,7 +321,7 @@ export interface RemotePackage extends Package {
321
321
  */
322
322
  download(
323
323
  downloadProgressCallback?: DownloadProgressCallback,
324
- updateArchiveResultCallback?: (result: UpdateArchiveResult) => void,
324
+ updateArchiveResultCallback?: (result: UpdateArchiveResult) => void | Promise<void>,
325
325
  ): Promise<LocalPackage>;
326
326
 
327
327
  /**
@@ -404,7 +404,7 @@ export interface SyncOptions {
404
404
  * depends on the callback: registering none changes nothing, and one that throws is
405
405
  * logged and does not fail the download it is reporting on.
406
406
  */
407
- onUpdateArchiveResult?: (label: string, result: UpdateArchiveResult) => void;
407
+ onUpdateArchiveResult?: (label: string, result: UpdateArchiveResult) => void | Promise<void>;
408
408
 
409
409
  /**
410
410
  * Specifies whether to ignore the update if the installation fails.