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

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.
Files changed (40) hide show
  1. package/CodePush.podspec +5 -0
  2. package/README.md +15 -145
  3. package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
  4. package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
  5. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
  6. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
  7. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
  12. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
  13. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
  14. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
  15. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
  16. package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
  17. package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
  18. package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
  19. package/cli/commands/releaseCommand/release.test.ts +3 -3
  20. package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
  21. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
  22. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
  23. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
  24. package/cli/dist/commands/releaseCommand/release.test.js +3 -3
  25. package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
  26. package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
  27. package/cli/functions/stageReleaseHistoryFile.ts +38 -0
  28. package/ios/CodePush/CodePush.h +6 -3
  29. package/ios/CodePush/CodePush.mm +10 -10
  30. package/ios/CodePush/CodePushBinaryPatch.h +18 -8
  31. package/ios/CodePush/CodePushBinaryPatch.m +23 -22
  32. package/ios/CodePush/CodePushDownloadHandler.m +2 -1
  33. package/ios/CodePush/CodePushErrorUtils.m +55 -0
  34. package/ios/CodePush/CodePushPackage.m +242 -71
  35. package/package.json +2 -1
  36. package/src/CodePush.js +28 -18
  37. package/src/CodePush.test.js +110 -55
  38. package/src/package-mixins.js +19 -10
  39. package/typings/react-native-code-push.d.ts +110 -29
  40. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
@@ -60,16 +60,16 @@ function diffRelease() {
60
60
  }
61
61
 
62
62
  /**
63
- * @param binaryPatchResult what the native side reports the patch attempt ended in, which
64
- * only a download that had a patch to try comes back with.
63
+ * @param updateArchiveResult what the native side reports the archive attempts ended in,
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({ binaryPatchResult, installedPackage } = {}) {
67
+ function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
68
68
  return {
69
69
  addDownloadProgressListener: jest.fn(() => ({ remove: jest.fn() })),
70
70
  downloadUpdate: jest.fn(async (updatePackage) => ({
71
71
  ...updatePackage,
72
- ...(binaryPatchResult ? { binaryPatchResult } : {}),
72
+ ...(updateArchiveResult ? { updateArchiveResult } : {}),
73
73
  })),
74
74
  // Without an installed CodePush update, the app runs the bundle of its binary.
75
75
  getUpdateMetadata: jest.fn(async () => installedPackage ?? null),
@@ -89,13 +89,13 @@ function createNativeBridge({ binaryPatchResult, installedPackage } = {}) {
89
89
  * native bridge live on the module itself - configured the way an app configures it: the
90
90
  * decorator registers the app-wide options, and every sync below is one the app asks for.
91
91
  *
92
- * @param onBinaryPatchResult a callback the app registers on the decorator, as opposed to
92
+ * @param onUpdateArchiveResult a callback the app registers on the decorator, as opposed to
93
93
  * one it passes to a single `sync()` call.
94
94
  */
95
- function loadCodePush({ releaseHistory = {}, updateChecker, binaryPatchResult, onBinaryPatchResult, installedPackage } = {}) {
95
+ function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult, onUpdateArchiveResult, installedPackage } = {}) {
96
96
  jest.resetModules();
97
97
  const CodePush = require('./CodePush');
98
- const nativeBridge = createNativeBridge({ binaryPatchResult, installedPackage });
98
+ const nativeBridge = createNativeBridge({ updateArchiveResult, installedPackage });
99
99
 
100
100
  CodePush.setUpTestDependencies(
101
101
  null,
@@ -106,7 +106,7 @@ function loadCodePush({ releaseHistory = {}, updateChecker, binaryPatchResult, o
106
106
  checkFrequency: CodePush.CheckFrequency.MANUAL,
107
107
  releaseHistoryFetcher: async () => releaseHistory,
108
108
  updateChecker,
109
- onBinaryPatchResult,
109
+ onUpdateArchiveResult,
110
110
  });
111
111
 
112
112
  return { CodePush, nativeBridge };
@@ -211,11 +211,13 @@ describe('checkForUpdate without a binary patch in the release history', () => {
211
211
  });
212
212
 
213
213
  /**
214
- * A diff archive only applies to the one release it was computed against, so which of the
215
- * two patch urls of a release is downloaded depends on what the app is running right now.
214
+ * A diff archive only applies to the one release it was computed against, so whether the
215
+ * diff url is handed to the native side at all depends on what the app is running right
216
+ * now. The patch url is always handed over with it, because the diff falling back to the
217
+ * patch archive is the native side's decision to make.
216
218
  */
217
219
  describe('checkForUpdate with asset diff packages in the release history', () => {
218
- it('downloads the diff archive when the installed update matches a diff base', async () => {
220
+ it('carries the diff url next to the patch url when the installed update matches a diff base', async () => {
219
221
  const { CodePush, nativeBridge } = loadCodePush({
220
222
  releaseHistory: diffRelease(),
221
223
  installedPackage: { packageHash: INSTALLED_HASH },
@@ -226,11 +228,12 @@ describe('checkForUpdate with asset diff packages in the release history', () =>
226
228
 
227
229
  expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
228
230
  downloadUrl: DOWNLOAD_URL,
229
- binaryPatchDownloadUrl: DIFF_URL,
231
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
232
+ assetDiffDownloadUrl: DIFF_URL,
230
233
  });
231
234
  });
232
235
 
233
- it('downloads the plain patch archive when the installed update matches no diff base', async () => {
236
+ it('leaves the diff url out when the installed update matches no diff base', async () => {
234
237
  const { CodePush, nativeBridge } = loadCodePush({
235
238
  releaseHistory: diffRelease(),
236
239
  installedPackage: { packageHash: 'other-hash' },
@@ -239,20 +242,20 @@ describe('checkForUpdate with asset diff packages in the release history', () =>
239
242
  const remotePackage = await CodePush.checkForUpdate();
240
243
  await remotePackage.download();
241
244
 
242
- expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
243
- binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
244
- });
245
+ const metadata = downloadedPackageMetadata(nativeBridge);
246
+ expect(metadata).toMatchObject({ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL });
247
+ expect(metadata).not.toHaveProperty('assetDiffDownloadUrl');
245
248
  });
246
249
 
247
- it('downloads the plain patch archive when no update is installed', async () => {
250
+ it('leaves the diff url out when no update is installed', async () => {
248
251
  const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: diffRelease() });
249
252
 
250
253
  const remotePackage = await CodePush.checkForUpdate();
251
254
  await remotePackage.download();
252
255
 
253
- expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
254
- binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
255
- });
256
+ const metadata = downloadedPackageMetadata(nativeBridge);
257
+ expect(metadata).toMatchObject({ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL });
258
+ expect(metadata).not.toHaveProperty('assetDiffDownloadUrl');
256
259
  });
257
260
 
258
261
  it('does not send the diff package map across the bridge', async () => {
@@ -310,13 +313,33 @@ describe('checkForUpdate through the deprecated updateChecker', () => {
310
313
  });
311
314
 
312
315
  /**
313
- * How a patch attempt went is reported to the app that asked to hear about it, and to
316
+ * How the archive attempts went is reported to the app that asked to hear about it, and to
314
317
  * nobody else: it is not part of the update, and an app that asked for nothing gets the
315
318
  * install it always got.
316
319
  */
317
- describe('the binary patch result of a sync', () => {
318
- const APPLIED = { status: 'applied', applyDurationMs: 812 };
319
- const FELL_BACK = { status: 'fallback', fallbackReason: 'base_hash_mismatch', applyDurationMs: 1503 };
320
+ describe('the update archive result of a sync', () => {
321
+ const APPLIED = {
322
+ status: 'applied',
323
+ archive: 'binary-patch',
324
+ totalDurationMs: 812,
325
+ attempts: [{ archive: 'binary-patch', durationMs: 812, applyDurationMs: 64 }],
326
+ };
327
+ const FELL_BACK = {
328
+ status: 'fallback',
329
+ archive: 'binary-patch',
330
+ fallbackReason: 'base_hash_mismatch',
331
+ totalDurationMs: 1503,
332
+ attempts: [{ archive: 'binary-patch', fallbackReason: 'base_hash_mismatch', durationMs: 1503 }],
333
+ };
334
+ const APPLIED_AFTER_DIFF_FELL_BACK = {
335
+ status: 'applied',
336
+ archive: 'binary-patch',
337
+ totalDurationMs: 1503,
338
+ attempts: [
339
+ { archive: 'asset-diff', fallbackReason: 'asset_merge_failed', durationMs: 690, applyDurationMs: 41 },
340
+ { archive: 'binary-patch', durationMs: 813, applyDurationMs: 58 },
341
+ ],
342
+ };
320
343
 
321
344
  /** The metadata the update is installed from, as the native module is given it. */
322
345
  function installedPackageMetadata(nativeBridge) {
@@ -327,46 +350,62 @@ describe('the binary patch result of a sync', () => {
327
350
  it('tells the app the update was installed from its patch, and how long that took', async () => {
328
351
  const { CodePush, nativeBridge } = loadCodePush({
329
352
  releaseHistory: patchedRelease(),
330
- binaryPatchResult: APPLIED,
353
+ updateArchiveResult: APPLIED,
331
354
  });
332
- const onBinaryPatchResult = jest.fn();
355
+ const onUpdateArchiveResult = jest.fn();
333
356
 
334
- const syncStatus = await CodePush.sync({ onBinaryPatchResult });
357
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
335
358
 
336
359
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
337
- expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
338
- expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, APPLIED);
360
+ expect(onUpdateArchiveResult).toHaveBeenCalledTimes(1);
361
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, APPLIED);
339
362
  expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
340
363
  });
341
364
 
342
365
  it('tells the app why the update came from the full archive instead', async () => {
343
366
  const { CodePush } = loadCodePush({
344
367
  releaseHistory: patchedRelease(),
345
- binaryPatchResult: FELL_BACK,
368
+ updateArchiveResult: FELL_BACK,
346
369
  });
347
- const onBinaryPatchResult = jest.fn();
370
+ const onUpdateArchiveResult = jest.fn();
348
371
 
349
372
  // A patch that could not be applied is not a failed update: the full archive installs.
350
- const syncStatus = await CodePush.sync({ onBinaryPatchResult });
373
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
351
374
 
352
375
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
353
- expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, FELL_BACK);
376
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, FELL_BACK);
377
+ });
378
+
379
+ it("the app is handed each archive's apply time and the whole path's total", async () => {
380
+ const { CodePush } = loadCodePush({
381
+ releaseHistory: diffRelease(),
382
+ installedPackage: { packageHash: INSTALLED_HASH },
383
+ updateArchiveResult: APPLIED_AFTER_DIFF_FELL_BACK,
384
+ });
385
+ const onUpdateArchiveResult = jest.fn();
386
+
387
+ await CodePush.sync({ onUpdateArchiveResult });
388
+
389
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, APPLIED_AFTER_DIFF_FELL_BACK);
390
+ const [, result] = onUpdateArchiveResult.mock.calls[0];
391
+ expect(result.totalDurationMs).toBe(1503);
392
+ expect(result.attempts.map((attempt) => attempt.applyDurationMs)).toEqual([41, 58]);
354
393
  });
355
394
 
356
395
  it('tells the app that registered the callback on the decorator, even about a sync it asked for itself', async () => {
357
- const onBinaryPatchResult = jest.fn();
396
+ const onUpdateArchiveResult = jest.fn();
358
397
  const { CodePush } = loadCodePush({
359
398
  releaseHistory: patchedRelease(),
360
- binaryPatchResult: APPLIED,
361
- onBinaryPatchResult,
399
+ updateArchiveResult: APPLIED,
400
+ onUpdateArchiveResult,
362
401
  });
363
402
 
364
403
  // The decorator checks nothing on its own, so this sync is the app's own call.
365
404
  const syncStatus = await CodePush.sync();
366
405
 
367
406
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
368
- expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
369
- expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, APPLIED);
407
+ expect(onUpdateArchiveResult).toHaveBeenCalledTimes(1);
408
+ expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, APPLIED);
370
409
  });
371
410
 
372
411
  it('tells only the callback of the sync call when one was passed to it', async () => {
@@ -374,11 +413,11 @@ describe('the binary patch result of a sync', () => {
374
413
  const passedToTheSyncCall = jest.fn();
375
414
  const { CodePush } = loadCodePush({
376
415
  releaseHistory: patchedRelease(),
377
- binaryPatchResult: APPLIED,
378
- onBinaryPatchResult: registeredOnTheDecorator,
416
+ updateArchiveResult: APPLIED,
417
+ onUpdateArchiveResult: registeredOnTheDecorator,
379
418
  });
380
419
 
381
- const syncStatus = await CodePush.sync({ onBinaryPatchResult: passedToTheSyncCall });
420
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult: passedToTheSyncCall });
382
421
 
383
422
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
384
423
  expect(passedToTheSyncCall).toHaveBeenCalledTimes(1);
@@ -388,65 +427,81 @@ describe('the binary patch result of a sync', () => {
388
427
 
389
428
  it('says nothing about a download that had no patch to try', async () => {
390
429
  const { CodePush } = loadCodePush({ releaseHistory: fullOnlyRelease() });
391
- const onBinaryPatchResult = jest.fn();
430
+ const onUpdateArchiveResult = jest.fn();
392
431
 
393
- const syncStatus = await CodePush.sync({ onBinaryPatchResult });
432
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
394
433
 
395
434
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
396
- expect(onBinaryPatchResult).not.toHaveBeenCalled();
435
+ expect(onUpdateArchiveResult).not.toHaveBeenCalled();
397
436
  });
398
437
 
399
438
  it('installs the update even when the callback throws', async () => {
400
439
  const { CodePush, nativeBridge } = loadCodePush({
401
440
  releaseHistory: patchedRelease(),
402
- binaryPatchResult: APPLIED,
441
+ updateArchiveResult: APPLIED,
403
442
  });
404
- const onBinaryPatchResult = jest.fn(() => {
443
+ const onUpdateArchiveResult = jest.fn(() => {
405
444
  throw new Error('the telemetry the app sends the result to is down');
406
445
  });
407
446
 
408
- const syncStatus = await CodePush.sync({ onBinaryPatchResult });
447
+ const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
409
448
 
410
449
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
411
- expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
450
+ expect(onUpdateArchiveResult).toHaveBeenCalledTimes(1);
412
451
  expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
413
452
  });
414
453
 
415
454
  it('keeps the result out of the package the update is installed from', async () => {
416
455
  const { CodePush, nativeBridge } = loadCodePush({
417
456
  releaseHistory: patchedRelease(),
418
- binaryPatchResult: APPLIED,
457
+ updateArchiveResult: APPLIED,
419
458
  });
420
459
 
421
- await CodePush.sync({ onBinaryPatchResult: jest.fn() });
460
+ await CodePush.sync({ onUpdateArchiveResult: jest.fn() });
422
461
 
423
462
  const installedPackage = installedPackageMetadata(nativeBridge);
424
463
  expect(installedPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
425
- expect(installedPackage).not.toHaveProperty('binaryPatchResult');
464
+ expect(installedPackage).not.toHaveProperty('updateArchiveResult');
426
465
  });
427
466
 
428
467
  it('installs the same update, without the result, when no callback is registered', async () => {
429
468
  const { CodePush, nativeBridge } = loadCodePush({
430
469
  releaseHistory: patchedRelease(),
431
- binaryPatchResult: FELL_BACK,
470
+ updateArchiveResult: FELL_BACK,
432
471
  });
433
472
 
434
473
  const syncStatus = await CodePush.sync();
435
474
 
436
475
  expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
437
- expect(installedPackageMetadata(nativeBridge)).not.toHaveProperty('binaryPatchResult');
476
+ expect(installedPackageMetadata(nativeBridge)).not.toHaveProperty('updateArchiveResult');
477
+ });
478
+
479
+ it('resolves a direct download even when the callback passed to it throws', async () => {
480
+ const { CodePush } = loadCodePush({
481
+ releaseHistory: patchedRelease(),
482
+ updateArchiveResult: APPLIED,
483
+ });
484
+ const onUpdateArchiveResult = jest.fn(() => {
485
+ throw new Error('the telemetry the app sends the result to is down');
486
+ });
487
+
488
+ const remotePackage = await CodePush.checkForUpdate();
489
+ const localPackage = await remotePackage.download(undefined, onUpdateArchiveResult);
490
+
491
+ expect(onUpdateArchiveResult).toHaveBeenCalledTimes(1);
492
+ expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
438
493
  });
439
494
 
440
495
  it('leaves the result off the package a download resolves with', async () => {
441
496
  const { CodePush } = loadCodePush({
442
497
  releaseHistory: patchedRelease(),
443
- binaryPatchResult: APPLIED,
498
+ updateArchiveResult: APPLIED,
444
499
  });
445
500
 
446
501
  const remotePackage = await CodePush.checkForUpdate();
447
502
  const localPackage = await remotePackage.download();
448
503
 
449
504
  expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
450
- expect(localPackage).not.toHaveProperty('binaryPatchResult');
505
+ expect(localPackage).not.toHaveProperty('updateArchiveResult');
451
506
  });
452
507
  });
@@ -1,3 +1,5 @@
1
+ const log = require("./logging");
2
+
1
3
  // This function is used to augment remote and local
2
4
  // package objects with additional functionality/properties
3
5
  // beyond what is included in the metadata sent by the server.
@@ -6,13 +8,14 @@ module.exports = (NativeCodePush) => {
6
8
  return {
7
9
  /**
8
10
  * @param downloadProgressCallback Called as the archive is received.
9
- * @param binaryPatchResultCallback Called with `{ status, fallbackReason?, applyDurationMs }`
10
- * when the update was published with a binary patch, so that the app can observe
11
- * how the patch went. It says nothing about whether the download succeeded - a
12
- * patch that could not be applied is reported here and the update is downloaded
13
- * in full - and it is the only place the result is ever available.
11
+ * @param updateArchiveResultCallback Called with `{ status, archive, fallbackReason?,
12
+ * totalDurationMs, attempts }` when the update was published with a binary patch,
13
+ * so that the app can observe how the archives went. It says nothing about whether
14
+ * the download succeeded - archives that could not be applied are reported here
15
+ * and the update is downloaded in full - and it is the only place the result is
16
+ * ever available.
14
17
  */
15
- async download(downloadProgressCallback, binaryPatchResultCallback) {
18
+ async download(downloadProgressCallback, updateArchiveResultCallback) {
16
19
  if (!this.downloadUrl) {
17
20
  throw new Error("Cannot download an update without a download url");
18
21
  }
@@ -33,14 +36,20 @@ module.exports = (NativeCodePush) => {
33
36
 
34
37
  const downloadResult = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
35
38
 
36
- // The patch result describes the download that just happened, not the update it
39
+ // The archive result describes the download that just happened, not the update it
37
40
  // delivered, and the package is handed around as the update's metadata from here
38
41
  // on - it is even written back to the native side on install. So the result is
39
42
  // taken off the package and reported on its own, leaving the package exactly what
40
43
  // the native side saved.
41
- const { binaryPatchResult, ...downloadedPackage } = downloadResult ?? {};
42
- if (binaryPatchResult) {
43
- binaryPatchResultCallback?.(binaryPatchResult);
44
+ const { updateArchiveResult, ...downloadedPackage } = downloadResult ?? {};
45
+ if (updateArchiveResult && updateArchiveResultCallback) {
46
+ // The result is an observation, so an observer that throws must not turn a
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
+ }
44
53
  }
45
54
 
46
55
  return { ...downloadedPackage, ...local };
@@ -36,9 +36,11 @@ export interface ReleaseInfo {
36
36
  binaryPatchDownloadUrl?: string;
37
37
  /**
38
38
  * URLs of patch archives that also carry an asset diff, keyed by the packageHash of the
39
- * release each archive was diffed against. A client whose installed update matches one of
40
- * these keys downloads that archive instead of `binaryPatchDownloadUrl`; every other client
41
- * ignores this field. Only present when the release was published with asset diff archives.
39
+ * release each archive was diffed against. Give a key to every release worth diffing
40
+ * against: a client whose installed update matches one downloads that archive first, and
41
+ * every other client ignores this field. Only present when the release was published with
42
+ * asset diff archives. `UpdateArchiveResult` describes what a client does with one it
43
+ * cannot use.
42
44
  */
43
45
  diffPackages?: Record<string, string>;
44
46
  packageHash: string;
@@ -54,6 +56,13 @@ export interface UpdateCheckResponse {
54
56
  * that cannot use it downloads the full update from `download_url` instead.
55
57
  */
56
58
  binary_patch_download_url?: string;
59
+ /**
60
+ * The URL of the asset diff archive built against the update the client is running.
61
+ * It is only present when the release publishes a diff against exactly that update, and
62
+ * it accompanies `binary_patch_download_url` rather than replacing it: a response can
63
+ * carry both. `UpdateArchiveResult` describes the order the client tries them in.
64
+ */
65
+ asset_diff_download_url?: string;
57
66
  description?: string;
58
67
  is_available: boolean;
59
68
  is_disabled?: boolean;
@@ -71,7 +80,7 @@ export interface UpdateCheckResponse {
71
80
  * instead. These are the words every platform's applier reports, so a rollout can be
72
81
  * judged by them whichever platform it is running on.
73
82
  */
74
- export type BinaryPatchFallbackReason =
83
+ export type ArchiveFallbackReason =
75
84
  /** The bundle inside the app binary could not be opened or read. */
76
85
  | "base_bundle_unavailable"
77
86
  /** The bundle inside the app binary is not the one the patch was computed against. */
@@ -84,30 +93,88 @@ export type BinaryPatchFallbackReason =
84
93
  | "patch_apply_failed"
85
94
  /** The restored bundle is not the one the manifest promised. */
86
95
  | "target_verification_failed"
96
+ /** The asset diff could not be merged with the installed update it was built against. */
97
+ | "asset_merge_failed"
87
98
  /** The update restored from the patch did not pass the checks that follow the restore. */
88
99
  | "package_verification_failed";
89
100
 
90
101
  /**
91
- * How installing an update from its binary patch archive went.
102
+ * The archives on the patch path; the full archive is not one of them. There are two: the
103
+ * patch archive built against the app binary's bundle, and an asset diff archive that
104
+ * additionally leaves out the assets an installed base update already holds.
105
+ */
106
+ export type UpdateArchive = "binary-patch" | "asset-diff";
107
+
108
+ /**
109
+ * One archive tried on the patch path: which archive it was, how long the try took, and,
110
+ * when it was given up on, why.
111
+ */
112
+ export interface UpdateArchiveAttempt {
113
+ archive: UpdateArchive;
114
+
115
+ /**
116
+ * Why this archive was given up on. Absent for the attempt the downloaded update came
117
+ * from, and also when the attempt ended in an error none of the appliers has a word
118
+ * for.
119
+ */
120
+ fallbackReason?: ArchiveFallbackReason;
121
+
122
+ /**
123
+ * How long this attempt ran, in milliseconds, whichever way it ended: from the archive
124
+ * starting to download to the try being finished with.
125
+ */
126
+ durationMs: number;
127
+
128
+ /**
129
+ * How long the applier took to rebuild the bundle from this archive's patch, in
130
+ * milliseconds. Absent when the attempt ended before the bundle was restored.
131
+ */
132
+ applyDurationMs?: number;
133
+ }
134
+
135
+ /**
136
+ * Which of an update's patch archives the download came from, and what the patch path did
137
+ * on the way there. It is reported once the update has been downloaded and before the
138
+ * `LocalPackage` it resolved to is installed, so nothing here speaks to how installing goes.
92
139
  */
93
- export interface BinaryPatchResult {
140
+ export interface UpdateArchiveResult {
94
141
  /**
95
- * Whether the update was installed from its patch archive, or had to be downloaded in
96
- * full instead. A fallback is not an error: the update is installed either way.
142
+ * Whether one of the update's patch archives produced it, or the full archive had to be
143
+ * downloaded instead. A fallback is not an error: the update arrives either way.
97
144
  */
98
145
  status: "applied" | "fallback";
99
146
 
100
147
  /**
101
- * Why the full archive had to be downloaded. Absent when the patch was applied, and
102
- * also when the attempt ended in an error none of the appliers has a word for.
148
+ * The archive of the last attempt: the one the downloaded update came from when a patch
149
+ * archive produced it, and the last one given up on when none did.
150
+ */
151
+ archive: UpdateArchive;
152
+
153
+ /**
154
+ * Why the full archive had to be downloaded: the reason the last attempt ended in.
155
+ * Absent when a patch archive produced the update, and also when the attempt ended in
156
+ * an error none of the appliers has a word for.
103
157
  */
104
- fallbackReason?: BinaryPatchFallbackReason;
158
+ fallbackReason?: ArchiveFallbackReason;
105
159
 
106
160
  /**
107
- * How long the patch work took, in milliseconds: applying the patch when it was
108
- * applied, and the whole attempt when it was given up on.
161
+ * How long the whole patch path took, in milliseconds: from the first archive starting
162
+ * to download to the last attempt being finished with. The full download that follows a
163
+ * fallback is not part of it, because that is not time the patch path spent.
109
164
  */
110
- applyDurationMs: number;
165
+ totalDurationMs: number;
166
+
167
+ /**
168
+ * Every archive that was tried, in the order it was tried. The full archive is never
169
+ * among them, because it is downloaded only once the patch path has given up.
170
+ *
171
+ * Most downloads leave a single entry. A second one appears when the asset diff failed
172
+ * after its bundle was restored - an asset-side failure the patch archive is not
173
+ * implicated in - and the patch archive was tried in its place. A diff that fails before
174
+ * its bundle is restored skips the patch archive instead, because both archives carry the
175
+ * same bundle patch and it would fail the same way.
176
+ */
177
+ attempts: UpdateArchiveAttempt[];
111
178
  }
112
179
 
113
180
  export interface CodePushOptions extends SyncOptions {
@@ -250,8 +317,12 @@ export interface RemotePackage extends Package {
250
317
  * Downloads the available update from the CodePush service.
251
318
  *
252
319
  * @param downloadProgressCallback An optional callback that allows tracking the progress of the update while it is being downloaded.
320
+ * @param updateArchiveResultCallback An optional callback for observing which archive an update published with a binary patch was downloaded from. It is called once when the download had such an archive to try, and only after that download has succeeded; what it says nothing about is whether installing the resolved `LocalPackage` succeeds. An archive that could not be applied is reported here and the update is downloaded in full.
253
321
  */
254
- download(downloadProgressCallback?: DownloadProgressCallback): Promise<LocalPackage>;
322
+ download(
323
+ downloadProgressCallback?: DownloadProgressCallback,
324
+ updateArchiveResultCallback?: (result: UpdateArchiveResult) => void,
325
+ ): Promise<LocalPackage>;
255
326
 
256
327
  /**
257
328
  * The URL at which the package is available for download.
@@ -265,6 +336,15 @@ export interface RemotePackage extends Package {
265
336
  * package is downloaded in full from `downloadUrl`.
266
337
  */
267
338
  binaryPatchDownloadUrl?: string;
339
+
340
+ /**
341
+ * The URL of the asset diff archive built against the update this app is running.
342
+ * It is only present when the release publishes a diff against exactly that update, and
343
+ * it accompanies `binaryPatchDownloadUrl` rather than replacing it, so a package can hold
344
+ * both. `download` chooses between them and reports the choice as an
345
+ * `UpdateArchiveResult`.
346
+ */
347
+ assetDiffDownloadUrl?: string;
268
348
  }
269
349
 
270
350
  export interface SyncOptions {
@@ -297,8 +377,8 @@ export interface SyncOptions {
297
377
 
298
378
  /**
299
379
  * An "options" object used to determine whether a confirmation dialog should be displayed to the end user when an update is available,
300
- * and if so, what strings to use. Defaults to null, which has the effect of disabling the dialog completely. Setting this to any truthy
301
- * value will enable the dialog with the default strings, and passing an object to this parameter allows enabling the dialog as well as
380
+ * and if so, what strings to use. Defaults to null, which has the effect of disabling the dialog completely. Setting this to `true`
381
+ * will enable the dialog with the default strings, and passing an object to this parameter allows enabling the dialog as well as
302
382
  * overriding one or more of the default strings.
303
383
  */
304
384
  updateDialog?: UpdateDialog | true;
@@ -306,24 +386,25 @@ export interface SyncOptions {
306
386
  /**
307
387
  * The rollback retry mechanism allows the application to attempt to reinstall an update that was previously rolled back (with the restrictions
308
388
  * specified in the options). It is an "options" object used to determine whether a rollback retry should occur, and if so, what settings to use
309
- * for the rollback retry. This defaults to null, which has the effect of disabling the retry mechanism. Setting this to any truthy value will enable
310
- * the retry mechanism with the default settings, and passing an object to this parameter allows enabling the rollback retry as well as overriding
311
- * one or more of the default values.
389
+ * for the rollback retry. This defaults to null, which has the effect of disabling the retry mechanism. Passing an object to this parameter enables
390
+ * the rollback retry: an empty object retries with the default settings, and each value the object specifies overrides the default for that
391
+ * setting.
312
392
  */
313
393
  rollbackRetryOptions?: RollbackRetryOptions;
314
394
 
315
395
  /**
316
- * An optional callback for observing how an update published with a binary patch was
317
- * installed: whether it came from the patch archive, why it did not when it did not, and
318
- * how long the patch work took. It is called once per download that had a patch to try,
319
- * with the label of the release being installed.
396
+ * An optional callback for observing which archive an update published with a binary
397
+ * patch was downloaded from: whether it came from one of its patch archives, why each
398
+ * archive that was given up on was, and how long the patch work took. It is called once
399
+ * per download that had such an archive to try, after that download has succeeded and
400
+ * before the update is installed, with the label of the release it carries.
320
401
  *
321
402
  * Purely for observation. The library neither stores the result nor sends it anywhere -
322
403
  * an app that wants it in its telemetry sends it itself - and nothing about the update
323
404
  * depends on the callback: registering none changes nothing, and one that throws is
324
- * logged and does not fail the install.
405
+ * logged and does not fail the download it is reporting on.
325
406
  */
326
- onBinaryPatchResult?: (label: string, result: BinaryPatchResult) => void;
407
+ onUpdateArchiveResult?: (label: string, result: UpdateArchiveResult) => void;
327
408
 
328
409
  /**
329
410
  * Specifies whether to ignore the update if the installation fails.
@@ -583,13 +664,13 @@ declare namespace CodePush {
583
664
  RUNNING,
584
665
 
585
666
  /**
586
- * Indicates than an update has been installed, but the
667
+ * Indicates that an update has been installed, but the
587
668
  * app hasn't been restarted yet in order to apply it.
588
669
  */
589
670
  PENDING,
590
671
 
591
672
  /**
592
- * Indicates than an update represents the latest available
673
+ * Indicates that an update represents the latest available
593
674
  * release, and can be either currently running or pending.
594
675
  */
595
676
  LATEST
@@ -625,7 +706,7 @@ declare namespace CodePush {
625
706
  ON_APP_RESUME,
626
707
 
627
708
  /**
628
- * Don't automatically check for updates, but only do it when codePush.sync() is manully called inside app code.
709
+ * Don't automatically check for updates, but only do it when codePush.sync() is manually called inside app code.
629
710
  */
630
711
  MANUAL
631
712
  }