@bravemobile/react-native-code-push 13.1.0-beta.5 → 13.1.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.
@@ -0,0 +1,118 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
5
+ import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
6
+
7
+ /**
8
+ * The command is a thin wrapper around the build script this package ships. What it has
9
+ * to get right is what it hands the script: the directory the CLI will later look in, and
10
+ * `--force` when asked. A script that records what it was handed proves both without
11
+ * cloning and compiling HDiffPatch. The shipped script is run once at the end, against an
12
+ * install it already finds complete, to show the wrapper speaks its actual interface.
13
+ */
14
+
15
+ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
16
+ const SHIPPED_BUILD_SCRIPT = path.join(REPO_ROOT, "scripts", "binary-patch", "build-hdiffpatch.sh");
17
+ /** Where `jest.globalSetup.ts` built the tools for this run. */
18
+ const INSTALLED_TOOLS_DIR = process.env.HDIFFPATCH_TOOLS_DIR || path.join(REPO_ROOT, ".hdiffpatch-tools");
19
+
20
+ let workDir: string;
21
+
22
+ beforeEach(() => {
23
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "build-patch-tools-"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(workDir, { recursive: true, force: true });
28
+ });
29
+
30
+ function writeScript(body: string): string {
31
+ const scriptPath = path.join(workDir, "build.sh");
32
+ fs.writeFileSync(scriptPath, `#!/bin/sh\n${body}\n`, { mode: 0o755 });
33
+ return scriptPath;
34
+ }
35
+
36
+ /** A script that writes the install directory it was given, then each argument, one per line. */
37
+ function writeRecordingScript(): string {
38
+ return writeScript('printf \'%s\\n\' "$HDIFFPATCH_TOOLS_DIR" "$@" > "$HDIFFPATCH_TOOLS_DIR/invocation"');
39
+ }
40
+
41
+ function readInvocation(toolsDir: string): string[] {
42
+ return fs.readFileSync(path.join(toolsDir, "invocation"), "utf8").trimEnd().split("\n");
43
+ }
44
+
45
+ describe("buildPatchTools", () => {
46
+ it("runs the build script against the tools directory it was given, without forcing a rebuild", () => {
47
+ const toolsDir = path.join(workDir, "tools");
48
+ fs.mkdirSync(toolsDir);
49
+
50
+ buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: false });
51
+
52
+ expect(readInvocation(toolsDir)).toEqual([toolsDir]);
53
+ });
54
+
55
+ it("asks the build script to rebuild when forced", () => {
56
+ const toolsDir = path.join(workDir, "tools");
57
+ fs.mkdirSync(toolsDir);
58
+
59
+ buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: true });
60
+
61
+ expect(readInvocation(toolsDir)).toEqual([toolsDir, "--force"]);
62
+ });
63
+
64
+ it("fails with the exit code when the build script fails", () => {
65
+ const buildScriptPath = writeScript("exit 3");
66
+
67
+ expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(/exit code 3/);
68
+ });
69
+
70
+ it("fails naming the script when it cannot be started", () => {
71
+ const buildScriptPath = path.join(workDir, "no-such-script.sh");
72
+
73
+ expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(
74
+ /no-such-script\.sh/,
75
+ );
76
+ });
77
+
78
+ it("succeeds against an install the shipped script already finds complete", () => {
79
+ // Without an install the shipped script clones and compiles for minutes, and
80
+ // `spawnSync` holds the worker past any jest timeout. `jest.globalSetup.ts`
81
+ // provides the install; this says so when it did not.
82
+ const missing = ["hdiffz", "hpatchz"].filter((tool) => !fs.existsSync(path.join(INSTALLED_TOOLS_DIR, tool)));
83
+ if (missing.length > 0) {
84
+ throw new Error(
85
+ `${missing.join(", ")} not installed in ${INSTALLED_TOOLS_DIR}: jest.globalSetup.ts should have built the tools first`,
86
+ );
87
+ }
88
+
89
+ expect(() =>
90
+ buildPatchTools({ buildScriptPath: SHIPPED_BUILD_SCRIPT, toolsDir: INSTALLED_TOOLS_DIR, force: false }),
91
+ ).not.toThrow();
92
+ });
93
+ });
94
+
95
+ /**
96
+ * A CI cache of the installed tools is keyed by this hash, so it has to stay the same for as
97
+ * long as the script would build the same tools, and change as soon as it would not.
98
+ */
99
+ describe("hashBuildScript", () => {
100
+ it("hashes two scripts with the same contents to the same hex digest", () => {
101
+ const first = path.join(workDir, "first.sh");
102
+ const second = path.join(workDir, "second.sh");
103
+ fs.writeFileSync(first, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
104
+ fs.writeFileSync(second, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
105
+
106
+ expect(hashBuildScript(first)).toBe(hashBuildScript(second));
107
+ expect(hashBuildScript(first)).toMatch(/^[0-9a-f]{64}$/);
108
+ });
109
+
110
+ it("hashes a script whose pinned version changed to a different value", () => {
111
+ const before = path.join(workDir, "before.sh");
112
+ const after = path.join(workDir, "after.sh");
113
+ fs.writeFileSync(before, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
114
+ fs.writeFileSync(after, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.4\n");
115
+
116
+ expect(hashBuildScript(after)).not.toBe(hashBuildScript(before));
117
+ });
118
+ });
@@ -0,0 +1,46 @@
1
+ import { spawnSync } from "child_process";
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { TOOLS_DIR_ENV_NAME } from "../../utils/binaryPatch.js";
6
+
7
+ interface BuildPatchToolsOptions {
8
+ /** The `build-hdiffpatch.sh` this package ships. */
9
+ buildScriptPath: string;
10
+ /** Where the script installs `hdiffz` and `hpatchz`. It has to be a place the CLI looks in. */
11
+ toolsDir: string;
12
+ /** Rebuild even when both tools are already there. */
13
+ force: boolean;
14
+ }
15
+
16
+ /**
17
+ * Runs the build script with the install directory the caller chose.
18
+ *
19
+ * The script's output goes straight to the terminal: a first build clones and compiles
20
+ * HDiffPatch, which takes minutes, and a silent wait would look like a hang. So when the
21
+ * script fails, its own message has already been shown, and the error raised here only
22
+ * has to say that it did.
23
+ */
24
+ export function buildPatchTools({ buildScriptPath, toolsDir, force }: BuildPatchToolsOptions): void {
25
+ const result = spawnSync(buildScriptPath, force ? ['--force'] : [], {
26
+ stdio: 'inherit',
27
+ env: { ...process.env, [TOOLS_DIR_ENV_NAME]: toolsDir },
28
+ });
29
+
30
+ if (result.error) {
31
+ throw new Error(`failed to run ${buildScriptPath}: ${result.error.message}`);
32
+ }
33
+ if (result.status !== 0) {
34
+ const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
35
+ throw new Error(`${path.basename(buildScriptPath)} failed with ${reason}`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * SHA-256 of the build script's bytes, for keying a CI cache of the installed tools. The
41
+ * script pins the sources and the build flags, so it changes whenever the tools it would
42
+ * build do.
43
+ */
44
+ export function hashBuildScript(buildScriptPath: string): string {
45
+ return crypto.createHash('sha256').update(fs.readFileSync(buildScriptPath)).digest('hex');
46
+ }
@@ -0,0 +1,44 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { program } from "commander";
4
+ import { TOOLS_DIR_ENV_NAME, TOOLS_DIR_NAME } from "../../utils/binaryPatch.js";
5
+ import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
6
+
7
+ type Options = {
8
+ toolsDir: string;
9
+ force: boolean;
10
+ printHash: boolean;
11
+ }
12
+
13
+ /**
14
+ * The CLI runs compiled, from `cli/dist/commands/buildPatchToolsCommand/`, which puts the
15
+ * package root - and the script shipped under it - four levels up.
16
+ */
17
+ const BUILD_SCRIPT_PATH = fileURLToPath(
18
+ new URL("../../../../scripts/binary-patch/build-hdiffpatch.sh", import.meta.url),
19
+ );
20
+
21
+ /**
22
+ * Installs where `release` will look: `HDIFFPATCH_TOOLS_DIR` when it is set, and otherwise
23
+ * a `.hdiffpatch-tools` directory in the working directory, the first place the lookup
24
+ * checks before walking up.
25
+ */
26
+ const DEFAULT_TOOLS_DIR = process.env[TOOLS_DIR_ENV_NAME] || path.resolve(process.cwd(), TOOLS_DIR_NAME);
27
+
28
+ program.command('build-patch-tools')
29
+ .description('Builds hdiffz and hpatchz from source and installs them where `release --binary-bundle-path` looks for them.\nThe build clones the pinned HDiffPatch sources, so it needs git, make, a C/C++ compiler and network access. It does nothing when the tools are already installed.')
30
+ .option('--tools-dir <path>', 'directory to install the tools into', DEFAULT_TOOLS_DIR)
31
+ .option('--force', 'rebuild even when the tools are already installed', false)
32
+ .option('--print-hash', 'print a hash of the build script, for keying a CI cache of the tools, and exit without building', false)
33
+ .action((options: Options) => {
34
+ if (options.printHash) {
35
+ console.log(hashBuildScript(BUILD_SCRIPT_PATH));
36
+ return;
37
+ }
38
+
39
+ buildPatchTools({
40
+ buildScriptPath: BUILD_SCRIPT_PATH,
41
+ toolsDir: path.resolve(options.toolsDir),
42
+ force: options.force,
43
+ });
44
+ });
@@ -0,0 +1,34 @@
1
+ import { spawnSync } from "child_process";
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { TOOLS_DIR_ENV_NAME } from "../../utils/binaryPatch.js";
6
+ /**
7
+ * Runs the build script with the install directory the caller chose.
8
+ *
9
+ * The script's output goes straight to the terminal: a first build clones and compiles
10
+ * HDiffPatch, which takes minutes, and a silent wait would look like a hang. So when the
11
+ * script fails, its own message has already been shown, and the error raised here only
12
+ * has to say that it did.
13
+ */
14
+ export function buildPatchTools({ buildScriptPath, toolsDir, force }) {
15
+ const result = spawnSync(buildScriptPath, force ? ['--force'] : [], {
16
+ stdio: 'inherit',
17
+ env: { ...process.env, [TOOLS_DIR_ENV_NAME]: toolsDir },
18
+ });
19
+ if (result.error) {
20
+ throw new Error(`failed to run ${buildScriptPath}: ${result.error.message}`);
21
+ }
22
+ if (result.status !== 0) {
23
+ const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
24
+ throw new Error(`${path.basename(buildScriptPath)} failed with ${reason}`);
25
+ }
26
+ }
27
+ /**
28
+ * SHA-256 of the build script's bytes, for keying a CI cache of the installed tools. The
29
+ * script pins the sources and the build flags, so it changes whenever the tools it would
30
+ * build do.
31
+ */
32
+ export function hashBuildScript(buildScriptPath) {
33
+ return crypto.createHash('sha256').update(fs.readFileSync(buildScriptPath)).digest('hex');
34
+ }
@@ -0,0 +1,88 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
5
+ import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
6
+ /**
7
+ * The command is a thin wrapper around the build script this package ships. What it has
8
+ * to get right is what it hands the script: the directory the CLI will later look in, and
9
+ * `--force` when asked. A script that records what it was handed proves both without
10
+ * cloning and compiling HDiffPatch. The shipped script is run once at the end, against an
11
+ * install it already finds complete, to show the wrapper speaks its actual interface.
12
+ */
13
+ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
14
+ const SHIPPED_BUILD_SCRIPT = path.join(REPO_ROOT, "scripts", "binary-patch", "build-hdiffpatch.sh");
15
+ /** Where `jest.globalSetup.ts` built the tools for this run. */
16
+ const INSTALLED_TOOLS_DIR = process.env.HDIFFPATCH_TOOLS_DIR || path.join(REPO_ROOT, ".hdiffpatch-tools");
17
+ let workDir;
18
+ beforeEach(() => {
19
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "build-patch-tools-"));
20
+ });
21
+ afterEach(() => {
22
+ fs.rmSync(workDir, { recursive: true, force: true });
23
+ });
24
+ function writeScript(body) {
25
+ const scriptPath = path.join(workDir, "build.sh");
26
+ fs.writeFileSync(scriptPath, `#!/bin/sh\n${body}\n`, { mode: 0o755 });
27
+ return scriptPath;
28
+ }
29
+ /** A script that writes the install directory it was given, then each argument, one per line. */
30
+ function writeRecordingScript() {
31
+ return writeScript('printf \'%s\\n\' "$HDIFFPATCH_TOOLS_DIR" "$@" > "$HDIFFPATCH_TOOLS_DIR/invocation"');
32
+ }
33
+ function readInvocation(toolsDir) {
34
+ return fs.readFileSync(path.join(toolsDir, "invocation"), "utf8").trimEnd().split("\n");
35
+ }
36
+ describe("buildPatchTools", () => {
37
+ it("runs the build script against the tools directory it was given, without forcing a rebuild", () => {
38
+ const toolsDir = path.join(workDir, "tools");
39
+ fs.mkdirSync(toolsDir);
40
+ buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: false });
41
+ expect(readInvocation(toolsDir)).toEqual([toolsDir]);
42
+ });
43
+ it("asks the build script to rebuild when forced", () => {
44
+ const toolsDir = path.join(workDir, "tools");
45
+ fs.mkdirSync(toolsDir);
46
+ buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: true });
47
+ expect(readInvocation(toolsDir)).toEqual([toolsDir, "--force"]);
48
+ });
49
+ it("fails with the exit code when the build script fails", () => {
50
+ const buildScriptPath = writeScript("exit 3");
51
+ expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(/exit code 3/);
52
+ });
53
+ it("fails naming the script when it cannot be started", () => {
54
+ const buildScriptPath = path.join(workDir, "no-such-script.sh");
55
+ expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(/no-such-script\.sh/);
56
+ });
57
+ it("succeeds against an install the shipped script already finds complete", () => {
58
+ // Without an install the shipped script clones and compiles for minutes, and
59
+ // `spawnSync` holds the worker past any jest timeout. `jest.globalSetup.ts`
60
+ // provides the install; this says so when it did not.
61
+ const missing = ["hdiffz", "hpatchz"].filter((tool) => !fs.existsSync(path.join(INSTALLED_TOOLS_DIR, tool)));
62
+ if (missing.length > 0) {
63
+ throw new Error(`${missing.join(", ")} not installed in ${INSTALLED_TOOLS_DIR}: jest.globalSetup.ts should have built the tools first`);
64
+ }
65
+ expect(() => buildPatchTools({ buildScriptPath: SHIPPED_BUILD_SCRIPT, toolsDir: INSTALLED_TOOLS_DIR, force: false })).not.toThrow();
66
+ });
67
+ });
68
+ /**
69
+ * A CI cache of the installed tools is keyed by this hash, so it has to stay the same for as
70
+ * long as the script would build the same tools, and change as soon as it would not.
71
+ */
72
+ describe("hashBuildScript", () => {
73
+ it("hashes two scripts with the same contents to the same hex digest", () => {
74
+ const first = path.join(workDir, "first.sh");
75
+ const second = path.join(workDir, "second.sh");
76
+ fs.writeFileSync(first, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
77
+ fs.writeFileSync(second, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
78
+ expect(hashBuildScript(first)).toBe(hashBuildScript(second));
79
+ expect(hashBuildScript(first)).toMatch(/^[0-9a-f]{64}$/);
80
+ });
81
+ it("hashes a script whose pinned version changed to a different value", () => {
82
+ const before = path.join(workDir, "before.sh");
83
+ const after = path.join(workDir, "after.sh");
84
+ fs.writeFileSync(before, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
85
+ fs.writeFileSync(after, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.4\n");
86
+ expect(hashBuildScript(after)).not.toBe(hashBuildScript(before));
87
+ });
88
+ });
@@ -0,0 +1,32 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { program } from "commander";
4
+ import { TOOLS_DIR_ENV_NAME, TOOLS_DIR_NAME } from "../../utils/binaryPatch.js";
5
+ import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
6
+ /**
7
+ * The CLI runs compiled, from `cli/dist/commands/buildPatchToolsCommand/`, which puts the
8
+ * package root - and the script shipped under it - four levels up.
9
+ */
10
+ const BUILD_SCRIPT_PATH = fileURLToPath(new URL("../../../../scripts/binary-patch/build-hdiffpatch.sh", import.meta.url));
11
+ /**
12
+ * Installs where `release` will look: `HDIFFPATCH_TOOLS_DIR` when it is set, and otherwise
13
+ * a `.hdiffpatch-tools` directory in the working directory, the first place the lookup
14
+ * checks before walking up.
15
+ */
16
+ const DEFAULT_TOOLS_DIR = process.env[TOOLS_DIR_ENV_NAME] || path.resolve(process.cwd(), TOOLS_DIR_NAME);
17
+ program.command('build-patch-tools')
18
+ .description('Builds hdiffz and hpatchz from source and installs them where `release --binary-bundle-path` looks for them.\nThe build clones the pinned HDiffPatch sources, so it needs git, make, a C/C++ compiler and network access. It does nothing when the tools are already installed.')
19
+ .option('--tools-dir <path>', 'directory to install the tools into', DEFAULT_TOOLS_DIR)
20
+ .option('--force', 'rebuild even when the tools are already installed', false)
21
+ .option('--print-hash', 'print a hash of the build script, for keying a CI cache of the tools, and exit without building', false)
22
+ .action((options) => {
23
+ if (options.printHash) {
24
+ console.log(hashBuildScript(BUILD_SCRIPT_PATH));
25
+ return;
26
+ }
27
+ buildPatchTools({
28
+ buildScriptPath: BUILD_SCRIPT_PATH,
29
+ toolsDir: path.resolve(options.toolsDir),
30
+ force: options.force,
31
+ });
32
+ });
package/cli/dist/index.js CHANGED
@@ -26,6 +26,10 @@ import "./commands/showHistoryCommand/index.js";
26
26
  * npx code-push init
27
27
  */
28
28
  import "./commands/initCommand/index.js";
29
+ /**
30
+ * npx code-push build-patch-tools
31
+ */
32
+ import "./commands/buildPatchToolsCommand/index.js";
29
33
  shell.set("-e");
30
34
  shell.set("+v");
31
35
  program
@@ -31,14 +31,17 @@ export const BINARY_PATCH_ALGORITHM = 'hdiffpatch-m-zstd';
31
31
  export const BINARY_PATCH_FORMAT_VERSION = 1;
32
32
  const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
33
33
  const HPATCHZ_OPTIONS = ['-f', '-m'];
34
- const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
35
- const TOOLS_DIR_NAME = '.hdiffpatch-tools';
36
- const BUILD_SCRIPT_PATH = 'scripts/binary-patch/build-hdiffpatch.sh';
34
+ export const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
35
+ export const TOOLS_DIR_NAME = '.hdiffpatch-tools';
36
+ const BUILD_COMMAND = 'npx code-push build-patch-tools';
37
37
  /**
38
38
  * Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
39
39
  * then at a `.hdiffpatch-tools` directory in the working directory or any directory
40
40
  * above it. The tools are built from source rather than installed as a package
41
41
  * dependency, so the error explains how to get them.
42
+ *
43
+ * `build-patch-tools` (`cli/commands/buildPatchToolsCommand/index.ts`) installs into the
44
+ * first place this looks; a change to the lookup order has to be mirrored there.
42
45
  */
43
46
  export function resolveBinaryPatchTool(tool) {
44
47
  const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
@@ -48,7 +51,7 @@ export function resolveBinaryPatchTool(tool) {
48
51
  return configured;
49
52
  }
50
53
  throw new Error(`${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
51
- `Build the tools with '${BUILD_SCRIPT_PATH}'.`);
54
+ `Build the tools with '${BUILD_COMMAND}'.`);
52
55
  }
53
56
  let directory = path.resolve(process.cwd());
54
57
  for (;;) {
@@ -63,7 +66,7 @@ export function resolveBinaryPatchTool(tool) {
63
66
  directory = parent;
64
67
  }
65
68
  throw new Error(`'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
66
- `Build it with '${BUILD_SCRIPT_PATH}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`);
69
+ `Build it with '${BUILD_COMMAND}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`);
67
70
  }
68
71
  /** Writes the patch that turns the bundle at `basePath` into the one at `targetPath`. */
69
72
  export function generatePatch(basePath, targetPath, patchPath) {
@@ -82,7 +82,7 @@ describe("generatePatch/applyPatch", () => {
82
82
  const previous = process.env.HDIFFPATCH_TOOLS_DIR;
83
83
  process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
84
84
  try {
85
- expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-hdiffpatch\.sh/);
85
+ expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/npx code-push build-patch-tools/);
86
86
  }
87
87
  finally {
88
88
  if (previous === undefined) {
package/cli/index.ts CHANGED
@@ -34,6 +34,11 @@ import "./commands/showHistoryCommand/index.js";
34
34
  */
35
35
  import "./commands/initCommand/index.js";
36
36
 
37
+ /**
38
+ * npx code-push build-patch-tools
39
+ */
40
+ import "./commands/buildPatchToolsCommand/index.js";
41
+
37
42
  shell.set("-e");
38
43
  shell.set("+v");
39
44
 
@@ -101,7 +101,7 @@ describe("generatePatch/applyPatch", () => {
101
101
  const previous = process.env.HDIFFPATCH_TOOLS_DIR;
102
102
  process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
103
103
  try {
104
- expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-hdiffpatch\.sh/);
104
+ expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/npx code-push build-patch-tools/);
105
105
  } finally {
106
106
  if (previous === undefined) {
107
107
  delete process.env.HDIFFPATCH_TOOLS_DIR;
@@ -37,15 +37,18 @@ export type BinaryPatchTool = 'hdiffz' | 'hpatchz';
37
37
  const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
38
38
  const HPATCHZ_OPTIONS = ['-f', '-m'];
39
39
 
40
- const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
41
- const TOOLS_DIR_NAME = '.hdiffpatch-tools';
42
- const BUILD_SCRIPT_PATH = 'scripts/binary-patch/build-hdiffpatch.sh';
40
+ export const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
41
+ export const TOOLS_DIR_NAME = '.hdiffpatch-tools';
42
+ const BUILD_COMMAND = 'npx code-push build-patch-tools';
43
43
 
44
44
  /**
45
45
  * Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
46
46
  * then at a `.hdiffpatch-tools` directory in the working directory or any directory
47
47
  * above it. The tools are built from source rather than installed as a package
48
48
  * dependency, so the error explains how to get them.
49
+ *
50
+ * `build-patch-tools` (`cli/commands/buildPatchToolsCommand/index.ts`) installs into the
51
+ * first place this looks; a change to the lookup order has to be mirrored there.
49
52
  */
50
53
  export function resolveBinaryPatchTool(tool: BinaryPatchTool): string {
51
54
  const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
@@ -56,7 +59,7 @@ export function resolveBinaryPatchTool(tool: BinaryPatchTool): string {
56
59
  }
57
60
  throw new Error(
58
61
  `${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
59
- `Build the tools with '${BUILD_SCRIPT_PATH}'.`,
62
+ `Build the tools with '${BUILD_COMMAND}'.`,
60
63
  );
61
64
  }
62
65
 
@@ -75,7 +78,7 @@ export function resolveBinaryPatchTool(tool: BinaryPatchTool): string {
75
78
 
76
79
  throw new Error(
77
80
  `'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
78
- `Build it with '${BUILD_SCRIPT_PATH}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`,
81
+ `Build it with '${BUILD_COMMAND}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`,
79
82
  );
80
83
  }
81
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.5",
3
+ "version": "13.1.0",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
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?.(latestVersion);
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?.(label);
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?.(label);
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?.(remotePackageLabel);
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?.(remotePackageLabel);
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?.(remotePackageLabel ?? 'unknown', error);
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 = {
@@ -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
- * @param onUpdateArchiveResult a callback the app registers on the decorator, as opposed to
93
- * one it passes to a single `sync()` call.
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, onUpdateArchiveResult, installedPackage } = {}) {
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
- onUpdateArchiveResult,
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
+ };
@@ -1,4 +1,4 @@
1
- const log = require("./logging");
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 && updateArchiveResultCallback) {
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
- try {
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.