@bravemobile/react-native-code-push 13.1.0-beta.6 → 13.2.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.
- package/README.md +14 -1
- package/android/app/proguard-rules.pro +0 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +3 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +25 -0
- package/cli/dist/utils/fsUtils.js +35 -9
- package/cli/package.json +5 -1
- package/cli/utils/fsUtils.ts +43 -9
- package/ios/CodePush/CodePush.h +8 -3
- package/ios/CodePush/CodePush.mm +61 -1
- package/package.json +14 -6
- package/src/CodePush.js +22 -33
- package/src/CodePush.test.js +151 -7
- package/src/invokeTelemetryCallback.js +18 -0
- package/src/package-mixins.js +3 -7
- package/typings/react-native-code-push.d.ts +8 -8
package/README.md
CHANGED
|
@@ -394,7 +394,18 @@ module.exports = Config;
|
|
|
394
394
|
- Modifying an existing release history with the `update-history` command.
|
|
395
395
|
|
|
396
396
|
|
|
397
|
-
**(2)
|
|
397
|
+
**(2) A `code-push.config.ts` (TypeScript) file needs a loader installed.**
|
|
398
|
+
|
|
399
|
+
Install `tsx`, which requires no further setup:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
npm install --save-dev tsx
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`ts-node` also keeps working, and is used when `tsx` is not installed. Note that `ts-node` is no
|
|
406
|
+
longer maintained, so support for it is **deprecated and will be removed in a future major
|
|
407
|
+
version** — please migrate to `tsx`. Until then, `ts-node` needs the following `tsconfig.json`
|
|
408
|
+
setup:
|
|
398
409
|
|
|
399
410
|
```diff
|
|
400
411
|
{
|
|
@@ -414,6 +425,8 @@ module.exports = Config;
|
|
|
414
425
|
|
|
415
426
|
```
|
|
416
427
|
|
|
428
|
+
A `code-push.config.js` (JavaScript) file needs neither.
|
|
429
|
+
|
|
417
430
|
|
|
418
431
|
### 6. Diff Updates (Optional)
|
|
419
432
|
|
|
@@ -21,10 +21,6 @@
|
|
|
21
21
|
private final ** mBundleLoader;
|
|
22
22
|
}
|
|
23
23
|
# bridgeless
|
|
24
|
-
-keepclassmembers class com.facebook.react.defaults.DefaultReactHostDelegate {
|
|
25
|
-
private ** jsBundleLoader;
|
|
26
|
-
}
|
|
27
|
-
# bridgeless
|
|
28
24
|
-keepclassmembers class * extends com.facebook.react.runtime.ReactHostDelegate {
|
|
29
25
|
private ** jsBundleLoader;
|
|
30
26
|
private ** _jsBundleLoader;
|
|
@@ -35,9 +31,6 @@
|
|
|
35
31
|
private final ** reactHostDelegate; # RN 0.81+
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
# Can't find referenced class org.bouncycastle.**
|
|
39
|
-
-dontwarn com.nimbusds.jose.**
|
|
40
|
-
|
|
41
34
|
# The binary patch applier is found by name from the native library.
|
|
42
35
|
-keepclasseswithmembernames class com.microsoft.codepush.react.HDiffPatchNative {
|
|
43
36
|
native <methods>;
|
|
@@ -51,6 +51,9 @@ public class CodePush extends TurboReactPackage {
|
|
|
51
51
|
return mServerUrl;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Entry point used by React Native autolinking to create and retain the package instance.
|
|
56
|
+
*/
|
|
54
57
|
public static synchronized CodePush getInstance(Context context, boolean isDebugMode) {
|
|
55
58
|
if (mCurrentInstance == null) {
|
|
56
59
|
mCurrentInstance = new CodePush(context, isDebugMode);
|
|
@@ -59,6 +59,15 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
59
59
|
private boolean _restartInProgress = false;
|
|
60
60
|
private ArrayList<Boolean> _restartQueue = new ArrayList<>();
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether the React Native instance this module was created for is still the one running.
|
|
64
|
+
*
|
|
65
|
+
* A reload replaces the instance while work this module started can still be in flight,
|
|
66
|
+
* and a frame callback posted to the process-wide choreographer outlives the executor
|
|
67
|
+
* this module shuts down. Read from whichever thread that work is on, so it is volatile.
|
|
68
|
+
*/
|
|
69
|
+
private volatile boolean mGenerationAlive = true;
|
|
70
|
+
|
|
62
71
|
public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) {
|
|
63
72
|
super(reactContext);
|
|
64
73
|
|
|
@@ -85,6 +94,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
85
94
|
|
|
86
95
|
@Override
|
|
87
96
|
public void invalidate() {
|
|
97
|
+
mGenerationAlive = false;
|
|
88
98
|
clearLifecycleEventListener();
|
|
89
99
|
mBackgroundExecutor.shutdownNow();
|
|
90
100
|
super.invalidate();
|
|
@@ -264,6 +274,14 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
private void emitDownloadProgressEvent(DownloadProgress downloadProgress) {
|
|
277
|
+
// Every progress event arrives here, including the ones a frame callback delivers
|
|
278
|
+
// after this module was invalidated: shutting the executor down does not cancel a
|
|
279
|
+
// callback already posted to the choreographer, and the context it would emit
|
|
280
|
+
// through has been destroyed by then.
|
|
281
|
+
if (!mGenerationAlive) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
267
285
|
if (mEventEmitterCallback != null) {
|
|
268
286
|
emitOnDownloadProgress(downloadProgress.createWritableMap());
|
|
269
287
|
return;
|
|
@@ -275,6 +293,13 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
275
293
|
}
|
|
276
294
|
|
|
277
295
|
private void restartAppInternal(boolean onlyIfUpdateIsPending) {
|
|
296
|
+
// A restart requested by an instance that has been replaced would reload the one
|
|
297
|
+
// that replaced it, which nobody asked for.
|
|
298
|
+
if (!mGenerationAlive) {
|
|
299
|
+
CodePushUtils.log("Ignoring a restart requested by a React Native instance that is no longer running");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
278
303
|
if (this._restartInProgress) {
|
|
279
304
|
CodePushUtils.log("Restart request queued until the current restart is completed");
|
|
280
305
|
this._restartQueue.add(onlyIfUpdateIsPending);
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
|
-
const nodeRequire = createRequire(import.meta.url);
|
|
5
4
|
/**
|
|
6
5
|
* allows to require a config file with .ts extension
|
|
7
6
|
*/
|
|
8
7
|
function requireConfig(filePath) {
|
|
9
8
|
const ext = path.extname(filePath);
|
|
9
|
+
// Resolve the loader from the project the config file lives in, not from the CLI install.
|
|
10
|
+
const projectRequire = createRequire(filePath);
|
|
10
11
|
if (ext === '.ts') {
|
|
11
|
-
|
|
12
|
-
nodeRequire('ts-node/register');
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
console.error('ts-node not found. Please install ts-node as a devDependency.');
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
12
|
+
return unwrapDefaultExport(requireTsConfig(projectRequire, filePath));
|
|
18
13
|
}
|
|
19
14
|
else if (ext === '.js') {
|
|
20
15
|
// do nothing
|
|
@@ -22,7 +17,38 @@ function requireConfig(filePath) {
|
|
|
22
17
|
else {
|
|
23
18
|
throw new Error(`Unsupported file extension: ${ext}`);
|
|
24
19
|
}
|
|
25
|
-
return
|
|
20
|
+
return unwrapDefaultExport(projectRequire(filePath));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* tsx is preferred: it needs no tsconfig setup and resolves tsconfig `paths` aliases on its own.
|
|
24
|
+
* ts-node keeps working for projects that already have it configured.
|
|
25
|
+
*/
|
|
26
|
+
function requireTsConfig(projectRequire, filePath) {
|
|
27
|
+
if (canResolve(projectRequire, 'tsx/cjs/api')) {
|
|
28
|
+
const { require: tsxRequire } = projectRequire('tsx/cjs/api');
|
|
29
|
+
return tsxRequire(filePath, filePath);
|
|
30
|
+
}
|
|
31
|
+
if (canResolve(projectRequire, 'ts-node/register')) {
|
|
32
|
+
console.warn('warn: Loading the config file with ts-node, which is no longer maintained. Support for it ' +
|
|
33
|
+
'will be removed in a future major version - please install tsx instead (`npm i -D tsx`).');
|
|
34
|
+
projectRequire('ts-node/register');
|
|
35
|
+
return projectRequire(filePath);
|
|
36
|
+
}
|
|
37
|
+
console.error('A TypeScript config file needs a loader. Please install tsx as a devDependency (`npm i -D tsx`).');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
function canResolve(projectRequire, id) {
|
|
41
|
+
try {
|
|
42
|
+
projectRequire.resolve(id);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// `export default` compiles to `exports.default`; `module.exports =` is returned as is.
|
|
50
|
+
function unwrapDefaultExport(loaded) {
|
|
51
|
+
return (loaded.default ?? loaded);
|
|
26
52
|
}
|
|
27
53
|
export function findAndReadConfigFile(startDir, configFileName) {
|
|
28
54
|
let dir = startDir;
|
package/cli/package.json
CHANGED
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
"yauzl": "^3.2.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"ts-node": ">=10"
|
|
22
|
+
"ts-node": ">=10",
|
|
23
|
+
"tsx": ">=4"
|
|
23
24
|
},
|
|
24
25
|
"peerDependenciesMeta": {
|
|
25
26
|
"ts-node": {
|
|
26
27
|
"optional": true
|
|
28
|
+
},
|
|
29
|
+
"tsx": {
|
|
30
|
+
"optional": true
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"engines": {
|
package/cli/utils/fsUtils.ts
CHANGED
|
@@ -3,28 +3,62 @@ import path from "path";
|
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
import type { CliConfigInterface } from "../../typings/react-native-code-push.d.ts";
|
|
5
5
|
|
|
6
|
-
const nodeRequire = createRequire(import.meta.url);
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* allows to require a config file with .ts extension
|
|
10
8
|
*/
|
|
11
9
|
function requireConfig(filePath: string): CliConfigInterface {
|
|
12
10
|
const ext = path.extname(filePath);
|
|
11
|
+
// Resolve the loader from the project the config file lives in, not from the CLI install.
|
|
12
|
+
const projectRequire = createRequire(filePath);
|
|
13
13
|
|
|
14
14
|
if (ext === '.ts') {
|
|
15
|
-
|
|
16
|
-
nodeRequire('ts-node/register');
|
|
17
|
-
} catch {
|
|
18
|
-
console.error('ts-node not found. Please install ts-node as a devDependency.');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
15
|
+
return unwrapDefaultExport(requireTsConfig(projectRequire, filePath));
|
|
21
16
|
} else if (ext === '.js') {
|
|
22
17
|
// do nothing
|
|
23
18
|
} else {
|
|
24
19
|
throw new Error(`Unsupported file extension: ${ext}`);
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
return
|
|
22
|
+
return unwrapDefaultExport(projectRequire(filePath));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* tsx is preferred: it needs no tsconfig setup and resolves tsconfig `paths` aliases on its own.
|
|
27
|
+
* ts-node keeps working for projects that already have it configured.
|
|
28
|
+
*/
|
|
29
|
+
function requireTsConfig(projectRequire: NodeRequire, filePath: string): { default?: unknown } {
|
|
30
|
+
if (canResolve(projectRequire, 'tsx/cjs/api')) {
|
|
31
|
+
const { require: tsxRequire } = projectRequire('tsx/cjs/api') as {
|
|
32
|
+
require: (id: string, fromFile: string) => { default?: unknown };
|
|
33
|
+
};
|
|
34
|
+
return tsxRequire(filePath, filePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (canResolve(projectRequire, 'ts-node/register')) {
|
|
38
|
+
console.warn(
|
|
39
|
+
'warn: Loading the config file with ts-node, which is no longer maintained. Support for it ' +
|
|
40
|
+
'will be removed in a future major version - please install tsx instead (`npm i -D tsx`).',
|
|
41
|
+
);
|
|
42
|
+
projectRequire('ts-node/register');
|
|
43
|
+
return projectRequire(filePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.error('A TypeScript config file needs a loader. Please install tsx as a devDependency (`npm i -D tsx`).');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function canResolve(projectRequire: NodeRequire, id: string): boolean {
|
|
51
|
+
try {
|
|
52
|
+
projectRequire.resolve(id);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `export default` compiles to `exports.default`; `module.exports =` is returned as is.
|
|
60
|
+
function unwrapDefaultExport(loaded: { default?: unknown }): CliConfigInterface {
|
|
61
|
+
return (loaded.default ?? loaded) as CliConfigInterface;
|
|
28
62
|
}
|
|
29
63
|
|
|
30
64
|
export function findAndReadConfigFile(startDir: string, configFileName: string): CliConfigInterface {
|
package/ios/CodePush/CodePush.h
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
@interface CodePush : RCTEventEmitter
|
|
37
37
|
#endif
|
|
38
38
|
|
|
39
|
+
/*
|
|
40
|
+
* Returns the bundle embedded in the binary, without resolving an installed update.
|
|
41
|
+
* The update pipeline uses this as the base for binary patches and asset diffs.
|
|
42
|
+
*/
|
|
39
43
|
+ (NSURL *)binaryBundleURL;
|
|
40
44
|
/*
|
|
41
45
|
* This method is used to retrieve the URL for the most recent
|
|
@@ -64,8 +68,10 @@
|
|
|
64
68
|
subdirectory:(NSString *)resourceSubdirectory
|
|
65
69
|
bundle:(NSBundle *)resourceBundle;
|
|
66
70
|
|
|
71
|
+
/* Internal storage root used by CodePushPackage. */
|
|
67
72
|
+ (NSString *)getApplicationSupportDirectory;
|
|
68
73
|
|
|
74
|
+
/* Returns the embedded assets path used as the base for asset diffs. */
|
|
69
75
|
+ (NSString *)bundleAssetsPath;
|
|
70
76
|
|
|
71
77
|
/*
|
|
@@ -76,9 +82,8 @@
|
|
|
76
82
|
+ (void)overrideAppVersion:(NSString *)appVersion;
|
|
77
83
|
|
|
78
84
|
/*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* the Info.plist file's CodePushDeploymentKey setting.
|
|
85
|
+
* Retained for compatibility. The current implementation ignores the argument
|
|
86
|
+
* and uses the deprecated deployment key placeholder.
|
|
82
87
|
*/
|
|
83
88
|
+ (void)setDeploymentKey:(NSString *)deploymentKey;
|
|
84
89
|
|
package/ios/CodePush/CodePush.mm
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#import <React/RCTAssert.h>
|
|
2
2
|
#import <React/RCTBridgeModule.h>
|
|
3
3
|
#import <React/RCTConvert.h>
|
|
4
|
+
#import <React/RCTInvalidating.h>
|
|
4
5
|
#import <React/RCTEventDispatcher.h>
|
|
5
6
|
#import <React/RCTRootView.h>
|
|
6
7
|
#import <React/RCTUtils.h>
|
|
@@ -8,7 +9,9 @@
|
|
|
8
9
|
|
|
9
10
|
#import "CodePush.h"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
#include <atomic>
|
|
13
|
+
|
|
14
|
+
@interface CodePush () <RCTFrameUpdateObserver, RCTInvalidating
|
|
12
15
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
13
16
|
, NativeCodePushSpec
|
|
14
17
|
#endif
|
|
@@ -32,6 +35,17 @@
|
|
|
32
35
|
BOOL _allowed;
|
|
33
36
|
BOOL _restartInProgress;
|
|
34
37
|
NSMutableArray *_restartQueue;
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* Whether the React Native instance this module was created for is still the one
|
|
41
|
+
* running.
|
|
42
|
+
*
|
|
43
|
+
* A reload starts the next instance before the one being replaced has finished being
|
|
44
|
+
* torn down, so work this instance started can still be running with nothing left to
|
|
45
|
+
* hand its result to. Read from whichever thread that work is on, written when the
|
|
46
|
+
* instance is invalidated, so it is atomic.
|
|
47
|
+
*/
|
|
48
|
+
std::atomic<bool> _generationAlive;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
RCT_EXPORT_MODULE()
|
|
@@ -307,7 +321,42 @@ static NSString *const LatestRollbackCountKey = @"count";
|
|
|
307
321
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
/*
|
|
325
|
+
* Called when the React Native instance this module belongs to is being replaced.
|
|
326
|
+
*
|
|
327
|
+
* A download in flight keeps this module alive past that point, and the suspend timer and
|
|
328
|
+
* the resume notifications keep it reachable too. None of them may act afterwards: the
|
|
329
|
+
* runtime they would report to is gone, and the restart they would trigger belongs to a
|
|
330
|
+
* generation that is no longer on screen.
|
|
331
|
+
*/
|
|
332
|
+
- (void)invalidate
|
|
333
|
+
{
|
|
334
|
+
_generationAlive.store(false);
|
|
335
|
+
|
|
336
|
+
[_appSuspendTimer invalidate];
|
|
337
|
+
_appSuspendTimer = nil;
|
|
338
|
+
_hasResumeListener = NO;
|
|
339
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
340
|
+
|
|
341
|
+
#ifndef RCT_NEW_ARCH_ENABLED
|
|
342
|
+
[super invalidate];
|
|
343
|
+
#endif
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Whether this module still belongs to the React Native instance that is running. */
|
|
347
|
+
- (BOOL)isGenerationAlive
|
|
348
|
+
{
|
|
349
|
+
return _generationAlive.load();
|
|
350
|
+
}
|
|
351
|
+
|
|
310
352
|
- (void)dispatchDownloadProgressEvent {
|
|
353
|
+
// The download that is reporting here can be one this module started before the app was
|
|
354
|
+
// reloaded, in which case the event emitter it would reach belongs to a runtime that has
|
|
355
|
+
// been torn down.
|
|
356
|
+
if (![self isGenerationAlive]) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
311
360
|
// Notify the script-side about the progress
|
|
312
361
|
NSDictionary *progress = @{
|
|
313
362
|
@"totalBytes" : [NSNumber numberWithLongLong:_latestExpectedContentLength],
|
|
@@ -375,6 +424,7 @@ static NSString *const LatestRollbackCountKey = @"count";
|
|
|
375
424
|
_restartInProgress = NO;
|
|
376
425
|
_restartQueue = [NSMutableArray arrayWithCapacity:1];
|
|
377
426
|
_lastProgressEventTime = 0;
|
|
427
|
+
_generationAlive.store(true);
|
|
378
428
|
|
|
379
429
|
self = [super init];
|
|
380
430
|
if (self) {
|
|
@@ -533,6 +583,11 @@ static NSString *const LatestRollbackCountKey = @"count";
|
|
|
533
583
|
*/
|
|
534
584
|
- (void)loadBundle
|
|
535
585
|
{
|
|
586
|
+
if (![self isGenerationAlive]) {
|
|
587
|
+
CPLog(@"Ignoring a reload requested by a React Native instance that is no longer running.");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
536
591
|
@synchronized([CodePush class]) {
|
|
537
592
|
hasInitializedUpdateAfterRestartForCurrentLoad = NO;
|
|
538
593
|
}
|
|
@@ -802,6 +857,11 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
802
857
|
|
|
803
858
|
- (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
|
|
804
859
|
{
|
|
860
|
+
if (![self isGenerationAlive]) {
|
|
861
|
+
CPLog(@"Ignoring a restart requested by a React Native instance that is no longer running.");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
805
865
|
if (_restartInProgress) {
|
|
806
866
|
CPLog(@"Restart request queued until the current restart is completed.");
|
|
807
867
|
[_restartQueue addObject:@(onlyIfUpdateIsPending)];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bravemobile/react-native-code-push",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.2.0",
|
|
4
4
|
"description": "React Native plugin for the CodePush service",
|
|
5
5
|
"main": "src/CodePush.js",
|
|
6
6
|
"react-native": "src/CodePush.js",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
],
|
|
72
72
|
"scripts": {
|
|
73
73
|
"setup": "npm install --quiet --no-progress",
|
|
74
|
-
"setup-example-app": "
|
|
75
|
-
"setup-expo-example-app": "
|
|
74
|
+
"setup-example-app": "tsx scripts/setupExampleApp/runSetupExampleApp.ts",
|
|
75
|
+
"setup-expo-example-app": "tsx scripts/setupExpoExampleApp/runSetupExpoExampleApp.ts",
|
|
76
76
|
"type:src": "tsc --project tsconfig.json --noEmit",
|
|
77
77
|
"type:cli": "npm run --workspace cli typecheck",
|
|
78
78
|
"type:scripts": "tsc --project scripts/tsconfig.json --noEmit",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"prepack": "npm run build:cli",
|
|
83
83
|
"eslint": "eslint --quiet .",
|
|
84
84
|
"jest": "jest src/CodePush.test.js src/versioning/* expo/* && npm run --workspace cli test",
|
|
85
|
-
"e2e": "
|
|
85
|
+
"e2e": "tsx e2e/run.ts"
|
|
86
86
|
},
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
|
@@ -102,11 +102,19 @@
|
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"expo": ">=50.0.0",
|
|
105
|
-
"react-native": "*"
|
|
105
|
+
"react-native": "*",
|
|
106
|
+
"ts-node": ">=10",
|
|
107
|
+
"tsx": ">=4"
|
|
106
108
|
},
|
|
107
109
|
"peerDependenciesMeta": {
|
|
108
110
|
"expo": {
|
|
109
111
|
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"ts-node": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
116
|
+
"tsx": {
|
|
117
|
+
"optional": true
|
|
110
118
|
}
|
|
111
119
|
},
|
|
112
120
|
"devDependencies": {
|
|
@@ -128,7 +136,7 @@
|
|
|
128
136
|
"jest": "^29.7.0",
|
|
129
137
|
"react": "19.2.3",
|
|
130
138
|
"react-native": "0.84.1",
|
|
131
|
-
"
|
|
139
|
+
"tsx": "^4.23.13",
|
|
132
140
|
"typescript": "5.0.4",
|
|
133
141
|
"typescript-eslint": "^8.11.0"
|
|
134
142
|
},
|
package/src/CodePush.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Alert, AppState, Platform } from "react-native";
|
|
|
2
2
|
import log from "./logging";
|
|
3
3
|
import hoistStatics from 'hoist-non-react-statics';
|
|
4
4
|
import { SemverVersioning } from './versioning/SemverVersioning'
|
|
5
|
+
import invokeTelemetryCallback from './invokeTelemetryCallback';
|
|
5
6
|
|
|
6
7
|
const NativeCodePushModule = require("./native/NativeCodePush");
|
|
7
8
|
let NativeCodePush = NativeCodePushModule.getNativeCodePush?.() ?? NativeCodePushModule.default;
|
|
@@ -83,7 +84,7 @@ async function decideLatestReleaseIsInRollout(versioning, clientId, onRolloutSki
|
|
|
83
84
|
|
|
84
85
|
if (!inRollout) {
|
|
85
86
|
log(`Skipping update due to rollout. Bucket ${bucket} is not smaller than rollout range ${latestReleaseInfo.rollout}.`);
|
|
86
|
-
onRolloutSkipped
|
|
87
|
+
invokeTelemetryCallback('onRolloutSkipped', onRolloutSkipped, latestVersion);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
return inRollout;
|
|
@@ -378,11 +379,11 @@ async function tryReportStatus(statusReport, retryOnAppResume) {
|
|
|
378
379
|
const label = statusReport.package.label;
|
|
379
380
|
if (statusReport.status === "DeploymentSucceeded") {
|
|
380
381
|
log(`Reporting CodePush update success (${label})`);
|
|
381
|
-
sharedCodePushOptions?.onUpdateSuccess
|
|
382
|
+
invokeTelemetryCallback('onUpdateSuccess', sharedCodePushOptions?.onUpdateSuccess, label);
|
|
382
383
|
} else {
|
|
383
384
|
log(`Reporting CodePush update rollback (${label})`);
|
|
384
385
|
await nativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash);
|
|
385
|
-
sharedCodePushOptions?.onUpdateRollback
|
|
386
|
+
invokeTelemetryCallback('onUpdateRollback', sharedCodePushOptions?.onUpdateRollback, label);
|
|
386
387
|
}
|
|
387
388
|
}
|
|
388
389
|
|
|
@@ -565,20 +566,8 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
565
566
|
...options,
|
|
566
567
|
};
|
|
567
568
|
|
|
568
|
-
/*
|
|
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
|
|
572
|
-
* download exactly as it was before this option existed.
|
|
573
|
-
*/
|
|
574
569
|
const onUpdateArchiveResult = typeof syncOptions.onUpdateArchiveResult === "function"
|
|
575
|
-
? (label, result) =>
|
|
576
|
-
try {
|
|
577
|
-
syncOptions.onUpdateArchiveResult(label, result);
|
|
578
|
-
} catch (error) {
|
|
579
|
-
log(`An error has occurred : ${error.stack}`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
570
|
+
? (label, result) => invokeTelemetryCallback('onUpdateArchiveResult', syncOptions.onUpdateArchiveResult, label, result)
|
|
582
571
|
: null;
|
|
583
572
|
|
|
584
573
|
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
|
|
@@ -630,14 +619,14 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
630
619
|
|
|
631
620
|
const doDownloadAndInstall = async () => {
|
|
632
621
|
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
|
|
633
|
-
sharedCodePushOptions.onDownloadStart
|
|
622
|
+
invokeTelemetryCallback('onDownloadStart', sharedCodePushOptions.onDownloadStart, remotePackageLabel);
|
|
634
623
|
|
|
635
624
|
const localPackage = await remotePackage.download(
|
|
636
625
|
downloadProgressCallback,
|
|
637
626
|
onUpdateArchiveResult && ((result) => onUpdateArchiveResult(remotePackageLabel, result)),
|
|
638
627
|
);
|
|
639
628
|
|
|
640
|
-
sharedCodePushOptions.onDownloadSuccess
|
|
629
|
+
invokeTelemetryCallback('onDownloadSuccess', sharedCodePushOptions.onDownloadSuccess, remotePackageLabel);
|
|
641
630
|
|
|
642
631
|
// Determine the correct install mode based on whether the update is mandatory or not.
|
|
643
632
|
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
|
|
@@ -721,7 +710,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
721
710
|
}
|
|
722
711
|
} catch (error) {
|
|
723
712
|
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
|
|
724
|
-
sharedCodePushOptions?.onSyncError
|
|
713
|
+
invokeTelemetryCallback('onSyncError', sharedCodePushOptions?.onSyncError, remotePackageLabel ?? 'unknown', error);
|
|
725
714
|
log(error.message);
|
|
726
715
|
throw error;
|
|
727
716
|
}
|
|
@@ -752,26 +741,26 @@ let CodePush;
|
|
|
752
741
|
* updateChecker: updateChecker | undefined,
|
|
753
742
|
* setUpdateChecker(updateCheckerFunction: updateChecker | undefined): void,
|
|
754
743
|
*
|
|
755
|
-
* onUpdateSuccess: (label: string) => void | undefined,
|
|
756
|
-
* setOnUpdateSuccess(onUpdateSuccessFunction: (label: string) => void | undefined): void,
|
|
744
|
+
* onUpdateSuccess: (label: string) => void | Promise<void> | undefined,
|
|
745
|
+
* setOnUpdateSuccess(onUpdateSuccessFunction: (label: string) => void | Promise<void> | undefined): void,
|
|
757
746
|
*
|
|
758
|
-
* onUpdateRollback: (label: string) => void | undefined,
|
|
759
|
-
* setOnUpdateRollback(onUpdateRollbackFunction: (label: string) => void | undefined): void,
|
|
747
|
+
* onUpdateRollback: (label: string) => void | Promise<void> | undefined,
|
|
748
|
+
* setOnUpdateRollback(onUpdateRollbackFunction: (label: string) => void | Promise<void> | undefined): void,
|
|
760
749
|
*
|
|
761
|
-
* onDownloadStart: (label: string) => void | undefined,
|
|
762
|
-
* setOnDownloadStart(onDownloadStartFunction: (label: string) => void | undefined): void,
|
|
750
|
+
* onDownloadStart: (label: string) => void | Promise<void> | undefined,
|
|
751
|
+
* setOnDownloadStart(onDownloadStartFunction: (label: string) => void | Promise<void> | undefined): void,
|
|
763
752
|
*
|
|
764
|
-
* onDownloadSuccess: (label: string) => void | undefined,
|
|
765
|
-
* setOnDownloadSuccess(onDownloadSuccessFunction: (label: string) => void | undefined): void,
|
|
753
|
+
* onDownloadSuccess: (label: string) => void | Promise<void> | undefined,
|
|
754
|
+
* setOnDownloadSuccess(onDownloadSuccessFunction: (label: string) => void | Promise<void> | undefined): void,
|
|
766
755
|
*
|
|
767
|
-
* onSyncError: (label: string, error: Error) => void | undefined,
|
|
768
|
-
* setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
|
|
756
|
+
* onSyncError: (label: string, error: Error) => void | Promise<void> | undefined,
|
|
757
|
+
* setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | Promise<void> | undefined): void,
|
|
769
758
|
*
|
|
770
|
-
* onRolloutSkipped: (label: string, error: Error) => void | undefined,
|
|
771
|
-
* setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
|
|
759
|
+
* onRolloutSkipped: (label: string, error: Error) => void | Promise<void> | undefined,
|
|
760
|
+
* setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | Promise<void> | undefined): void,
|
|
772
761
|
*
|
|
773
|
-
* onUpdateArchiveResult: (label: string, result: object) => void | undefined,
|
|
774
|
-
* setOnUpdateArchiveResult(onUpdateArchiveResultFunction: (label: string, result: object) => void | undefined): void,
|
|
762
|
+
* onUpdateArchiveResult: (label: string, result: object) => void | Promise<void> | undefined,
|
|
763
|
+
* setOnUpdateArchiveResult(onUpdateArchiveResultFunction: (label: string, result: object) => void | Promise<void> | undefined): void,
|
|
775
764
|
* }}
|
|
776
765
|
*/
|
|
777
766
|
const sharedCodePushOptions = {
|
package/src/CodePush.test.js
CHANGED
|
@@ -64,7 +64,7 @@ function diffRelease() {
|
|
|
64
64
|
* which only a download that had an archive to try comes back with.
|
|
65
65
|
* @param installedPackage the CodePush update the app is running, if any.
|
|
66
66
|
*/
|
|
67
|
-
function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
|
|
67
|
+
function createNativeBridge({ updateArchiveResult, installedPackage, statusReport } = {}) {
|
|
68
68
|
return {
|
|
69
69
|
addDownloadProgressListener: jest.fn(() => ({ remove: jest.fn() })),
|
|
70
70
|
downloadUpdate: jest.fn(async (updatePackage) => ({
|
|
@@ -78,9 +78,12 @@ function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
|
|
|
78
78
|
// The rest of what a full `sync()` reaches for.
|
|
79
79
|
InstallMode,
|
|
80
80
|
clearPendingRestart: jest.fn(),
|
|
81
|
-
getNewStatusReport: jest.fn(async () => null),
|
|
81
|
+
getNewStatusReport: jest.fn(async () => statusReport ?? null),
|
|
82
82
|
installUpdate: jest.fn(async () => {}),
|
|
83
83
|
notifyApplicationReady: jest.fn(async () => {}),
|
|
84
|
+
recordStatusReported: jest.fn(),
|
|
85
|
+
saveStatusReportForRetry: jest.fn(),
|
|
86
|
+
setLatestRollbackInfo: jest.fn(async () => {}),
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -89,13 +92,13 @@ function createNativeBridge({ updateArchiveResult, installedPackage } = {}) {
|
|
|
89
92
|
* native bridge live on the module itself - configured the way an app configures it: the
|
|
90
93
|
* decorator registers the app-wide options, and every sync below is one the app asks for.
|
|
91
94
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
95
|
+
* Callback options left after the bridge setup values are removed are registered on the
|
|
96
|
+
* decorator, the same way an app registers them.
|
|
94
97
|
*/
|
|
95
|
-
function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult,
|
|
98
|
+
function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult, installedPackage, statusReport, ...codePushOptions } = {}) {
|
|
96
99
|
jest.resetModules();
|
|
97
100
|
const CodePush = require('./CodePush');
|
|
98
|
-
const nativeBridge = createNativeBridge({ updateArchiveResult, installedPackage });
|
|
101
|
+
const nativeBridge = createNativeBridge({ updateArchiveResult, installedPackage, statusReport });
|
|
99
102
|
|
|
100
103
|
CodePush.setUpTestDependencies(
|
|
101
104
|
null,
|
|
@@ -106,7 +109,7 @@ function loadCodePush({ releaseHistory = {}, updateChecker, updateArchiveResult,
|
|
|
106
109
|
checkFrequency: CodePush.CheckFrequency.MANUAL,
|
|
107
110
|
releaseHistoryFetcher: async () => releaseHistory,
|
|
108
111
|
updateChecker,
|
|
109
|
-
|
|
112
|
+
...codePushOptions,
|
|
110
113
|
});
|
|
111
114
|
|
|
112
115
|
return { CodePush, nativeBridge };
|
|
@@ -492,6 +495,40 @@ describe('the update archive result of a sync', () => {
|
|
|
492
495
|
expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
|
|
493
496
|
});
|
|
494
497
|
|
|
498
|
+
it('installs the update when the callback returns a rejected Promise', async () => {
|
|
499
|
+
const { CodePush, nativeBridge } = loadCodePush({
|
|
500
|
+
releaseHistory: patchedRelease(),
|
|
501
|
+
updateArchiveResult: APPLIED,
|
|
502
|
+
});
|
|
503
|
+
const onUpdateArchiveResult = jest.fn(async () => {
|
|
504
|
+
throw new Error('the telemetry request failed');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const syncStatus = await CodePush.sync({ onUpdateArchiveResult });
|
|
508
|
+
await Promise.resolve();
|
|
509
|
+
|
|
510
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
511
|
+
expect(onUpdateArchiveResult).toHaveBeenCalledWith(LABEL, APPLIED);
|
|
512
|
+
expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('resolves a direct download when the callback returns a rejected Promise', async () => {
|
|
516
|
+
const { CodePush } = loadCodePush({
|
|
517
|
+
releaseHistory: patchedRelease(),
|
|
518
|
+
updateArchiveResult: APPLIED,
|
|
519
|
+
});
|
|
520
|
+
const onUpdateArchiveResult = jest.fn(async () => {
|
|
521
|
+
throw new Error('the telemetry request failed');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const remotePackage = await CodePush.checkForUpdate();
|
|
525
|
+
const localPackage = await remotePackage.download(undefined, onUpdateArchiveResult);
|
|
526
|
+
await Promise.resolve();
|
|
527
|
+
|
|
528
|
+
expect(onUpdateArchiveResult).toHaveBeenCalledWith(APPLIED);
|
|
529
|
+
expect(localPackage).toMatchObject({ label: LABEL, packageHash: PACKAGE_HASH });
|
|
530
|
+
});
|
|
531
|
+
|
|
495
532
|
it('leaves the result off the package a download resolves with', async () => {
|
|
496
533
|
const { CodePush } = loadCodePush({
|
|
497
534
|
releaseHistory: patchedRelease(),
|
|
@@ -505,3 +542,110 @@ describe('the update archive result of a sync', () => {
|
|
|
505
542
|
expect(localPackage).not.toHaveProperty('updateArchiveResult');
|
|
506
543
|
});
|
|
507
544
|
});
|
|
545
|
+
|
|
546
|
+
describe('telemetry callback errors', () => {
|
|
547
|
+
it('installs the update when onDownloadStart throws', async () => {
|
|
548
|
+
const onDownloadStart = jest.fn(() => {
|
|
549
|
+
throw new Error('the telemetry client is unavailable');
|
|
550
|
+
});
|
|
551
|
+
const { CodePush, nativeBridge } = loadCodePush({
|
|
552
|
+
releaseHistory: fullOnlyRelease(),
|
|
553
|
+
onDownloadStart,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const syncStatus = await CodePush.sync();
|
|
557
|
+
|
|
558
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
559
|
+
expect(onDownloadStart).toHaveBeenCalledWith(LABEL);
|
|
560
|
+
expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('installs the update when onDownloadSuccess returns a rejected Promise', async () => {
|
|
564
|
+
const onDownloadSuccess = jest.fn(async () => {
|
|
565
|
+
throw new Error('the telemetry request failed');
|
|
566
|
+
});
|
|
567
|
+
const { CodePush, nativeBridge } = loadCodePush({
|
|
568
|
+
releaseHistory: fullOnlyRelease(),
|
|
569
|
+
onDownloadSuccess,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const syncStatus = await CodePush.sync();
|
|
573
|
+
await Promise.resolve();
|
|
574
|
+
|
|
575
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
576
|
+
expect(onDownloadSuccess).toHaveBeenCalledWith(LABEL);
|
|
577
|
+
expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('uses the next release when onRolloutSkipped throws', async () => {
|
|
581
|
+
const onRolloutSkipped = jest.fn(() => {
|
|
582
|
+
throw new Error('the telemetry client is unavailable');
|
|
583
|
+
});
|
|
584
|
+
const { CodePush } = loadCodePush({
|
|
585
|
+
releaseHistory: {
|
|
586
|
+
...fullOnlyRelease(),
|
|
587
|
+
'1.0.2': {
|
|
588
|
+
...fullOnlyRelease()[LABEL],
|
|
589
|
+
rollout: 0,
|
|
590
|
+
packageHash: 'b'.repeat(64),
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
onRolloutSkipped,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const remotePackage = await CodePush.checkForUpdate();
|
|
597
|
+
|
|
598
|
+
expect(onRolloutSkipped).toHaveBeenCalledWith('1.0.2');
|
|
599
|
+
expect(remotePackage.label).toBe(LABEL);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('rejects with the original error when onSyncError throws', async () => {
|
|
603
|
+
const downloadError = new Error('the update download failed');
|
|
604
|
+
const onSyncError = jest.fn(() => {
|
|
605
|
+
throw new Error('the telemetry client is unavailable');
|
|
606
|
+
});
|
|
607
|
+
const { CodePush, nativeBridge } = loadCodePush({
|
|
608
|
+
releaseHistory: fullOnlyRelease(),
|
|
609
|
+
onSyncError,
|
|
610
|
+
});
|
|
611
|
+
nativeBridge.downloadUpdate.mockRejectedValueOnce(downloadError);
|
|
612
|
+
|
|
613
|
+
await expect(CodePush.sync()).rejects.toBe(downloadError);
|
|
614
|
+
expect(onSyncError).toHaveBeenCalledWith(LABEL, downloadError);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('records a successful update when onUpdateSuccess throws', async () => {
|
|
618
|
+
const statusReport = {
|
|
619
|
+
status: 'DeploymentSucceeded',
|
|
620
|
+
package: { label: LABEL, packageHash: PACKAGE_HASH },
|
|
621
|
+
};
|
|
622
|
+
const onUpdateSuccess = jest.fn(() => {
|
|
623
|
+
throw new Error('the telemetry client is unavailable');
|
|
624
|
+
});
|
|
625
|
+
const { CodePush, nativeBridge } = loadCodePush({ statusReport, onUpdateSuccess });
|
|
626
|
+
|
|
627
|
+
await CodePush.notifyAppReady();
|
|
628
|
+
|
|
629
|
+
expect(onUpdateSuccess).toHaveBeenCalledWith(LABEL);
|
|
630
|
+
expect(nativeBridge.recordStatusReported).toHaveBeenCalledWith(statusReport);
|
|
631
|
+
expect(nativeBridge.saveStatusReportForRetry).not.toHaveBeenCalled();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('records a rollback when onUpdateRollback returns a rejected Promise', async () => {
|
|
635
|
+
const statusReport = {
|
|
636
|
+
status: 'DeploymentFailed',
|
|
637
|
+
package: { label: LABEL, packageHash: PACKAGE_HASH },
|
|
638
|
+
};
|
|
639
|
+
const onUpdateRollback = jest.fn(async () => {
|
|
640
|
+
throw new Error('the telemetry request failed');
|
|
641
|
+
});
|
|
642
|
+
const { CodePush, nativeBridge } = loadCodePush({ statusReport, onUpdateRollback });
|
|
643
|
+
|
|
644
|
+
await CodePush.notifyAppReady();
|
|
645
|
+
await Promise.resolve();
|
|
646
|
+
|
|
647
|
+
expect(onUpdateRollback).toHaveBeenCalledWith(LABEL);
|
|
648
|
+
expect(nativeBridge.recordStatusReported).toHaveBeenCalledWith(statusReport);
|
|
649
|
+
expect(nativeBridge.saveStatusReportForRetry).not.toHaveBeenCalled();
|
|
650
|
+
});
|
|
651
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const log = require("./logging");
|
|
2
|
+
|
|
3
|
+
module.exports = function invokeTelemetryCallback(name, callback, ...args) {
|
|
4
|
+
if (typeof callback !== "function") return;
|
|
5
|
+
|
|
6
|
+
const logError = (error) => {
|
|
7
|
+
log(`The ${name} callback failed: ${error?.message ?? error}`);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const result = callback(...args);
|
|
12
|
+
if (result && typeof result.then === "function") {
|
|
13
|
+
Promise.resolve(result).catch(logError);
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
logError(error);
|
|
17
|
+
}
|
|
18
|
+
};
|
package/src/package-mixins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const invokeTelemetryCallback = require("./invokeTelemetryCallback");
|
|
2
2
|
|
|
3
3
|
// This function is used to augment remote and local
|
|
4
4
|
// package objects with additional functionality/properties
|
|
@@ -42,14 +42,10 @@ module.exports = (NativeCodePush) => {
|
|
|
42
42
|
// taken off the package and reported on its own, leaving the package exactly what
|
|
43
43
|
// the native side saved.
|
|
44
44
|
const { updateArchiveResult, ...downloadedPackage } = downloadResult ?? {};
|
|
45
|
-
if (updateArchiveResult
|
|
45
|
+
if (updateArchiveResult) {
|
|
46
46
|
// The result is an observation, so an observer that throws must not turn a
|
|
47
47
|
// downloaded update into a failed one.
|
|
48
|
-
|
|
49
|
-
updateArchiveResultCallback(updateArchiveResult);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
log(`The update archive result callback threw: ${error?.message ?? error}`);
|
|
52
|
-
}
|
|
48
|
+
invokeTelemetryCallback('onUpdateArchiveResult', updateArchiveResultCallback, updateArchiveResult);
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
return { ...downloadedPackage, ...local };
|
|
@@ -211,27 +211,27 @@ export interface CodePushOptions extends SyncOptions {
|
|
|
211
211
|
/**
|
|
212
212
|
* Callback function that is called when the update installation succeeds.
|
|
213
213
|
*/
|
|
214
|
-
onUpdateSuccess?: (label: string) => void
|
|
214
|
+
onUpdateSuccess?: (label: string) => void | Promise<void>;
|
|
215
215
|
/**
|
|
216
216
|
* Callback function that is called when the update rolled back.
|
|
217
217
|
*/
|
|
218
|
-
onUpdateRollback?: (label: string) => void
|
|
218
|
+
onUpdateRollback?: (label: string) => void | Promise<void>;
|
|
219
219
|
/**
|
|
220
220
|
* Callback function that is called when download starts.
|
|
221
221
|
*/
|
|
222
|
-
onDownloadStart?: (label: string) => void
|
|
222
|
+
onDownloadStart?: (label: string) => void | Promise<void>;
|
|
223
223
|
/**
|
|
224
224
|
* Callback function that is called when download finished successfully.
|
|
225
225
|
*/
|
|
226
|
-
onDownloadSuccess?: (label: string) => void
|
|
226
|
+
onDownloadSuccess?: (label: string) => void | Promise<void>;
|
|
227
227
|
/**
|
|
228
228
|
* Callback function that is called when sync process failed.
|
|
229
229
|
*/
|
|
230
|
-
onSyncError?: (label: string, error: Error) => void
|
|
230
|
+
onSyncError?: (label: string, error: Error) => void | Promise<void>;
|
|
231
231
|
/**
|
|
232
232
|
* Callback function that is called when rollout is skipped.
|
|
233
233
|
*/
|
|
234
|
-
onRolloutSkipped?: (label: string, error: Error) => void
|
|
234
|
+
onRolloutSkipped?: (label: string, error: Error) => void | Promise<void>;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
export interface DownloadProgress {
|
|
@@ -321,7 +321,7 @@ export interface RemotePackage extends Package {
|
|
|
321
321
|
*/
|
|
322
322
|
download(
|
|
323
323
|
downloadProgressCallback?: DownloadProgressCallback,
|
|
324
|
-
updateArchiveResultCallback?: (result: UpdateArchiveResult) => void
|
|
324
|
+
updateArchiveResultCallback?: (result: UpdateArchiveResult) => void | Promise<void>,
|
|
325
325
|
): Promise<LocalPackage>;
|
|
326
326
|
|
|
327
327
|
/**
|
|
@@ -404,7 +404,7 @@ export interface SyncOptions {
|
|
|
404
404
|
* depends on the callback: registering none changes nothing, and one that throws is
|
|
405
405
|
* logged and does not fail the download it is reporting on.
|
|
406
406
|
*/
|
|
407
|
-
onUpdateArchiveResult?: (label: string, result: UpdateArchiveResult) => void
|
|
407
|
+
onUpdateArchiveResult?: (label: string, result: UpdateArchiveResult) => void | Promise<void>;
|
|
408
408
|
|
|
409
409
|
/**
|
|
410
410
|
* Specifies whether to ignore the update if the installation fails.
|