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

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/README.md CHANGED
@@ -304,8 +304,10 @@ Please refer to the [CodePushOptions](https://github.com/Soomgo-Mobile/react-nat
304
304
  A `"fallback"` is not a failed update: the update is downloaded in full instead and installed as usual, so the result
305
305
  is there to be observed and nothing more. The library neither stores it nor sends it anywhere - an app that wants it in
306
306
  its telemetry sends it itself. A callback that throws never fails the update - the error is only logged to the console.
307
+ A fallback also shows in `downloadProgressCallback`: the full download reports a second progress stream that counts
308
+ `receivedBytes` from zero again, against its own, larger `totalBytes`.
307
309
 
308
- Register it on `CodePush({ ... })` like the callbacks above and it covers every sync, whatever the `checkFrequency` is
310
+ Register `onBinaryPatchResult` on `CodePush({ ... })` like the callbacks above and it covers every sync, whatever the `checkFrequency` is
309
311
  and including the `CodePush.sync()` calls you make yourself.
310
312
 
311
313
  ```typescript
@@ -338,7 +338,6 @@ public class CodePushNativeModule extends NativeCodePushSpec {
338
338
  JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
339
339
  JSONObject binaryPatchResult = mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
340
340
  private volatile boolean hasScheduledNextFrame = false;
341
- private volatile DownloadProgress latestDownloadProgress = null;
342
341
  private final DownloadProgressEventDispatcher progressEventDispatcher = new DownloadProgressEventDispatcher(
343
342
  new DownloadProgressEventDispatcher.EventEmitter() {
344
343
  @Override
@@ -347,16 +346,21 @@ public class CodePushNativeModule extends NativeCodePushSpec {
347
346
  }
348
347
  });
349
348
 
349
+ @Override
350
+ public void onDownloadStart() {
351
+ progressEventDispatcher.reset();
352
+ }
353
+
350
354
  @Override
351
355
  public void call(DownloadProgress downloadProgress) {
352
356
  if (!notifyProgress || downloadProgress == null) {
353
357
  return;
354
358
  }
355
359
 
356
- latestDownloadProgress = downloadProgress;
360
+ progressEventDispatcher.record(downloadProgress);
357
361
  // If the download is completed, synchronously send the last event.
358
362
  if (downloadProgress.isCompleted()) {
359
- progressEventDispatcher.dispatch(downloadProgress);
363
+ progressEventDispatcher.dispatchLatest();
360
364
  return;
361
365
  }
362
366
 
@@ -372,7 +376,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
372
376
  @Override
373
377
  public void doFrame(long frameTimeNanos) {
374
378
  try {
375
- progressEventDispatcher.dispatch(latestDownloadProgress);
379
+ progressEventDispatcher.dispatchLatest();
376
380
  } finally {
377
381
  hasScheduledNextFrame = false;
378
382
  }
@@ -275,9 +275,18 @@ public class CodePushUpdateManager {
275
275
  connection.setRequestProperty("Accept-Encoding", "identity");
276
276
  bin = new BufferedInputStream(connection.getInputStream());
277
277
 
278
+ // Announced only once the response is flowing, so a connection that fails to
279
+ // open never announces an empty stream.
280
+ progressCallback.onDownloadStart();
278
281
  long totalBytes = connection.getContentLength();
279
282
  long receivedBytes = 0;
280
283
 
284
+ if (totalBytes > 0) {
285
+ // The zero-received event hands the listener this download's total up front,
286
+ // so a fallback download shows as restarting rather than running backwards.
287
+ progressCallback.call(new DownloadProgress(totalBytes, 0));
288
+ }
289
+
281
290
  File downloadFolder = new File(getCodePushPath());
282
291
  downloadFolder.mkdirs();
283
292
  downloadFile = new File(downloadFolder, CodePushConstants.DOWNLOAD_FILE_NAME);
@@ -1,5 +1,12 @@
1
1
  package com.microsoft.codepush.react;
2
2
 
3
3
  interface DownloadProgressCallback {
4
+ /**
5
+ * A download is starting its own progress stream: received bytes count from zero again,
6
+ * against this download's own total. A binary patch fallback makes this happen twice
7
+ * within one downloaded update.
8
+ */
9
+ void onDownloadStart();
10
+
4
11
  void call(DownloadProgress downloadProgress);
5
12
  }
@@ -1,23 +1,44 @@
1
1
  package com.microsoft.codepush.react;
2
2
 
3
+ /**
4
+ * Serializes progress events from the download thread and the UI frame callback into one
5
+ * monotonic stream per download: whatever order the two threads dispatch in, the JS side
6
+ * never sees the same download run backwards. The latest progress lives here rather than
7
+ * with the caller so that reading it and dispatching it happen under one lock.
8
+ */
3
9
  class DownloadProgressEventDispatcher {
4
10
  interface EventEmitter {
5
11
  void emit(DownloadProgress downloadProgress);
6
12
  }
7
13
 
8
14
  private final EventEmitter mEventEmitter;
15
+ private DownloadProgress mLatestDownloadProgress = null;
9
16
  private long mLastDispatchedBytes = -1;
10
17
 
11
18
  DownloadProgressEventDispatcher(EventEmitter eventEmitter) {
12
19
  mEventEmitter = eventEmitter;
13
20
  }
14
21
 
15
- synchronized void dispatch(DownloadProgress downloadProgress) {
16
- if (downloadProgress == null || downloadProgress.getReceivedBytes() <= mLastDispatchedBytes) {
22
+ synchronized void record(DownloadProgress downloadProgress) {
23
+ mLatestDownloadProgress = downloadProgress;
24
+ }
25
+
26
+ synchronized void dispatchLatest() {
27
+ if (mLatestDownloadProgress == null || mLatestDownloadProgress.getReceivedBytes() <= mLastDispatchedBytes) {
17
28
  return;
18
29
  }
19
30
 
20
- mEventEmitter.emit(downloadProgress);
21
- mLastDispatchedBytes = downloadProgress.getReceivedBytes();
31
+ mEventEmitter.emit(mLatestDownloadProgress);
32
+ mLastDispatchedBytes = mLatestDownloadProgress.getReceivedBytes();
33
+ }
34
+
35
+ /**
36
+ * Forgets the download dispatched so far. The next download counts its bytes from zero,
37
+ * and without the reset the monotonic guard would swallow its events as the previous
38
+ * download running backwards.
39
+ */
40
+ synchronized void reset() {
41
+ mLatestDownloadProgress = null;
42
+ mLastDispatchedBytes = -1;
22
43
  }
23
44
  }
@@ -146,6 +146,10 @@ public class CodePushUpdateManagerBinaryPatchTest {
146
146
 
147
147
  private static DownloadProgressCallback ignoreProgress() {
148
148
  return new DownloadProgressCallback() {
149
+ @Override
150
+ public void onDownloadStart() {
151
+ }
152
+
149
153
  @Override
150
154
  public void call(DownloadProgress downloadProgress) {
151
155
  }
@@ -161,6 +161,42 @@ public class CodePushUpdateManagerDownloadTest {
161
161
  assertFallbackResult(patchResult, BinaryPatchResult.REASON_PACKAGE_VERIFICATION_FAILED);
162
162
  }
163
163
 
164
+ @Test
165
+ public void announcesEachDownloadOfAFallbackAsItsOwnProgressStream() throws IOException {
166
+ byte[] fullArchive = zipOf(fullArchiveContents());
167
+ String patchUrl = serve("/patch.zip", ERROR_PAGE);
168
+ String fullUrl = serve("/full.zip", fullArchive);
169
+ final List<List<Long>> streams = new ArrayList<>();
170
+ final List<Long> completedBytes = new ArrayList<>();
171
+
172
+ updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
173
+ updatePackage(fullUrl, patchUrl), BUNDLE_FILE_NAME, new DownloadProgressCallback() {
174
+ @Override
175
+ public void onDownloadStart() {
176
+ streams.add(new ArrayList<Long>());
177
+ }
178
+
179
+ @Override
180
+ public void call(DownloadProgress downloadProgress) {
181
+ streams.get(streams.size() - 1).add(downloadProgress.getReceivedBytes());
182
+ if (downloadProgress.isCompleted()) {
183
+ completedBytes.add(downloadProgress.getReceivedBytes());
184
+ }
185
+ }
186
+ });
187
+
188
+ assertEquals("each download announces a stream of its own", 2, streams.size());
189
+ for (List<Long> stream : streams) {
190
+ assertEquals("a stream opens by putting its total on the table at zero received",
191
+ Long.valueOf(0), stream.get(0));
192
+ for (int i = 1; i < stream.size(); i++) {
193
+ assertTrue("a stream never runs backwards", stream.get(i) >= stream.get(i - 1));
194
+ }
195
+ }
196
+ assertEquals("both downloads complete at the size of the body they served",
197
+ Arrays.asList((long) ERROR_PAGE.length, (long) fullArchive.length), completedBytes);
198
+ }
199
+
164
200
  /** The result of a download the update had to be downloaded in full for. */
165
201
  private static void assertFallbackResult(JSONObject patchResult, String expectedReason) {
166
202
  assertEquals("fallback", patchResult.optString("status", null));
@@ -393,6 +429,10 @@ public class CodePushUpdateManagerDownloadTest {
393
429
 
394
430
  private static DownloadProgressCallback ignoreProgress() {
395
431
  return new DownloadProgressCallback() {
432
+ @Override
433
+ public void onDownloadStart() {
434
+ }
435
+
396
436
  @Override
397
437
  public void call(DownloadProgress downloadProgress) {
398
438
  }
@@ -0,0 +1,80 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+
5
+ import org.junit.Test;
6
+
7
+ import java.util.ArrayList;
8
+ import java.util.Arrays;
9
+ import java.util.Collections;
10
+ import java.util.List;
11
+
12
+ /**
13
+ * The dispatcher is the seam between the download thread and the UI frame callback. These
14
+ * pin the property the two threads rely on: per download, the JS side sees one monotonic
15
+ * stream, and after a reset the next download's stream starts fresh instead of being
16
+ * mistaken for the previous one running backwards.
17
+ */
18
+ public class DownloadProgressEventDispatcherTest {
19
+
20
+ private final List<Long> mEmittedBytes = new ArrayList<>();
21
+ private final DownloadProgressEventDispatcher mDispatcher = new DownloadProgressEventDispatcher(
22
+ new DownloadProgressEventDispatcher.EventEmitter() {
23
+ @Override
24
+ public void emit(DownloadProgress downloadProgress) {
25
+ mEmittedBytes.add(downloadProgress.getReceivedBytes());
26
+ }
27
+ });
28
+
29
+ @Test
30
+ public void aFrameCallbackWithNothingRecordedEmitsNothing() {
31
+ mDispatcher.dispatchLatest();
32
+
33
+ assertEquals(Collections.emptyList(), mEmittedBytes);
34
+ }
35
+
36
+ @Test
37
+ public void emitsOnlyTheLatestRecordedProgress() {
38
+ mDispatcher.record(new DownloadProgress(100, 60));
39
+ mDispatcher.record(new DownloadProgress(100, 80));
40
+
41
+ mDispatcher.dispatchLatest();
42
+
43
+ assertEquals(Arrays.asList(80L), mEmittedBytes);
44
+ }
45
+
46
+ @Test
47
+ public void aFrameCallbackFiringAfterTheCompletionEventEmitsNothingMore() {
48
+ mDispatcher.record(new DownloadProgress(100, 90));
49
+ mDispatcher.dispatchLatest();
50
+ mDispatcher.record(new DownloadProgress(100, 100));
51
+ mDispatcher.dispatchLatest();
52
+
53
+ // The frame callback that was already scheduled when the download completed.
54
+ mDispatcher.dispatchLatest();
55
+
56
+ assertEquals(Arrays.asList(90L, 100L), mEmittedBytes);
57
+ }
58
+
59
+ @Test
60
+ public void aResetLetsTheNextDownloadCountFromZeroAgain() {
61
+ mDispatcher.record(new DownloadProgress(100, 100));
62
+ mDispatcher.dispatchLatest();
63
+
64
+ mDispatcher.reset();
65
+ mDispatcher.record(new DownloadProgress(5000, 10));
66
+ mDispatcher.dispatchLatest();
67
+
68
+ assertEquals(Arrays.asList(100L, 10L), mEmittedBytes);
69
+ }
70
+
71
+ @Test
72
+ public void progressRecordedBeforeAResetIsNotEmittedAfterIt() {
73
+ mDispatcher.record(new DownloadProgress(100, 100));
74
+
75
+ mDispatcher.reset();
76
+ mDispatcher.dispatchLatest();
77
+
78
+ assertEquals(Collections.emptyList(), mEmittedBytes);
79
+ }
80
+ }
@@ -24,6 +24,10 @@ export function runExpoBundleCommand(bundleName, outputPath, platform, sourcemap
24
24
  'false',
25
25
  '--entry-file',
26
26
  entryFile,
27
+ // Same reason as `runReactNativeBundleCommand`: the native build embeds an
28
+ // unminified bundle, and diverging from it inflates every binary patch.
29
+ '--minify',
30
+ 'false',
27
31
  '--platform',
28
32
  platform,
29
33
  '--sourcemap-output',
@@ -28,6 +28,16 @@ export function runReactNativeBundleCommand(bundleName, outputPath, platform, so
28
28
  'false',
29
29
  '--entry-file',
30
30
  entryFile,
31
+ /**
32
+ * Match React Native's Hermes builds, which disable JS minification. Minifying an
33
+ * update barely changes how large its bytecode is, but it changes how that bytecode
34
+ * is laid out - enough that a binary patch against an unminified binary costs far
35
+ * more than the source change does.
36
+ *
37
+ * Listed before the caller's options so an explicit `--minify` still wins.
38
+ */
39
+ '--minify',
40
+ 'false',
31
41
  '--platform',
32
42
  platform,
33
43
  '--sourcemap-output',
@@ -32,6 +32,10 @@ export function runExpoBundleCommand(
32
32
  'false',
33
33
  '--entry-file',
34
34
  entryFile,
35
+ // Same reason as `runReactNativeBundleCommand`: the native build embeds an
36
+ // unminified bundle, and diverging from it inflates every binary patch.
37
+ '--minify',
38
+ 'false',
35
39
  '--platform',
36
40
  platform,
37
41
  '--sourcemap-output',
@@ -38,6 +38,16 @@ export function runReactNativeBundleCommand(
38
38
  'false',
39
39
  '--entry-file',
40
40
  entryFile,
41
+ /**
42
+ * Match React Native's Hermes builds, which disable JS minification. Minifying an
43
+ * update barely changes how large its bytecode is, but it changes how that bytecode
44
+ * is laid out - enough that a binary patch against an unminified binary costs far
45
+ * more than the source change does.
46
+ *
47
+ * Listed before the caller's options so an explicit `--minify` still wins.
48
+ */
49
+ '--minify',
50
+ 'false',
41
51
  '--platform',
42
52
  platform,
43
53
  '--sourcemap-output',
@@ -739,6 +739,14 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
739
739
  operationQueue:_methodQueue
740
740
  // The download is progressing forward
741
741
  progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
742
+ // Received bytes only run backwards when a new download starts a stream of its
743
+ // own - the full archive after a binary patch fallback. The pause left over
744
+ // from the finished patch download would swallow that whole stream, so frame
745
+ // observation resumes here and the throttle forgets the previous stream.
746
+ if (notifyProgress && receivedContentLength < _latestReceivedConentLength) {
747
+ self.paused = NO;
748
+ _lastProgressEventTime = 0;
749
+ }
742
750
  // Update the download progress so that the frame observer can notify the JS side
743
751
  _latestExpectedContentLength = expectedContentLength;
744
752
  _latestReceivedConentLength = receivedContentLength;
@@ -61,6 +61,12 @@ failCallback:(void (^)(NSError *err))failCallback {
61
61
  }
62
62
 
63
63
  self.expectedContentLength = response.expectedContentLength;
64
+ if (self.expectedContentLength > 0) {
65
+ // Each download opens its own progress stream. The zero-received event hands the
66
+ // listener the new total up front, so the full download after a binary patch
67
+ // fallback shows as restarting rather than running backwards.
68
+ self.progressCallback(self.expectedContentLength, 0);
69
+ }
64
70
  [self.outputFileStream open];
65
71
  }
66
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.0",
3
+ "version": "13.1.0-beta.2",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
@@ -162,7 +162,9 @@ export interface CodePushOptions extends SyncOptions {
162
162
 
163
163
  export interface DownloadProgress {
164
164
  /**
165
- * The total number of bytes expected to be received for this update.
165
+ * The total number of bytes expected to be received for this download. An update
166
+ * published with a binary patch can be downloaded twice: when applying the patch
167
+ * fails, the full archive is downloaded as a second stream with its own total.
166
168
  */
167
169
  totalBytes: number;
168
170