@hot-updater/react-native 1.0.8 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,163 @@
1
+ /* Copyright 2015 Google Inc. All Rights Reserved.
2
+
3
+ Distributed under MIT license.
4
+ See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
5
+ */
6
+
7
+ package com.hotupdater.vendor.brotli.dec;
8
+
9
+ import static com.hotupdater.vendor.brotli.dec.BrotliError.BROTLI_ERROR_READ_FAILED;
10
+ import static com.hotupdater.vendor.brotli.dec.BrotliError.BROTLI_OK;
11
+ import static com.hotupdater.vendor.brotli.dec.BrotliError.BROTLI_PANIC;
12
+
13
+ import java.io.ByteArrayInputStream;
14
+ import java.io.IOException;
15
+ import java.io.InputStream;
16
+ import java.io.UnsupportedEncodingException;
17
+ import java.nio.Buffer;
18
+ import java.nio.ByteBuffer;
19
+
20
+ /**
21
+ * A set of utility methods.
22
+ */
23
+ final class Utils {
24
+
25
+ private static final byte[] BYTE_ZEROES = new byte[1024];
26
+
27
+ private static final int[] INT_ZEROES = new int[1024];
28
+
29
+ /**
30
+ * Fills byte array with zeroes.
31
+ *
32
+ * <p> Current implementation uses {@link System#arraycopy}, so it should be used for length not
33
+ * less than 16.
34
+ *
35
+ * @param dest array to fill with zeroes
36
+ * @param start the first item to fill
37
+ * @param end the last item to fill (exclusive)
38
+ */
39
+ static void fillBytesWithZeroes(byte[] dest, int start, int end) {
40
+ int cursor = start;
41
+ while (cursor < end) {
42
+ int step = Math.min(cursor + 1024, end) - cursor;
43
+ System.arraycopy(BYTE_ZEROES, 0, dest, cursor, step);
44
+ cursor += step;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fills int array with zeroes.
50
+ *
51
+ * <p> Current implementation uses {@link System#arraycopy}, so it should be used for length not
52
+ * less than 16.
53
+ *
54
+ * @param dest array to fill with zeroes
55
+ * @param start the first item to fill
56
+ * @param end the last item to fill (exclusive)
57
+ */
58
+ static void fillIntsWithZeroes(int[] dest, int start, int end) {
59
+ int cursor = start;
60
+ while (cursor < end) {
61
+ int step = Math.min(cursor + 1024, end) - cursor;
62
+ System.arraycopy(INT_ZEROES, 0, dest, cursor, step);
63
+ cursor += step;
64
+ }
65
+ }
66
+
67
+ static void copyBytes(byte[] dst, int target, byte[] src, int start, int end) {
68
+ System.arraycopy(src, start, dst, target, end - start);
69
+ }
70
+
71
+ static void copyBytesWithin(byte[] bytes, int target, int start, int end) {
72
+ System.arraycopy(bytes, start, bytes, target, end - start);
73
+ }
74
+
75
+ static int readInput(State s, byte[] dst, int offset, int length) {
76
+ try {
77
+ return s.input.read(dst, offset, length);
78
+ } catch (IOException e) {
79
+ return makeError(s, BROTLI_ERROR_READ_FAILED);
80
+ }
81
+ }
82
+
83
+ static InputStream makeEmptyInput() {
84
+ return new ByteArrayInputStream(new byte[0]);
85
+ }
86
+
87
+ static void closeInput(State s) throws IOException {
88
+ s.input.close();
89
+ s.input = makeEmptyInput();
90
+ }
91
+
92
+ static byte[] toUsAsciiBytes(String src) {
93
+ try {
94
+ // NB: String#getBytes(String) is present in JDK 1.1, while other variants require JDK 1.6 and
95
+ // above.
96
+ return src.getBytes("US-ASCII");
97
+ } catch (UnsupportedEncodingException e) {
98
+ throw new RuntimeException(e); // cannot happen
99
+ }
100
+ }
101
+
102
+ static int[] toUtf8Runes(String src) {
103
+ int[] result = new int[src.length()];
104
+ for (int i = 0; i < src.length(); i++) {
105
+ result[i] = (int) src.charAt(i);
106
+ }
107
+ return result;
108
+ }
109
+
110
+ static ByteBuffer asReadOnlyBuffer(ByteBuffer src) {
111
+ return src.asReadOnlyBuffer();
112
+ }
113
+
114
+ static int isReadOnly(ByteBuffer src) {
115
+ return src.isReadOnly() ? 1 : 0;
116
+ }
117
+
118
+ static int isDirect(ByteBuffer src) {
119
+ return src.isDirect() ? 1 : 0;
120
+ }
121
+
122
+ // Crazy pills factory: code compiled for JDK8 does not work on JRE9.
123
+ static void flipBuffer(Buffer buffer) {
124
+ buffer.flip();
125
+ }
126
+
127
+ static int isDebugMode() {
128
+ boolean assertsEnabled = Boolean.parseBoolean(System.getProperty("BROTLI_ENABLE_ASSERTS"));
129
+ return assertsEnabled ? 1 : 0;
130
+ }
131
+
132
+ // See BitReader.LOG_BITNESS
133
+ static int getLogBintness() {
134
+ boolean isLongExpensive = Boolean.parseBoolean(System.getProperty("BROTLI_32_BIT_CPU"));
135
+ return isLongExpensive ? 5 : 6;
136
+ }
137
+
138
+ static int shr32(int x, int y) {
139
+ return x >>> y;
140
+ }
141
+
142
+ static long shr64(long x, int y) {
143
+ return x >>> y;
144
+ }
145
+
146
+ static int min(int a, int b) {
147
+ return Math.min(a, b);
148
+ }
149
+
150
+ static int makeError(State s, int code) {
151
+ if (code >= BROTLI_OK) {
152
+ return code;
153
+ }
154
+ if (s.runningState >= 0) {
155
+ s.runningState = code; // Only the first error is remembered.
156
+ }
157
+ // TODO(eustas): expand codes to messages, if ever necessary.
158
+ if (code <= BROTLI_PANIC) {
159
+ throw new IllegalStateException("Brotli error code: " + code);
160
+ }
161
+ throw new BrotliRuntimeException("Error code: " + code);
162
+ }
163
+ }
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -1,5 +1,6 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.app.Activity
3
4
  import android.content.Context
4
5
  import com.facebook.react.ReactHost
5
6
 
@@ -20,7 +21,9 @@ class HotUpdater {
20
21
  */
21
22
  fun getInstance(context: Context): HotUpdaterImpl =
22
23
  instance ?: synchronized(this) {
23
- instance ?: HotUpdaterImpl(context.applicationContext).also {
24
+ // Built from one snapshot of main's values, which options and
25
+ // HotUpdater.configure can no longer change.
26
+ instance ?: MainInstanceConfiguration.shared.createImpl(context.applicationContext).also {
24
27
  instance = it
25
28
  }
26
29
  }
@@ -62,14 +65,6 @@ class HotUpdater {
62
65
  )
63
66
  }
64
67
 
65
- /**
66
- * Reloads the React Native application using the default singleton instance
67
- * @param context Context (preferably ReactApplicationContext)
68
- */
69
- suspend fun reload(context: Context) {
70
- getInstance(context).reload(context)
71
- }
72
-
73
68
  /**
74
69
  * Gets the app version - delegates to HotUpdaterImpl static method
75
70
  * @param context Application context
@@ -98,9 +93,22 @@ class HotUpdater {
98
93
  fun getChannel(context: Context): String = HotUpdaterImpl.getChannel(context)
99
94
 
100
95
  /**
101
- * Programmatic configuration for brownfield/AAR setups where manifest
102
- * metadata or string resources in the host app are not desirable.
96
+ * Main's configuration, which [HotUpdaterOptions] of [init] now set.
97
+ * Each call replaces all four values; a null argument means no
98
+ * override. [isolationKey] replaces main's storage key verbatim.
99
+ * Main's native state reads the values once, when it is created, so a
100
+ * later call no longer changes it. Named instances never read them.
103
101
  */
102
+ @Deprecated(
103
+ "Pass channel, fingerprintHash, and publicKey to HotUpdaterOptions. " +
104
+ "Fingerprint downloads now survive native releases that ship the same artifact.",
105
+ ReplaceWith(
106
+ "HotUpdater.init(context, HotUpdaterOptions(baseURL, channel = channel, " +
107
+ "fingerprintHash = fingerprintHash, publicKey = publicKey))",
108
+ ),
109
+ )
110
+ @JvmStatic
111
+ @Suppress("DEPRECATION")
104
112
  fun configure(
105
113
  fingerprintHash: String? = null,
106
114
  publicKey: String? = null,
@@ -116,9 +124,8 @@ class HotUpdater {
116
124
  }
117
125
 
118
126
  /**
119
- * Sets the ReactHost for brownfield apps (New Architecture).
120
- * Sets the ReactHost for brownfield apps that don't have ReactApplication.
121
- * When set, reload() will use this ReactHost instead of accessing Application.
127
+ * Registers the ReactHost a brownfield app runs without a ReactApplication
128
+ * (New Architecture).
122
129
  * @param reactHost The ReactHost instance
123
130
  */
124
131
  fun setReactHost(reactHost: ReactHost) {
@@ -132,6 +139,37 @@ class HotUpdater {
132
139
  ReactHostHolder.clear()
133
140
  }
134
141
 
135
- suspend fun resetChannel(context: Context): Boolean = getInstance(context).resetChannel()
142
+ /**
143
+ * The native counterpart of JavaScript's `HotUpdater.init`: an
144
+ * instance with this server and these request defaults, for checking
145
+ * for and staging updates before React Native starts.
146
+ * @throws IllegalArgumentException when [HotUpdaterOptions.baseURL] is
147
+ * not an absolute HTTP(S) URL, as `HotUpdater.init` throws, when
148
+ * [HotUpdaterOptions.name] is not 1 to 32 lowercase letters, digits, or
149
+ * hyphens, or when the name was already created with another channel,
150
+ * fingerprint hash, public key, or bundle asset name. `main` refuses
151
+ * a bundle asset name other than `index.android.bundle`, and a
152
+ * channel, fingerprint hash, or public key other than the one an
153
+ * earlier `init` or `HotUpdater.configure` set or, once its native
154
+ * state was first read, the one it uses
155
+ */
156
+ @JvmStatic
157
+ fun init(
158
+ context: Context,
159
+ options: HotUpdaterOptions,
160
+ ): HotUpdaterInstance {
161
+ requireValidBaseURL(options.baseURL)
162
+ // An activity started before this process followed activities is
163
+ // known only from the context the app hands over.
164
+ (context as? Activity)?.let(HotUpdaterRecoveryAttribution.shared::noteActivity)
165
+ return HotUpdaterInstance.create(context.applicationContext, options)
166
+ }
167
+
168
+ /**
169
+ * Clears the runtime channel switch once other native update work
170
+ * finishes, as [HotUpdaterInstance.resetChannel] does.
171
+ */
172
+ suspend fun resetChannel(context: Context): Boolean =
173
+ NativeUpdateCheck.resetChannel(context.applicationContext) { getInstance(context) }
136
174
  }
137
175
  }
@@ -36,16 +36,39 @@ class HotUpdaterModule internal constructor(
36
36
  return value.toLong().takeIf { it in 0..9_007_199_254_740_991L }
37
37
  }
38
38
 
39
+ override fun initialize() {
40
+ super.initialize()
41
+ // Each runtime has its own module, so this reaches a named instance's
42
+ // runtime, whose host the app doesn't expose, and main's, whose root
43
+ // views are looked for too; main's errors are left alone.
44
+ registeredInstanceName = HotUpdaterRecoveryManager.registerRuntime(mReactApplicationContext)
45
+ }
46
+
47
+ // The named instance whose runtime this module registered; the runtime,
48
+ // whichever instance's, is unregistered when it is torn down.
49
+ private var registeredInstanceName: String? = null
50
+
51
+ private fun unregisterRuntime() {
52
+ HotUpdaterRecoveryManager.unregisterRuntime(mReactApplicationContext, registeredInstanceName)
53
+ registeredInstanceName = null
54
+ }
55
+
39
56
  override fun invalidate() {
40
57
  super.invalidate()
58
+ unregisterRuntime()
41
59
  // Cancel all ongoing coroutines when module is destroyed
42
60
  moduleScope.cancel()
43
61
  }
44
62
 
63
+ // The instance whose bundle this module's runtime runs, read from the
64
+ // URL React Native loaded it from.
65
+ private val runtimeInstance = HotUpdaterRuntimeInstance(mReactApplicationContext, { mReactApplicationContext.sourceURL })
66
+
45
67
  /**
46
- * Gets the singleton HotUpdaterImpl instance
68
+ * The impl of this runtime's instance: main's, or a named instance's
69
+ * when the runtime runs its bundle
47
70
  */
48
- private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
71
+ private fun getInstance(): HotUpdaterImpl = runtimeInstance.impl
49
72
 
50
73
  /**
51
74
  * Every changed file with its hash and URL or Hermes patch; with
@@ -210,7 +233,14 @@ class HotUpdaterModule internal constructor(
210
233
  )
211
234
  }
212
235
 
236
+ // Legacy: released JavaScript calls reload() on Android when the app chose
237
+ // setReloadBehavior("reload"). The in-place React Native reload is gone, so
238
+ // reload() restarts the process, and reloadProcess() runs the same code.
213
239
  override fun reload(promise: Promise) {
240
+ runtimeInstance.reloadUnsupportedReason()?.let { reason ->
241
+ promise.reject(RELOAD_UNSUPPORTED_CODE, reason)
242
+ return
243
+ }
214
244
  moduleScope.launch {
215
245
  try {
216
246
  val impl = getInstance()
@@ -223,18 +253,9 @@ class HotUpdaterModule internal constructor(
223
253
  }
224
254
  }
225
255
 
226
- override fun reloadProcess(promise: Promise) {
227
- moduleScope.launch {
228
- try {
229
- val impl = getInstance()
230
- impl.reloadProcess(mReactApplicationContext)
231
- promise.resolve(null)
232
- } catch (e: Exception) {
233
- Log.d("HotUpdater", "Failed to restart process", e)
234
- promise.reject("reloadProcess", e)
235
- }
236
- }
237
- }
256
+ // Deprecated: JavaScript calls reloadProcess() on Android, both released
257
+ // JavaScript and this SDK's, so it stays until the next major release.
258
+ override fun reloadProcess(promise: Promise) = reload(promise)
238
259
 
239
260
  override fun updateBundle(
240
261
  params: ReadableMap,
@@ -466,7 +487,7 @@ class HotUpdaterModule internal constructor(
466
487
  constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
467
488
  constants["CHANNEL"] = getInstance().getChannel()
468
489
  constants["DEFAULT_CHANNEL"] = getInstance().getDefaultChannel()
469
- constants["FINGERPRINT_HASH"] = HotUpdater.getFingerprintHash(mReactApplicationContext)
490
+ constants["FINGERPRINT_HASH"] = getInstance().getFingerprintHash()
470
491
  return constants
471
492
  }
472
493
 
@@ -520,11 +541,15 @@ class HotUpdaterModule internal constructor(
520
541
  getInstance().setStorageItem(key, value)
521
542
  }
522
543
 
544
+ override fun takeNativeUpdateEvents(): String? = getInstance().takeNativeUpdateEvents()
545
+
523
546
  override fun resetChannel(promise: Promise) {
524
547
  moduleScope.launch {
525
548
  try {
526
- val impl = getInstance()
527
- val success = impl.resetChannel()
549
+ // Waits for a native check or download, which would save its
550
+ // channel again after the reset.
551
+ val name = runtimeInstance.name
552
+ val success = NativeUpdateCheck.resetChannel(mReactApplicationContext, name) { runtimeInstance.impl(name) }
528
553
  promise.resolve(success)
529
554
  } catch (e: HotUpdaterException) {
530
555
  promise.reject(e.code, e.message)
@@ -1,16 +1,47 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.content.Context
3
4
  import com.facebook.react.ReactApplication
5
+ import com.facebook.react.ReactHost
6
+ import com.facebook.react.bridge.ReactContext
4
7
 
5
8
  /**
6
9
  * Routes JavaScript errors of the running React instance to crash recovery.
7
- * The New Architecture reaches the instance through ReactHost.
10
+ * The New Architecture reaches the instance through ReactHost: the one a
11
+ * brownfield app passes to `HotUpdater.setReactHost`, the application's, or
12
+ * the host of a runtime whose HotUpdater module was created. An error belongs
13
+ * to the instance whose bundle the host's runtime runs.
8
14
  */
9
15
  internal object JavaScriptExceptionHooks {
10
- private val patchedReactHostDelegateIds = mutableSetOf<Int>()
16
+ private val patchedReactHostDelegates = WeakIdentitySet<Any>()
11
17
 
12
- fun install(application: ReactApplication) {
13
- val reactHost = application.reactHost ?: return
18
+ fun install(appContext: Context) {
19
+ val reactHost =
20
+ ReactHostHolder.getReactHost()
21
+ ?: (appContext as? ReactApplication)?.reactHost
22
+ ?: return
23
+ patchReactHost(reactHost)
24
+ }
25
+
26
+ /**
27
+ * Hooks the host of [reactContext]'s runtime. Without a bridge, a fatal
28
+ * error reaches only the host's delegate, and the host of a named
29
+ * instance's runtime is reached only through its context.
30
+ */
31
+ fun installForRuntime(reactContext: ReactContext) {
32
+ HotUpdaterRecoveryManager.patchReactContextExceptionHandler(reactContext)
33
+ // BridgelessReactContext's host: `mReactHost` before React Native 0.80's Kotlin rewrite.
34
+ val reactHost =
35
+ listOf("reactHost", "mReactHost")
36
+ .firstNotNullOfOrNull { name -> HotUpdaterRecoveryManager.findField(reactContext.javaClass, name) }
37
+ ?.let { field ->
38
+ field.isAccessible = true
39
+ field.get(reactContext)
40
+ } as? ReactHost ?: return
41
+ patchReactHost(reactHost)
42
+ }
43
+
44
+ private fun patchReactHost(reactHost: ReactHost) {
14
45
  val reactHostDelegate =
15
46
  HotUpdaterRecoveryManager.findField(reactHost.javaClass, "mReactHostDelegate")?.let { field ->
16
47
  field.isAccessible = true
@@ -22,12 +53,12 @@ internal object JavaScriptExceptionHooks {
22
53
  }
23
54
  ?: return
24
55
 
25
- val delegateIdentity = System.identityHashCode(reactHostDelegate)
26
- if (patchedReactHostDelegateIds.add(delegateIdentity)) {
56
+ val patches = patchedReactHostDelegates.add(reactHostDelegate)
57
+ if (patches) {
27
58
  val exceptionHandlerField =
28
59
  HotUpdaterRecoveryManager.findField(reactHostDelegate.javaClass, "exceptionHandler")
29
60
  if (exceptionHandlerField == null) {
30
- patchedReactHostDelegateIds.remove(delegateIdentity)
61
+ patchedReactHostDelegates.remove(reactHostDelegate)
31
62
  return
32
63
  }
33
64
 
@@ -37,8 +68,14 @@ internal object JavaScriptExceptionHooks {
37
68
  exceptionHandlerField.get(reactHostDelegate) as? (Exception) -> Unit
38
69
 
39
70
  exceptionHandlerField.set(reactHostDelegate) { exception: Exception ->
40
- if (!HotUpdaterRecoveryManager.handleJavaScriptExceptionForRecovery(exception)) {
71
+ // The host still holds the runtime that failed: it tears it down after the delegate.
72
+ val runtime = runCatching { reactHost.currentReactContext }.getOrNull()
73
+ val sourceURL = runCatching { runtime?.sourceURL }.getOrNull()
74
+ val outcome = HotUpdaterRecoveryManager.handleJavaScriptError(exception, sourceURL, runtime)
75
+ if (!outcome.recovered) {
41
76
  previousHandler?.invoke(exception) ?: throw exception
77
+ // The handler returned: the error did not end the process.
78
+ HotUpdaterRecoveryManager.handlerReturned(outcome)
42
79
  }
43
80
  }
44
81
  }
@@ -1,5 +1,6 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.app.Activity
3
4
  import android.content.Context
4
5
 
5
6
  /**
@@ -19,7 +20,9 @@ class HotUpdater {
19
20
  */
20
21
  fun getInstance(context: Context): HotUpdaterImpl =
21
22
  instance ?: synchronized(this) {
22
- instance ?: HotUpdaterImpl(context.applicationContext).also {
23
+ // Built from one snapshot of main's values, which options and
24
+ // HotUpdater.configure can no longer change.
25
+ instance ?: MainInstanceConfiguration.shared.createImpl(context.applicationContext).also {
23
26
  instance = it
24
27
  }
25
28
  }
@@ -61,14 +64,6 @@ class HotUpdater {
61
64
  )
62
65
  }
63
66
 
64
- /**
65
- * Reloads the React Native application using the default singleton instance
66
- * @param context Context (preferably ReactApplicationContext)
67
- */
68
- suspend fun reload(context: Context) {
69
- getInstance(context).reload(context)
70
- }
71
-
72
67
  /**
73
68
  * Gets the app version - delegates to HotUpdaterImpl static method
74
69
  * @param context Application context
@@ -97,9 +92,22 @@ class HotUpdater {
97
92
  fun getChannel(context: Context): String = HotUpdaterImpl.getChannel(context)
98
93
 
99
94
  /**
100
- * Programmatic configuration for brownfield/AAR setups where manifest
101
- * metadata or string resources in the host app are not desirable.
95
+ * Main's configuration, which [HotUpdaterOptions] of [init] now set.
96
+ * Each call replaces all four values; a null argument means no
97
+ * override. [isolationKey] replaces main's storage key verbatim.
98
+ * Main's native state reads the values once, when it is created, so a
99
+ * later call no longer changes it. Named instances never read them.
102
100
  */
101
+ @Deprecated(
102
+ "Pass channel, fingerprintHash, and publicKey to HotUpdaterOptions. " +
103
+ "Fingerprint downloads now survive native releases that ship the same artifact.",
104
+ ReplaceWith(
105
+ "HotUpdater.init(context, HotUpdaterOptions(baseURL, channel = channel, " +
106
+ "fingerprintHash = fingerprintHash, publicKey = publicKey))",
107
+ ),
108
+ )
109
+ @JvmStatic
110
+ @Suppress("DEPRECATION")
103
111
  fun configure(
104
112
  fingerprintHash: String? = null,
105
113
  publicKey: String? = null,
@@ -130,6 +138,37 @@ class HotUpdater {
130
138
  // No-op for old architecture
131
139
  }
132
140
 
133
- suspend fun resetChannel(context: Context): Boolean = getInstance(context).resetChannel()
141
+ /**
142
+ * The native counterpart of JavaScript's `HotUpdater.init`: an
143
+ * instance with this server and these request defaults, for checking
144
+ * for and staging updates before React Native starts.
145
+ * @throws IllegalArgumentException when [HotUpdaterOptions.baseURL] is
146
+ * not an absolute HTTP(S) URL, as `HotUpdater.init` throws, when
147
+ * [HotUpdaterOptions.name] is not 1 to 32 lowercase letters, digits, or
148
+ * hyphens, or when the name was already created with another channel,
149
+ * fingerprint hash, public key, or bundle asset name. `main` refuses
150
+ * a bundle asset name other than `index.android.bundle`, and a
151
+ * channel, fingerprint hash, or public key other than the one an
152
+ * earlier `init` or `HotUpdater.configure` set or, once its native
153
+ * state was first read, the one it uses
154
+ */
155
+ @JvmStatic
156
+ fun init(
157
+ context: Context,
158
+ options: HotUpdaterOptions,
159
+ ): HotUpdaterInstance {
160
+ requireValidBaseURL(options.baseURL)
161
+ // An activity started before this process followed activities is
162
+ // known only from the context the app hands over.
163
+ (context as? Activity)?.let(HotUpdaterRecoveryAttribution.shared::noteActivity)
164
+ return HotUpdaterInstance.create(context.applicationContext, options)
165
+ }
166
+
167
+ /**
168
+ * Clears the runtime channel switch once other native update work
169
+ * finishes, as [HotUpdaterInstance.resetChannel] does.
170
+ */
171
+ suspend fun resetChannel(context: Context): Boolean =
172
+ NativeUpdateCheck.resetChannel(context.applicationContext) { getInstance(context) }
134
173
  }
135
174
  }