@hot-updater/react-native 1.0.6 → 1.0.8
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/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +31 -0
- package/android/src/newarch/HotUpdaterModule.kt +108 -57
- package/android/src/newarch/ReactIntegrationManager.kt +27 -0
- package/android/src/oldarch/HotUpdaterModule.kt +109 -57
- package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +3 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +49 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +150 -62
- package/lib/commonjs/checkForUpdate.js +16 -9
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/httpClient.js +14 -20
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/native.js +36 -10
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js +5 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +17 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/httpClient.js +14 -20
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/native.js +35 -10
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js +7 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +20 -12
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +17 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +31 -7
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +20 -12
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +17 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +31 -7
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +13 -8
- package/src/httpClient.ts +35 -31
- package/src/native.ts +53 -16
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +33 -7
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import com.facebook.react.ReactRootView
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A New Architecture reload restarts each surface in its existing view, and a
|
|
10
|
+
* view logs CONTENT_APPEARED only for its first content (the old architecture
|
|
11
|
+
* asks for it again when it runs the app). Asks every React root view
|
|
12
|
+
* of the activity to log it again, so the reloaded bundle's first content
|
|
13
|
+
* completes its launch, as a cold start does. Otherwise the next process rolls
|
|
14
|
+
* the working bundle back as an unfinished launch.
|
|
15
|
+
*/
|
|
16
|
+
internal object ContentAppearedLog {
|
|
17
|
+
fun rearm(activity: Activity?) {
|
|
18
|
+
activity?.window?.decorView?.let(::rearm)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private fun rearm(view: View) {
|
|
22
|
+
if (view is ReactRootView) {
|
|
23
|
+
view.setShouldLogContentAppeared(true)
|
|
24
|
+
}
|
|
25
|
+
if (view is ViewGroup) {
|
|
26
|
+
for (index in 0 until view.childCount) {
|
|
27
|
+
rearm(view.getChildAt(index))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -47,7 +47,14 @@ class HotUpdaterModule internal constructor(
|
|
|
47
47
|
*/
|
|
48
48
|
private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Every changed file with its hash and URL or Hermes patch; with
|
|
52
|
+
* [requireFileHash] false, also entries that hold only the Hermes patch.
|
|
53
|
+
*/
|
|
54
|
+
private fun parseAssets(
|
|
55
|
+
params: ReadableMap,
|
|
56
|
+
requireFileHash: Boolean,
|
|
57
|
+
): Map<String, ChangedAssetDescriptor>? {
|
|
51
58
|
if (!params.hasKey("assets") || params.isNull("assets")) {
|
|
52
59
|
return null
|
|
53
60
|
}
|
|
@@ -59,39 +66,10 @@ class HotUpdaterModule internal constructor(
|
|
|
59
66
|
while (iterator.hasNextKey()) {
|
|
60
67
|
val assetPath = iterator.nextKey()
|
|
61
68
|
val assetMap = changedAssetsMap.getMap(assetPath) ?: continue
|
|
62
|
-
// In artifact protocol v2, an entry holds only the Hermes patch.
|
|
63
69
|
val assetHash = if (assetMap.hasKey("fileHash")) assetMap.getString("fileHash") else null
|
|
70
|
+
if (assetHash == null && requireFileHash) continue
|
|
64
71
|
val patchMap = assetMap.getMap("patch")
|
|
65
|
-
val patch =
|
|
66
|
-
if (patchMap != null) {
|
|
67
|
-
val algorithm = patchMap.getString("algorithm")
|
|
68
|
-
val baseBundleId = patchMap.getString("baseBundleId")
|
|
69
|
-
val baseFileHash = patchMap.getString("baseFileHash")
|
|
70
|
-
val patchFileHash = patchMap.getString("patchFileHash")
|
|
71
|
-
val patchUrl = patchMap.getString("patchUrl")
|
|
72
|
-
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
algorithm != null &&
|
|
76
|
-
baseBundleId != null &&
|
|
77
|
-
baseFileHash != null &&
|
|
78
|
-
patchFileHash != null &&
|
|
79
|
-
patchUrl != null
|
|
80
|
-
) {
|
|
81
|
-
BsdiffPatchDescriptor(
|
|
82
|
-
algorithm = algorithm,
|
|
83
|
-
baseBundleId = baseBundleId,
|
|
84
|
-
baseFileHash = baseFileHash,
|
|
85
|
-
patchFileHash = patchFileHash,
|
|
86
|
-
patchUrl = patchUrl,
|
|
87
|
-
byteSize = byteSize,
|
|
88
|
-
)
|
|
89
|
-
} else {
|
|
90
|
-
null
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
null
|
|
94
|
-
}
|
|
72
|
+
val patch = parsePatch(patchMap)
|
|
95
73
|
val fileMap =
|
|
96
74
|
if (assetMap.hasKey("file") && !assetMap.isNull("file")) {
|
|
97
75
|
assetMap.getMap("file")
|
|
@@ -138,6 +116,79 @@ class HotUpdaterModule internal constructor(
|
|
|
138
116
|
return parsed
|
|
139
117
|
}
|
|
140
118
|
|
|
119
|
+
private fun parsePatch(patchMap: ReadableMap?): BsdiffPatchDescriptor? {
|
|
120
|
+
if (patchMap == null) return null
|
|
121
|
+
val algorithm = patchMap.getString("algorithm")
|
|
122
|
+
val baseBundleId = patchMap.getString("baseBundleId")
|
|
123
|
+
val baseFileHash = patchMap.getString("baseFileHash")
|
|
124
|
+
val patchFileHash = patchMap.getString("patchFileHash")
|
|
125
|
+
val patchUrl = patchMap.getString("patchUrl")
|
|
126
|
+
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
127
|
+
|
|
128
|
+
return if (
|
|
129
|
+
algorithm != null &&
|
|
130
|
+
baseBundleId != null &&
|
|
131
|
+
baseFileHash != null &&
|
|
132
|
+
patchFileHash != null &&
|
|
133
|
+
patchUrl != null
|
|
134
|
+
) {
|
|
135
|
+
BsdiffPatchDescriptor(
|
|
136
|
+
algorithm = algorithm,
|
|
137
|
+
baseBundleId = baseBundleId,
|
|
138
|
+
baseFileHash = baseFileHash,
|
|
139
|
+
patchFileHash = patchFileHash,
|
|
140
|
+
patchUrl = patchUrl,
|
|
141
|
+
byteSize = byteSize,
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The resolver that asks `filesUrl` for the files native downloads one by
|
|
150
|
+
* one; null when `filesUrl` is missing.
|
|
151
|
+
*/
|
|
152
|
+
private fun parseFileResolver(params: ReadableMap): ArtifactFileResolver? {
|
|
153
|
+
val filesUrl =
|
|
154
|
+
if (params.hasKey("filesUrl") && !params.isNull("filesUrl")) {
|
|
155
|
+
params.getString("filesUrl")?.takeIf { it.isNotEmpty() }
|
|
156
|
+
} else {
|
|
157
|
+
null
|
|
158
|
+
} ?: return null
|
|
159
|
+
try {
|
|
160
|
+
java.net.URL(filesUrl)
|
|
161
|
+
} catch (e: java.net.MalformedURLException) {
|
|
162
|
+
throw HotUpdaterException.invalidFileUrl("filesUrl", filesUrl, UpdateFailureResource.FILE)
|
|
163
|
+
}
|
|
164
|
+
val requestTimeoutMs =
|
|
165
|
+
if (params.hasKey("requestTimeout") && !params.isNull("requestTimeout")) {
|
|
166
|
+
params.getDouble("requestTimeout").toLong().takeIf { it > 0 }
|
|
167
|
+
} else {
|
|
168
|
+
null
|
|
169
|
+
} ?: HttpArtifactFileResolver.DEFAULT_TIMEOUT_MS
|
|
170
|
+
return HttpArtifactFileResolver(filesUrl, parseRequestHeaders(params), requestTimeoutMs)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Artifact protocol v2: the Hermes patch as the only planned asset, and
|
|
175
|
+
* the resolver that asks `filesUrl` for the files native downloads one by
|
|
176
|
+
* one; null when `filesUrl` is missing.
|
|
177
|
+
*/
|
|
178
|
+
private fun parseArtifactInstall(params: ReadableMap): Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>? {
|
|
179
|
+
val fileResolver = parseFileResolver(params) ?: return null
|
|
180
|
+
val patchMap = if (params.hasKey("patch") && !params.isNull("patch")) params.getMap("patch") else null
|
|
181
|
+
val assetPath = patchMap?.getString("assetPath")
|
|
182
|
+
val patch = parsePatch(patchMap)
|
|
183
|
+
val assets =
|
|
184
|
+
if (assetPath != null && patch != null) {
|
|
185
|
+
mapOf(assetPath to ChangedAssetDescriptor(fileUrl = null, fileHash = "", patch = patch))
|
|
186
|
+
} else {
|
|
187
|
+
emptyMap()
|
|
188
|
+
}
|
|
189
|
+
return assets to fileResolver
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
private fun parseSelection(params: ReadableMap): PersistedSelection? {
|
|
142
193
|
if (!params.hasKey("selection") || params.isNull("selection")) return null
|
|
143
194
|
val selection = params.getMap("selection") ?: return null
|
|
@@ -188,6 +239,28 @@ class HotUpdaterModule internal constructor(
|
|
|
188
239
|
override fun updateBundle(
|
|
189
240
|
params: ReadableMap,
|
|
190
241
|
promise: Promise,
|
|
242
|
+
) {
|
|
243
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
244
|
+
// JavaScript from the first protocol v2 SDK also sends `filesUrl`
|
|
245
|
+
// here, with only the Hermes patch in `assets`; its OTA Bundles must
|
|
246
|
+
// still install.
|
|
247
|
+
stage(params, promise) {
|
|
248
|
+
val fileResolver = parseFileResolver(params)
|
|
249
|
+
parseAssets(params, requireFileHash = fileResolver == null)?.let { it to fileResolver }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
override fun installArtifact(
|
|
254
|
+
params: ReadableMap,
|
|
255
|
+
promise: Promise,
|
|
256
|
+
) {
|
|
257
|
+
stage(params, promise) { parseArtifactInstall(params) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun stage(
|
|
261
|
+
params: ReadableMap,
|
|
262
|
+
promise: Promise,
|
|
263
|
+
plan: () -> Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>?,
|
|
191
264
|
) {
|
|
192
265
|
moduleScope.launch {
|
|
193
266
|
try {
|
|
@@ -199,7 +272,8 @@ class HotUpdaterModule internal constructor(
|
|
|
199
272
|
|
|
200
273
|
val manifestUrl = params.getString("manifestUrl")
|
|
201
274
|
val manifestFileHash = params.getString("manifestFileHash")
|
|
202
|
-
val
|
|
275
|
+
val planned = plan()
|
|
276
|
+
val assets = planned?.first
|
|
203
277
|
if (manifestUrl.isNullOrEmpty() || manifestFileHash.isNullOrEmpty() || assets == null) {
|
|
204
278
|
promise.rejectUpdateBundle(HotUpdaterException.invalidManifestParams())
|
|
205
279
|
return@launch
|
|
@@ -230,30 +304,7 @@ class HotUpdaterModule internal constructor(
|
|
|
230
304
|
}
|
|
231
305
|
}
|
|
232
306
|
val selection = parseSelection(params)
|
|
233
|
-
val
|
|
234
|
-
if (params.hasKey("filesUrl") && !params.isNull("filesUrl")) {
|
|
235
|
-
params.getString("filesUrl")
|
|
236
|
-
} else {
|
|
237
|
-
null
|
|
238
|
-
}
|
|
239
|
-
if (filesUrl != null) {
|
|
240
|
-
try {
|
|
241
|
-
java.net.URL(filesUrl)
|
|
242
|
-
} catch (e: java.net.MalformedURLException) {
|
|
243
|
-
promise.rejectUpdateBundle(
|
|
244
|
-
HotUpdaterException.invalidFileUrl("filesUrl", filesUrl, UpdateFailureResource.FILE),
|
|
245
|
-
)
|
|
246
|
-
return@launch
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
val requestTimeoutMs =
|
|
250
|
-
if (params.hasKey("requestTimeout") && !params.isNull("requestTimeout")) {
|
|
251
|
-
params.getDouble("requestTimeout").toLong().takeIf { it > 0 }
|
|
252
|
-
} else {
|
|
253
|
-
null
|
|
254
|
-
} ?: HttpArtifactFileResolver.DEFAULT_TIMEOUT_MS
|
|
255
|
-
val fileResolver =
|
|
256
|
-
filesUrl?.let { HttpArtifactFileResolver(it, parseRequestHeaders(params), requestTimeoutMs) }
|
|
307
|
+
val fileResolver = planned.second
|
|
257
308
|
|
|
258
309
|
val impl = getInstance()
|
|
259
310
|
|
|
@@ -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 android.util.Log
|
|
5
6
|
import com.facebook.react.ReactApplication
|
|
@@ -7,6 +8,7 @@ import com.facebook.react.ReactHost
|
|
|
7
8
|
import com.facebook.react.ReactInstanceEventListener
|
|
8
9
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
10
|
import com.facebook.react.bridge.ReactContext
|
|
11
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
10
12
|
import com.facebook.react.common.LifecycleState
|
|
11
13
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
12
14
|
import kotlin.coroutines.resume
|
|
@@ -67,12 +69,37 @@ class ReactIntegrationManager(
|
|
|
67
69
|
if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) {
|
|
68
70
|
reactHost.onHostResume(activity)
|
|
69
71
|
}
|
|
72
|
+
rearmContentAppearedAfterReload(reactHost, activity)
|
|
70
73
|
reactHost.reload("Requested by HotUpdater")
|
|
71
74
|
} catch (e: Exception) {
|
|
72
75
|
Log.d("HotUpdater", "Failed to reload with ReactHost: ${e.message}")
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Lets the reloaded bundle log its first content (see [ContentAppearedLog]).
|
|
81
|
+
*
|
|
82
|
+
* The views are re-armed once the new instance exists: by then reload has
|
|
83
|
+
* stopped and cleared the old surfaces, so the old bundle cannot complete the
|
|
84
|
+
* new bundle's launch, and the new bundle has not mounted anything yet.
|
|
85
|
+
*/
|
|
86
|
+
private fun rearmContentAppearedAfterReload(
|
|
87
|
+
reactHost: ReactHost,
|
|
88
|
+
activity: Activity?,
|
|
89
|
+
) {
|
|
90
|
+
val listener =
|
|
91
|
+
object : ReactInstanceEventListener {
|
|
92
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
93
|
+
reactHost.removeReactInstanceEventListener(this)
|
|
94
|
+
val rearm = { ContentAppearedLog.rearm(context.currentActivity ?: activity) }
|
|
95
|
+
// ReactHostImpl calls this on the UI thread, before it restarts
|
|
96
|
+
// the surfaces. Another host may not.
|
|
97
|
+
if (UiThreadUtil.isOnUiThread()) rearm() else UiThreadUtil.runOnUiThread(rearm)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
reactHost.addReactInstanceEventListener(listener)
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
private fun getReactApplication(): ReactApplication? {
|
|
77
104
|
if (context is ReactApplicationContext) {
|
|
78
105
|
val activity = context.currentActivity
|
|
@@ -47,7 +47,14 @@ class HotUpdaterModule internal constructor(
|
|
|
47
47
|
*/
|
|
48
48
|
private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Every changed file with its hash and URL or Hermes patch; with
|
|
52
|
+
* [requireFileHash] false, also entries that hold only the Hermes patch.
|
|
53
|
+
*/
|
|
54
|
+
private fun parseAssets(
|
|
55
|
+
params: ReadableMap,
|
|
56
|
+
requireFileHash: Boolean,
|
|
57
|
+
): Map<String, ChangedAssetDescriptor>? {
|
|
51
58
|
if (!params.hasKey("assets") || params.isNull("assets")) {
|
|
52
59
|
return null
|
|
53
60
|
}
|
|
@@ -59,39 +66,10 @@ class HotUpdaterModule internal constructor(
|
|
|
59
66
|
while (iterator.hasNextKey()) {
|
|
60
67
|
val assetPath = iterator.nextKey()
|
|
61
68
|
val assetMap = changedAssetsMap.getMap(assetPath) ?: continue
|
|
62
|
-
// In artifact protocol v2, an entry holds only the Hermes patch.
|
|
63
69
|
val assetHash = if (assetMap.hasKey("fileHash")) assetMap.getString("fileHash") else null
|
|
70
|
+
if (assetHash == null && requireFileHash) continue
|
|
64
71
|
val patchMap = assetMap.getMap("patch")
|
|
65
|
-
val patch =
|
|
66
|
-
if (patchMap != null) {
|
|
67
|
-
val algorithm = patchMap.getString("algorithm")
|
|
68
|
-
val baseBundleId = patchMap.getString("baseBundleId")
|
|
69
|
-
val baseFileHash = patchMap.getString("baseFileHash")
|
|
70
|
-
val patchFileHash = patchMap.getString("patchFileHash")
|
|
71
|
-
val patchUrl = patchMap.getString("patchUrl")
|
|
72
|
-
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
algorithm != null &&
|
|
76
|
-
baseBundleId != null &&
|
|
77
|
-
baseFileHash != null &&
|
|
78
|
-
patchFileHash != null &&
|
|
79
|
-
patchUrl != null
|
|
80
|
-
) {
|
|
81
|
-
BsdiffPatchDescriptor(
|
|
82
|
-
algorithm = algorithm,
|
|
83
|
-
baseBundleId = baseBundleId,
|
|
84
|
-
baseFileHash = baseFileHash,
|
|
85
|
-
patchFileHash = patchFileHash,
|
|
86
|
-
patchUrl = patchUrl,
|
|
87
|
-
byteSize = byteSize,
|
|
88
|
-
)
|
|
89
|
-
} else {
|
|
90
|
-
null
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
null
|
|
94
|
-
}
|
|
72
|
+
val patch = parsePatch(patchMap)
|
|
95
73
|
val fileMap =
|
|
96
74
|
if (assetMap.hasKey("file") && !assetMap.isNull("file")) {
|
|
97
75
|
assetMap.getMap("file")
|
|
@@ -138,6 +116,79 @@ class HotUpdaterModule internal constructor(
|
|
|
138
116
|
return parsed
|
|
139
117
|
}
|
|
140
118
|
|
|
119
|
+
private fun parsePatch(patchMap: ReadableMap?): BsdiffPatchDescriptor? {
|
|
120
|
+
if (patchMap == null) return null
|
|
121
|
+
val algorithm = patchMap.getString("algorithm")
|
|
122
|
+
val baseBundleId = patchMap.getString("baseBundleId")
|
|
123
|
+
val baseFileHash = patchMap.getString("baseFileHash")
|
|
124
|
+
val patchFileHash = patchMap.getString("patchFileHash")
|
|
125
|
+
val patchUrl = patchMap.getString("patchUrl")
|
|
126
|
+
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
127
|
+
|
|
128
|
+
return if (
|
|
129
|
+
algorithm != null &&
|
|
130
|
+
baseBundleId != null &&
|
|
131
|
+
baseFileHash != null &&
|
|
132
|
+
patchFileHash != null &&
|
|
133
|
+
patchUrl != null
|
|
134
|
+
) {
|
|
135
|
+
BsdiffPatchDescriptor(
|
|
136
|
+
algorithm = algorithm,
|
|
137
|
+
baseBundleId = baseBundleId,
|
|
138
|
+
baseFileHash = baseFileHash,
|
|
139
|
+
patchFileHash = patchFileHash,
|
|
140
|
+
patchUrl = patchUrl,
|
|
141
|
+
byteSize = byteSize,
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The resolver that asks `filesUrl` for the files native downloads one by
|
|
150
|
+
* one; null when `filesUrl` is missing.
|
|
151
|
+
*/
|
|
152
|
+
private fun parseFileResolver(params: ReadableMap): ArtifactFileResolver? {
|
|
153
|
+
val filesUrl =
|
|
154
|
+
if (params.hasKey("filesUrl") && !params.isNull("filesUrl")) {
|
|
155
|
+
params.getString("filesUrl")?.takeIf { it.isNotEmpty() }
|
|
156
|
+
} else {
|
|
157
|
+
null
|
|
158
|
+
} ?: return null
|
|
159
|
+
try {
|
|
160
|
+
java.net.URL(filesUrl)
|
|
161
|
+
} catch (e: java.net.MalformedURLException) {
|
|
162
|
+
throw HotUpdaterException.invalidFileUrl("filesUrl", filesUrl, UpdateFailureResource.FILE)
|
|
163
|
+
}
|
|
164
|
+
val requestTimeoutMs =
|
|
165
|
+
if (params.hasKey("requestTimeout") && !params.isNull("requestTimeout")) {
|
|
166
|
+
params.getDouble("requestTimeout").toLong().takeIf { it > 0 }
|
|
167
|
+
} else {
|
|
168
|
+
null
|
|
169
|
+
} ?: HttpArtifactFileResolver.DEFAULT_TIMEOUT_MS
|
|
170
|
+
return HttpArtifactFileResolver(filesUrl, parseRequestHeaders(params), requestTimeoutMs)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Artifact protocol v2: the Hermes patch as the only planned asset, and
|
|
175
|
+
* the resolver that asks `filesUrl` for the files native downloads one by
|
|
176
|
+
* one; null when `filesUrl` is missing.
|
|
177
|
+
*/
|
|
178
|
+
private fun parseArtifactInstall(params: ReadableMap): Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>? {
|
|
179
|
+
val fileResolver = parseFileResolver(params) ?: return null
|
|
180
|
+
val patchMap = if (params.hasKey("patch") && !params.isNull("patch")) params.getMap("patch") else null
|
|
181
|
+
val assetPath = patchMap?.getString("assetPath")
|
|
182
|
+
val patch = parsePatch(patchMap)
|
|
183
|
+
val assets =
|
|
184
|
+
if (assetPath != null && patch != null) {
|
|
185
|
+
mapOf(assetPath to ChangedAssetDescriptor(fileUrl = null, fileHash = "", patch = patch))
|
|
186
|
+
} else {
|
|
187
|
+
emptyMap()
|
|
188
|
+
}
|
|
189
|
+
return assets to fileResolver
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
private fun parseSelection(params: ReadableMap): PersistedSelection? {
|
|
142
193
|
if (!params.hasKey("selection") || params.isNull("selection")) return null
|
|
143
194
|
val selection = params.getMap("selection") ?: return null
|
|
@@ -192,6 +243,29 @@ class HotUpdaterModule internal constructor(
|
|
|
192
243
|
override fun updateBundle(
|
|
193
244
|
params: ReadableMap,
|
|
194
245
|
promise: Promise,
|
|
246
|
+
) {
|
|
247
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
248
|
+
// JavaScript from the first protocol v2 SDK also sends `filesUrl`
|
|
249
|
+
// here, with only the Hermes patch in `assets`; its OTA Bundles must
|
|
250
|
+
// still install.
|
|
251
|
+
stage(params, promise) {
|
|
252
|
+
val fileResolver = parseFileResolver(params)
|
|
253
|
+
parseAssets(params, requireFileHash = fileResolver == null)?.let { it to fileResolver }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@ReactMethod
|
|
258
|
+
override fun installArtifact(
|
|
259
|
+
params: ReadableMap,
|
|
260
|
+
promise: Promise,
|
|
261
|
+
) {
|
|
262
|
+
stage(params, promise) { parseArtifactInstall(params) }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private fun stage(
|
|
266
|
+
params: ReadableMap,
|
|
267
|
+
promise: Promise,
|
|
268
|
+
plan: () -> Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>?,
|
|
195
269
|
) {
|
|
196
270
|
moduleScope.launch {
|
|
197
271
|
try {
|
|
@@ -203,7 +277,8 @@ class HotUpdaterModule internal constructor(
|
|
|
203
277
|
|
|
204
278
|
val manifestUrl = params.getString("manifestUrl")
|
|
205
279
|
val manifestFileHash = params.getString("manifestFileHash")
|
|
206
|
-
val
|
|
280
|
+
val planned = plan()
|
|
281
|
+
val assets = planned?.first
|
|
207
282
|
if (manifestUrl.isNullOrEmpty() || manifestFileHash.isNullOrEmpty() || assets == null) {
|
|
208
283
|
promise.rejectUpdateBundle(HotUpdaterException.invalidManifestParams())
|
|
209
284
|
return@launch
|
|
@@ -234,30 +309,7 @@ class HotUpdaterModule internal constructor(
|
|
|
234
309
|
}
|
|
235
310
|
}
|
|
236
311
|
val selection = parseSelection(params)
|
|
237
|
-
val
|
|
238
|
-
if (params.hasKey("filesUrl") && !params.isNull("filesUrl")) {
|
|
239
|
-
params.getString("filesUrl")
|
|
240
|
-
} else {
|
|
241
|
-
null
|
|
242
|
-
}
|
|
243
|
-
if (filesUrl != null) {
|
|
244
|
-
try {
|
|
245
|
-
java.net.URL(filesUrl)
|
|
246
|
-
} catch (e: java.net.MalformedURLException) {
|
|
247
|
-
promise.rejectUpdateBundle(
|
|
248
|
-
HotUpdaterException.invalidFileUrl("filesUrl", filesUrl, UpdateFailureResource.FILE),
|
|
249
|
-
)
|
|
250
|
-
return@launch
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
val requestTimeoutMs =
|
|
254
|
-
if (params.hasKey("requestTimeout") && !params.isNull("requestTimeout")) {
|
|
255
|
-
params.getDouble("requestTimeout").toLong().takeIf { it > 0 }
|
|
256
|
-
} else {
|
|
257
|
-
null
|
|
258
|
-
} ?: HttpArtifactFileResolver.DEFAULT_TIMEOUT_MS
|
|
259
|
-
val fileResolver =
|
|
260
|
-
filesUrl?.let { HttpArtifactFileResolver(it, parseRequestHeaders(params), requestTimeoutMs) }
|
|
312
|
+
val fileResolver = planned.second
|
|
261
313
|
|
|
262
314
|
val impl = getInstance()
|
|
263
315
|
|
|
@@ -13,6 +13,11 @@ abstract class HotUpdaterSpec internal constructor(
|
|
|
13
13
|
promise: Promise,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
abstract fun installArtifact(
|
|
17
|
+
params: ReadableMap,
|
|
18
|
+
promise: Promise,
|
|
19
|
+
)
|
|
20
|
+
|
|
16
21
|
abstract fun acceptReleaseCatalog(params: ReadableMap): Boolean
|
|
17
22
|
|
|
18
23
|
abstract fun getActiveUpdateState(): String
|
|
@@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
21
21
|
- (void)updateBundle:(nullable NSDictionary *)params
|
|
22
22
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
23
23
|
rejecter:(RCTPromiseRejectBlock)reject;
|
|
24
|
+
- (void)installArtifact:(nullable NSDictionary *)params
|
|
25
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
rejecter:(RCTPromiseRejectBlock)reject;
|
|
24
27
|
- (BOOL)acceptReleaseCatalog:(nullable NSDictionary *)params;
|
|
25
28
|
- (NSDictionary<NSString *, id> *)getActiveUpdateState;
|
|
26
29
|
- (void)getReleaseCatalogCache:(NSString *)partition
|
|
@@ -532,6 +532,7 @@ RCT_EXPORT_MODULE();
|
|
|
532
532
|
if (params.selection()) {
|
|
533
533
|
paramDict[@"selection"] = params.selection();
|
|
534
534
|
}
|
|
535
|
+
// Legacy: what JavaScript from the first artifact protocol v2 SDK sends.
|
|
535
536
|
if (params.filesUrl()) {
|
|
536
537
|
paramDict[@"filesUrl"] = params.filesUrl();
|
|
537
538
|
}
|
|
@@ -546,6 +547,46 @@ RCT_EXPORT_MODULE();
|
|
|
546
547
|
[impl updateBundle:paramDict resolver:resolve rejecter:reject];
|
|
547
548
|
}
|
|
548
549
|
|
|
550
|
+
- (void)installArtifact:(JS::NativeHotUpdater::InstallArtifactParams &)params
|
|
551
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
552
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
553
|
+
NSLog(@"[HotUpdater.mm] installArtifact called.");
|
|
554
|
+
NSMutableDictionary *paramDict = [NSMutableDictionary dictionary];
|
|
555
|
+
if (params.bundleId()) {
|
|
556
|
+
paramDict[@"bundleId"] = params.bundleId();
|
|
557
|
+
}
|
|
558
|
+
if (params.manifestUrl()) {
|
|
559
|
+
paramDict[@"manifestUrl"] = params.manifestUrl();
|
|
560
|
+
}
|
|
561
|
+
if (params.manifestFileHash()) {
|
|
562
|
+
paramDict[@"manifestFileHash"] = params.manifestFileHash();
|
|
563
|
+
}
|
|
564
|
+
if (params.archiveUrl()) {
|
|
565
|
+
paramDict[@"archiveUrl"] = params.archiveUrl();
|
|
566
|
+
}
|
|
567
|
+
if (params.patch()) {
|
|
568
|
+
paramDict[@"patch"] = params.patch();
|
|
569
|
+
}
|
|
570
|
+
if (params.filesUrl()) {
|
|
571
|
+
paramDict[@"filesUrl"] = params.filesUrl();
|
|
572
|
+
}
|
|
573
|
+
if (params.requestHeaders()) {
|
|
574
|
+
paramDict[@"requestHeaders"] = params.requestHeaders();
|
|
575
|
+
}
|
|
576
|
+
if (params.requestTimeout().has_value()) {
|
|
577
|
+
paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
|
|
578
|
+
}
|
|
579
|
+
if (params.channel()) {
|
|
580
|
+
paramDict[@"channel"] = params.channel();
|
|
581
|
+
}
|
|
582
|
+
if (params.selection()) {
|
|
583
|
+
paramDict[@"selection"] = params.selection();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
587
|
+
[impl installArtifact:paramDict resolver:resolve rejecter:reject];
|
|
588
|
+
}
|
|
589
|
+
|
|
549
590
|
- (NSNumber *)acceptReleaseCatalog:(NSDictionary *)params {
|
|
550
591
|
return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
|
|
551
592
|
}
|
|
@@ -711,6 +752,14 @@ RCT_EXPORT_METHOD(updateBundle:(NSDictionary *)params
|
|
|
711
752
|
[impl updateBundle:params resolver:resolve rejecter:reject];
|
|
712
753
|
}
|
|
713
754
|
|
|
755
|
+
RCT_EXPORT_METHOD(installArtifact:(NSDictionary *)params
|
|
756
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
757
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
758
|
+
NSLog(@"[HotUpdater.mm] installArtifact called.");
|
|
759
|
+
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
760
|
+
[impl installArtifact:params resolver:resolve rejecter:reject];
|
|
761
|
+
}
|
|
762
|
+
|
|
714
763
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(acceptReleaseCatalog:(NSDictionary *)params) {
|
|
715
764
|
return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
|
|
716
765
|
}
|