appium-xcuitest-driver 12.2.0 → 12.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [12.3.0](https://github.com/appium/appium-xcuitest-driver/compare/v12.2.1...v12.3.0) (2026-08-08)
2
+
3
+ ### Features
4
+
5
+ * Add customizable exponential backoff for tunnel reconnect retries ([#2936](https://github.com/appium/appium-xcuitest-driver/issues/2936)) ([84ed39d](https://github.com/appium/appium-xcuitest-driver/commit/84ed39dcaf274368f28fed85c980cf7d23ce470e))
6
+
7
+ ## [12.2.1](https://github.com/appium/appium-xcuitest-driver/compare/v12.2.0...v12.2.1) (2026-08-07)
8
+
9
+ ### Miscellaneous Chores
10
+
11
+ * Drop chai ([#2935](https://github.com/appium/appium-xcuitest-driver/issues/2935)) ([d31bba4](https://github.com/appium/appium-xcuitest-driver/commit/d31bba46420d816098de8acc4bb0a27060c80b81))
12
+ * rename safe occurrences of JWProxy ([#2937](https://github.com/appium/appium-xcuitest-driver/issues/2937)) ([b479f7b](https://github.com/appium/appium-xcuitest-driver/commit/b479f7b12ceb32eba97e08f0b6a1eb44b9652b81))
13
+
1
14
  ## [12.2.0](https://github.com/appium/appium-xcuitest-driver/compare/v12.1.4...v12.2.0) (2026-08-04)
2
15
 
3
16
  ### Features
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "12.2.0",
3
+ "version": "12.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "12.2.0",
9
+ "version": "12.3.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/css-locator-to-native": "^1.0.1",
@@ -15,7 +15,7 @@
15
15
  "appium-ios-device": "^3.1.12",
16
16
  "appium-ios-simulator": "^9.0.0",
17
17
  "appium-remote-debugger": "^16.0.0",
18
- "appium-webdriveragent": "^16.0.0",
18
+ "appium-webdriveragent": "^16.1.4",
19
19
  "appium-xcode": "^7.0.0",
20
20
  "async-lock": "^1.4.0",
21
21
  "asyncbox": "^6.3.0",
@@ -39,15 +39,11 @@
39
39
  "@appium/tsconfig": "^1.2.0",
40
40
  "@appium/types": "^1.5.0",
41
41
  "@types/async-lock": "1.4.2",
42
- "@types/chai": "5.2.3",
43
- "@types/chai-as-promised": "8.0.2",
44
42
  "@types/node": "^26.0.0",
45
43
  "@types/pem": "1.14.4",
46
44
  "@types/portscanner": "^2.1.1",
47
- "@types/semver": "7.7.1",
45
+ "@types/semver": "7.8.0",
48
46
  "@types/sinon": "22.0.0",
49
- "chai": "^6.0.0",
50
- "chai-as-promised": "^8.0.0",
51
47
  "pem": "^1.14.8",
52
48
  "sharp": "^0.x",
53
49
  "sinon": "^22.0.0",
@@ -482,13 +478,16 @@
482
478
  }
483
479
  },
484
480
  "node_modules/ansi-styles": {
485
- "version": "6.2.3",
486
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
487
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
488
- "devOptional": true,
481
+ "version": "4.3.0",
482
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
483
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
484
+ "extraneous": true,
489
485
  "license": "MIT",
486
+ "dependencies": {
487
+ "color-convert": "^2.0.1"
488
+ },
490
489
  "engines": {
491
- "node": ">=12"
490
+ "node": ">=8"
492
491
  },
493
492
  "funding": {
494
493
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
@@ -513,9 +512,9 @@
513
512
  }
514
513
  },
515
514
  "node_modules/appium-ios-remotexpc": {
516
- "version": "5.13.3",
517
- "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-5.13.3.tgz",
518
- "integrity": "sha512-ykUkvBTjGj69rzbgKJSoru+suT5kalaTpKSc9daKgvzHtRYRkXjyo/uFLtK2vCk+6UITD5c3iW31AhS+zbxwRg==",
515
+ "version": "5.13.6",
516
+ "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-5.13.6.tgz",
517
+ "integrity": "sha512-leWj0oCe/DgIPlMZc3FCLvSnyZF57uW+Gq4OkZeqqFojpY1tgTN6YGN3AFYSk/VfF/uvbYtNhT1fn+R6Lf8zow==",
519
518
  "license": "Apache-2.0",
520
519
  "optional": true,
521
520
  "dependencies": {
@@ -527,8 +526,7 @@
527
526
  "axios": "^1.15.1",
528
527
  "commander": "^14.0.1",
529
528
  "minimatch": "^10.1.1",
530
- "node-devicectl": "^1.2.0",
531
- "npm-run-all2": "^8.0.4",
529
+ "node-devicectl": "^2.0.0",
532
530
  "path-to-regexp": "^8.3.0"
533
531
  },
534
532
  "engines": {
@@ -536,21 +534,6 @@
536
534
  "npm": ">=10"
537
535
  }
538
536
  },
539
- "node_modules/appium-ios-remotexpc/node_modules/node-devicectl": {
540
- "version": "1.4.3",
541
- "resolved": "https://registry.npmjs.org/node-devicectl/-/node-devicectl-1.4.3.tgz",
542
- "integrity": "sha512-AnyroE1FzGhQd5oJ9vo/IJl37XypMFFkTEU7PFcrSDpbJo0KCXdLd5twbgZyUyqtma1KgK7VuE6NsIoMpaBfGA==",
543
- "license": "Apache-2.0",
544
- "optional": true,
545
- "dependencies": {
546
- "@appium/logger": "^2.0.0-rc.1",
547
- "teen_process": "^4.1.0"
548
- },
549
- "engines": {
550
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
551
- "npm": ">=10"
552
- }
553
- },
554
537
  "node_modules/appium-ios-simulator": {
555
538
  "version": "9.1.2",
556
539
  "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-9.1.2.tgz",
@@ -589,9 +572,9 @@
589
572
  }
590
573
  },
591
574
  "node_modules/appium-remote-debugger": {
592
- "version": "16.0.3",
593
- "resolved": "https://registry.npmjs.org/appium-remote-debugger/-/appium-remote-debugger-16.0.3.tgz",
594
- "integrity": "sha512-IJEHciiooDtA29WfXy/W9kEBpySWQ7rSmnNti2hSmh0+f3Xvi0QgW7aEsaYCHVxAc4lvmX7u8d8I2qrFS9BqyA==",
575
+ "version": "16.4.0",
576
+ "resolved": "https://registry.npmjs.org/appium-remote-debugger/-/appium-remote-debugger-16.4.0.tgz",
577
+ "integrity": "sha512-Rr0u0CNZPte2aEBozGIUJxRclzyqNUkBBrDZLo4rxwwEfnalFcpWfTJa9/f1YtkdakMfTRysJA3YFwmGuVFQ8w==",
595
578
  "license": "Apache-2.0",
596
579
  "dependencies": {
597
580
  "@appium/base-driver": "^10.0.0-rc.1",
@@ -610,9 +593,9 @@
610
593
  }
611
594
  },
612
595
  "node_modules/appium-webdriveragent": {
613
- "version": "16.1.3",
614
- "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-16.1.3.tgz",
615
- "integrity": "sha512-fn9jrX9CSS/5EMu897WMMwyZSJAuSxJG9sl5rHmh6xQtS19raNAIt0cebb6OnJH0OfW1Yi7C63kaf63+KBSJsg==",
596
+ "version": "16.1.5",
597
+ "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-16.1.5.tgz",
598
+ "integrity": "sha512-R8SM8NEak5TAVbvnzBjTy9F5yJpgOE1d0kVahIcTvipFPU6g06Jg1moNPIZs0+nsykyRLIvED3zFZA0G/eNZCQ==",
616
599
  "license": "Apache-2.0",
617
600
  "dependencies": {
618
601
  "@appium/base-driver": "^10.3.0",
@@ -803,9 +786,9 @@
803
786
  }
804
787
  },
805
788
  "node_modules/bare-url": {
806
- "version": "2.4.7",
807
- "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.7.tgz",
808
- "integrity": "sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==",
789
+ "version": "2.5.1",
790
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.1.tgz",
791
+ "integrity": "sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==",
809
792
  "license": "Apache-2.0",
810
793
  "dependencies": {
811
794
  "bare-path": "^3.0.0"
@@ -979,42 +962,6 @@
979
962
  "url": "https://github.com/chalk/chalk?sponsor=1"
980
963
  }
981
964
  },
982
- "node_modules/chalk/node_modules/ansi-styles": {
983
- "version": "4.3.0",
984
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
985
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
986
- "extraneous": true,
987
- "license": "MIT",
988
- "dependencies": {
989
- "color-convert": "^2.0.1"
990
- },
991
- "engines": {
992
- "node": ">=8"
993
- },
994
- "funding": {
995
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
996
- }
997
- },
998
- "node_modules/chalk/node_modules/color-convert": {
999
- "version": "2.0.1",
1000
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1001
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1002
- "extraneous": true,
1003
- "license": "MIT",
1004
- "dependencies": {
1005
- "color-name": "~1.1.4"
1006
- },
1007
- "engines": {
1008
- "node": ">=7.0.0"
1009
- }
1010
- },
1011
- "node_modules/chalk/node_modules/color-name": {
1012
- "version": "1.1.4",
1013
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1014
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1015
- "extraneous": true,
1016
- "license": "MIT"
1017
- },
1018
965
  "node_modules/cliui": {
1019
966
  "version": "9.0.1",
1020
967
  "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
@@ -1085,18 +1032,38 @@
1085
1032
  }
1086
1033
  },
1087
1034
  "node_modules/color-convert": {
1088
- "version": "3.1.3",
1089
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
1090
- "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
1035
+ "version": "2.0.1",
1036
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1037
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1038
+ "extraneous": true,
1091
1039
  "license": "MIT",
1092
1040
  "dependencies": {
1093
- "color-name": "^2.0.0"
1041
+ "color-name": "~1.1.4"
1094
1042
  },
1095
1043
  "engines": {
1096
- "node": ">=14.6"
1044
+ "node": ">=7.0.0"
1097
1045
  }
1098
1046
  },
1099
1047
  "node_modules/color-name": {
1048
+ "version": "1.1.4",
1049
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1050
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1051
+ "extraneous": true,
1052
+ "license": "MIT"
1053
+ },
1054
+ "node_modules/color-string": {
1055
+ "version": "2.1.4",
1056
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
1057
+ "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
1058
+ "license": "MIT",
1059
+ "dependencies": {
1060
+ "color-name": "^2.0.0"
1061
+ },
1062
+ "engines": {
1063
+ "node": ">=18"
1064
+ }
1065
+ },
1066
+ "node_modules/color-string/node_modules/color-name": {
1100
1067
  "version": "2.1.1",
1101
1068
  "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz",
1102
1069
  "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==",
@@ -1105,16 +1072,25 @@
1105
1072
  "node": ">=12.20"
1106
1073
  }
1107
1074
  },
1108
- "node_modules/color-string": {
1109
- "version": "2.1.4",
1110
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
1111
- "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
1075
+ "node_modules/color/node_modules/color-convert": {
1076
+ "version": "3.1.3",
1077
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
1078
+ "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
1112
1079
  "license": "MIT",
1113
1080
  "dependencies": {
1114
1081
  "color-name": "^2.0.0"
1115
1082
  },
1116
1083
  "engines": {
1117
- "node": ">=18"
1084
+ "node": ">=14.6"
1085
+ }
1086
+ },
1087
+ "node_modules/color/node_modules/color-name": {
1088
+ "version": "2.1.1",
1089
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz",
1090
+ "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==",
1091
+ "license": "MIT",
1092
+ "engines": {
1093
+ "node": ">=12.20"
1118
1094
  }
1119
1095
  },
1120
1096
  "node_modules/combined-stream": {
@@ -1239,44 +1215,6 @@
1239
1215
  "node": ">=18"
1240
1216
  }
1241
1217
  },
1242
- "node_modules/cross-spawn": {
1243
- "version": "7.0.6",
1244
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1245
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1246
- "license": "MIT",
1247
- "optional": true,
1248
- "dependencies": {
1249
- "path-key": "^3.1.0",
1250
- "shebang-command": "^2.0.0",
1251
- "which": "^2.0.1"
1252
- },
1253
- "engines": {
1254
- "node": ">= 8"
1255
- }
1256
- },
1257
- "node_modules/cross-spawn/node_modules/isexe": {
1258
- "version": "2.0.0",
1259
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1260
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1261
- "license": "ISC",
1262
- "optional": true
1263
- },
1264
- "node_modules/cross-spawn/node_modules/which": {
1265
- "version": "2.0.2",
1266
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
1267
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1268
- "license": "ISC",
1269
- "optional": true,
1270
- "dependencies": {
1271
- "isexe": "^2.0.0"
1272
- },
1273
- "bin": {
1274
- "node-which": "bin/node-which"
1275
- },
1276
- "engines": {
1277
- "node": ">= 8"
1278
- }
1279
- },
1280
1218
  "node_modules/css-selector-parser": {
1281
1219
  "version": "3.3.0",
1282
1220
  "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz",
@@ -2112,16 +2050,6 @@
2112
2050
  "ms": "^2.1.1"
2113
2051
  }
2114
2052
  },
2115
- "node_modules/json-parse-even-better-errors": {
2116
- "version": "4.0.0",
2117
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
2118
- "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
2119
- "license": "MIT",
2120
- "optional": true,
2121
- "engines": {
2122
- "node": "^18.17.0 || >=20.5.0"
2123
- }
2124
- },
2125
2053
  "node_modules/json-schema": {
2126
2054
  "version": "0.4.0",
2127
2055
  "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@@ -2267,15 +2195,6 @@
2267
2195
  "url": "https://opencollective.com/express"
2268
2196
  }
2269
2197
  },
2270
- "node_modules/memorystream": {
2271
- "version": "0.3.1",
2272
- "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
2273
- "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
2274
- "optional": true,
2275
- "engines": {
2276
- "node": ">= 0.10.0"
2277
- }
2278
- },
2279
2198
  "node_modules/merge-descriptors": {
2280
2199
  "version": "2.0.0",
2281
2200
  "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -2510,82 +2429,6 @@
2510
2429
  "node": ">=0.10.0"
2511
2430
  }
2512
2431
  },
2513
- "node_modules/npm-normalize-package-bin": {
2514
- "version": "4.0.0",
2515
- "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
2516
- "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
2517
- "license": "ISC",
2518
- "optional": true,
2519
- "engines": {
2520
- "node": "^18.17.0 || >=20.5.0"
2521
- }
2522
- },
2523
- "node_modules/npm-run-all2": {
2524
- "version": "8.0.4",
2525
- "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz",
2526
- "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==",
2527
- "license": "MIT",
2528
- "optional": true,
2529
- "dependencies": {
2530
- "ansi-styles": "^6.2.1",
2531
- "cross-spawn": "^7.0.6",
2532
- "memorystream": "^0.3.1",
2533
- "picomatch": "^4.0.2",
2534
- "pidtree": "^0.6.0",
2535
- "read-package-json-fast": "^4.0.0",
2536
- "shell-quote": "^1.7.3",
2537
- "which": "^5.0.0"
2538
- },
2539
- "bin": {
2540
- "npm-run-all": "bin/npm-run-all/index.js",
2541
- "npm-run-all2": "bin/npm-run-all/index.js",
2542
- "run-p": "bin/run-p/index.js",
2543
- "run-s": "bin/run-s/index.js"
2544
- },
2545
- "engines": {
2546
- "node": "^20.5.0 || >=22.0.0",
2547
- "npm": ">= 10"
2548
- }
2549
- },
2550
- "node_modules/npm-run-all2/node_modules/isexe": {
2551
- "version": "3.1.5",
2552
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
2553
- "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
2554
- "license": "BlueOak-1.0.0",
2555
- "optional": true,
2556
- "engines": {
2557
- "node": ">=18"
2558
- }
2559
- },
2560
- "node_modules/npm-run-all2/node_modules/picomatch": {
2561
- "version": "4.0.5",
2562
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
2563
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
2564
- "license": "MIT",
2565
- "optional": true,
2566
- "engines": {
2567
- "node": ">=12"
2568
- },
2569
- "funding": {
2570
- "url": "https://github.com/sponsors/jonschlinkert"
2571
- }
2572
- },
2573
- "node_modules/npm-run-all2/node_modules/which": {
2574
- "version": "5.0.0",
2575
- "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
2576
- "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
2577
- "license": "ISC",
2578
- "optional": true,
2579
- "dependencies": {
2580
- "isexe": "^3.1.1"
2581
- },
2582
- "bin": {
2583
- "node-which": "bin/which.js"
2584
- },
2585
- "engines": {
2586
- "node": "^18.17.0 || >=20.5.0"
2587
- }
2588
- },
2589
2432
  "node_modules/object-inspect": {
2590
2433
  "version": "1.13.4",
2591
2434
  "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -2682,16 +2525,6 @@
2682
2525
  "node": ">= 0.8"
2683
2526
  }
2684
2527
  },
2685
- "node_modules/path-key": {
2686
- "version": "3.1.1",
2687
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2688
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2689
- "license": "MIT",
2690
- "optional": true,
2691
- "engines": {
2692
- "node": ">=8"
2693
- }
2694
- },
2695
2528
  "node_modules/path-scurry": {
2696
2529
  "version": "2.0.2",
2697
2530
  "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
@@ -2724,19 +2557,6 @@
2724
2557
  "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
2725
2558
  "license": "MIT"
2726
2559
  },
2727
- "node_modules/pidtree": {
2728
- "version": "0.6.1",
2729
- "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.1.tgz",
2730
- "integrity": "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw==",
2731
- "license": "MIT",
2732
- "optional": true,
2733
- "bin": {
2734
- "pidtree": "bin/pidtree.js"
2735
- },
2736
- "engines": {
2737
- "node": ">=0.10"
2738
- }
2739
- },
2740
2560
  "node_modules/plist": {
2741
2561
  "version": "4.0.0",
2742
2562
  "resolved": "https://registry.npmjs.org/plist/-/plist-4.0.0.tgz",
@@ -2863,20 +2683,6 @@
2863
2683
  "node": ">= 0.10"
2864
2684
  }
2865
2685
  },
2866
- "node_modules/read-package-json-fast": {
2867
- "version": "4.0.0",
2868
- "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
2869
- "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==",
2870
- "license": "ISC",
2871
- "optional": true,
2872
- "dependencies": {
2873
- "json-parse-even-better-errors": "^4.0.0",
2874
- "npm-normalize-package-bin": "^4.0.0"
2875
- },
2876
- "engines": {
2877
- "node": "^18.17.0 || >=20.5.0"
2878
- }
2879
- },
2880
2686
  "node_modules/readable-stream": {
2881
2687
  "version": "4.7.0",
2882
2688
  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
@@ -3162,29 +2968,6 @@
3162
2968
  }
3163
2969
  }
3164
2970
  },
3165
- "node_modules/shebang-command": {
3166
- "version": "2.0.0",
3167
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
3168
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
3169
- "license": "MIT",
3170
- "optional": true,
3171
- "dependencies": {
3172
- "shebang-regex": "^3.0.0"
3173
- },
3174
- "engines": {
3175
- "node": ">=8"
3176
- }
3177
- },
3178
- "node_modules/shebang-regex": {
3179
- "version": "3.0.0",
3180
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
3181
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
3182
- "license": "MIT",
3183
- "optional": true,
3184
- "engines": {
3185
- "node": ">=8"
3186
- }
3187
- },
3188
2971
  "node_modules/shell-quote": {
3189
2972
  "version": "1.10.0",
3190
2973
  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
@@ -3776,6 +3559,19 @@
3776
3559
  "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
3777
3560
  }
3778
3561
  },
3562
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
3563
+ "version": "6.2.3",
3564
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
3565
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
3566
+ "dev": true,
3567
+ "license": "MIT",
3568
+ "engines": {
3569
+ "node": ">=12"
3570
+ },
3571
+ "funding": {
3572
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
3573
+ }
3574
+ },
3779
3575
  "node_modules/wrap-ansi/node_modules/emoji-regex": {
3780
3576
  "version": "10.6.0",
3781
3577
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
@@ -3824,9 +3620,9 @@
3824
3620
  "license": "ISC"
3825
3621
  },
3826
3622
  "node_modules/ws": {
3827
- "version": "8.21.2",
3828
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
3829
- "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
3623
+ "version": "8.21.3",
3624
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
3625
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
3830
3626
  "license": "MIT",
3831
3627
  "engines": {
3832
3628
  "node": ">=10.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "12.2.0",
3
+ "version": "12.3.0",
4
4
  "description": "Appium driver for iOS using XCUITest for backend",
5
5
  "keywords": [
6
6
  "appium",
@@ -74,7 +74,7 @@
74
74
  "appium-ios-device": "^3.1.12",
75
75
  "appium-ios-simulator": "^9.0.0",
76
76
  "appium-remote-debugger": "^16.0.0",
77
- "appium-webdriveragent": "^16.0.0",
77
+ "appium-webdriveragent": "^16.1.4",
78
78
  "appium-xcode": "^7.0.0",
79
79
  "async-lock": "^1.4.0",
80
80
  "asyncbox": "^6.3.0",
@@ -98,15 +98,11 @@
98
98
  "@appium/tsconfig": "^1.2.0",
99
99
  "@appium/types": "^1.5.0",
100
100
  "@types/async-lock": "1.4.2",
101
- "@types/chai": "5.2.3",
102
- "@types/chai-as-promised": "8.0.2",
103
101
  "@types/node": "^26.0.0",
104
102
  "@types/pem": "1.14.4",
105
103
  "@types/portscanner": "^2.1.1",
106
- "@types/semver": "7.7.1",
104
+ "@types/semver": "7.8.0",
107
105
  "@types/sinon": "22.0.0",
108
- "chai": "^6.0.0",
109
- "chai-as-promised": "^8.0.0",
110
106
  "pem": "^1.14.8",
111
107
  "sharp": "^0.x",
112
108
  "sinon": "^22.0.0",
@@ -0,0 +1,127 @@
1
+ export const DEFAULT_DISCONNECT_RETRY_INTERVAL_MS = 1000;
2
+ export const DEFAULT_DISCONNECT_RETRY_BACKOFF_MULTIPLIER = 2;
3
+ export const DEFAULT_DISCONNECT_RETRY_BACKOFF_MAX_INTERVAL_MS = 30_000;
4
+ export const DEFAULT_DISCONNECT_RETRY_BACKOFF_JITTER = 0.5;
5
+
6
+ /**
7
+ * @abstract
8
+ * Common attempt-counting behavior shared by all retry policies. Subclasses only need to implement
9
+ * `getDelayMs()`.
10
+ */
11
+ export class RetryPolicy {
12
+ /**
13
+ * @param {{maxAttempts: number | null, intervalMs: number}} opts - maxAttempts: null disables
14
+ * retries entirely; 0 means unlimited retries
15
+ */
16
+ constructor({maxAttempts, intervalMs}) {
17
+ if (new.target === RetryPolicy) {
18
+ throw new TypeError('RetryPolicy is abstract and cannot be instantiated directly');
19
+ }
20
+ this.maxAttempts = maxAttempts;
21
+ this._intervalMs = intervalMs;
22
+ }
23
+
24
+ get isEnabled() {
25
+ return this.maxAttempts !== null;
26
+ }
27
+
28
+ /**
29
+ * @param {number} attemptsMade - number of attempts already made
30
+ * @returns {boolean}
31
+ */
32
+ hasAttemptsRemaining(attemptsMade) {
33
+ const {maxAttempts} = this;
34
+ if (maxAttempts === null) {
35
+ return false;
36
+ }
37
+ return maxAttempts === 0 || attemptsMade < maxAttempts;
38
+ }
39
+
40
+ /**
41
+ * @abstract
42
+ * @param {number} _attempt - 1-based number of the attempt about to be made
43
+ * @returns {number}
44
+ */
45
+ getDelayMs(_attempt) {
46
+ throw new Error('Not implemented');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Fixed-interval retry policy: waits the same delay between every attempt.
52
+ */
53
+ export class FixedIntervalRetryPolicy extends RetryPolicy {
54
+ /**
55
+ * @param {number} _attempt - 1-based number of the attempt about to be made
56
+ * @returns {number}
57
+ */
58
+ getDelayMs(_attempt) {
59
+ return this._intervalMs;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Exponential-backoff retry policy: the delay grows by a multiplier on each attempt, capped at a
65
+ * configured maximum.
66
+ */
67
+ export class ExponentialBackoffRetryPolicy extends RetryPolicy {
68
+ /**
69
+ * @param {{maxAttempts: number | null, intervalMs: number, backoffMultiplier: number, backoffMaxIntervalMs: number, jitter: number}} opts
70
+ * - jitter: fraction of the delay to randomize away, in [0, 1]. 0 always returns the exact
71
+ * computed delay; 1 returns a value uniformly distributed between 0 and the computed delay.
72
+ * Jitter spreads out devices that dropped together instead of having them retry in lockstep and
73
+ * stay synchronised once they reach the cap.
74
+ */
75
+ constructor({maxAttempts, intervalMs, backoffMultiplier, backoffMaxIntervalMs, jitter}) {
76
+ super({maxAttempts, intervalMs});
77
+ if (backoffMaxIntervalMs < intervalMs) {
78
+ throw new Error(
79
+ `Invalid disconnect retry backoff max interval: ${backoffMaxIntervalMs}. ` +
80
+ `Expected a value >= the initial interval (${intervalMs}).`,
81
+ );
82
+ }
83
+ if (jitter < 0 || jitter > 1) {
84
+ throw new Error(`Invalid disconnect retry backoff jitter: ${jitter}. Expected a value between 0 and 1.`);
85
+ }
86
+ this._backoffMultiplier = backoffMultiplier;
87
+ this._backoffMaxIntervalMs = backoffMaxIntervalMs;
88
+ this._jitter = jitter;
89
+ }
90
+
91
+ /**
92
+ * @param {number} attempt - 1-based number of the attempt about to be made
93
+ * @returns {number}
94
+ */
95
+ getDelayMs(attempt) {
96
+ const delayMs = this._intervalMs * this._backoffMultiplier ** (attempt - 1);
97
+ const cappedDelayMs = Math.min(delayMs, this._backoffMaxIntervalMs);
98
+ return Math.round(cappedDelayMs * (1 - this._jitter * Math.random()));
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {{strategy: 'fixed' | 'exponential', maxAttempts: number | null, intervalMs: number, backoffMultiplier?: number, backoffMaxIntervalMs?: number, jitter?: number}} opts
104
+ * @returns {RetryPolicy}
105
+ */
106
+ export function createDisconnectRetryPolicy({
107
+ strategy,
108
+ maxAttempts,
109
+ intervalMs,
110
+ backoffMultiplier = DEFAULT_DISCONNECT_RETRY_BACKOFF_MULTIPLIER,
111
+ backoffMaxIntervalMs = DEFAULT_DISCONNECT_RETRY_BACKOFF_MAX_INTERVAL_MS,
112
+ jitter = DEFAULT_DISCONNECT_RETRY_BACKOFF_JITTER,
113
+ }) {
114
+ if (strategy === 'fixed') {
115
+ return new FixedIntervalRetryPolicy({maxAttempts, intervalMs});
116
+ }
117
+ if (strategy === 'exponential') {
118
+ return new ExponentialBackoffRetryPolicy({
119
+ maxAttempts,
120
+ intervalMs,
121
+ backoffMultiplier,
122
+ backoffMaxIntervalMs,
123
+ jitter,
124
+ });
125
+ }
126
+ throw new Error(`Invalid disconnect retry strategy: ${strategy}. Expected 'fixed' or 'exponential'.`);
127
+ }
@@ -27,6 +27,14 @@ import {Command} from 'commander';
27
27
 
28
28
  import {parsePositiveIntegerOption} from './lib/options.mjs';
29
29
  import {startTimeoutProgressLogger} from './lib/progress.mjs';
30
+ import {
31
+ createDisconnectRetryPolicy,
32
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_JITTER,
33
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_MAX_INTERVAL_MS,
34
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_MULTIPLIER,
35
+ DEFAULT_DISCONNECT_RETRY_INTERVAL_MS,
36
+ FixedIntervalRetryPolicy,
37
+ } from './lib/retry-policy.mjs';
30
38
  import {assertRoot} from './lib/root.mjs';
31
39
 
32
40
  const log = logger.getLogger('TunnelCreation');
@@ -65,9 +73,11 @@ class TunnelCreator {
65
73
  /** @type {Map<string, import('appium-ios-remotexpc').AppleTVTunnelService>} */
66
74
  this._appletvTunnelServicesByUdid = new Map();
67
75
  this._isCleaningUp = false;
68
- /** @type {number | null} */
69
- this._disconnectRetryMaxAttempts = null;
70
- this._disconnectRetryIntervalMs = 1000;
76
+ /** @type {import('./lib/retry-policy.mjs').RetryPolicy} */
77
+ this._disconnectRetryPolicy = new FixedIntervalRetryPolicy({
78
+ maxAttempts: null,
79
+ intervalMs: DEFAULT_DISCONNECT_RETRY_INTERVAL_MS,
80
+ });
71
81
  }
72
82
 
73
83
  get tunnelRegistryPort() {
@@ -83,12 +93,10 @@ class TunnelCreator {
83
93
  }
84
94
 
85
95
  /**
86
- * @param {number | null} maxAttempts - null disables retries; 0 means unlimited retries
87
- * @param {number} intervalMs
96
+ * @param {import('./lib/retry-policy.mjs').RetryPolicy} policy
88
97
  */
89
- setDisconnectRetryPolicy(maxAttempts, intervalMs) {
90
- this._disconnectRetryMaxAttempts = maxAttempts;
91
- this._disconnectRetryIntervalMs = intervalMs;
98
+ setDisconnectRetryPolicy(policy) {
99
+ this._disconnectRetryPolicy = policy;
92
100
  }
93
101
 
94
102
  /**
@@ -576,10 +584,6 @@ class TunnelCreator {
576
584
  if (!this._isRetryEnabledForUdid(udid)) {
577
585
  return;
578
586
  }
579
- const maxAttempts = this._disconnectRetryMaxAttempts;
580
- if (maxAttempts === null) {
581
- return;
582
- }
583
587
 
584
588
  const device = this._usbDevicesByUdid.get(udid);
585
589
  const isAppleTV = this._appleTVDeviceIds.has(udid);
@@ -587,17 +591,19 @@ class TunnelCreator {
587
591
  return;
588
592
  }
589
593
 
590
- let attempt = 0;
594
+ const policy = this._disconnectRetryPolicy;
595
+ let attemptsMade = 0;
591
596
  while (!this._isCleaningUp) {
592
- if (maxAttempts !== 0 && attempt >= maxAttempts) {
597
+ if (!policy.hasAttemptsRemaining(attemptsMade)) {
593
598
  log.warn(`Retry limit reached for ${udid}; keeping it removed from the registry`);
594
599
  return;
595
600
  }
596
- attempt += 1;
601
+ attemptsMade += 1;
602
+ const delayMs = policy.getDelayMs(attemptsMade);
597
603
  log.warn(
598
- `Retrying tunnel creation for ${udid} (attempt ${attempt}${maxAttempts === 0 ? ', unlimited' : `/${maxAttempts}`}) in ${this._disconnectRetryIntervalMs}ms...`,
604
+ `Retrying tunnel creation for ${udid} (attempt ${attemptsMade}${policy.maxAttempts === 0 ? ', unlimited' : `/${policy.maxAttempts}`}) in ${delayMs}ms...`,
599
605
  );
600
- await sleep(this._disconnectRetryIntervalMs);
606
+ await sleep(delayMs);
601
607
 
602
608
  try {
603
609
  this._registryServer?.markTunnelPending(udid);
@@ -635,8 +641,7 @@ class TunnelCreator {
635
641
  */
636
642
  _isRetryEnabledForUdid(udid) {
637
643
  return (
638
- (this._usbDevicesByUdid.has(udid) || this._appleTVDeviceIds.has(udid)) &&
639
- this._disconnectRetryMaxAttempts !== null
644
+ (this._usbDevicesByUdid.has(udid) || this._appleTVDeviceIds.has(udid)) && this._disconnectRetryPolicy.isEnabled
640
645
  );
641
646
  }
642
647
 
@@ -800,6 +805,43 @@ function parseNonNegativeIntegerOption(value, label) {
800
805
  return count;
801
806
  }
802
807
 
808
+ /**
809
+ * @param {string} value
810
+ * @returns {'fixed' | 'exponential'}
811
+ */
812
+ function parseRetryStrategyOption(value) {
813
+ if (value !== 'fixed' && value !== 'exponential') {
814
+ throw new Error(`Invalid disconnect retry strategy: ${value}. Expected 'fixed' or 'exponential'.`);
815
+ }
816
+ return value;
817
+ }
818
+
819
+ /**
820
+ * @param {string} value
821
+ * @param {string} label
822
+ * @returns {number}
823
+ */
824
+ function parseRetryBackoffMultiplierOption(value, label) {
825
+ const multiplier = Number.parseFloat(value);
826
+ if (!Number.isFinite(multiplier) || multiplier <= 1) {
827
+ throw new Error(`Invalid ${label}: ${value}. Expected a number greater than 1.`);
828
+ }
829
+ return multiplier;
830
+ }
831
+
832
+ /**
833
+ * @param {string} value
834
+ * @param {string} label
835
+ * @returns {number}
836
+ */
837
+ function parseRetryBackoffJitterOption(value, label) {
838
+ const jitter = Number.parseFloat(value);
839
+ if (!Number.isFinite(jitter) || jitter < 0 || jitter > 1) {
840
+ throw new Error(`Invalid ${label}: ${value}. Expected a number between 0 and 1.`);
841
+ }
842
+ return jitter;
843
+ }
844
+
803
845
  /**
804
846
  * @param {string} value
805
847
  * @param {string[]} previous
@@ -906,11 +948,38 @@ async function main() {
906
948
  'Max tunnel recreation attempts after unexpected disconnect: 0 = unlimited; omit to disable retries',
907
949
  (value) => parseNonNegativeIntegerOption(value, 'disconnect retry max attempts'),
908
950
  )
951
+ .option(
952
+ '--disconnect-retry-strategy <strategy>',
953
+ "Delay strategy between tunnel recreation attempts: 'fixed' or 'exponential'",
954
+ (value) => parseRetryStrategyOption(value),
955
+ 'fixed',
956
+ )
909
957
  .option(
910
958
  '--disconnect-retry-interval-ms <ms>',
911
- 'Delay between tunnel recreation attempts in milliseconds (default 1000)',
959
+ 'Delay between tunnel recreation attempts in milliseconds. ' +
960
+ 'With --disconnect-retry-strategy exponential, this is the initial delay, before it starts growing',
912
961
  (value) => parsePositiveIntegerOption(value, 'disconnect retry interval'),
913
- 1000,
962
+ DEFAULT_DISCONNECT_RETRY_INTERVAL_MS,
963
+ )
964
+ .option(
965
+ '--disconnect-retry-backoff-multiplier <factor>',
966
+ 'Factor the delay grows by on each attempt when --disconnect-retry-strategy is exponential (must be > 1)',
967
+ (value) => parseRetryBackoffMultiplierOption(value, 'disconnect retry backoff multiplier'),
968
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_MULTIPLIER,
969
+ )
970
+ .option(
971
+ '--disconnect-retry-backoff-max-interval-ms <ms>',
972
+ 'Upper bound on the delay between tunnel recreation attempts when --disconnect-retry-strategy is exponential',
973
+ (value) => parsePositiveIntegerOption(value, 'disconnect retry backoff max interval'),
974
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_MAX_INTERVAL_MS,
975
+ )
976
+ .option(
977
+ '--disconnect-retry-backoff-jitter <factor>',
978
+ 'Fraction of the delay to randomize away on each attempt when --disconnect-retry-strategy is exponential, ' +
979
+ 'between 0 (no jitter) and 1 (full jitter). Spreads out devices that dropped together instead of having ' +
980
+ 'them retry in lockstep',
981
+ (value) => parseRetryBackoffJitterOption(value, 'disconnect retry backoff jitter'),
982
+ DEFAULT_DISCONNECT_RETRY_BACKOFF_JITTER,
914
983
  );
915
984
 
916
985
  program.parse(process.argv);
@@ -922,6 +991,24 @@ async function main() {
922
991
  const shouldRunUsbFlow = !hasRequestedAppleTVIds || hasRequestedUdids;
923
992
  const shouldRunAppleTVFlow = !hasRequestedUdids || hasRequestedAppleTVIds;
924
993
 
994
+ /** @type {import('./lib/retry-policy.mjs').RetryPolicy} */
995
+ let disconnectRetryPolicy;
996
+ try {
997
+ disconnectRetryPolicy = createDisconnectRetryPolicy({
998
+ strategy: options.disconnectRetryStrategy,
999
+ maxAttempts: options.disconnectRetryMaxAttempts ?? null,
1000
+ intervalMs: options.disconnectRetryIntervalMs,
1001
+ backoffMultiplier: options.disconnectRetryBackoffMultiplier,
1002
+ backoffMaxIntervalMs: options.disconnectRetryBackoffMaxIntervalMs,
1003
+ jitter: options.disconnectRetryBackoffJitter,
1004
+ });
1005
+ } catch (err) {
1006
+ // program.error() never returns (it exits the process); the throw below is unreachable and
1007
+ // only here so TS can see disconnectRetryPolicy is always assigned past this point.
1008
+ program.error(/** @type {Error} */ (err).message);
1009
+ throw err;
1010
+ }
1011
+
925
1012
  await assertRoot(path.parse(fileURLToPath(import.meta.url)).name);
926
1013
 
927
1014
  const tunnelCreator = new TunnelCreator({
@@ -953,10 +1040,7 @@ async function main() {
953
1040
  });
954
1041
  }
955
1042
 
956
- tunnelCreator.setDisconnectRetryPolicy(
957
- options.disconnectRetryMaxAttempts ?? null,
958
- options.disconnectRetryIntervalMs,
959
- );
1043
+ tunnelCreator.setDisconnectRetryPolicy(disconnectRetryPolicy);
960
1044
 
961
1045
  await tunnelCreator.startRegistryServer();
962
1046