expo-updates 55.0.27 → 55.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.29 — 2026-08-27
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [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))
18
+ - [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))
19
+
20
+ ### 💡 Others
21
+
22
+ - [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))
23
+
24
+ ## 55.0.28 — 2026-08-25
25
+
26
+ ### 🐛 Bug fixes
27
+
28
+ - [Android] Register the embedded update in a single transaction. An interrupted registration previously left an update row with no launch asset, which is treated as launchable and then fails every cold start with "Launch asset not found for update"; it now leaves no row, so the next launch registers it cleanly. ([#49130](https://github.com/expo/expo/pull/49130) by [@gwdp](https://github.com/gwdp))
29
+
30
+ ### 💡 Others
31
+
32
+ - [Android] Replace the "this should never happen" wording in the missing launch asset error with the likely cause and how the state resolves. ([#49130](https://github.com/expo/expo/pull/49130) by [@gwdp](https://github.com/gwdp))
33
+
13
34
  ## 55.0.27 — 2026-08-17
14
35
 
15
36
  ### 🐛 Bug fixes
@@ -42,7 +42,7 @@ expoModule {
42
42
  }
43
43
 
44
44
  group = 'host.exp.exponent'
45
- version = '55.0.27'
45
+ version = '55.0.29'
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.27'
92
+ versionName '55.0.29'
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
- logger.error("UpdatesLogReader: error in purgeLogEntries", it, UpdatesErrorCode.Unknown)
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
- runCatching {
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
- }.onFailure { e ->
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
- // no-op
395
+ controllerScope.cancel()
389
396
  }
390
397
 
391
398
  companion object {
@@ -87,7 +87,7 @@ class DatabaseLauncher(
87
87
  // verify that we have all assets on disk
88
88
  // according to the database, we should, but something could have gone wrong on disk
89
89
  val launchAsset = database.updateDao().loadLaunchAssetForUpdate(launchedUpdate!!.id)
90
- ?: throw Exception("Launch asset not found for update; this should never happen. Debug info: ${launchedUpdate!!.debugInfo()}")
90
+ ?: throw Exception("Launch asset not found for update. The update row has no launch asset; an interrupted registration in an older version of expo-updates can leave an update in this state, and it is replaced once a newer update is downloaded. Debug info: ${launchedUpdate!!.debugInfo()}")
91
91
 
92
92
  if (launchAsset.relativePath == null) {
93
93
  throw Exception("Launch asset relative path should not be null. Debug info: ${launchedUpdate!!.debugInfo()}")
@@ -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
- ) : this(context, configuration, logger, database, updatesDirectory, LoaderFiles())
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,
@@ -1,6 +1,7 @@
1
1
  package expo.modules.updates.loader
2
2
 
3
3
  import android.content.Context
4
+ import androidx.room.withTransaction
4
5
  import expo.modules.updates.UpdatesConfiguration
5
6
  import expo.modules.updates.UpdatesUtils
6
7
  import expo.modules.updates.db.UpdatesDatabase
@@ -17,12 +18,14 @@ import expo.modules.updates.logging.UpdatesErrorCode
17
18
  import expo.modules.updates.logging.UpdatesLogger
18
19
  import expo.modules.updates.manifest.ManifestMetadata
19
20
  import expo.modules.updates.manifest.Update
21
+ import kotlinx.coroutines.CancellationException
20
22
  import kotlinx.coroutines.CoroutineScope
21
23
  import kotlinx.coroutines.Dispatchers
22
24
  import kotlinx.coroutines.SupervisorJob
23
25
  import java.io.File
24
26
  import java.io.IOException
25
27
  import java.util.*
28
+ import java.util.Collections
26
29
  import java.util.concurrent.ConcurrentHashMap
27
30
 
28
31
  /**
@@ -44,9 +47,9 @@ abstract class Loader protected constructor(
44
47
  private var updateResponse: UpdateResponse? = null
45
48
  private var updateEntity: UpdateEntity? = null
46
49
  private var assetTotal = 0
47
- private var erroredAssetList = mutableListOf<AssetEntity>()
48
- private var existingAssetList = mutableListOf<AssetEntity>()
49
- private var finishedAssetList = mutableListOf<AssetEntity>()
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())
50
53
  private val _progressFlow = MutableSharedFlow<AssetLoadProgress>()
51
54
  private var assetProgressMap: MutableMap<AssetEntity, Double> = ConcurrentHashMap()
52
55
 
@@ -115,9 +118,9 @@ abstract class Loader protected constructor(
115
118
  updateResponse = null
116
119
  updateEntity = null
117
120
  assetTotal = 0
118
- erroredAssetList = mutableListOf()
119
- existingAssetList = mutableListOf()
120
- finishedAssetList = mutableListOf()
121
+ erroredAssetList = Collections.synchronizedList(mutableListOf())
122
+ existingAssetList = Collections.synchronizedList(mutableListOf())
123
+ finishedAssetList = Collections.synchronizedList(mutableListOf())
121
124
  assetProgressMap = ConcurrentHashMap()
122
125
  assetLoadProgressBlock = null
123
126
  }
@@ -168,16 +171,23 @@ abstract class Loader protected constructor(
168
171
  updateEntity = existingUpdateEntity
169
172
  return finish()
170
173
  } else {
174
+ val insertUpdateEntityOnFinish: Boolean
171
175
  if (existingUpdateEntity == null) {
172
- // no update already exists with this ID, so we need to insert it and download everything.
176
+ // no update already exists with this ID, so we need to download everything.
173
177
  updateEntity = newUpdateEntity
174
- database.updateDao().insertUpdate(updateEntity!!)
178
+ // EMBEDDED is already in the launchable set, so a row inserted before its launch asset exists
179
+ // gets picked as launchable and then fails every launch.
180
+ insertUpdateEntityOnFinish = newUpdateEntity.status == UpdateStatus.EMBEDDED
181
+ if (!insertUpdateEntityOnFinish) {
182
+ database.updateDao().insertUpdate(updateEntity!!)
183
+ }
175
184
  } else {
176
185
  // we've already partially downloaded the update, so we should use the existing entity.
177
186
  // however, it's not ready, so we should try to download all the assets again.
178
187
  updateEntity = existingUpdateEntity
188
+ insertUpdateEntityOnFinish = false
179
189
  }
180
- return downloadAllAssets(update)
190
+ return downloadAllAssets(update, insertUpdateEntityOnFinish)
181
191
  }
182
192
  }
183
193
 
@@ -187,7 +197,7 @@ abstract class Loader protected constructor(
187
197
  ERRORED
188
198
  }
189
199
 
190
- private suspend fun downloadAllAssets(update: Update): LoaderResult {
200
+ private suspend fun downloadAllAssets(update: Update, insertUpdateEntityOnFinish: Boolean): LoaderResult {
191
201
  val assetList = update.assetEntityList.distinctBy { it.key }
192
202
  assetTotal = assetList.size
193
203
 
@@ -234,26 +244,34 @@ abstract class Loader protected constructor(
234
244
  assetDownloadJobs.awaitAll()
235
245
 
236
246
  try {
237
- for (asset in existingAssetList) {
238
- val existingAssetFound = database.assetDao()
239
- .addExistingAssetToUpdate(updateEntity!!, asset, asset.isLaunchAsset)
240
- if (!existingAssetFound) {
241
- // the database and filesystem have gotten out of sync
242
- // do our best to create a new entry for this file even though it already existed on disk
243
- // TODO: we should probably get rid of this assumption that if an asset exists on disk with the same filename, it's the same asset
244
- var hash: ByteArray? = null
245
- try {
246
- hash = UpdatesUtils.sha256(File(updatesDirectory, asset.relativePath))
247
- } catch (_: Exception) {
247
+ database.withTransaction {
248
+ if (insertUpdateEntityOnFinish) {
249
+ database.updateDao().insertUpdate(updateEntity!!)
250
+ }
251
+
252
+ for (asset in existingAssetList) {
253
+ val existingAssetFound = database.assetDao()
254
+ .addExistingAssetToUpdate(updateEntity!!, asset, asset.isLaunchAsset)
255
+ if (!existingAssetFound) {
256
+ // the database and filesystem have gotten out of sync
257
+ // do our best to create a new entry for this file even though it already existed on disk
258
+ // TODO: we should probably get rid of this assumption that if an asset exists on disk with the same filename, it's the same asset
259
+ var hash: ByteArray? = null
260
+ try {
261
+ hash = UpdatesUtils.sha256(File(updatesDirectory, asset.relativePath))
262
+ } catch (_: Exception) {
263
+ }
264
+ asset.downloadTime = Date()
265
+ asset.hash = hash
266
+ finishedAssetList.add(asset)
248
267
  }
249
- asset.downloadTime = Date()
250
- asset.hash = hash
251
- finishedAssetList.add(asset)
252
268
  }
253
- }
254
269
 
255
- database.assetDao().insertAssets(finishedAssetList, updateEntity!!)
256
- database.updateDao().markUpdateFinished(updateEntity!!)
270
+ database.assetDao().insertAssets(finishedAssetList, updateEntity!!)
271
+ database.updateDao().markUpdateFinished(updateEntity!!)
272
+ }
273
+ } catch (e: CancellationException) {
274
+ throw e
257
275
  } catch (e: Exception) {
258
276
  throw IOException("Error while adding new update to database", e)
259
277
  }
@@ -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
- ) : Loader(context, configuration, logger, database, updatesDirectory, loaderFiles) {
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
- ) : this(context, configuration, logger, database, fileDownloader, updatesDirectory, launchedUpdate, LoaderFiles())
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: JSONException) {
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
  }
@@ -54,6 +54,6 @@ class UpdatesLogReader(
54
54
  }
55
55
 
56
56
  companion object {
57
- private const val ONE_DAY_MILLISECONDS = 86400
57
+ private const val ONE_DAY_MILLISECONDS = 86_400_000L
58
58
  }
59
59
  }
@@ -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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "55.0.27",
3
+ "version": "55.0.29",
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",
@@ -45,7 +45,7 @@
45
45
  "chalk": "^4.1.2",
46
46
  "debug": "^4.3.4",
47
47
  "expo-eas-client": "~55.0.5",
48
- "expo-manifests": "~55.0.20",
48
+ "expo-manifests": "~55.0.21",
49
49
  "expo-structured-headers": "~55.0.2",
50
50
  "expo-updates-interface": "~55.1.6",
51
51
  "getenv": "^2.0.0",
@@ -71,5 +71,5 @@
71
71
  "react": "*",
72
72
  "react-native": "*"
73
73
  },
74
- "gitHead": "764641f87074c1e49d004790c5a5983f4b5bfc6b"
74
+ "gitHead": "5defc36eae34b72f7bee8385276200242e5458fe"
75
75
  }