@bravemobile/react-native-code-push 12.4.1 → 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 (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -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 +46 -11
  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 +79 -88
  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 +98 -27
  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 +112 -26
  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 +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  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/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -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
+ });
@@ -0,0 +1,162 @@
1
+ import { NativeEventEmitter, NativeModules } from 'react-native';
2
+ import { getNativeCodePushTurboModule } from '../specs/NativeCodePush';
3
+ import type { DownloadProgress, Spec as NativeCodePushSpec } from '../specs/NativeCodePush';
4
+
5
+ export const DOWNLOAD_PROGRESS_EVENT_NAME = 'CodePushDownloadProgress';
6
+
7
+ export const InstallMode = Object.freeze({
8
+ IMMEDIATE: 0,
9
+ ON_NEXT_RESTART: 1,
10
+ ON_NEXT_RESUME: 2,
11
+ ON_NEXT_SUSPEND: 3,
12
+ });
13
+
14
+ export const UpdateState = Object.freeze({
15
+ RUNNING: 0,
16
+ PENDING: 1,
17
+ LATEST: 2,
18
+ });
19
+
20
+ type Subscription = {
21
+ remove: () => void;
22
+ };
23
+
24
+ type LegacyCodePushModule = {
25
+ addListener?: (eventName: string) => void;
26
+ allow: () => void | Promise<void>;
27
+ clearPendingRestart: () => void | Promise<void>;
28
+ clearUpdates: () => void;
29
+ disallow: () => void | Promise<void>;
30
+ downloadAndReplaceCurrentBundle?: (remoteBundleUrl: string) => void;
31
+ downloadUpdate: (updatePackage: object, notifyProgress: boolean) => Promise<object>;
32
+ getConfiguration: () => Promise<object>;
33
+ getLatestRollbackInfo: () => Promise<object | null>;
34
+ getNewStatusReport: () => Promise<object | string | null>;
35
+ getUpdateMetadata: (updateState: number) => Promise<object | null>;
36
+ installUpdate: (
37
+ updatePackage: object,
38
+ installMode: number,
39
+ minimumBackgroundDuration: number,
40
+ ) => Promise<void>;
41
+ isFailedUpdate: (packageHash: string) => Promise<boolean>;
42
+ isFirstRun: (packageHash: string) => Promise<boolean>;
43
+ notifyApplicationReady: () => Promise<void>;
44
+ recordStatusReported: (statusReport: object) => void;
45
+ removeListeners?: (count: number) => void;
46
+ restartApp: (onlyIfUpdateIsPending: boolean) => void | Promise<void>;
47
+ saveStatusReportForRetry: (statusReport: object) => void;
48
+ setLatestRollbackInfo: (packageHash: string) => Promise<void>;
49
+ };
50
+
51
+ type CodePushModule = LegacyCodePushModule & {
52
+ onDownloadProgress?: NativeCodePushSpec['onDownloadProgress'];
53
+ };
54
+
55
+ type EventEmitterCapableCodePushModule = LegacyCodePushModule & {
56
+ addListener: NonNullable<LegacyCodePushModule['addListener']>;
57
+ removeListeners: NonNullable<LegacyCodePushModule['removeListeners']>;
58
+ };
59
+
60
+ function getTurboModule(): CodePushModule | null {
61
+ return getNativeCodePushTurboModule() as CodePushModule | null;
62
+ }
63
+
64
+ function getLegacyModule(): LegacyCodePushModule | null {
65
+ return (NativeModules.CodePush ?? null) as LegacyCodePushModule | null;
66
+ }
67
+
68
+ function getNativeModule(): CodePushModule | null {
69
+ const turboModule = getTurboModule();
70
+ if (turboModule) {
71
+ return turboModule;
72
+ }
73
+
74
+ return getLegacyModule();
75
+ }
76
+
77
+ function normalizeStatusReport(statusReport: object | string | null | undefined) {
78
+ if (!statusReport || typeof statusReport === 'string') {
79
+ return null;
80
+ }
81
+
82
+ return statusReport;
83
+ }
84
+
85
+ function canUseNativeEventEmitter(
86
+ module: LegacyCodePushModule,
87
+ ): module is EventEmitterCapableCodePushModule {
88
+ return (
89
+ typeof module.addListener === 'function' &&
90
+ typeof module.removeListeners === 'function'
91
+ );
92
+ }
93
+
94
+ function addDownloadProgressListener(
95
+ listener: (progress: DownloadProgress) => void,
96
+ ): Subscription {
97
+ const nativeModule = getNativeModule();
98
+ if (!nativeModule) {
99
+ return {
100
+ remove() {},
101
+ };
102
+ }
103
+
104
+ const turboModule = getTurboModule();
105
+ if (turboModule?.onDownloadProgress) {
106
+ return turboModule.onDownloadProgress(listener);
107
+ }
108
+
109
+ const legacyModule = getLegacyModule();
110
+ if (!legacyModule || !canUseNativeEventEmitter(legacyModule)) {
111
+ return {
112
+ remove() {},
113
+ };
114
+ }
115
+
116
+ const eventEmitter = new NativeEventEmitter(legacyModule);
117
+ return eventEmitter.addListener(DOWNLOAD_PROGRESS_EVENT_NAME, listener);
118
+ }
119
+
120
+ function createNativeCodePush() {
121
+ return {
122
+ InstallMode,
123
+ UpdateState,
124
+ addDownloadProgressListener,
125
+ allow: () => getNativeModule()?.allow(),
126
+ clearPendingRestart: () => getNativeModule()?.clearPendingRestart(),
127
+ clearUpdates: () => getNativeModule()?.clearUpdates(),
128
+ disallow: () => getNativeModule()?.disallow(),
129
+ downloadAndReplaceCurrentBundle: (remoteBundleUrl: string) =>
130
+ getNativeModule()?.downloadAndReplaceCurrentBundle?.(remoteBundleUrl),
131
+ downloadUpdate: (updatePackage: object, notifyProgress: boolean) =>
132
+ getNativeModule()?.downloadUpdate(updatePackage, notifyProgress),
133
+ getConfiguration: () => getNativeModule()?.getConfiguration(),
134
+ getLatestRollbackInfo: () => getNativeModule()?.getLatestRollbackInfo(),
135
+ getNewStatusReport: async () =>
136
+ normalizeStatusReport(await getNativeModule()?.getNewStatusReport()),
137
+ getUpdateMetadata: (updateState: number) => getNativeModule()?.getUpdateMetadata(updateState),
138
+ installUpdate: (
139
+ updatePackage: object,
140
+ installMode: number,
141
+ minimumBackgroundDuration: number,
142
+ ) => getNativeModule()?.installUpdate(updatePackage, installMode, minimumBackgroundDuration),
143
+ isFailedUpdate: (packageHash: string) => getNativeModule()?.isFailedUpdate(packageHash),
144
+ isFirstRun: (packageHash: string) => getNativeModule()?.isFirstRun(packageHash),
145
+ notifyApplicationReady: () => getNativeModule()?.notifyApplicationReady(),
146
+ recordStatusReported: (statusReport: object) =>
147
+ getNativeModule()?.recordStatusReported(statusReport),
148
+ restartApp: (onlyIfUpdateIsPending: boolean) =>
149
+ getNativeModule()?.restartApp(onlyIfUpdateIsPending),
150
+ saveStatusReportForRetry: (statusReport: object) =>
151
+ getNativeModule()?.saveStatusReportForRetry(statusReport),
152
+ setLatestRollbackInfo: (packageHash: string) =>
153
+ getNativeModule()?.setLatestRollbackInfo(packageHash),
154
+ };
155
+ }
156
+
157
+ export function getNativeCodePush() {
158
+ const nativeModule = getNativeModule();
159
+ return nativeModule ? createNativeCodePush() : null;
160
+ }
161
+
162
+ export default getNativeCodePush();
@@ -1,23 +1,26 @@
1
- import { NativeEventEmitter } from "react-native";
2
- import log from "./logging";
3
-
4
1
  // This function is used to augment remote and local
5
2
  // package objects with additional functionality/properties
6
3
  // beyond what is included in the metadata sent by the server.
7
4
  module.exports = (NativeCodePush) => {
8
5
  const remote = () => {
9
6
  return {
10
- 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) {
11
16
  if (!this.downloadUrl) {
12
17
  throw new Error("Cannot download an update without a download url");
13
18
  }
14
19
 
15
20
  let downloadProgressSubscription;
16
21
  if (downloadProgressCallback) {
17
- const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
18
22
  // Use event subscription to obtain download progress.
19
- downloadProgressSubscription = codePushEventEmitter.addListener(
20
- "CodePushDownloadProgress",
23
+ downloadProgressSubscription = NativeCodePush.addDownloadProgressListener(
21
24
  downloadProgressCallback
22
25
  );
23
26
  }
@@ -28,7 +31,17 @@ module.exports = (NativeCodePush) => {
28
31
  const updatePackageCopy = Object.assign({}, this);
29
32
  Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]);
30
33
 
31
- 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
+ }
32
45
 
33
46
  return { ...downloadedPackage, ...local };
34
47
  } finally {
@@ -41,12 +54,12 @@ module.exports = (NativeCodePush) => {
41
54
  };
42
55
 
43
56
  const local = {
44
- async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) {
57
+ async install(installMode = NativeCodePush.InstallMode.ON_NEXT_RESTART, minimumBackgroundDuration = 0, updateInstalledCallback) {
45
58
  const localPackage = this;
46
59
  const localPackageCopy = Object.assign({}, localPackage); // In dev mode, React Native deep freezes any object queued over the bridge
47
60
  await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
48
61
  updateInstalledCallback && updateInstalledCallback();
49
- if (installMode == NativeCodePush.codePushInstallModeImmediate) {
62
+ if (installMode == NativeCodePush.InstallMode.IMMEDIATE) {
50
63
  NativeCodePush.restartApp(false);
51
64
  } else {
52
65
  NativeCodePush.clearPendingRestart();
@@ -58,4 +71,4 @@ module.exports = (NativeCodePush) => {
58
71
  };
59
72
 
60
73
  return { local, remote };
61
- };
74
+ };
@@ -0,0 +1,43 @@
1
+ import { TurboModuleRegistry } from 'react-native';
2
+ import type { TurboModule } from 'react-native';
3
+ import type {
4
+ EventEmitter,
5
+ UnsafeObject,
6
+ } from 'react-native/Libraries/Types/CodegenTypes';
7
+
8
+ export type DownloadProgress = {
9
+ receivedBytes: number;
10
+ totalBytes: number;
11
+ };
12
+
13
+ export interface Spec extends TurboModule {
14
+ downloadUpdate(updatePackage: UnsafeObject, notifyProgress: boolean): Promise<UnsafeObject>;
15
+ getConfiguration(): Promise<UnsafeObject>;
16
+ getUpdateMetadata(updateState: number): Promise<UnsafeObject | null>;
17
+ getNewStatusReport(): Promise<UnsafeObject | null>;
18
+ installUpdate(
19
+ updatePackage: UnsafeObject,
20
+ installMode: number,
21
+ minimumBackgroundDuration: number,
22
+ ): Promise<void>;
23
+ isFailedUpdate(packageHash: string): Promise<boolean>;
24
+ getLatestRollbackInfo(): Promise<UnsafeObject | null>;
25
+ setLatestRollbackInfo(packageHash: string): Promise<void>;
26
+ isFirstRun(packageHash: string): Promise<boolean>;
27
+ notifyApplicationReady(): Promise<void>;
28
+ allow(): void;
29
+ disallow(): void;
30
+ clearPendingRestart(): void;
31
+ restartApp(onlyIfUpdateIsPending: boolean): void;
32
+ recordStatusReported(statusReport: UnsafeObject): void;
33
+ saveStatusReportForRetry(statusReport: UnsafeObject): void;
34
+ clearUpdates(): void;
35
+
36
+ readonly onDownloadProgress: EventEmitter<DownloadProgress>;
37
+ }
38
+
39
+ export function getNativeCodePushTurboModule(): Spec | null {
40
+ return TurboModuleRegistry.get<Spec>('CodePush');
41
+ }
42
+
43
+ export default getNativeCodePushTurboModule();