@hot-updater/react-native 0.19.4 → 0.19.6

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.
@@ -100,6 +100,15 @@ class BundleFileStorageService(
100
100
  bundleStoreDir.mkdirs()
101
101
  }
102
102
 
103
+ val currentBundleId =
104
+ getCachedBundleURL()?.let { cachedUrl ->
105
+ // Only consider cached bundles, not fallback bundles
106
+ if (!cachedUrl.startsWith("assets://")) {
107
+ File(cachedUrl).parentFile?.name
108
+ } else {
109
+ null
110
+ }
111
+ }
103
112
  val finalBundleDir = File(bundleStoreDir, bundleId)
104
113
  if (finalBundleDir.exists()) {
105
114
  Log.d("BundleStorage", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
@@ -108,7 +117,7 @@ class BundleFileStorageService(
108
117
  // Update last modified time and set the cached bundle URL
109
118
  finalBundleDir.setLastModified(System.currentTimeMillis())
110
119
  setBundleURL(existingIndexFile.absolutePath)
111
- cleanupOldBundles(bundleStoreDir)
120
+ cleanupOldBundles(bundleStoreDir, currentBundleId, bundleId)
112
121
  return true
113
122
  } else {
114
123
  // If index.android.bundle is missing, delete and re-download
@@ -203,7 +212,7 @@ class BundleFileStorageService(
203
212
  tempDir.deleteRecursively()
204
213
 
205
214
  // 10) Remove old bundles
206
- cleanupOldBundles(bundleStoreDir)
215
+ cleanupOldBundles(bundleStoreDir, currentBundleId, bundleId)
207
216
 
208
217
  Log.d("BundleStorage", "Downloaded and activated bundle successfully.")
209
218
  return@withContext true
@@ -213,25 +222,59 @@ class BundleFileStorageService(
213
222
  }
214
223
 
215
224
  /**
216
- * Removes older bundles and any leftover .tmp directories
225
+ * Removes old bundles except for the specified bundle IDs, and any leftover .tmp directories
217
226
  */
218
- private fun cleanupOldBundles(bundleStoreDir: File) {
219
- // List only directories that are not .tmp
220
- val bundles = bundleStoreDir.listFiles { file -> file.isDirectory && !file.name.endsWith(".tmp") }?.toList() ?: return
221
- // Sort bundles by last modified (newest first)
222
- val sortedBundles = bundles.sortedByDescending { it.lastModified() }
223
- if (sortedBundles.size > 1) {
224
- // Keep the most recent bundle, delete the rest
225
- sortedBundles.drop(1).forEach { oldBundle ->
226
- Log.d("BundleStorage", "Removing old bundle: ${oldBundle.name}")
227
- oldBundle.deleteRecursively()
227
+ private fun cleanupOldBundles(
228
+ bundleStoreDir: File,
229
+ currentBundleId: String?,
230
+ bundleId: String,
231
+ ) {
232
+ try {
233
+ // List only directories that are not .tmp
234
+ val bundles =
235
+ bundleStoreDir
236
+ .listFiles { file ->
237
+ file.isDirectory && !file.name.endsWith(".tmp")
238
+ }?.toList() ?: return
239
+
240
+ // Keep only the specified bundle IDs (filter out null values)
241
+ val bundleIdsToKeep = setOfNotNull(currentBundleId, bundleId).filter { it.isNotBlank() }
242
+
243
+ bundles.forEach { bundle ->
244
+ try {
245
+ if (bundle.name !in bundleIdsToKeep) {
246
+ Log.d("BundleStorage", "Removing old bundle: ${bundle.name}")
247
+ if (bundle.deleteRecursively()) {
248
+ Log.d("BundleStorage", "Successfully removed old bundle: ${bundle.name}")
249
+ } else {
250
+ Log.w("BundleStorage", "Failed to remove old bundle: ${bundle.name}")
251
+ }
252
+ } else {
253
+ Log.d("BundleStorage", "Keeping bundle: ${bundle.name}")
254
+ }
255
+ } catch (e: Exception) {
256
+ Log.e("BundleStorage", "Error removing bundle ${bundle.name}: ${e.message}")
257
+ }
228
258
  }
229
- }
230
259
 
231
- // Remove any leftover .tmp directories
232
- bundleStoreDir.listFiles { file -> file.isDirectory && file.name.endsWith(".tmp") }?.forEach { staleTmp ->
233
- Log.d("BundleStorage", "Removing stale tmp directory: ${staleTmp.name}")
234
- staleTmp.deleteRecursively()
260
+ // Remove any leftover .tmp directories
261
+ bundleStoreDir
262
+ .listFiles { file ->
263
+ file.isDirectory && file.name.endsWith(".tmp")
264
+ }?.forEach { staleTmp ->
265
+ try {
266
+ Log.d("BundleStorage", "Removing stale tmp directory: ${staleTmp.name}")
267
+ if (staleTmp.deleteRecursively()) {
268
+ Log.d("BundleStorage", "Successfully removed tmp directory: ${staleTmp.name}")
269
+ } else {
270
+ Log.w("BundleStorage", "Failed to remove tmp directory: ${staleTmp.name}")
271
+ }
272
+ } catch (e: Exception) {
273
+ Log.e("BundleStorage", "Error removing tmp directory ${staleTmp.name}: ${e.message}")
274
+ }
275
+ }
276
+ } catch (e: Exception) {
277
+ Log.e("BundleStorage", "Error during cleanup: ${e.message}")
235
278
  }
236
279
  }
237
280
  }
@@ -159,115 +159,67 @@ class BundleFileStorageService: BundleStorageService {
159
159
  return .failure(error)
160
160
  }
161
161
  }
162
-
162
+
163
163
  /**
164
- * Cleans up old bundles, keeping only the current and latest bundles.
165
- * Executes synchronously on the calling thread.
166
- * @param currentBundleId ID of the current active bundle (optional)
167
- * @return Result of operation
168
- */
169
- func cleanupOldBundles(currentBundleId: String?) -> Result<Void, Error> {
164
+ * Cleans up old bundles, keeping only the current and new bundles.
165
+ * Executes synchronously on the calling thread.
166
+ * @param currentBundleId ID of the current active bundle (optional)
167
+ * @param bundleId ID of the new bundle to keep (optional)
168
+ * @return Result of operation
169
+ */
170
+ func cleanupOldBundles(currentBundleId: String?, bundleId: String?) -> Result<Void, Error> {
170
171
  let storeDirResult = bundleStoreDir()
171
172
 
172
173
  guard case .success(let storeDir) = storeDirResult else {
173
174
  return .failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil))
174
175
  }
175
176
 
177
+ // List only directories that are not .tmp
178
+ let contents: [String]
176
179
  do {
177
- var contents: [String]
178
- do {
179
- contents = try self.fileSystem.contentsOfDirectory(atPath: storeDir)
180
- } catch let error {
181
- NSLog("[BundleStorage] Failed to list contents of bundle store directory: \(storeDir)")
182
- return .failure(BundleStorageError.fileSystemError(error))
183
- }
184
-
185
- if contents.isEmpty {
186
- NSLog("[BundleStorage] No bundles to clean up.")
187
- return .success(())
188
- }
189
-
190
- let currentBundlePath = currentBundleId != nil ?
191
- (storeDir as NSString).appendingPathComponent(currentBundleId!) : nil
192
-
193
- var latestBundlePath: String? = nil
194
- var latestModDate: Date = .distantPast
180
+ contents = try self.fileSystem.contentsOfDirectory(atPath: storeDir)
181
+ } catch let error {
182
+ NSLog("[BundleStorage] Failed to list contents of bundle store directory: \(storeDir)")
183
+ return .failure(BundleStorageError.fileSystemError(error))
184
+ }
185
+
186
+ let bundles = contents.compactMap { item -> String? in
187
+ let fullPath = (storeDir as NSString).appendingPathComponent(item)
188
+ return (!item.hasSuffix(".tmp") && self.fileSystem.fileExists(atPath: fullPath)) ? fullPath : nil
189
+ }
190
+
191
+ // Keep only the specified bundle IDs
192
+ let bundleIdsToKeep = Set([currentBundleId, bundleId].compactMap { $0 })
193
+
194
+ bundles.forEach { bundlePath in
195
+ let bundleName = (bundlePath as NSString).lastPathComponent
195
196
 
196
- for item in contents {
197
- let fullPath = (storeDir as NSString).appendingPathComponent(item)
198
-
199
- // Skip .tmp directories
200
- if item.hasSuffix(".tmp") {
201
- continue
202
- }
203
-
204
- if let currentPath = currentBundlePath, fullPath == currentPath {
205
- continue
206
- }
207
-
208
- if self.fileSystem.fileExists(atPath: fullPath) {
209
- do {
210
- let attributes = try self.fileSystem.attributesOfItem(atPath: fullPath)
211
- if let modDate = attributes[FileAttributeKey.modificationDate] as? Date {
212
- if modDate > latestModDate {
213
- latestModDate = modDate
214
- latestBundlePath = fullPath
215
- }
216
- }
217
- } catch {
218
- NSLog("[BundleStorage] Warning: Could not get attributes for \(fullPath): \(error)")
219
- }
197
+ if !bundleIdsToKeep.contains(bundleName) {
198
+ do {
199
+ try self.fileSystem.removeItem(atPath: bundlePath)
200
+ NSLog("[BundleStorage] Removing old bundle: \(bundleName)")
201
+ } catch {
202
+ NSLog("[BundleStorage] Failed to remove old bundle at \(bundlePath): \(error)")
220
203
  }
204
+ } else {
205
+ NSLog("[BundleStorage] Keeping bundle: \(bundleName)")
221
206
  }
222
-
223
- var bundlesToKeep = Set<String>()
224
-
225
- if let currentPath = currentBundlePath, self.fileSystem.fileExists(atPath: currentPath) {
226
- bundlesToKeep.insert(currentPath)
227
- NSLog("[BundleStorage] Keeping current bundle: \(currentBundleId!)")
228
- }
229
-
230
- if let latestPath = latestBundlePath {
231
- bundlesToKeep.insert(latestPath)
232
- NSLog("[BundleStorage] Keeping latest bundle: \((latestPath as NSString).lastPathComponent)")
233
- }
234
-
235
- var removedCount = 0
236
- for item in contents {
207
+ }
208
+
209
+ // Remove any leftover .tmp directories
210
+ contents.forEach { item in
211
+ if item.hasSuffix(".tmp") {
237
212
  let fullPath = (storeDir as NSString).appendingPathComponent(item)
238
- // Skip .tmp directories as well
239
- if item.hasSuffix(".tmp") {
240
- // Clean up any stale .tmp directories
241
- do {
242
- try self.fileSystem.removeItem(atPath: fullPath)
243
- NSLog("[BundleStorage] Removed stale tmp directory: \(fullPath)")
244
- } catch {
245
- NSLog("[BundleStorage] Failed to remove stale tmp directory \(fullPath): \(error)")
246
- }
247
- continue
248
- }
249
-
250
- if !bundlesToKeep.contains(fullPath) {
251
- do {
252
- try self.fileSystem.removeItem(atPath: fullPath)
253
- removedCount += 1
254
- NSLog("[BundleStorage] Removed old bundle: \(item)")
255
- } catch {
256
- NSLog("[BundleStorage] Failed to remove old bundle at \(fullPath): \(error)")
257
- }
213
+ do {
214
+ try self.fileSystem.removeItem(atPath: fullPath)
215
+ NSLog("[BundleStorage] Removing stale tmp directory: \(item)")
216
+ } catch {
217
+ NSLog("[BundleStorage] Failed to remove stale tmp directory \(fullPath): \(error)")
258
218
  }
259
219
  }
260
-
261
- if removedCount == 0 {
262
- NSLog("[BundleStorage] No old bundles to remove.")
263
- } else {
264
- NSLog("[BundleStorage] Removed \(removedCount) old bundle(s).")
265
- }
266
-
267
- return .success(())
268
- } catch let error {
269
- return .failure(error)
270
220
  }
221
+
222
+ return .success(())
271
223
  }
272
224
 
273
225
  /**
@@ -324,6 +276,9 @@ class BundleFileStorageService: BundleStorageService {
324
276
  * @param completion Callback with result of the operation
325
277
  */
326
278
  func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void) {
279
+ // Get the current bundle ID from the cached bundle URL (exclude fallback bundles)
280
+ let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
281
+
327
282
  guard let validFileUrl = fileUrl else {
328
283
  NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
329
284
  // Dispatch the sequence to the file operation queue to ensure completion is called asynchronously
@@ -332,7 +287,7 @@ class BundleFileStorageService: BundleStorageService {
332
287
  let setResult = self.setBundleURL(localPath: nil)
333
288
  switch setResult {
334
289
  case .success:
335
- let cleanupResult = self.cleanupOldBundles(currentBundleId: nil)
290
+ let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
336
291
  switch cleanupResult {
337
292
  case .success:
338
293
  completion(.success(true))
@@ -350,6 +305,7 @@ class BundleFileStorageService: BundleStorageService {
350
305
 
351
306
  // Start the bundle update process on a background queue
352
307
  fileOperationQueue.async {
308
+
353
309
  let storeDirResult = self.bundleStoreDir()
354
310
  guard case .success(let storeDir) = storeDirResult else {
355
311
  completion(.failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil)))
@@ -368,7 +324,7 @@ class BundleFileStorageService: BundleStorageService {
368
324
  let setResult = self.setBundleURL(localPath: bundlePath)
369
325
  switch setResult {
370
326
  case .success:
371
- let cleanupResult = self.cleanupOldBundles(currentBundleId: bundleId)
327
+ let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
372
328
  switch cleanupResult {
373
329
  case .success:
374
330
  completion(.success(true))
@@ -478,7 +434,7 @@ class BundleFileStorageService: BundleStorageService {
478
434
  }
479
435
 
480
436
  /**
481
- * Processes a downloaded bundle file using the “.tmp rename approach.
437
+ * Processes a downloaded bundle file using the "tmp" rename approach.
482
438
  * This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
483
439
  * @param location URL of the downloaded file
484
440
  * @param tempZipFile Path to store the downloaded zip file
@@ -495,6 +451,7 @@ class BundleFileStorageService: BundleStorageService {
495
451
  tempDirectory: String,
496
452
  completion: @escaping (Result<Bool, Error>) -> Void
497
453
  ) {
454
+ let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
498
455
  NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
499
456
 
500
457
  // 1) Ensure the ZIP file exists
@@ -556,7 +513,7 @@ class BundleFileStorageService: BundleStorageService {
556
513
  self.cleanupTemporaryFiles([tempDirectory])
557
514
 
558
515
  // 13) Clean up old bundles, preserving current and latest
559
- let _ = self.cleanupOldBundles(currentBundleId: bundleId)
516
+ let _ = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
560
517
 
561
518
  // 14) Complete with success
562
519
  completion(.success(true))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -119,12 +119,12 @@
119
119
  "react-native": "0.79.1",
120
120
  "react-native-builder-bob": "^0.40.10",
121
121
  "typescript": "^5.8.3",
122
- "hot-updater": "0.19.4"
122
+ "hot-updater": "0.19.6"
123
123
  },
124
124
  "dependencies": {
125
125
  "use-sync-external-store": "1.5.0",
126
- "@hot-updater/js": "0.19.4",
127
- "@hot-updater/core": "0.19.4"
126
+ "@hot-updater/js": "0.19.6",
127
+ "@hot-updater/core": "0.19.6"
128
128
  },
129
129
  "scripts": {
130
130
  "build": "bob build && tsc -p plugin/tsconfig.json",
@@ -50,7 +50,7 @@ var getFingerprint = function () { return __awaiter(void 0, void 0, void 0, func
50
50
  if (fingerprintCache) {
51
51
  return [2 /*return*/, fingerprintCache];
52
52
  }
53
- return [4 /*yield*/, (0, hot_updater_1.createFingerprintJson)()];
53
+ return [4 /*yield*/, (0, hot_updater_1.createAndInjectFingerprintFiles)()];
54
54
  case 1:
55
55
  fingerprintCache = _a.sent();
56
56
  return [2 /*return*/, fingerprintCache];