expo-updates 55.0.28 → 55.0.30
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 +20 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/updates/EnabledUpdatesController.kt +15 -5
- package/android/src/main/java/expo/modules/updates/UpdatesDevLauncherController.kt +9 -2
- package/android/src/main/java/expo/modules/updates/launcher/DatabaseLauncher.kt +9 -0
- package/android/src/main/java/expo/modules/updates/loader/EmbeddedLoader.kt +10 -4
- package/android/src/main/java/expo/modules/updates/loader/Loader.kt +12 -7
- package/android/src/main/java/expo/modules/updates/loader/LoaderTask.kt +9 -2
- package/android/src/main/java/expo/modules/updates/loader/RemoteLoader.kt +9 -4
- package/android/src/main/java/expo/modules/updates/logging/UpdatesLogEntry.kt +5 -2
- package/android/src/main/java/expo/modules/updates/logging/UpdatesLogReader.kt +1 -1
- package/android/src/main/java/expo/modules/updates/procedures/CheckForUpdateProcedure.kt +2 -0
- package/android/src/main/java/expo/modules/updates/procedures/FetchUpdateProcedure.kt +6 -1
- package/android/src/main/java/expo/modules/updates/procedures/RelaunchProcedure.kt +5 -0
- package/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt +4 -1
- package/android/src/main/java/expo/modules/updates/reloadscreen/ReloadScreenView.kt +3 -0
- package/ios/EXUpdates/Database/UpdatesDatabase.swift +20 -4
- package/package.json +2 -2
- package/utils/build/createManifestForBuildAsync.js +4 -2
- package/utils/src/createManifestForBuildAsync.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.30 — 2026-08-28
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [iOS] Propagate database failures from `addNewAssets` instead of swallowing them, which let an update be marked ready without its launch asset and then fail every launch. ([#49456](https://github.com/expo/expo/pull/49456) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
- [iOS] Repair ready updates that are missing their launch asset by demoting them to pending during launcher selection, so a corrupted row is skipped and retried instead of failing every launch. ([#49457](https://github.com/expo/expo/pull/49457) by [@alanjhughes](https://github.com/alanjhughes))
|
|
19
|
+
- [Android] Skip and repair updates that are missing their launch asset instead of selecting them for launch, which previously failed every cold start with "Launch asset not found for update". ([#49470](https://github.com/expo/expo/pull/49470) by [@alanjhughes](https://github.com/alanjhughes), based on [#48733](https://github.com/expo/expo/pull/48733) by [@martintreurnicht](https://github.com/martintreurnicht))
|
|
20
|
+
- [iOS] Fix the embedded manifest recording the wrong `packagerHash` for assets that ship scale variants iOS does not allow (such as `@1.5x` and `@4x`): the hashes were read by the filtered scale index instead of the asset's own, so those images resolved to an empty URI and rendered blank in release builds. ([#48811](https://github.com/expo/expo/pull/48811) by [@expo-bot](https://github.com/expo-bot))
|
|
21
|
+
|
|
22
|
+
## 55.0.29 — 2026-08-27
|
|
23
|
+
|
|
24
|
+
### 🐛 Bug fixes
|
|
25
|
+
|
|
26
|
+
- [Android] Widen `UpdatesLogEntry.create`'s catch from `JSONException` to `Exception` so log-line parse failures consistently degrade to "skip the entry" instead of propagating. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
27
|
+
- [Android] Correct `UpdatesLogReader.ONE_DAY_MILLISECONDS` from `86400` (seconds) to `86_400_000` (milliseconds), so the "older than one day" purge filter actually retains a day's worth of entries instead of ~86 seconds' worth. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
28
|
+
|
|
29
|
+
### 💡 Others
|
|
30
|
+
|
|
31
|
+
- [Android] Log purge completion errors via `android.util.Log.e` directly instead of `logger.error`, so the failure path doesn't re-enter the `PersistentFileLog` dispatch queue from inside one of its own tasks. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
32
|
+
|
|
13
33
|
## 55.0.28 — 2026-08-25
|
|
14
34
|
|
|
15
35
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -42,7 +42,7 @@ expoModule {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
group = 'host.exp.exponent'
|
|
45
|
-
version = '55.0.
|
|
45
|
+
version = '55.0.30'
|
|
46
46
|
|
|
47
47
|
// Utility method to derive boolean values from the environment or from Java properties,
|
|
48
48
|
// and return them as strings to be used in BuildConfig fields
|
|
@@ -89,7 +89,7 @@ android {
|
|
|
89
89
|
namespace "expo.modules.updates"
|
|
90
90
|
defaultConfig {
|
|
91
91
|
versionCode 31
|
|
92
|
-
versionName '55.0.
|
|
92
|
+
versionName '55.0.30'
|
|
93
93
|
consumerProguardFiles("proguard-rules.pro")
|
|
94
94
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
95
95
|
|
|
@@ -4,6 +4,7 @@ import android.app.Activity
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.net.Uri
|
|
6
6
|
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
7
8
|
import com.facebook.react.ReactHost
|
|
8
9
|
import com.facebook.react.bridge.ReactContext
|
|
9
10
|
import com.facebook.react.devsupport.interfaces.DevSupportManager
|
|
@@ -18,7 +19,6 @@ import expo.modules.updates.events.IUpdatesEventManager
|
|
|
18
19
|
import expo.modules.updates.events.UpdatesEventManager
|
|
19
20
|
import expo.modules.updates.launcher.Launcher.LauncherCallback
|
|
20
21
|
import expo.modules.updates.loader.FileDownloader
|
|
21
|
-
import expo.modules.updates.logging.UpdatesErrorCode
|
|
22
22
|
import expo.modules.updates.logging.UpdatesLogReader
|
|
23
23
|
import expo.modules.updates.logging.UpdatesLogger
|
|
24
24
|
import expo.modules.updates.manifest.EmbeddedManifestUtils
|
|
@@ -35,6 +35,7 @@ import expo.modules.updates.statemachine.UpdatesStateValue
|
|
|
35
35
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
36
36
|
import expo.modules.updatesinterface.UpdatesStateChangeListener
|
|
37
37
|
import expo.modules.updatesinterface.UpdatesStateChangeSubscription
|
|
38
|
+
import kotlinx.coroutines.CancellationException
|
|
38
39
|
import kotlinx.coroutines.CompletableDeferred
|
|
39
40
|
import kotlinx.coroutines.CoroutineScope
|
|
40
41
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -89,7 +90,12 @@ class EnabledUpdatesController(
|
|
|
89
90
|
private fun purgeUpdatesLogsOlderThanOneDay() {
|
|
90
91
|
UpdatesLogReader(context.filesDir).purgeLogEntries {
|
|
91
92
|
if (it != null) {
|
|
92
|
-
|
|
93
|
+
// Log directly via android.util.Log rather than through `logger.error`,
|
|
94
|
+
// which writes via the PersistentFileLog dispatch queue. This callback
|
|
95
|
+
// is invoked from inside one of that queue's own tasks, so feeding
|
|
96
|
+
// another entry back into the queue from here re-enters it. Bypassing
|
|
97
|
+
// the queue keeps the failure path off its own back.
|
|
98
|
+
Log.e("expo-updates", "UpdatesLogReader: error in purgeLogEntries", it)
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -256,7 +262,7 @@ class EnabledUpdatesController(
|
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
override suspend fun fetchUpdate() = suspendCancellableCoroutine { continuation ->
|
|
259
|
-
val procedure = FetchUpdateProcedure(context, updatesConfiguration, logger, databaseHolder, updatesDirectory, fileDownloader, selectionPolicy, launchedUpdate) {
|
|
265
|
+
val procedure = FetchUpdateProcedure(context, updatesConfiguration, logger, databaseHolder, updatesDirectory, fileDownloader, selectionPolicy, launchedUpdate, controllerScope) {
|
|
260
266
|
continuation.resume(it)
|
|
261
267
|
}
|
|
262
268
|
stateMachine.queueExecution(procedure)
|
|
@@ -280,6 +286,8 @@ class EnabledUpdatesController(
|
|
|
280
286
|
}
|
|
281
287
|
}
|
|
282
288
|
continuation.resume(resultMap)
|
|
289
|
+
} catch (e: CancellationException) {
|
|
290
|
+
throw e
|
|
283
291
|
} catch (e: Exception) {
|
|
284
292
|
continuation.resumeWithException(e.toCodedException())
|
|
285
293
|
}
|
|
@@ -288,7 +296,7 @@ class EnabledUpdatesController(
|
|
|
288
296
|
|
|
289
297
|
override suspend fun setExtraParam(key: String, value: String?) = suspendCancellableCoroutine { continuation ->
|
|
290
298
|
controllerScope.launch {
|
|
291
|
-
|
|
299
|
+
try {
|
|
292
300
|
ManifestMetadata.setExtraParam(
|
|
293
301
|
databaseHolder.database,
|
|
294
302
|
updatesConfiguration,
|
|
@@ -296,7 +304,9 @@ class EnabledUpdatesController(
|
|
|
296
304
|
value
|
|
297
305
|
)
|
|
298
306
|
continuation.resume(Unit)
|
|
299
|
-
}
|
|
307
|
+
} catch (e: CancellationException) {
|
|
308
|
+
throw e
|
|
309
|
+
} catch (e: Exception) {
|
|
300
310
|
continuation.resumeWithException(e.toCodedException())
|
|
301
311
|
}
|
|
302
312
|
}
|
|
@@ -34,9 +34,11 @@ import expo.modules.updatesinterface.UpdatesDevLauncherInterface
|
|
|
34
34
|
import expo.modules.updatesinterface.UpdatesInterfaceCallbacks
|
|
35
35
|
import expo.modules.updatesinterface.UpdatesStateChangeListener
|
|
36
36
|
import expo.modules.updatesinterface.UpdatesStateChangeSubscription
|
|
37
|
+
import kotlinx.coroutines.CancellationException
|
|
37
38
|
import kotlinx.coroutines.CoroutineScope
|
|
38
39
|
import kotlinx.coroutines.Dispatchers
|
|
39
40
|
import kotlinx.coroutines.SupervisorJob
|
|
41
|
+
import kotlinx.coroutines.cancel
|
|
40
42
|
import kotlinx.coroutines.launch
|
|
41
43
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
42
44
|
import org.json.JSONObject
|
|
@@ -183,7 +185,8 @@ class UpdatesDevLauncherController(
|
|
|
183
185
|
databaseHolder.database,
|
|
184
186
|
fileDownloader,
|
|
185
187
|
updatesDirectory,
|
|
186
|
-
null
|
|
188
|
+
null,
|
|
189
|
+
controllerScope
|
|
187
190
|
)
|
|
188
191
|
controllerScope.launch {
|
|
189
192
|
val progressJob = launch {
|
|
@@ -215,6 +218,8 @@ class UpdatesDevLauncherController(
|
|
|
215
218
|
return@launch
|
|
216
219
|
}
|
|
217
220
|
launchUpdate(loaderResult.updateEntity, updatesConfiguration!!, fileDownloader, callback)
|
|
221
|
+
} catch (e: CancellationException) {
|
|
222
|
+
throw e
|
|
218
223
|
} catch (e: Exception) {
|
|
219
224
|
// reset controller's configuration to what it was before this request
|
|
220
225
|
updatesConfiguration = previousUpdatesConfiguration
|
|
@@ -309,6 +314,8 @@ class UpdatesDevLauncherController(
|
|
|
309
314
|
get() = launcher.launchAssetFile!!
|
|
310
315
|
})
|
|
311
316
|
runReaper()
|
|
317
|
+
} catch (e: CancellationException) {
|
|
318
|
+
throw e
|
|
312
319
|
} catch (e: Exception) {
|
|
313
320
|
// reset controller's configuration to what it was before this request
|
|
314
321
|
updatesConfiguration = previousUpdatesConfiguration
|
|
@@ -385,7 +392,7 @@ class UpdatesDevLauncherController(
|
|
|
385
392
|
}
|
|
386
393
|
|
|
387
394
|
override fun shutdown() {
|
|
388
|
-
|
|
395
|
+
controllerScope.cancel()
|
|
389
396
|
}
|
|
390
397
|
|
|
391
398
|
companion object {
|
|
@@ -161,6 +161,15 @@ class DatabaseLauncher(
|
|
|
161
161
|
if (!configuration.hasEmbeddedUpdate && embeddedUpdate?.updateEntity?.id == update.id) {
|
|
162
162
|
continue
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// An update with no launch asset can never launch. Excluding it here lets the loader
|
|
166
|
+
// re-run and repair the row instead of failing every cold start.
|
|
167
|
+
if (update.status != UpdateStatus.DEVELOPMENT &&
|
|
168
|
+
database.updateDao().loadLaunchAssetForUpdate(update.id) == null
|
|
169
|
+
) {
|
|
170
|
+
logger.warn("Skipping launchable update with no launch asset. Debug info: ${update.debugInfo()}")
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
164
173
|
filteredLaunchableUpdates.add(update)
|
|
165
174
|
}
|
|
166
175
|
val manifestFilters = ManifestMetadata.getManifestFilters(database, configuration)
|
|
@@ -9,6 +9,9 @@ import expo.modules.updates.UpdatesUtils
|
|
|
9
9
|
import expo.modules.updates.db.entity.UpdateEntity
|
|
10
10
|
import expo.modules.updates.logging.UpdatesLogger
|
|
11
11
|
import expo.modules.updates.utils.AndroidResourceAssetUtils
|
|
12
|
+
import kotlinx.coroutines.CoroutineScope
|
|
13
|
+
import kotlinx.coroutines.Dispatchers
|
|
14
|
+
import kotlinx.coroutines.SupervisorJob
|
|
12
15
|
import java.io.File
|
|
13
16
|
import java.io.FileNotFoundException
|
|
14
17
|
import java.lang.AssertionError
|
|
@@ -32,14 +35,16 @@ class EmbeddedLoader internal constructor(
|
|
|
32
35
|
database: UpdatesDatabase,
|
|
33
36
|
updatesDirectory: File,
|
|
34
37
|
private val loaderFiles: LoaderFiles,
|
|
35
|
-
private val shouldCopyEmbeddedAssets: Boolean = BuildConfig.EX_UPDATES_COPY_EMBEDDED_ASSETS
|
|
38
|
+
private val shouldCopyEmbeddedAssets: Boolean = BuildConfig.EX_UPDATES_COPY_EMBEDDED_ASSETS,
|
|
39
|
+
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
36
40
|
) : Loader(
|
|
37
41
|
context,
|
|
38
42
|
configuration,
|
|
39
43
|
logger,
|
|
40
44
|
database,
|
|
41
45
|
updatesDirectory,
|
|
42
|
-
loaderFiles
|
|
46
|
+
loaderFiles,
|
|
47
|
+
scope
|
|
43
48
|
) {
|
|
44
49
|
|
|
45
50
|
constructor(
|
|
@@ -47,8 +52,9 @@ class EmbeddedLoader internal constructor(
|
|
|
47
52
|
configuration: UpdatesConfiguration,
|
|
48
53
|
logger: UpdatesLogger,
|
|
49
54
|
database: UpdatesDatabase,
|
|
50
|
-
updatesDirectory: File
|
|
51
|
-
|
|
55
|
+
updatesDirectory: File,
|
|
56
|
+
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
57
|
+
) : this(context, configuration, logger, database, updatesDirectory, LoaderFiles(), scope = scope)
|
|
52
58
|
|
|
53
59
|
override suspend fun loadRemoteUpdate(
|
|
54
60
|
database: UpdatesDatabase,
|
|
@@ -25,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob
|
|
|
25
25
|
import java.io.File
|
|
26
26
|
import java.io.IOException
|
|
27
27
|
import java.util.*
|
|
28
|
+
import java.util.Collections
|
|
28
29
|
import java.util.concurrent.ConcurrentHashMap
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -46,9 +47,9 @@ abstract class Loader protected constructor(
|
|
|
46
47
|
private var updateResponse: UpdateResponse? = null
|
|
47
48
|
private var updateEntity: UpdateEntity? = null
|
|
48
49
|
private var assetTotal = 0
|
|
49
|
-
private var erroredAssetList
|
|
50
|
-
private var existingAssetList
|
|
51
|
-
private var finishedAssetList
|
|
50
|
+
private var erroredAssetList: MutableList<AssetEntity> = Collections.synchronizedList(mutableListOf())
|
|
51
|
+
private var existingAssetList: MutableList<AssetEntity> = Collections.synchronizedList(mutableListOf())
|
|
52
|
+
private var finishedAssetList: MutableList<AssetEntity> = Collections.synchronizedList(mutableListOf())
|
|
52
53
|
private val _progressFlow = MutableSharedFlow<AssetLoadProgress>()
|
|
53
54
|
private var assetProgressMap: MutableMap<AssetEntity, Double> = ConcurrentHashMap()
|
|
54
55
|
|
|
@@ -117,9 +118,9 @@ abstract class Loader protected constructor(
|
|
|
117
118
|
updateResponse = null
|
|
118
119
|
updateEntity = null
|
|
119
120
|
assetTotal = 0
|
|
120
|
-
erroredAssetList = mutableListOf()
|
|
121
|
-
existingAssetList = mutableListOf()
|
|
122
|
-
finishedAssetList = mutableListOf()
|
|
121
|
+
erroredAssetList = Collections.synchronizedList(mutableListOf())
|
|
122
|
+
existingAssetList = Collections.synchronizedList(mutableListOf())
|
|
123
|
+
finishedAssetList = Collections.synchronizedList(mutableListOf())
|
|
123
124
|
assetProgressMap = ConcurrentHashMap()
|
|
124
125
|
assetLoadProgressBlock = null
|
|
125
126
|
}
|
|
@@ -165,7 +166,11 @@ abstract class Loader protected constructor(
|
|
|
165
166
|
database.updateDao().setUpdateScopeKey(existingUpdateEntity, newUpdateEntity.scopeKey)
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
// A READY update with no launch asset is not actually ready. Fall through to
|
|
170
|
+
// downloadAllAssets so its assets are re-registered instead of staying broken forever.
|
|
171
|
+
if (existingUpdateEntity != null && existingUpdateEntity.status == UpdateStatus.READY &&
|
|
172
|
+
database.updateDao().loadLaunchAssetForUpdate(existingUpdateEntity.id) != null
|
|
173
|
+
) {
|
|
169
174
|
// hooray, we already have this update downloaded and ready to go!
|
|
170
175
|
updateEntity = existingUpdateEntity
|
|
171
176
|
return finish()
|
|
@@ -17,6 +17,7 @@ import expo.modules.updates.manifest.EmbeddedManifestUtils
|
|
|
17
17
|
import expo.modules.updates.manifest.ManifestMetadata
|
|
18
18
|
import expo.modules.updates.manifest.Update
|
|
19
19
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
20
|
+
import kotlinx.coroutines.CancellationException
|
|
20
21
|
import kotlinx.coroutines.launch
|
|
21
22
|
import kotlinx.coroutines.CoroutineScope
|
|
22
23
|
import org.json.JSONObject
|
|
@@ -189,6 +190,8 @@ class LoaderTask(
|
|
|
189
190
|
callback.onFinishedAllLoading()
|
|
190
191
|
}
|
|
191
192
|
}
|
|
193
|
+
} catch (e: CancellationException) {
|
|
194
|
+
throw e
|
|
192
195
|
} catch (e: Exception) {
|
|
193
196
|
if (!shouldCheckForUpdate) {
|
|
194
197
|
finish(e)
|
|
@@ -209,6 +212,8 @@ class LoaderTask(
|
|
|
209
212
|
isRunning = false
|
|
210
213
|
runReaper()
|
|
211
214
|
callback.onFinishedAllLoading()
|
|
215
|
+
} catch (e: CancellationException) {
|
|
216
|
+
throw e
|
|
212
217
|
} catch (e: Exception) {
|
|
213
218
|
finish(e)
|
|
214
219
|
isRunning = false
|
|
@@ -296,10 +301,12 @@ class LoaderTask(
|
|
|
296
301
|
)
|
|
297
302
|
) {
|
|
298
303
|
try {
|
|
299
|
-
val embeddedLoader = EmbeddedLoader(context, configuration, logger, database, directory)
|
|
304
|
+
val embeddedLoader = EmbeddedLoader(context, configuration, logger, database, directory, scope)
|
|
300
305
|
embeddedLoader.load { _ ->
|
|
301
306
|
Loader.OnUpdateResponseLoadedResult(shouldDownloadManifestIfPresentInResponse = true)
|
|
302
307
|
}
|
|
308
|
+
} catch (e: CancellationException) {
|
|
309
|
+
throw e
|
|
303
310
|
} catch (e: Exception) {
|
|
304
311
|
logger.error("Unexpected error copying embedded update", e, UpdatesErrorCode.Unknown)
|
|
305
312
|
}
|
|
@@ -315,7 +322,7 @@ class LoaderTask(
|
|
|
315
322
|
private suspend fun launchRemoteUpdateInBackground() {
|
|
316
323
|
val database = databaseHolder.database
|
|
317
324
|
callback.onRemoteCheckForUpdateStarted()
|
|
318
|
-
val remoteLoader = RemoteLoader(context, configuration, logger, database, fileDownloader, directory, candidateLauncher?.launchedUpdate)
|
|
325
|
+
val remoteLoader = RemoteLoader(context, configuration, logger, database, fileDownloader, directory, candidateLauncher?.launchedUpdate, scope)
|
|
319
326
|
|
|
320
327
|
remoteLoader.assetLoadProgressBlock = { progress ->
|
|
321
328
|
callback.onRemoteUpdateProgressChanged(progress)
|
|
@@ -10,6 +10,9 @@ import expo.modules.updates.logging.UpdatesLogger
|
|
|
10
10
|
import expo.modules.updates.manifest.EmbeddedManifestUtils
|
|
11
11
|
import expo.modules.updates.manifest.ManifestMetadata
|
|
12
12
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
13
|
+
import kotlinx.coroutines.CoroutineScope
|
|
14
|
+
import kotlinx.coroutines.Dispatchers
|
|
15
|
+
import kotlinx.coroutines.SupervisorJob
|
|
13
16
|
import java.io.File
|
|
14
17
|
|
|
15
18
|
data class ProcessSuccessLoaderResult(
|
|
@@ -32,8 +35,9 @@ class RemoteLoader internal constructor(
|
|
|
32
35
|
private val mFileDownloader: FileDownloader,
|
|
33
36
|
updatesDirectory: File,
|
|
34
37
|
private val launchedUpdate: UpdateEntity?,
|
|
35
|
-
private val loaderFiles: LoaderFiles
|
|
36
|
-
|
|
38
|
+
private val loaderFiles: LoaderFiles,
|
|
39
|
+
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
40
|
+
) : Loader(context, configuration, logger, database, updatesDirectory, loaderFiles, scope) {
|
|
37
41
|
constructor(
|
|
38
42
|
context: Context,
|
|
39
43
|
configuration: UpdatesConfiguration,
|
|
@@ -41,8 +45,9 @@ class RemoteLoader internal constructor(
|
|
|
41
45
|
database: UpdatesDatabase,
|
|
42
46
|
fileDownloader: FileDownloader,
|
|
43
47
|
updatesDirectory: File,
|
|
44
|
-
launchedUpdate: UpdateEntity
|
|
45
|
-
|
|
48
|
+
launchedUpdate: UpdateEntity?,
|
|
49
|
+
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
50
|
+
) : this(context, configuration, logger, database, fileDownloader, updatesDirectory, launchedUpdate, LoaderFiles(), scope)
|
|
46
51
|
|
|
47
52
|
override suspend fun loadRemoteUpdate(
|
|
48
53
|
database: UpdatesDatabase,
|
|
@@ -3,7 +3,6 @@ package expo.modules.updates.logging
|
|
|
3
3
|
import expo.modules.jsonutils.getNullable
|
|
4
4
|
import expo.modules.jsonutils.require
|
|
5
5
|
import org.json.JSONArray
|
|
6
|
-
import org.json.JSONException
|
|
7
6
|
import org.json.JSONObject
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -59,7 +58,11 @@ data class UpdatesLogEntry(
|
|
|
59
58
|
List(jsonArray.length()) { i -> jsonArray.getString(i) }
|
|
60
59
|
}
|
|
61
60
|
)
|
|
62
|
-
} catch (e:
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
// `JSONObject.require<T>` can surface a ClassCastException via a blind
|
|
63
|
+
// cast in the catch-all branch (e.g. an int-sized numeric `duration`
|
|
64
|
+
// token surfaces as `Integer`), which would otherwise escape and
|
|
65
|
+
// propagate to the caller.
|
|
63
66
|
null
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -43,6 +43,8 @@ class CheckForUpdateProcedure(
|
|
|
43
43
|
try {
|
|
44
44
|
val updateResponse = downloadRemoteUpdate(extraHeaders)
|
|
45
45
|
processUpdatesResponse(updateResponse, procedureContext, embeddedUpdate)
|
|
46
|
+
} catch (e: CancellationException) {
|
|
47
|
+
throw e
|
|
46
48
|
} catch (e: Exception) {
|
|
47
49
|
procedureContext.processStateEvent(UpdatesStateEvent.CheckError(e.localizedMessageWithCauseLocalizedMessage()))
|
|
48
50
|
callback(IUpdatesController.CheckForUpdateResult.ErrorResult(e))
|
|
@@ -14,6 +14,7 @@ import expo.modules.updates.logging.UpdatesLogger
|
|
|
14
14
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
15
15
|
import expo.modules.updates.statemachine.UpdatesStateEvent
|
|
16
16
|
import kotlinx.coroutines.CancellationException
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
17
18
|
import java.io.File
|
|
18
19
|
|
|
19
20
|
class FetchUpdateProcedure(
|
|
@@ -25,6 +26,7 @@ class FetchUpdateProcedure(
|
|
|
25
26
|
private val fileDownloader: FileDownloader,
|
|
26
27
|
private val selectionPolicy: SelectionPolicy,
|
|
27
28
|
private val launchedUpdate: UpdateEntity?,
|
|
29
|
+
private val scope: CoroutineScope,
|
|
28
30
|
private val callback: (IUpdatesController.FetchUpdateResult) -> Unit
|
|
29
31
|
) : StateMachineProcedure() {
|
|
30
32
|
override val loggerTimerLabel = "timer-fetch-update"
|
|
@@ -36,6 +38,8 @@ class FetchUpdateProcedure(
|
|
|
36
38
|
try {
|
|
37
39
|
val loaderResult = startRemoteLoader(database, procedureContext)
|
|
38
40
|
processSuccessLoaderResult(loaderResult, procedureContext)
|
|
41
|
+
} catch (e: CancellationException) {
|
|
42
|
+
throw e
|
|
39
43
|
} catch (e: Exception) {
|
|
40
44
|
logger.error("Failed to download new update", e)
|
|
41
45
|
procedureContext.processStateEvent(
|
|
@@ -55,7 +59,8 @@ class FetchUpdateProcedure(
|
|
|
55
59
|
database,
|
|
56
60
|
fileDownloader,
|
|
57
61
|
updatesDirectory,
|
|
58
|
-
launchedUpdate
|
|
62
|
+
launchedUpdate,
|
|
63
|
+
scope
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
remoteLoader.assetLoadProgressBlock = { progress ->
|
|
@@ -13,6 +13,7 @@ import expo.modules.updates.logging.UpdatesLogger
|
|
|
13
13
|
import expo.modules.updates.reloadscreen.ReloadScreenManager
|
|
14
14
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
15
15
|
import expo.modules.updates.statemachine.UpdatesStateEvent
|
|
16
|
+
import kotlinx.coroutines.CancellationException
|
|
16
17
|
import kotlinx.coroutines.CoroutineScope
|
|
17
18
|
import kotlinx.coroutines.Dispatchers
|
|
18
19
|
import kotlinx.coroutines.launch
|
|
@@ -57,6 +58,8 @@ class RelaunchProcedure(
|
|
|
57
58
|
)
|
|
58
59
|
try {
|
|
59
60
|
launchWith(newLauncher)
|
|
61
|
+
} catch (e: CancellationException) {
|
|
62
|
+
throw e
|
|
60
63
|
} catch (e: Exception) {
|
|
61
64
|
logger.error("Error launching new launcher", e, UpdatesErrorCode.Unknown)
|
|
62
65
|
callback.onFailure(e)
|
|
@@ -91,6 +94,8 @@ class RelaunchProcedure(
|
|
|
91
94
|
getCurrentLauncher().launchedUpdate,
|
|
92
95
|
selectionPolicy
|
|
93
96
|
)
|
|
97
|
+
} catch (e: CancellationException) {
|
|
98
|
+
throw e
|
|
94
99
|
} catch (e: Exception) {
|
|
95
100
|
logger.error("Could not run Reaper.", e, UpdatesErrorCode.Unknown)
|
|
96
101
|
}
|
|
@@ -21,6 +21,7 @@ import expo.modules.updates.manifest.Update
|
|
|
21
21
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
22
22
|
import expo.modules.updates.statemachine.UpdatesStateEvent
|
|
23
23
|
import expo.modules.updates.statemachine.UpdatesStateValue
|
|
24
|
+
import kotlinx.coroutines.CancellationException
|
|
24
25
|
import kotlinx.coroutines.CoroutineScope
|
|
25
26
|
import kotlinx.coroutines.Dispatchers
|
|
26
27
|
import kotlinx.coroutines.launch
|
|
@@ -242,7 +243,7 @@ class StartupProcedure(
|
|
|
242
243
|
return
|
|
243
244
|
}
|
|
244
245
|
remoteLoadStatus = ErrorRecoveryDelegate.RemoteLoadStatus.NEW_UPDATE_LOADING
|
|
245
|
-
val remoteLoader = RemoteLoader(context, updatesConfiguration, logger, databaseHolder.database, fileDownloader, updatesDirectory, launchedUpdate)
|
|
246
|
+
val remoteLoader = RemoteLoader(context, updatesConfiguration, logger, databaseHolder.database, fileDownloader, updatesDirectory, launchedUpdate, procedureScope)
|
|
246
247
|
procedureScope.launch {
|
|
247
248
|
try {
|
|
248
249
|
val loaderResult = remoteLoader.load { updateResponse ->
|
|
@@ -267,6 +268,8 @@ class StartupProcedure(
|
|
|
267
268
|
ErrorRecoveryDelegate.RemoteLoadStatus.IDLE
|
|
268
269
|
}
|
|
269
270
|
)
|
|
271
|
+
} catch (e: CancellationException) {
|
|
272
|
+
throw e
|
|
270
273
|
} catch (e: Exception) {
|
|
271
274
|
logger.error("UpdatesController loadRemoteUpdate onFailure", e, UpdatesErrorCode.UpdateFailedToLoad, launchedUpdate?.loggingId, null)
|
|
272
275
|
setRemoteLoadStatus(ErrorRecoveryDelegate.RemoteLoadStatus.IDLE)
|
|
@@ -11,6 +11,7 @@ import android.view.View
|
|
|
11
11
|
import android.widget.FrameLayout
|
|
12
12
|
import android.widget.ImageView
|
|
13
13
|
import android.widget.ProgressBar
|
|
14
|
+
import kotlinx.coroutines.CancellationException
|
|
14
15
|
import kotlinx.coroutines.CoroutineScope
|
|
15
16
|
import kotlinx.coroutines.Dispatchers
|
|
16
17
|
import kotlinx.coroutines.cancel
|
|
@@ -135,6 +136,8 @@ class ReloadScreenView @JvmOverloads constructor(
|
|
|
135
136
|
imageView.setImageBitmap(it)
|
|
136
137
|
} ?: handleImageLoadFailure()
|
|
137
138
|
}
|
|
139
|
+
} catch (e: CancellationException) {
|
|
140
|
+
throw e
|
|
138
141
|
} catch (e: Exception) {
|
|
139
142
|
withContext(Dispatchers.Main) {
|
|
140
143
|
handleImageLoadFailure()
|
|
@@ -160,7 +160,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
160
160
|
)
|
|
161
161
|
} catch {
|
|
162
162
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
163
|
-
|
|
163
|
+
throw error
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// statements must stay in precisely this order for last_insert_rowid() to work correctly
|
|
@@ -170,7 +170,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
170
170
|
_ = try execute(sql: updateSql, withArgs: [updateId])
|
|
171
171
|
} catch {
|
|
172
172
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
173
|
-
|
|
173
|
+
throw error
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -181,7 +181,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
181
181
|
_ = try execute(sql: updateInsertSql, withArgs: [updateId])
|
|
182
182
|
} catch {
|
|
183
183
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
184
|
-
|
|
184
|
+
throw error
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -444,7 +444,23 @@ public final class UpdatesDatabase: NSObject {
|
|
|
444
444
|
)
|
|
445
445
|
|
|
446
446
|
let rows = try execute(sql: sql, withArgs: [config.scopeKey])
|
|
447
|
-
|
|
447
|
+
|
|
448
|
+
// A ready row with no launch asset is corrupt (e.g. from an interrupted registration) and
|
|
449
|
+
// would fail every launch. Demote it for the loader to retry instead of offering it.
|
|
450
|
+
var launchableRows: [[String: Any?]] = []
|
|
451
|
+
for row in rows {
|
|
452
|
+
let status: NSNumber = row.requiredValue(forKey: "status")
|
|
453
|
+
let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id")
|
|
454
|
+
if status.intValue == UpdateStatus.StatusReady.rawValue && launchAssetId == nil {
|
|
455
|
+
let updateId: UUID = row.requiredValue(forKey: "id")
|
|
456
|
+
let demoteSql = "UPDATE updates SET status = ?1 WHERE id = ?2;"
|
|
457
|
+
_ = try execute(sql: demoteSql, withArgs: [UpdateStatus.StatusPending.rawValue, updateId])
|
|
458
|
+
} else {
|
|
459
|
+
launchableRows.append(row)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return launchableRows.map { row in
|
|
448
464
|
update(withRow: row, config: config)
|
|
449
465
|
}
|
|
450
466
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-updates",
|
|
3
|
-
"version": "55.0.
|
|
3
|
+
"version": "55.0.30",
|
|
4
4
|
"description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"react": "*",
|
|
72
72
|
"react-native": "*"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "a7b74adf4f2d7ce2753db963616740ceec558a5f"
|
|
75
75
|
}
|
|
@@ -45,12 +45,14 @@ async function createManifestForBuildAsync(platform, projectRoot, destinationDir
|
|
|
45
45
|
if (!asset.fileHashes) {
|
|
46
46
|
throw new Error('The hashAssetFiles Metro plugin is not configured. You need to add a metro.config.js to your project that configures Metro to use this plugin. See https://github.com/expo/expo/blob/main/packages/expo-updates/README.md#metroconfigjs for an example.');
|
|
47
47
|
}
|
|
48
|
-
(0, filterPlatformAssetScales_1.filterPlatformAssetScales)(platform, asset.scales).forEach(function (scale
|
|
48
|
+
(0, filterPlatformAssetScales_1.filterPlatformAssetScales)(platform, asset.scales).forEach(function (scale) {
|
|
49
49
|
const baseAssetInfoForManifest = {
|
|
50
50
|
name: asset.name,
|
|
51
51
|
type: asset.type,
|
|
52
52
|
scale,
|
|
53
|
-
|
|
53
|
+
// `fileHashes` is parallel to the unfiltered `asset.scales`, so it must be indexed by the
|
|
54
|
+
// scale's position there rather than by its position in the filtered list.
|
|
55
|
+
packagerHash: asset.fileHashes[asset.scales.indexOf(scale)],
|
|
54
56
|
subdirectory: asset.httpServerLocation,
|
|
55
57
|
};
|
|
56
58
|
if (platform === 'ios') {
|
|
@@ -64,12 +64,14 @@ export async function createManifestForBuildAsync(
|
|
|
64
64
|
'The hashAssetFiles Metro plugin is not configured. You need to add a metro.config.js to your project that configures Metro to use this plugin. See https://github.com/expo/expo/blob/main/packages/expo-updates/README.md#metroconfigjs for an example.'
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
|
-
filterPlatformAssetScales(platform, asset.scales).forEach(function (scale
|
|
67
|
+
filterPlatformAssetScales(platform, asset.scales).forEach(function (scale) {
|
|
68
68
|
const baseAssetInfoForManifest = {
|
|
69
69
|
name: asset.name,
|
|
70
70
|
type: asset.type,
|
|
71
71
|
scale,
|
|
72
|
-
|
|
72
|
+
// `fileHashes` is parallel to the unfiltered `asset.scales`, so it must be indexed by the
|
|
73
|
+
// scale's position there rather than by its position in the filtered list.
|
|
74
|
+
packagerHash: asset.fileHashes[asset.scales.indexOf(scale)],
|
|
73
75
|
subdirectory: asset.httpServerLocation,
|
|
74
76
|
};
|
|
75
77
|
if (platform === 'ios') {
|