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

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 (52) 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/buildPatchToolsCommand/buildPatchTools.test.ts +118 -0
  17. package/cli/commands/buildPatchToolsCommand/buildPatchTools.ts +46 -0
  18. package/cli/commands/buildPatchToolsCommand/index.ts +44 -0
  19. package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
  20. package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
  21. package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
  22. package/cli/commands/releaseCommand/release.test.ts +3 -3
  23. package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
  24. package/cli/dist/commands/buildPatchToolsCommand/buildPatchTools.js +34 -0
  25. package/cli/dist/commands/buildPatchToolsCommand/buildPatchTools.test.js +88 -0
  26. package/cli/dist/commands/buildPatchToolsCommand/index.js +32 -0
  27. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
  28. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
  29. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
  30. package/cli/dist/commands/releaseCommand/release.test.js +3 -3
  31. package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
  32. package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
  33. package/cli/dist/index.js +4 -0
  34. package/cli/dist/utils/binaryPatch.js +8 -5
  35. package/cli/dist/utils/binaryPatch.test.js +1 -1
  36. package/cli/functions/stageReleaseHistoryFile.ts +38 -0
  37. package/cli/index.ts +5 -0
  38. package/cli/utils/binaryPatch.test.ts +1 -1
  39. package/cli/utils/binaryPatch.ts +8 -5
  40. package/ios/CodePush/CodePush.h +6 -3
  41. package/ios/CodePush/CodePush.mm +10 -10
  42. package/ios/CodePush/CodePushBinaryPatch.h +18 -8
  43. package/ios/CodePush/CodePushBinaryPatch.m +23 -22
  44. package/ios/CodePush/CodePushDownloadHandler.m +2 -1
  45. package/ios/CodePush/CodePushErrorUtils.m +55 -0
  46. package/ios/CodePush/CodePushPackage.m +242 -71
  47. package/package.json +2 -1
  48. package/src/CodePush.js +28 -18
  49. package/src/CodePush.test.js +110 -55
  50. package/src/package-mixins.js +19 -10
  51. package/typings/react-native-code-push.d.ts +110 -29
  52. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
@@ -10,15 +10,22 @@
10
10
 
11
11
  #pragma mark - Private constants
12
12
 
13
+ static NSString *const AssetDiffDownloadUrlKey = @"assetDiffDownloadUrl";
14
+ static NSString *const UpdateArchiveAssetDiff = @"asset-diff";
15
+ static NSString *const UpdateArchiveBinaryPatch = @"binary-patch";
13
16
  static NSString *const BinaryPatchDownloadUrlKey = @"binaryPatchDownloadUrl";
14
17
  static NSString *const BinaryPatchFolderName = @"binary-patch";
15
18
  // The fields of what a download that tried a patch reports back with. They are read by the
16
19
  // app that asked for the download and by nothing else, so they never reach a file.
17
- static NSString *const BinaryPatchResultApplyDurationMsKey = @"applyDurationMs";
18
- static NSString *const BinaryPatchResultFallbackReasonKey = @"fallbackReason";
19
- static NSString *const BinaryPatchResultStatusKey = @"status";
20
- static NSString *const BinaryPatchResultStatusApplied = @"applied";
21
- static NSString *const BinaryPatchResultStatusFallback = @"fallback";
20
+ static NSString *const UpdateArchiveAttemptApplyDurationMsKey = @"applyDurationMs";
21
+ static NSString *const UpdateArchiveAttemptDurationMsKey = @"durationMs";
22
+ static NSString *const UpdateArchiveResultArchiveKey = @"archive";
23
+ static NSString *const UpdateArchiveResultAttemptsKey = @"attempts";
24
+ static NSString *const UpdateArchiveResultFallbackReasonKey = @"fallbackReason";
25
+ static NSString *const UpdateArchiveResultStatusKey = @"status";
26
+ static NSString *const UpdateArchiveResultTotalDurationMsKey = @"totalDurationMs";
27
+ static NSString *const UpdateArchiveResultStatusApplied = @"applied";
28
+ static NSString *const UpdateArchiveResultStatusFallback = @"fallback";
22
29
  static NSString *const DiffManifestFileName = @"hotcodepush.json";
23
30
  static NSString *const DownloadFileName = @"download.zip";
24
31
  static NSString *const DownloadUrlKey = @"downloadUrl";
@@ -58,10 +65,28 @@ static NSString *const UnzippedFolderName = @"unzipped";
58
65
  expectedBundleFileName:(NSString *)expectedBundleFileName
59
66
  operationQueue:(dispatch_queue_t)operationQueue
60
67
  progressCallback:(void (^)(long long, long long))progressCallback
61
- doneCallback:(void (^)(NSDictionary *binaryPatchResult))doneCallback
68
+ doneCallback:(void (^)(NSDictionary *updateArchiveResult))doneCallback
62
69
  failCallback:(void (^)(NSError *err))failCallback
63
70
  {
64
- void (^downloadFullPackage)(NSDictionary *) = ^(NSDictionary *binaryPatchResult) {
71
+ // A release that was published with a binary patch offers up to three archives of the
72
+ // same update. The asset diff is the smallest and is tried first, the patch archive
73
+ // stands in when the diff fails on its asset side, and the full archive is always
74
+ // there when none of it works out.
75
+ NSMutableArray<NSDictionary *> *archivesToTry = [NSMutableArray array];
76
+ NSString *assetDiffDownloadUrl = updatePackage[AssetDiffDownloadUrlKey];
77
+ if ([assetDiffDownloadUrl isKindOfClass:[NSString class]] && [assetDiffDownloadUrl length] > 0) {
78
+ [archivesToTry addObject:@{ UpdateArchiveResultArchiveKey: UpdateArchiveAssetDiff,
79
+ DownloadUrlKey: assetDiffDownloadUrl }];
80
+ }
81
+
82
+ NSString *binaryPatchDownloadUrl = updatePackage[BinaryPatchDownloadUrlKey];
83
+ if ([binaryPatchDownloadUrl isKindOfClass:[NSString class]] && [binaryPatchDownloadUrl length] > 0) {
84
+ [archivesToTry addObject:@{ UpdateArchiveResultArchiveKey: UpdateArchiveBinaryPatch,
85
+ DownloadUrlKey: binaryPatchDownloadUrl }];
86
+ }
87
+
88
+ if ([archivesToTry count] == 0) {
89
+ // A download that never had a patch to try has nothing to report about one.
65
90
  [self downloadAndInstallPackage:updatePackage
66
91
  expectedBundleFileName:expectedBundleFileName
67
92
  operationQueue:operationQueue
@@ -69,94 +94,211 @@ static NSString *const UnzippedFolderName = @"unzipped";
69
94
  isBinaryPatchUpdate:NO
70
95
  progressCallback:progressCallback
71
96
  doneCallback:^{
72
- doneCallback(binaryPatchResult);
97
+ doneCallback(nil);
73
98
  }
74
99
  failCallback:failCallback
75
100
  patchAppliedCallback:nil
76
101
  patchFallbackCallback:nil];
77
- };
78
-
79
- // A release that was published with a binary patch offers two archives of the same
80
- // update. The patch is worth trying because it is a fraction of the size, and the
81
- // full archive is always there when it does not work out.
82
- NSString *binaryPatchDownloadUrl = updatePackage[BinaryPatchDownloadUrlKey];
83
- if (![binaryPatchDownloadUrl isKindOfClass:[NSString class]] || [binaryPatchDownloadUrl length] == 0) {
84
- // A download that never had a patch to try has nothing to report about one.
85
- return downloadFullPackage(nil);
102
+ return;
86
103
  }
87
104
 
88
- // Every way the patch can fail ends the same way, with the update being downloaded in
89
- // full instead, so none of them reaches the caller as an error. The fallback happens
90
- // exactly once without anything having to count it: the full archive is downloaded by
91
- // a call that is not allowed to take the patch path, so it has no failure of its own
92
- // to fall back from.
93
- NSDate *patchAttemptStartTime = [NSDate date];
105
+ [self tryNextArchive:archivesToTry
106
+ attemptsSoFar:[NSMutableArray array]
107
+ firstAttemptStartTime:[NSDate date]
108
+ updatePackage:updatePackage
109
+ expectedBundleFileName:expectedBundleFileName
110
+ operationQueue:operationQueue
111
+ progressCallback:progressCallback
112
+ doneCallback:doneCallback
113
+ failCallback:failCallback];
114
+ }
115
+
116
+ /*
117
+ * Tries the first archive of the queue, and decides what a failure of it means for the
118
+ * rest. A failure after the bundle was restored is on the asset side of the archive, so
119
+ * the next archive - which does not share it - is worth trying, and so is one the server
120
+ * never served, which is a verdict on one URL rather than on the archives at the others.
121
+ * A failure between those is in the bundle patch every archive carries byte for byte, or
122
+ * is something no verdict exists for, and either way the remaining archives are passed
123
+ * over: they could only fail the same way, and trying them would put more doomed downloads
124
+ * in front of the full one.
125
+ *
126
+ * Every verdict on an archive ends with the update installed - by an archive of the queue
127
+ * or by the full download behind it - so no verdict reaches the caller as an error, and the
128
+ * result retells the attempts one by one. A network that did not carry the archive is not a
129
+ * verdict on it: it is raised, because the full download is behind the same network.
130
+ */
131
+ + (void)tryNextArchive:(NSArray<NSDictionary *> *)archivesToTry
132
+ attemptsSoFar:(NSMutableArray<NSDictionary *> *)attempts
133
+ firstAttemptStartTime:(NSDate *)firstAttemptStartTime
134
+ updatePackage:(NSDictionary *)updatePackage
135
+ expectedBundleFileName:(NSString *)expectedBundleFileName
136
+ operationQueue:(dispatch_queue_t)operationQueue
137
+ progressCallback:(void (^)(long long, long long))progressCallback
138
+ doneCallback:(void (^)(NSDictionary *updateArchiveResult))doneCallback
139
+ failCallback:(void (^)(NSError *err))failCallback
140
+ {
141
+ NSString *archive = archivesToTry[0][UpdateArchiveResultArchiveKey];
142
+ NSArray<NSDictionary *> *remainingArchives = [archivesToTry subarrayWithRange:NSMakeRange(1, [archivesToTry count] - 1)];
143
+ NSDate *attemptStartTime = [NSDate date];
144
+
94
145
  // Set once the applier has restored the bundle, which is also what tells a failure
95
146
  // that follows apart from one that came before.
96
147
  __block NSNumber *applyDurationMs = nil;
148
+ // Set when the server answered this archive's URL with a status instead of the archive,
149
+ // which is a verdict on that URL and not on the archives at the others.
150
+ __block BOOL archiveWasNotServed = NO;
151
+
152
+ void (^giveUpAttempt)(NSString *failureReason) = ^(NSString *failureReason) {
153
+ [self deleteBinaryPatchFolder];
154
+ [attempts addObject:[self archiveAttemptEntry:archive
155
+ failureReason:failureReason
156
+ applyDurationMs:applyDurationMs
157
+ attemptStartTime:attemptStartTime]];
158
+
159
+ if ((applyDurationMs != nil || archiveWasNotServed) && [remainingArchives count] > 0) {
160
+ [self tryNextArchive:remainingArchives
161
+ attemptsSoFar:attempts
162
+ firstAttemptStartTime:firstAttemptStartTime
163
+ updatePackage:updatePackage
164
+ expectedBundleFileName:expectedBundleFileName
165
+ operationQueue:operationQueue
166
+ progressCallback:progressCallback
167
+ doneCallback:doneCallback
168
+ failCallback:failCallback];
169
+ return;
170
+ }
171
+
172
+ // Timed before the full download starts, because that is not time the patch
173
+ // path spent.
174
+ NSDictionary *fallbackResult = [self archiveFallbackResult:failureReason
175
+ archive:archive
176
+ attempts:attempts
177
+ firstAttemptStartTime:firstAttemptStartTime];
178
+ [self downloadAndInstallPackage:updatePackage
179
+ expectedBundleFileName:expectedBundleFileName
180
+ operationQueue:operationQueue
181
+ downloadUrl:updatePackage[DownloadUrlKey]
182
+ isBinaryPatchUpdate:NO
183
+ progressCallback:progressCallback
184
+ doneCallback:^{
185
+ doneCallback(fallbackResult);
186
+ }
187
+ failCallback:failCallback
188
+ patchAppliedCallback:nil
189
+ patchFallbackCallback:nil];
190
+ };
191
+
97
192
  [self downloadAndInstallPackage:updatePackage
98
193
  expectedBundleFileName:expectedBundleFileName
99
194
  operationQueue:operationQueue
100
- downloadUrl:binaryPatchDownloadUrl
195
+ downloadUrl:archivesToTry[0][DownloadUrlKey]
101
196
  isBinaryPatchUpdate:YES
102
197
  progressCallback:progressCallback
103
198
  doneCallback:^{
104
199
  [self deleteBinaryPatchFolder];
105
- doneCallback([self appliedBinaryPatchResult:applyDurationMs]);
200
+ [attempts addObject:[self archiveAttemptEntry:archive
201
+ failureReason:nil
202
+ applyDurationMs:applyDurationMs
203
+ attemptStartTime:attemptStartTime]];
204
+ doneCallback([self appliedArchiveResult:archive
205
+ attempts:attempts
206
+ firstAttemptStartTime:firstAttemptStartTime]);
106
207
  }
107
208
  failCallback:^(NSError *err) {
108
- [self deleteBinaryPatchFolder];
109
- CPLog(@"The binary patch update could not be completed (%@). Downloading the full update instead.", err.localizedDescription);
209
+ if ([CodePushErrorUtils isNetworkFailure:err]) {
210
+ // The network is what failed, not the archive, and the full
211
+ // archive is behind the same network - only larger, and
212
+ // started over from nothing. Falling back here would spend a
213
+ // second download to reach the failure already in hand.
214
+ CPLog(@"The %@ archive could not be downloaded (%@). Giving up on the download.",
215
+ archive, err.localizedDescription);
216
+ [self deleteBinaryPatchFolder];
217
+ failCallback(err);
218
+ return;
219
+ }
220
+
221
+ archiveWasNotServed = [CodePushErrorUtils isHttpStatusError:err];
222
+ CPLog(@"The %@ archive could not be applied (%@). Falling back.", archive, err.localizedDescription);
110
223
  // An error raised after the bundle was restored is the restored
111
224
  // update failing the checks every update passes before it is
112
225
  // installed. Before that point the appliers have no word for what
113
- // happened - the patch archive not being downloadable, say - and
226
+ // happened - the archive not being downloadable, say - and
114
227
  // inventing one here would put a value on the wire that no
115
228
  // platform reports, so the fallback is reported without a reason.
116
- NSString *failureReason = applyDurationMs == nil
229
+ giveUpAttempt(applyDurationMs == nil
117
230
  ? nil
118
- : CodePushBinaryPatchReasonPackageVerificationFailed;
119
- downloadFullPackage([self binaryPatchFallbackResult:failureReason
120
- attemptStartTime:patchAttemptStartTime]);
231
+ : CodePushArchiveFallbackReasonPackageVerificationFailed);
121
232
  }
122
233
  patchAppliedCallback:^(double durationMs) {
123
234
  applyDurationMs = @(durationMs);
124
235
  }
125
236
  patchFallbackCallback:^(NSString *failureReason) {
126
- [self deleteBinaryPatchFolder];
127
- CPLog(@"Binary patch update failed (%@). Downloading the full update instead.", failureReason);
128
- downloadFullPackage([self binaryPatchFallbackResult:failureReason
129
- attemptStartTime:patchAttemptStartTime]);
237
+ CPLog(@"The %@ archive failed (%@). Falling back.", archive, failureReason);
238
+ giveUpAttempt(failureReason);
130
239
  }];
131
240
  }
132
241
 
133
242
  /*
134
- * The result of a patch attempt the update was installed from, timed over the apply itself.
243
+ * One archive the ladder tried: which archive it was, why it was given up on when it was,
244
+ * how long the try ran whichever way it ended, and how long its apply took when it got that
245
+ * far.
135
246
  */
136
- + (NSDictionary *)appliedBinaryPatchResult:(NSNumber *)applyDurationMs
247
+ + (NSDictionary *)archiveAttemptEntry:(NSString *)archive
248
+ failureReason:(NSString *)failureReason
249
+ applyDurationMs:(NSNumber *)applyDurationMs
250
+ attemptStartTime:(NSDate *)attemptStartTime
251
+ {
252
+ NSMutableDictionary *entry = [NSMutableDictionary dictionaryWithDictionary:@{
253
+ UpdateArchiveResultArchiveKey: archive,
254
+ UpdateArchiveAttemptDurationMsKey: @(round([[NSDate date] timeIntervalSinceDate:attemptStartTime] * 1000)),
255
+ }];
256
+ if (failureReason) {
257
+ entry[UpdateArchiveResultFallbackReasonKey] = failureReason;
258
+ }
259
+ // An attempt that never restored the bundle has no apply to report.
260
+ if (applyDurationMs) {
261
+ entry[UpdateArchiveAttemptApplyDurationMsKey] = applyDurationMs;
262
+ }
263
+
264
+ return entry;
265
+ }
266
+
267
+ /*
268
+ * The result of an archive the update was installed from, timed over the whole path.
269
+ */
270
+ + (NSDictionary *)appliedArchiveResult:(NSString *)archive
271
+ attempts:(NSArray<NSDictionary *> *)attempts
272
+ firstAttemptStartTime:(NSDate *)firstAttemptStartTime
137
273
  {
138
274
  return @{
139
- BinaryPatchResultStatusKey: BinaryPatchResultStatusApplied,
140
- BinaryPatchResultApplyDurationMsKey: applyDurationMs ?: @0,
275
+ UpdateArchiveResultStatusKey: UpdateArchiveResultStatusApplied,
276
+ UpdateArchiveResultArchiveKey: archive,
277
+ UpdateArchiveResultTotalDurationMsKey: @(round([[NSDate date] timeIntervalSinceDate:firstAttemptStartTime] * 1000)),
278
+ UpdateArchiveResultAttemptsKey: [attempts copy],
141
279
  };
142
280
  }
143
281
 
144
282
  /*
145
- * The result of a patch attempt the update had to be downloaded in full after.
283
+ * The result of a patch path the update had to be downloaded in full after.
146
284
  *
147
- * There is no completed apply to time here, so the attempt itself is what is timed: from the
148
- * patch archive being asked for to the moment the attempt was given up on. The reason is left
149
- * out when the attempt ended in an error none of the appliers has a word for.
285
+ * There is no completed apply to time here, so the path itself is what is timed: from the
286
+ * first archive being asked for to the moment the last one was given up on. The reason is
287
+ * left out when the last attempt ended in an error none of the appliers has a word for.
150
288
  */
151
- + (NSDictionary *)binaryPatchFallbackResult:(NSString *)failureReason
152
- attemptStartTime:(NSDate *)attemptStartTime
289
+ + (NSDictionary *)archiveFallbackResult:(NSString *)failureReason
290
+ archive:(NSString *)archive
291
+ attempts:(NSArray<NSDictionary *> *)attempts
292
+ firstAttemptStartTime:(NSDate *)firstAttemptStartTime
153
293
  {
154
294
  NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
155
- BinaryPatchResultStatusKey: BinaryPatchResultStatusFallback,
156
- BinaryPatchResultApplyDurationMsKey: @(round([[NSDate date] timeIntervalSinceDate:attemptStartTime] * 1000)),
295
+ UpdateArchiveResultStatusKey: UpdateArchiveResultStatusFallback,
296
+ UpdateArchiveResultArchiveKey: archive,
297
+ UpdateArchiveResultTotalDurationMsKey: @(round([[NSDate date] timeIntervalSinceDate:firstAttemptStartTime] * 1000)),
298
+ UpdateArchiveResultAttemptsKey: [attempts copy],
157
299
  }];
158
300
  if (failureReason) {
159
- result[BinaryPatchResultFallbackReasonKey] = failureReason;
301
+ result[UpdateArchiveResultFallbackReasonKey] = failureReason;
160
302
  }
161
303
 
162
304
  return result;
@@ -166,17 +308,17 @@ static NSString *const UnzippedFolderName = @"unzipped";
166
308
  * Downloads an update from one of its archives and installs it.
167
309
  *
168
310
  * isBinaryPatchUpdate says whether the archive holds a binary patch of the JS bundle,
169
- * which has to be applied before the contents are the update. Only an archive downloaded
170
- * from the binary patch URL is treated that way, so an update being downloaded in full
171
- * can never end up on the patch path.
311
+ * which has to be applied before the contents are the update. Both the asset diff and the
312
+ * patch archive are downloaded that way; the full archive never is, so an update being
313
+ * downloaded in full can never end up on the patch path.
172
314
  *
173
315
  * patchAppliedCallback is called with how long the apply took the moment the bundle has
174
316
  * been restored, which is before the update is known to install. It is nil for a full
175
317
  * download, which has no patch to apply.
176
318
  *
177
319
  * patchFallbackCallback is called instead of doneCallback when the patch cannot be
178
- * applied: the update was not installed, and the caller has to download the full archive.
179
- * It is nil for a full download, which has nothing to fall back to.
320
+ * applied: the update was not installed, and the caller has to move on to the next
321
+ * archive. It is nil for a full download, which has nothing to fall back to.
180
322
  */
181
323
  + (void)downloadAndInstallPackage:(NSDictionary *)updatePackage
182
324
  expectedBundleFileName:(NSString *)expectedBundleFileName
@@ -270,13 +412,23 @@ static NSString *const UnzippedFolderName = @"unzipped";
270
412
  BOOL isDiffUpdate = [[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath];
271
413
 
272
414
  if (isDiffUpdate) {
415
+ // A merge that cannot complete has a word of its own, because it says
416
+ // the diff went wrong on its asset side - the one failure the patch
417
+ // archive, which carries every asset, is not implicated in. A diff
418
+ // manifest served outside the patch path keeps failing as the error
419
+ // it always was.
420
+ void (^mergeFailCallback)(NSError *) = !isBinaryPatchUpdate ? failCallback : ^(NSError *mergeError) {
421
+ CPLog(@"The asset diff could not be merged (%@).", mergeError.localizedDescription);
422
+ patchFallbackCallback(CodePushArchiveFallbackReasonAssetMergeFailed);
423
+ };
424
+
273
425
  // Copy the current package to the new package.
274
426
  NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error];
275
427
  if (error) {
276
- failCallback(error);
428
+ mergeFailCallback(error);
277
429
  return;
278
430
  }
279
-
431
+
280
432
  if (currentPackageFolderPath == nil) {
281
433
  // Currently running the binary version, copy files from the bundled resources
282
434
  NSString *newUpdateCodePushPath = [newUpdateFolderPath stringByAppendingPathComponent:[CodePushUpdateUtils manifestFolderPrefix]];
@@ -285,23 +437,23 @@ static NSString *const UnzippedFolderName = @"unzipped";
285
437
  attributes:nil
286
438
  error:&error];
287
439
  if (error) {
288
- failCallback(error);
440
+ mergeFailCallback(error);
289
441
  return;
290
442
  }
291
-
443
+
292
444
  [[NSFileManager defaultManager] copyItemAtPath:[CodePush bundleAssetsPath]
293
445
  toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[CodePushUpdateUtils assetsFolderName]]
294
446
  error:&error];
295
447
  if (error) {
296
- failCallback(error);
448
+ mergeFailCallback(error);
297
449
  return;
298
450
  }
299
-
451
+
300
452
  [[NSFileManager defaultManager] copyItemAtPath:[[CodePush binaryBundleURL] path]
301
453
  toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[[CodePush binaryBundleURL] lastPathComponent]]
302
454
  error:&error];
303
455
  if (error) {
304
- failCallback(error);
456
+ mergeFailCallback(error);
305
457
  return;
306
458
  }
307
459
  } else {
@@ -309,25 +461,44 @@ static NSString *const UnzippedFolderName = @"unzipped";
309
461
  toPath:newUpdateFolderPath
310
462
  error:&error];
311
463
  if (error) {
312
- failCallback(error);
464
+ mergeFailCallback(error);
313
465
  return;
314
466
  }
315
467
  }
316
-
468
+
317
469
  // Delete files mentioned in the manifest.
318
470
  NSString *manifestContent = [NSString stringWithContentsOfFile:diffManifestFilePath
319
471
  encoding:NSUTF8StringEncoding
320
472
  error:&error];
321
473
  if (error) {
322
- failCallback(error);
474
+ mergeFailCallback(error);
323
475
  return;
324
476
  }
325
-
477
+
326
478
  NSData *data = [manifestContent dataUsingEncoding:NSUTF8StringEncoding];
327
479
  NSDictionary *manifestJSON = [NSJSONSerialization JSONObjectWithData:data
328
480
  options:kNilOptions
329
481
  error:&error];
330
- NSArray *deletedFiles = manifestJSON[@"deletedFiles"];
482
+ if (error) {
483
+ mergeFailCallback(error);
484
+ return;
485
+ }
486
+
487
+ // A manifest that does not name the files to delete is not a manifest
488
+ // with nothing to delete - the CLI writes the key on every release, an
489
+ // empty list included. Iterating what is not there would skip every
490
+ // deletion and merge on, so it is refused here, the way the other
491
+ // platform's merge refuses it, for the two platforms to report one
492
+ // reason for one state.
493
+ NSArray *deletedFiles = [manifestJSON isKindOfClass:[NSDictionary class]]
494
+ ? manifestJSON[@"deletedFiles"]
495
+ : nil;
496
+ if (![deletedFiles isKindOfClass:[NSArray class]]) {
497
+ error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"The asset diff manifest at %@ does not name the files to delete.", diffManifestFilePath]];
498
+ mergeFailCallback(error);
499
+ return;
500
+ }
501
+
331
502
  for (NSString *deletedFileName in deletedFiles) {
332
503
  // The manifest is untrusted input, so an entry that reaches outside the
333
504
  // package folder is ignored rather than followed. Whether skipping it
@@ -338,16 +509,16 @@ static NSString *const UnzippedFolderName = @"unzipped";
338
509
  [[NSFileManager defaultManager] removeItemAtPath:absoluteDeletedFilePath
339
510
  error:&error];
340
511
  if (error) {
341
- failCallback(error);
512
+ mergeFailCallback(error);
342
513
  return;
343
514
  }
344
515
  }
345
516
  }
346
-
517
+
347
518
  [[NSFileManager defaultManager] removeItemAtPath:diffManifestFilePath
348
519
  error:&error];
349
520
  if (error) {
350
- failCallback(error);
521
+ mergeFailCallback(error);
351
522
  return;
352
523
  }
353
524
  }
@@ -414,11 +585,11 @@ static NSString *const UnzippedFolderName = @"unzipped";
414
585
 
415
586
  } else {
416
587
  if (isBinaryPatchUpdate) {
417
- // Whatever the patch URL served, it is not a patch archive - an error page
588
+ // Whatever the archive URL served, it is not an update archive - an error page
418
589
  // answered with a 200 looks like this too. Moving it into place would
419
590
  // install bytes no hash has ever been checked against, so the full archive
420
591
  // is downloaded instead.
421
- patchFallbackCallback(CodePushBinaryPatchReasonInvalidManifest);
592
+ patchFallbackCallback(CodePushArchiveFallbackReasonInvalidManifest);
422
593
  return;
423
594
  }
424
595
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.4",
3
+ "version": "13.1.0-beta.6",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
@@ -56,6 +56,7 @@
56
56
  "package.json",
57
57
  "THIRD-PARTY-NOTICES",
58
58
  "!ios/build",
59
+ "!ios/CodePushTests",
59
60
  "!android/build",
60
61
  "!android/app/build",
61
62
  "!android/gradle",
package/src/CodePush.js CHANGED
@@ -189,9 +189,16 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
189
189
  download_url: latestReleaseInfo.downloadUrl,
190
190
  /**
191
191
  * Only released updates that were published with a binary patch carry this.
192
- * When it is missing, the update is downloaded in full from `download_url`.
192
+ * A release offering neither this nor a diff is downloaded in full from
193
+ * `download_url`.
193
194
  */
194
- binary_patch_download_url: diffPackageDownloadUrl || latestReleaseInfo.binaryPatchDownloadUrl,
195
+ binary_patch_download_url: latestReleaseInfo.binaryPatchDownloadUrl,
196
+ /**
197
+ * The diff stands next to the patch url rather than in its place, so that the
198
+ * native side can try the diff first and still hold the patch archive to fall
199
+ * back on when the diff fails on its asset side.
200
+ */
201
+ asset_diff_download_url: diffPackageDownloadUrl,
195
202
  // (`enabled` will always be true in the release information obtained from the previous process.)
196
203
  is_available: latestReleaseInfo.enabled,
197
204
  package_hash: latestReleaseInfo.packageHash,
@@ -293,11 +300,14 @@ function mapToRemotePackageMetadata(updateInfo) {
293
300
  packageHash: updateInfo.package_hash ?? '',
294
301
  packageSize: updateInfo.package_size ?? 0,
295
302
  downloadUrl: updateInfo.download_url ?? '',
296
- // The field stays out of the package unless the update really has a binary patch,
303
+ // The fields stay out of the package unless the update really has these archives,
297
304
  // so that the native side sees exactly what it saw before patches existed.
298
305
  ...(updateInfo.binary_patch_download_url
299
306
  ? { binaryPatchDownloadUrl: updateInfo.binary_patch_download_url }
300
307
  : {}),
308
+ ...(updateInfo.asset_diff_download_url
309
+ ? { assetDiffDownloadUrl: updateInfo.asset_diff_download_url }
310
+ : {}),
301
311
  };
302
312
  }
303
313
 
@@ -551,20 +561,20 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
551
561
  updateDialog: null,
552
562
  // The callback registered on the decorator is the default for every sync, including a
553
563
  // manual one; a callback passed to this call overrides it for this call.
554
- onBinaryPatchResult: sharedCodePushOptions.onBinaryPatchResult ?? null,
564
+ onUpdateArchiveResult: sharedCodePushOptions.onUpdateArchiveResult ?? null,
555
565
  ...options,
556
566
  };
557
567
 
558
568
  /*
559
- * A callback an app registered to observe how a binary patch went must not be able to
560
- * cost it an update, so it is isolated the way the other sync callbacks are: whatever it
561
- * throws is logged and the install carries on. An app that registered none leaves the
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
562
572
  * download exactly as it was before this option existed.
563
573
  */
564
- const onBinaryPatchResult = typeof syncOptions.onBinaryPatchResult === "function"
574
+ const onUpdateArchiveResult = typeof syncOptions.onUpdateArchiveResult === "function"
565
575
  ? (label, result) => {
566
576
  try {
567
- syncOptions.onBinaryPatchResult(label, result);
577
+ syncOptions.onUpdateArchiveResult(label, result);
568
578
  } catch (error) {
569
579
  log(`An error has occurred : ${error.stack}`);
570
580
  }
@@ -624,7 +634,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
624
634
 
625
635
  const localPackage = await remotePackage.download(
626
636
  downloadProgressCallback,
627
- onBinaryPatchResult && ((result) => onBinaryPatchResult(remotePackageLabel, result)),
637
+ onUpdateArchiveResult && ((result) => onUpdateArchiveResult(remotePackageLabel, result)),
628
638
  );
629
639
 
630
640
  sharedCodePushOptions.onDownloadSuccess?.(remotePackageLabel);
@@ -760,8 +770,8 @@ let CodePush;
760
770
  * onRolloutSkipped: (label: string, error: Error) => void | undefined,
761
771
  * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
762
772
  *
763
- * onBinaryPatchResult: (label: string, result: object) => void | undefined,
764
- * setOnBinaryPatchResult(onBinaryPatchResultFunction: (label: string, result: object) => void | undefined): void,
773
+ * onUpdateArchiveResult: (label: string, result: object) => void | undefined,
774
+ * setOnUpdateArchiveResult(onUpdateArchiveResultFunction: (label: string, result: object) => void | undefined): void,
765
775
  * }}
766
776
  */
767
777
  const sharedCodePushOptions = {
@@ -812,11 +822,11 @@ const sharedCodePushOptions = {
812
822
  if (typeof onRolloutSkippedFunction !== 'function') throw new Error('Please pass a function to onRolloutSkipped');
813
823
  this.onRolloutSkipped = onRolloutSkippedFunction;
814
824
  },
815
- onBinaryPatchResult: undefined,
816
- setOnBinaryPatchResult(onBinaryPatchResultFunction) {
817
- if (!onBinaryPatchResultFunction) return;
818
- if (typeof onBinaryPatchResultFunction !== 'function') throw new Error('Please pass a function to onBinaryPatchResult');
819
- this.onBinaryPatchResult = onBinaryPatchResultFunction;
825
+ onUpdateArchiveResult: undefined,
826
+ setOnUpdateArchiveResult(onUpdateArchiveResultFunction) {
827
+ if (!onUpdateArchiveResultFunction) return;
828
+ if (typeof onUpdateArchiveResultFunction !== 'function') throw new Error('Please pass a function to onUpdateArchiveResult');
829
+ this.onUpdateArchiveResult = onUpdateArchiveResultFunction;
820
830
  },
821
831
  }
822
832
 
@@ -854,7 +864,7 @@ function codePushify(options = {}) {
854
864
  sharedCodePushOptions.setOnDownloadSuccess(options.onDownloadSuccess);
855
865
  sharedCodePushOptions.setOnSyncError(options.onSyncError);
856
866
  sharedCodePushOptions.setOnRolloutSkipped(options.onRolloutSkipped);
857
- sharedCodePushOptions.setOnBinaryPatchResult(options.onBinaryPatchResult);
867
+ sharedCodePushOptions.setOnUpdateArchiveResult(options.onUpdateArchiveResult);
858
868
 
859
869
  const decorator = (RootComponent) => {
860
870
  class CodePushComponent extends React.Component {