expo-updates 55.0.27 → 55.0.28

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,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.28 — 2026-08-25
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [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))
18
+
19
+ ### 💡 Others
20
+
21
+ - [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))
22
+
13
23
  ## 55.0.27 — 2026-08-17
14
24
 
15
25
  ### 🐛 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.28'
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.28'
93
93
  consumerProguardFiles("proguard-rules.pro")
94
94
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
95
95
 
@@ -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()}")
@@ -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,6 +18,7 @@ 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
@@ -168,16 +170,23 @@ abstract class Loader protected constructor(
168
170
  updateEntity = existingUpdateEntity
169
171
  return finish()
170
172
  } else {
173
+ val insertUpdateEntityOnFinish: Boolean
171
174
  if (existingUpdateEntity == null) {
172
- // no update already exists with this ID, so we need to insert it and download everything.
175
+ // no update already exists with this ID, so we need to download everything.
173
176
  updateEntity = newUpdateEntity
174
- database.updateDao().insertUpdate(updateEntity!!)
177
+ // EMBEDDED is already in the launchable set, so a row inserted before its launch asset exists
178
+ // gets picked as launchable and then fails every launch.
179
+ insertUpdateEntityOnFinish = newUpdateEntity.status == UpdateStatus.EMBEDDED
180
+ if (!insertUpdateEntityOnFinish) {
181
+ database.updateDao().insertUpdate(updateEntity!!)
182
+ }
175
183
  } else {
176
184
  // we've already partially downloaded the update, so we should use the existing entity.
177
185
  // however, it's not ready, so we should try to download all the assets again.
178
186
  updateEntity = existingUpdateEntity
187
+ insertUpdateEntityOnFinish = false
179
188
  }
180
- return downloadAllAssets(update)
189
+ return downloadAllAssets(update, insertUpdateEntityOnFinish)
181
190
  }
182
191
  }
183
192
 
@@ -187,7 +196,7 @@ abstract class Loader protected constructor(
187
196
  ERRORED
188
197
  }
189
198
 
190
- private suspend fun downloadAllAssets(update: Update): LoaderResult {
199
+ private suspend fun downloadAllAssets(update: Update, insertUpdateEntityOnFinish: Boolean): LoaderResult {
191
200
  val assetList = update.assetEntityList.distinctBy { it.key }
192
201
  assetTotal = assetList.size
193
202
 
@@ -234,26 +243,34 @@ abstract class Loader protected constructor(
234
243
  assetDownloadJobs.awaitAll()
235
244
 
236
245
  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) {
246
+ database.withTransaction {
247
+ if (insertUpdateEntityOnFinish) {
248
+ database.updateDao().insertUpdate(updateEntity!!)
249
+ }
250
+
251
+ for (asset in existingAssetList) {
252
+ val existingAssetFound = database.assetDao()
253
+ .addExistingAssetToUpdate(updateEntity!!, asset, asset.isLaunchAsset)
254
+ if (!existingAssetFound) {
255
+ // the database and filesystem have gotten out of sync
256
+ // do our best to create a new entry for this file even though it already existed on disk
257
+ // 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
258
+ var hash: ByteArray? = null
259
+ try {
260
+ hash = UpdatesUtils.sha256(File(updatesDirectory, asset.relativePath))
261
+ } catch (_: Exception) {
262
+ }
263
+ asset.downloadTime = Date()
264
+ asset.hash = hash
265
+ finishedAssetList.add(asset)
248
266
  }
249
- asset.downloadTime = Date()
250
- asset.hash = hash
251
- finishedAssetList.add(asset)
252
267
  }
253
- }
254
268
 
255
- database.assetDao().insertAssets(finishedAssetList, updateEntity!!)
256
- database.updateDao().markUpdateFinished(updateEntity!!)
269
+ database.assetDao().insertAssets(finishedAssetList, updateEntity!!)
270
+ database.updateDao().markUpdateFinished(updateEntity!!)
271
+ }
272
+ } catch (e: CancellationException) {
273
+ throw e
257
274
  } catch (e: Exception) {
258
275
  throw IOException("Error while adding new update to database", e)
259
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "55.0.27",
3
+ "version": "55.0.28",
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": "856b99321eeb04bd528b33f90c0e7fa2859a1fcb"
75
75
  }