@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.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.
Files changed (105) hide show
  1. package/CodePush.podspec +35 -3
  2. package/README.md +115 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +19 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -2
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +18 -1
  94. package/ios/CodePush/CodePush.mm +16 -1
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/package.json +10 -3
  100. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  101. package/scripts/export-embedded-bundle.sh +76 -0
  102. package/src/CodePush.js +46 -3
  103. package/src/CodePush.test.js +382 -0
  104. package/src/package-mixins.js +20 -2
  105. package/typings/react-native-code-push.d.ts +77 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * A binary patch is announced in the release history and consumed by the native side,
3
+ * which only ever sees the object handed to `downloadUpdate`. These cases pin that path:
4
+ * what the release history says, what `checkForUpdate` resolves to, what the native
5
+ * module is given when the update is downloaded, and what an app that asked to hear how
6
+ * the patch went is told about it.
7
+ */
8
+
9
+ jest.mock('react-native', () => ({
10
+ Alert: { alert: jest.fn() },
11
+ AppState: { addEventListener: jest.fn(() => ({ remove: jest.fn() })) },
12
+ NativeEventEmitter: class NativeEventEmitter {
13
+ addListener() {
14
+ return { remove: jest.fn() };
15
+ }
16
+ },
17
+ NativeModules: {},
18
+ Platform: { OS: 'ios' },
19
+ TurboModuleRegistry: { get: () => null },
20
+ }));
21
+
22
+ // The install modes are the ones of the native module the bridge below stands in for.
23
+ const { InstallMode } = require('./native/NativeCodePush');
24
+
25
+ const BINARY_VERSION = '1.0.0';
26
+ const LABEL = '1.0.1';
27
+ const PACKAGE_HASH = 'a'.repeat(64);
28
+ const DOWNLOAD_URL = 'https://cdn.example.com/full.zip';
29
+ const BINARY_PATCH_DOWNLOAD_URL = 'https://cdn.example.com/full.zip-patch.zip';
30
+
31
+ /** The release the CLI writes when only the full bundle was published. */
32
+ function fullOnlyRelease() {
33
+ return {
34
+ [LABEL]: {
35
+ enabled: true,
36
+ mandatory: false,
37
+ downloadUrl: DOWNLOAD_URL,
38
+ packageHash: PACKAGE_HASH,
39
+ },
40
+ };
41
+ }
42
+
43
+ /** The release the CLI writes when the update was published with a binary patch too. */
44
+ function patchedRelease() {
45
+ return {
46
+ [LABEL]: {
47
+ ...fullOnlyRelease()[LABEL],
48
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * @param binaryPatchResult what the native side reports the patch attempt ended in, which
55
+ * only a download that had a patch to try comes back with.
56
+ */
57
+ function createNativeBridge({ binaryPatchResult } = {}) {
58
+ return {
59
+ addDownloadProgressListener: jest.fn(() => ({ remove: jest.fn() })),
60
+ downloadUpdate: jest.fn(async (updatePackage) => ({
61
+ ...updatePackage,
62
+ ...(binaryPatchResult ? { binaryPatchResult } : {}),
63
+ })),
64
+ // No CodePush update is installed, so the app runs the bundle of its binary.
65
+ getUpdateMetadata: jest.fn(async () => null),
66
+ isFailedUpdate: jest.fn(async () => false),
67
+ isFirstRun: jest.fn(async () => false),
68
+ // The rest of what a full `sync()` reaches for.
69
+ InstallMode,
70
+ clearPendingRestart: jest.fn(),
71
+ getNewStatusReport: jest.fn(async () => null),
72
+ installUpdate: jest.fn(async () => {}),
73
+ notifyApplicationReady: jest.fn(async () => {}),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Loads a fresh copy of the module - the options passed to `codePush()` and the injected
79
+ * native bridge live on the module itself - configured the way an app configures it: the
80
+ * decorator registers the app-wide options, and every sync below is one the app asks for.
81
+ *
82
+ * @param onBinaryPatchResult a callback the app registers on the decorator, as opposed to
83
+ * one it passes to a single `sync()` call.
84
+ */
85
+ function loadCodePush({ releaseHistory = {}, updateChecker, binaryPatchResult, onBinaryPatchResult } = {}) {
86
+ jest.resetModules();
87
+ const CodePush = require('./CodePush');
88
+ const nativeBridge = createNativeBridge({ binaryPatchResult });
89
+
90
+ CodePush.setUpTestDependencies(
91
+ null,
92
+ { appVersion: BINARY_VERSION, clientUniqueId: 'test-client-id' },
93
+ nativeBridge,
94
+ );
95
+ CodePush({
96
+ checkFrequency: CodePush.CheckFrequency.MANUAL,
97
+ releaseHistoryFetcher: async () => releaseHistory,
98
+ updateChecker,
99
+ onBinaryPatchResult,
100
+ });
101
+
102
+ return { CodePush, nativeBridge };
103
+ }
104
+
105
+ /** The metadata the native module is given, without the functions mixed into the package. */
106
+ function downloadedPackageMetadata(nativeBridge) {
107
+ expect(nativeBridge.downloadUpdate).toHaveBeenCalledTimes(1);
108
+ return nativeBridge.downloadUpdate.mock.calls[0][0];
109
+ }
110
+
111
+ describe('checkForUpdate with a binary patch in the release history', () => {
112
+ it('carries the patch url into the update and on to the native module', async () => {
113
+ const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: patchedRelease() });
114
+
115
+ const remotePackage = await CodePush.checkForUpdate();
116
+
117
+ expect(remotePackage.downloadUrl).toBe(DOWNLOAD_URL);
118
+ expect(remotePackage.binaryPatchDownloadUrl).toBe(BINARY_PATCH_DOWNLOAD_URL);
119
+
120
+ await remotePackage.download();
121
+
122
+ expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
123
+ downloadUrl: DOWNLOAD_URL,
124
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
125
+ label: LABEL,
126
+ packageHash: PACKAGE_HASH,
127
+ });
128
+ });
129
+
130
+ it('reads the patch url of the release being installed, not of another one', async () => {
131
+ const { CodePush } = loadCodePush({
132
+ releaseHistory: {
133
+ '1.0.1': {
134
+ enabled: true,
135
+ mandatory: false,
136
+ downloadUrl: 'https://cdn.example.com/older.zip',
137
+ binaryPatchDownloadUrl: 'https://cdn.example.com/older.zip-patch.zip',
138
+ packageHash: 'b'.repeat(64),
139
+ },
140
+ '1.0.2': {
141
+ enabled: true,
142
+ mandatory: false,
143
+ downloadUrl: DOWNLOAD_URL,
144
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
145
+ packageHash: PACKAGE_HASH,
146
+ },
147
+ },
148
+ });
149
+
150
+ const remotePackage = await CodePush.checkForUpdate();
151
+
152
+ expect(remotePackage.label).toBe('1.0.2');
153
+ expect(remotePackage.binaryPatchDownloadUrl).toBe(BINARY_PATCH_DOWNLOAD_URL);
154
+ });
155
+ });
156
+
157
+ describe('checkForUpdate without a binary patch in the release history', () => {
158
+ it('leaves the patch url out of the update and of the native metadata', async () => {
159
+ const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: fullOnlyRelease() });
160
+
161
+ const remotePackage = await CodePush.checkForUpdate();
162
+
163
+ expect(remotePackage.downloadUrl).toBe(DOWNLOAD_URL);
164
+ expect(remotePackage).not.toHaveProperty('binaryPatchDownloadUrl');
165
+
166
+ await remotePackage.download();
167
+
168
+ expect(downloadedPackageMetadata(nativeBridge)).not.toHaveProperty('binaryPatchDownloadUrl');
169
+ });
170
+
171
+ it('serves a release history written before binary patches existed', async () => {
172
+ const { CodePush } = loadCodePush({
173
+ releaseHistory: {
174
+ [BINARY_VERSION]: {
175
+ enabled: true,
176
+ mandatory: false,
177
+ downloadUrl: 'https://cdn.example.com/binary.zip',
178
+ packageHash: 'c'.repeat(64),
179
+ },
180
+ [LABEL]: {
181
+ enabled: true,
182
+ mandatory: true,
183
+ downloadUrl: DOWNLOAD_URL,
184
+ packageHash: PACKAGE_HASH,
185
+ rollout: 100,
186
+ },
187
+ },
188
+ });
189
+
190
+ const remotePackage = await CodePush.checkForUpdate();
191
+
192
+ expect(remotePackage).toMatchObject({
193
+ label: LABEL,
194
+ appVersion: BINARY_VERSION,
195
+ downloadUrl: DOWNLOAD_URL,
196
+ packageHash: PACKAGE_HASH,
197
+ isMandatory: true,
198
+ });
199
+ expect(remotePackage).not.toHaveProperty('binaryPatchDownloadUrl');
200
+ });
201
+ });
202
+
203
+ describe('checkForUpdate through the deprecated updateChecker', () => {
204
+ it('carries the patch url of the update check response', async () => {
205
+ const { CodePush } = loadCodePush({
206
+ updateChecker: async () => ({
207
+ update_info: {
208
+ is_available: true,
209
+ download_url: DOWNLOAD_URL,
210
+ binary_patch_download_url: BINARY_PATCH_DOWNLOAD_URL,
211
+ target_binary_range: BINARY_VERSION,
212
+ label: LABEL,
213
+ package_hash: PACKAGE_HASH,
214
+ },
215
+ }),
216
+ });
217
+
218
+ const remotePackage = await CodePush.checkForUpdate();
219
+
220
+ expect(remotePackage.binaryPatchDownloadUrl).toBe(BINARY_PATCH_DOWNLOAD_URL);
221
+ });
222
+
223
+ it('leaves it out when the update check response has none', async () => {
224
+ const { CodePush } = loadCodePush({
225
+ updateChecker: async () => ({
226
+ update_info: {
227
+ is_available: true,
228
+ download_url: DOWNLOAD_URL,
229
+ target_binary_range: BINARY_VERSION,
230
+ label: LABEL,
231
+ package_hash: PACKAGE_HASH,
232
+ },
233
+ }),
234
+ });
235
+
236
+ const remotePackage = await CodePush.checkForUpdate();
237
+
238
+ expect(remotePackage).not.toHaveProperty('binaryPatchDownloadUrl');
239
+ });
240
+ });
241
+
242
+ /**
243
+ * How a patch attempt went is reported to the app that asked to hear about it, and to
244
+ * nobody else: it is not part of the update, and an app that asked for nothing gets the
245
+ * install it always got.
246
+ */
247
+ describe('the binary patch result of a sync', () => {
248
+ const APPLIED = { status: 'applied', applyDurationMs: 812 };
249
+ const FELL_BACK = { status: 'fallback', fallbackReason: 'base_hash_mismatch', applyDurationMs: 1503 };
250
+
251
+ /** The metadata the update is installed from, as the native module is given it. */
252
+ function installedPackageMetadata(nativeBridge) {
253
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
254
+ return nativeBridge.installUpdate.mock.calls[0][0];
255
+ }
256
+
257
+ it('tells the app the update was installed from its patch, and how long that took', async () => {
258
+ const { CodePush, nativeBridge } = loadCodePush({
259
+ releaseHistory: patchedRelease(),
260
+ binaryPatchResult: APPLIED,
261
+ });
262
+ const onBinaryPatchResult = jest.fn();
263
+
264
+ const syncStatus = await CodePush.sync({ onBinaryPatchResult });
265
+
266
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
267
+ expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
268
+ expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, APPLIED);
269
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
270
+ });
271
+
272
+ it('tells the app why the update came from the full archive instead', async () => {
273
+ const { CodePush } = loadCodePush({
274
+ releaseHistory: patchedRelease(),
275
+ binaryPatchResult: FELL_BACK,
276
+ });
277
+ const onBinaryPatchResult = jest.fn();
278
+
279
+ // A patch that could not be applied is not a failed update: the full archive installs.
280
+ const syncStatus = await CodePush.sync({ onBinaryPatchResult });
281
+
282
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
283
+ expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, FELL_BACK);
284
+ });
285
+
286
+ it('tells the app that registered the callback on the decorator, even about a sync it asked for itself', async () => {
287
+ const onBinaryPatchResult = jest.fn();
288
+ const { CodePush } = loadCodePush({
289
+ releaseHistory: patchedRelease(),
290
+ binaryPatchResult: APPLIED,
291
+ onBinaryPatchResult,
292
+ });
293
+
294
+ // The decorator checks nothing on its own, so this sync is the app's own call.
295
+ const syncStatus = await CodePush.sync();
296
+
297
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
298
+ expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
299
+ expect(onBinaryPatchResult).toHaveBeenCalledWith(LABEL, APPLIED);
300
+ });
301
+
302
+ it('tells only the callback of the sync call when one was passed to it', async () => {
303
+ const registeredOnTheDecorator = jest.fn();
304
+ const passedToTheSyncCall = jest.fn();
305
+ const { CodePush } = loadCodePush({
306
+ releaseHistory: patchedRelease(),
307
+ binaryPatchResult: APPLIED,
308
+ onBinaryPatchResult: registeredOnTheDecorator,
309
+ });
310
+
311
+ const syncStatus = await CodePush.sync({ onBinaryPatchResult: passedToTheSyncCall });
312
+
313
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
314
+ expect(passedToTheSyncCall).toHaveBeenCalledTimes(1);
315
+ expect(passedToTheSyncCall).toHaveBeenCalledWith(LABEL, APPLIED);
316
+ expect(registeredOnTheDecorator).not.toHaveBeenCalled();
317
+ });
318
+
319
+ it('says nothing about a download that had no patch to try', async () => {
320
+ const { CodePush } = loadCodePush({ releaseHistory: fullOnlyRelease() });
321
+ const onBinaryPatchResult = jest.fn();
322
+
323
+ const syncStatus = await CodePush.sync({ onBinaryPatchResult });
324
+
325
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
326
+ expect(onBinaryPatchResult).not.toHaveBeenCalled();
327
+ });
328
+
329
+ it('installs the update even when the callback throws', async () => {
330
+ const { CodePush, nativeBridge } = loadCodePush({
331
+ releaseHistory: patchedRelease(),
332
+ binaryPatchResult: APPLIED,
333
+ });
334
+ const onBinaryPatchResult = jest.fn(() => {
335
+ throw new Error('the telemetry the app sends the result to is down');
336
+ });
337
+
338
+ const syncStatus = await CodePush.sync({ onBinaryPatchResult });
339
+
340
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
341
+ expect(onBinaryPatchResult).toHaveBeenCalledTimes(1);
342
+ expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
343
+ });
344
+
345
+ it('keeps the result out of the package the update is installed from', async () => {
346
+ const { CodePush, nativeBridge } = loadCodePush({
347
+ releaseHistory: patchedRelease(),
348
+ binaryPatchResult: APPLIED,
349
+ });
350
+
351
+ await CodePush.sync({ onBinaryPatchResult: jest.fn() });
352
+
353
+ const installedPackage = installedPackageMetadata(nativeBridge);
354
+ expect(installedPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
355
+ expect(installedPackage).not.toHaveProperty('binaryPatchResult');
356
+ });
357
+
358
+ it('installs the same update, without the result, when no callback is registered', async () => {
359
+ const { CodePush, nativeBridge } = loadCodePush({
360
+ releaseHistory: patchedRelease(),
361
+ binaryPatchResult: FELL_BACK,
362
+ });
363
+
364
+ const syncStatus = await CodePush.sync();
365
+
366
+ expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
367
+ expect(installedPackageMetadata(nativeBridge)).not.toHaveProperty('binaryPatchResult');
368
+ });
369
+
370
+ it('leaves the result off the package a download resolves with', async () => {
371
+ const { CodePush } = loadCodePush({
372
+ releaseHistory: patchedRelease(),
373
+ binaryPatchResult: APPLIED,
374
+ });
375
+
376
+ const remotePackage = await CodePush.checkForUpdate();
377
+ const localPackage = await remotePackage.download();
378
+
379
+ expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
380
+ expect(localPackage).not.toHaveProperty('binaryPatchResult');
381
+ });
382
+ });
@@ -4,7 +4,15 @@
4
4
  module.exports = (NativeCodePush) => {
5
5
  const remote = () => {
6
6
  return {
7
- async download(downloadProgressCallback) {
7
+ /**
8
+ * @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.
14
+ */
15
+ async download(downloadProgressCallback, binaryPatchResultCallback) {
8
16
  if (!this.downloadUrl) {
9
17
  throw new Error("Cannot download an update without a download url");
10
18
  }
@@ -23,7 +31,17 @@ module.exports = (NativeCodePush) => {
23
31
  const updatePackageCopy = Object.assign({}, this);
24
32
  Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]);
25
33
 
26
- const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
34
+ const downloadResult = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
35
+
36
+ // The patch result describes the download that just happened, not the update it
37
+ // delivered, and the package is handed around as the update's metadata from here
38
+ // on - it is even written back to the native side on install. So the result is
39
+ // taken off the package and reported on its own, leaving the package exactly what
40
+ // the native side saved.
41
+ const { binaryPatchResult, ...downloadedPackage } = downloadResult ?? {};
42
+ if (binaryPatchResult) {
43
+ binaryPatchResultCallback?.(binaryPatchResult);
44
+ }
27
45
 
28
46
  return { ...downloadedPackage, ...local };
29
47
  } finally {
@@ -28,6 +28,12 @@ export interface ReleaseInfo {
28
28
  enabled: boolean;
29
29
  mandatory: boolean;
30
30
  downloadUrl: string;
31
+ /**
32
+ * The URL of a binary patch archive built against the JS bundle embedded in the app binary.
33
+ * It is only present when the release was published together with such a patch, and a client
34
+ * that cannot use it downloads the full update from `downloadUrl` instead.
35
+ */
36
+ binaryPatchDownloadUrl?: string;
31
37
  packageHash: string;
32
38
  rollout?: number;
33
39
  }
@@ -35,6 +41,12 @@ export interface ReleaseInfo {
35
41
  // from code-push SDK
36
42
  export interface UpdateCheckResponse {
37
43
  download_url?: string;
44
+ /**
45
+ * The URL of a binary patch archive built against the JS bundle embedded in the app binary.
46
+ * It is only present when the release was published together with such a patch, and a client
47
+ * that cannot use it downloads the full update from `download_url` instead.
48
+ */
49
+ binary_patch_download_url?: string;
38
50
  description?: string;
39
51
  is_available: boolean;
40
52
  is_disabled?: boolean;
@@ -47,6 +59,50 @@ export interface UpdateCheckResponse {
47
59
  is_mandatory?: boolean;
48
60
  }
49
61
 
62
+ /**
63
+ * Why an update that was published with a binary patch had to be downloaded in full
64
+ * instead. These are the words every platform's applier reports, so a rollout can be
65
+ * judged by them whichever platform it is running on.
66
+ */
67
+ export type BinaryPatchFallbackReason =
68
+ /** The bundle inside the app binary could not be opened or read. */
69
+ | "base_bundle_unavailable"
70
+ /** The bundle inside the app binary is not the one the patch was computed against. */
71
+ | "base_hash_mismatch"
72
+ /** The manifest is missing, malformed, points outside the archive, or asks for too much. */
73
+ | "invalid_manifest"
74
+ /** The patch was produced by a format or a codec this client cannot apply. */
75
+ | "unsupported_format"
76
+ /** The applier refused the patch, or the restored bundle could not be written. */
77
+ | "patch_apply_failed"
78
+ /** The restored bundle is not the one the manifest promised. */
79
+ | "target_verification_failed"
80
+ /** The update restored from the patch did not pass the checks that follow the restore. */
81
+ | "package_verification_failed";
82
+
83
+ /**
84
+ * How installing an update from its binary patch archive went.
85
+ */
86
+ export interface BinaryPatchResult {
87
+ /**
88
+ * Whether the update was installed from its patch archive, or had to be downloaded in
89
+ * full instead. A fallback is not an error: the update is installed either way.
90
+ */
91
+ status: "applied" | "fallback";
92
+
93
+ /**
94
+ * Why the full archive had to be downloaded. Absent when the patch was applied, and
95
+ * also when the attempt ended in an error none of the appliers has a word for.
96
+ */
97
+ fallbackReason?: BinaryPatchFallbackReason;
98
+
99
+ /**
100
+ * How long the patch work took, in milliseconds: applying the patch when it was
101
+ * applied, and the whole attempt when it was given up on.
102
+ */
103
+ applyDurationMs: number;
104
+ }
105
+
50
106
  export interface CodePushOptions extends SyncOptions {
51
107
  /**
52
108
  * The `ignoreFailedUpdates` option is only available as an option for `CodePush.sync()`.
@@ -192,6 +248,14 @@ export interface RemotePackage extends Package {
192
248
  * The URL at which the package is available for download.
193
249
  */
194
250
  downloadUrl: string;
251
+
252
+ /**
253
+ * The URL at which a binary patch archive of this package is available for download.
254
+ * The patch is built against the JS bundle embedded in the app binary, so it is only
255
+ * present when the release was published together with one. When it is absent, the
256
+ * package is downloaded in full from `downloadUrl`.
257
+ */
258
+ binaryPatchDownloadUrl?: string;
195
259
  }
196
260
 
197
261
  export interface SyncOptions {
@@ -239,6 +303,19 @@ export interface SyncOptions {
239
303
  */
240
304
  rollbackRetryOptions?: RollbackRetryOptions;
241
305
 
306
+ /**
307
+ * An optional callback for observing how an update published with a binary patch was
308
+ * installed: whether it came from the patch archive, why it did not when it did not, and
309
+ * how long the patch work took. It is called once per download that had a patch to try,
310
+ * with the label of the release being installed.
311
+ *
312
+ * Purely for observation. The library neither stores the result nor sends it anywhere -
313
+ * an app that wants it in its telemetry sends it itself - and nothing about the update
314
+ * depends on the callback: registering none changes nothing, and one that throws is
315
+ * logged and does not fail the install.
316
+ */
317
+ onBinaryPatchResult?: (label: string, result: BinaryPatchResult) => void;
318
+
242
319
  /**
243
320
  * Specifies whether to ignore the update if the installation fails.
244
321
  * If set to false, the update will be retried when the sync method is called, even if the previous installation attempt failed.