@react-native-firebase/storage 26.3.3 → 27.0.0

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/RNFBStorage.podspec +9 -4
  3. package/android/build.gradle +6 -0
  4. package/android/src/main/java/io/invertase/firebase/storage/NativeRNFBTurboStorage.java +24 -4
  5. package/android/src/main/java/io/invertase/firebase/storage/RNFBStorageTaskRegistry.kt +88 -0
  6. package/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageTask.java +25 -46
  7. package/android/src/main/java/io/invertase/firebase/storage/StoragePendingHandle.java +30 -0
  8. package/android/src/test/java/io/invertase/firebase/storage/RNFBStorageTaskRegistryTest.java +237 -0
  9. package/android/src/test/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageTaskTest.java +285 -0
  10. package/dist/module/index.js +1 -2
  11. package/dist/module/index.js.map +1 -1
  12. package/dist/module/version.js +1 -1
  13. package/dist/typescript/lib/index.d.ts +1 -1
  14. package/dist/typescript/lib/types/internal.d.ts +2 -3
  15. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  16. package/dist/typescript/lib/version.d.ts +1 -1
  17. package/ios/RNFBStorage/RNFBStorageCommon.m +17 -98
  18. package/ios/RNFBStorage/RNFBStorageErrorCodeMapper.swift +122 -0
  19. package/ios/RNFBStorage/RNFBStorageErrorSnapshotDictBuilder.swift +70 -0
  20. package/ios/RNFBStorage/RNFBStorageHelper.m +38 -13
  21. package/ios/RNFBStorage/RNFBStorageTaskRegistry.h +42 -0
  22. package/ios/RNFBStorage/RNFBStorageTaskRegistry.m +87 -0
  23. package/ios/RNFBStorage/RNFBStorageTaskRegistryStorage.swift +87 -0
  24. package/ios/RNFBStorage/RNFBStorageTaskStatusMapper.swift +57 -0
  25. package/ios/RNFBStorage.xcodeproj/project.pbxproj +26 -0
  26. package/ios/RNFBStorageUnitTests/RNFBStorageErrorCodeMapperFacadeTests.m +91 -0
  27. package/ios/RNFBStorageUnitTests/RNFBStorageErrorCodeMapperTests.swift +211 -0
  28. package/ios/RNFBStorageUnitTests/RNFBStorageErrorSnapshotDictBuilderFacadeTests.m +125 -0
  29. package/ios/RNFBStorageUnitTests/RNFBStorageTaskRegistryStorageTests.swift +154 -0
  30. package/ios/RNFBStorageUnitTests/RNFBStorageTaskRegistryTests.m +225 -0
  31. package/ios/RNFBStorageUnitTests/RNFBStorageTaskStatusMapperFacadeTests.m +52 -0
  32. package/ios/RNFBStorageUnitTests/RNFBStorageTaskStatusMapperTests.swift +58 -0
  33. package/ios/RNFBStorageUnitTests/RNFBStorageUnitTests.xcodeproj/project.pbxproj +301 -0
  34. package/ios/RNFBStorageUnitTests/RNFBStorageUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBStorageUnitTests.xcscheme +69 -0
  35. package/lib/index.ts +1 -1
  36. package/lib/types/internal.ts +5 -4
  37. package/lib/version.ts +1 -1
  38. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [27.0.0](https://github.com/invertase/react-native-firebase/compare/v26.4.0...v27.0.0) (2026-10-19)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13
11
+
12
+ ### Features
13
+
14
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13 ([0ca03d1](https://github.com/invertase/react-native-firebase/commit/0ca03d11b0c271fb37cfbee8a0c3074a21969f0b))
15
+
16
+ ### Bug Fixes
17
+
18
+ - **app, storage:** link Photos framework on tvOS ([56f5c1f](https://github.com/invertase/react-native-firebase/commit/56f5c1fcf83bc89d8cd80f019696e13bc96578dd))
19
+ - **app,database:** resolve platform native module when bundlers apply package exports ([959d91f](https://github.com/invertase/react-native-firebase/commit/959d91fc79d7155d1d9a08e1f836789e6e6d6156))
20
+
21
+ ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
22
+
23
+ ### Bug Fixes
24
+
25
+ - **storage:** use RNFBHandleMap for pending tasks ([acf77f1](https://github.com/invertase/react-native-firebase/commit/acf77f1ff1680f1ce9c65f72cf0056df7b998aa5))
26
+
6
27
  ## [26.3.3](https://github.com/invertase/react-native-firebase/compare/v26.3.2...v26.3.3) (2026-09-01)
7
28
 
8
29
  ### Bug Fixes
@@ -28,18 +28,23 @@ Pod::Spec.new do |s|
28
28
  s.ios.deployment_target = firebase_ios_target
29
29
  s.macos.deployment_target = firebase_macos_target
30
30
  s.tvos.deployment_target = firebase_tvos_target
31
- s.source_files = 'ios/**/*.{h,m,mm,cpp}'
31
+ s.swift_version = '5.10'
32
+ s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
32
33
  s.private_header_files = [
33
34
  'ios/RNFBStorage/*.h',
34
35
  'ios/generated/**/*.h',
35
36
  ]
36
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
37
+ # CocoaPods FileList uses FNM_PATHNAME: `ios/*UnitTests/**` does not match nested
38
+ # paths like `ios/RNFBStorageUnitTests/HostStubs/**` (those would compile into the pod).
39
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**/*'
37
40
 
38
41
  # Must be set before install_modules_dependencies so RN can append use_frameworks
39
42
  # HEADER_SEARCH_PATHS (React-debug etc.). Assigning after overwrites those paths
40
43
  # and breaks from-source builds: react/timing/primitives.h → react/debug/flags.h.
44
+ # DEFINES_MODULE is required for the generated RNFBStorage-Swift.h interface.
41
45
  s.pod_target_xcconfig = {
42
46
  'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBStorageTurboModules" "$(PODS_TARGET_SRCROOT)/ios/generated"',
47
+ "DEFINES_MODULE" => "YES",
43
48
  "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES" => "YES",
44
49
  }
45
50
 
@@ -61,10 +66,10 @@ Pod::Spec.new do |s|
61
66
 
62
67
  # RNFBStorageCommon.m uses PHAsset/PHAssetResource (Photos.framework) for local
63
68
  # asset uploads. See RNFBApp.podspec for why this must be declared explicitly
64
- # rather than relying on Clang autolinking. iOS/macOS only -- PhotoKit doesn't
65
- # exist on tvOS.
69
+ # rather than relying on Clang autolinking. PhotoKit is available on tvOS 10+.
66
70
  s.ios.frameworks = 'Photos'
67
71
  s.osx.frameworks = 'Photos'
72
+ s.tvos.frameworks = 'Photos'
68
73
 
69
74
  if defined?($RNFirebaseAsStaticFramework)
70
75
  Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'"
@@ -13,6 +13,7 @@ buildscript {
13
13
 
14
14
  dependencies {
15
15
  classpath("com.android.tools.build:gradle:8.4.0")
16
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${project.findProperty('kotlinVersion') ?: '2.4.0'}")
16
17
  }
17
18
  }
18
19
  }
@@ -21,6 +22,8 @@ plugins {
21
22
  id "io.invertase.gradle.build" version "1.5"
22
23
  }
23
24
 
25
+ apply plugin: "org.jetbrains.kotlin.android"
26
+
24
27
  def appProject
25
28
  if (findProject(':@react-native-firebase_app')) {
26
29
  appProject = project(':@react-native-firebase_app')
@@ -93,6 +96,9 @@ dependencies {
93
96
  api appProject
94
97
  implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}")
95
98
  implementation "com.google.firebase:firebase-storage"
99
+
100
+ testImplementation "junit:junit:4.13.2"
101
+ testImplementation "org.mockito:mockito-core:5.14.2"
96
102
  }
97
103
 
98
104
  ReactNative.shared.applyPackageVersion()
@@ -56,7 +56,7 @@ public class NativeRNFBTurboStorage extends NativeRNFBTurboStorageSpec {
56
56
 
57
57
  @Override
58
58
  public void invalidate() {
59
- ReactNativeFirebaseStorageTask.destroyAllTasks();
59
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.takeAllAndCancel();
60
60
  super.invalidate();
61
61
  }
62
62
 
@@ -274,6 +274,10 @@ public class NativeRNFBTurboStorage extends NativeRNFBTurboStorageSpec {
274
274
  ReactNativeFirebaseStorageDownloadTask storageTask =
275
275
  new ReactNativeFirebaseStorageDownloadTask((int) taskId, reference, appName);
276
276
  storageTask.begin(getTransactionalExecutor(), localFilePath);
277
+ if (storageTask.hasStorageTask() && !storageTask.registerPending()) {
278
+ rejectPromiseWithCodeAndMessage(promise, "internal", "Handle id already registered");
279
+ return;
280
+ }
277
281
  storageTask.addOnCompleteListener(getTransactionalExecutor(), promise);
278
282
  } catch (Exception e) {
279
283
  promiseRejectStorageException(promise, e);
@@ -294,6 +298,10 @@ public class NativeRNFBTurboStorage extends NativeRNFBTurboStorageSpec {
294
298
  ReactNativeFirebaseStorageUploadTask storageTask =
295
299
  new ReactNativeFirebaseStorageUploadTask((int) taskId, reference, appName);
296
300
  storageTask.begin(getTransactionalExecutor(), string, format, metadataMap);
301
+ if (!storageTask.registerPending()) {
302
+ rejectPromiseWithCodeAndMessage(promise, "internal", "Handle id already registered");
303
+ return;
304
+ }
297
305
  storageTask.addOnCompleteListener(getTransactionalExecutor(), promise);
298
306
  } catch (Exception e) {
299
307
  promiseRejectStorageException(promise, e);
@@ -313,6 +321,10 @@ public class NativeRNFBTurboStorage extends NativeRNFBTurboStorageSpec {
313
321
  ReactNativeFirebaseStorageUploadTask storageTask =
314
322
  new ReactNativeFirebaseStorageUploadTask((int) taskId, reference, appName);
315
323
  storageTask.begin(getTransactionalExecutor(), localFilePath, metadata);
324
+ if (!storageTask.registerPending()) {
325
+ rejectPromiseWithCodeAndMessage(promise, "internal", "Handle id already registered");
326
+ return;
327
+ }
316
328
  storageTask.addOnCompleteListener(getTransactionalExecutor(), promise);
317
329
  } catch (Exception e) {
318
330
  promiseRejectStorageException(promise, e);
@@ -321,13 +333,21 @@ public class NativeRNFBTurboStorage extends NativeRNFBTurboStorageSpec {
321
333
 
322
334
  @Override
323
335
  public boolean setTaskStatus(String appName, double taskId, double status) {
336
+ RNFBStorageTaskRegistry tasks = ReactNativeFirebaseStorageTask.PENDING_TASKS;
337
+ int id = (int) taskId;
324
338
  switch ((int) status) {
325
339
  case 0:
326
- return ReactNativeFirebaseStorageTask.pauseTaskById((int) taskId);
340
+ {
341
+ StoragePendingHandle handle = tasks.get(id);
342
+ return handle != null && handle.pause();
343
+ }
327
344
  case 1:
328
- return ReactNativeFirebaseStorageTask.resumeTaskById((int) taskId);
345
+ {
346
+ StoragePendingHandle handle = tasks.get(id);
347
+ return handle != null && handle.resume();
348
+ }
329
349
  case 2:
330
- return ReactNativeFirebaseStorageTask.cancelTaskById((int) taskId);
350
+ return tasks.takeAndCancel(id);
331
351
  default:
332
352
  return false;
333
353
  }
@@ -0,0 +1,88 @@
1
+ package io.invertase.firebase.storage
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+ import java.util.function.Predicate
23
+
24
+ /**
25
+ * Pending upload/download task map. Unique [put]; callers [take] or [takeAllAndCancel] then cancel
26
+ * outside the HandleMap lock.
27
+ */
28
+ internal class RNFBStorageTaskRegistry {
29
+ private val map = RNFBHandleMap<Int, StoragePendingHandle?>()
30
+
31
+ @Throws(RNFBHandleCollisionException::class)
32
+ fun put(
33
+ taskId: Int,
34
+ handle: StoragePendingHandle?,
35
+ ) {
36
+ map.put(taskId, handle)
37
+ }
38
+
39
+ /**
40
+ * Unique put. On collision, `cancel()` the incoming handle and leave the existing mapping.
41
+ *
42
+ * @return true if stored
43
+ */
44
+ fun putOrDiscard(
45
+ taskId: Int,
46
+ handle: StoragePendingHandle?,
47
+ ): Boolean {
48
+ if (map.putIfAbsent(taskId, handle)) {
49
+ return true
50
+ }
51
+ handle?.cancel()
52
+ return false
53
+ }
54
+
55
+ fun get(taskId: Int): StoragePendingHandle? = map.get(taskId)
56
+
57
+ fun take(taskId: Int): StoragePendingHandle? = map.take(taskId)
58
+
59
+ fun takeIf(
60
+ taskId: Int,
61
+ shouldTake: Predicate<StoragePendingHandle?>,
62
+ ): StoragePendingHandle? = map.takeIf(taskId, shouldTake)
63
+
64
+ /**
65
+ * Cancel outside the HandleMap lock; remove the mapping only when cancel succeeds. Returns `false`
66
+ * when no mapping existed or cancel failed. Trailing unregister is identity-gated so a
67
+ * replacement put after `cancel()` (e.g. production `destroyTask`) is not stolen.
68
+ *
69
+ * Shape: get → cancel → identity takeIf on success (keep mapping when cancel returns false). iOS
70
+ * registry mirrors get → cancel → identity takeIf, but FIRStorage cancel is void so there is no
71
+ * keep-on-false path — see `RNFBStorageTaskRegistry.takeAndCancel:` / helper `setTaskStatus`.
72
+ */
73
+ fun takeAndCancel(taskId: Int): Boolean {
74
+ val handle = map.get(taskId) ?: return false
75
+ val cancelled = handle.cancel()
76
+ if (cancelled) {
77
+ map.takeIf(taskId) { h -> h === handle }
78
+ }
79
+ return cancelled
80
+ }
81
+
82
+ fun takeAllAndCancel() {
83
+ val remaining = map.takeAll()
84
+ for (handle in remaining) {
85
+ handle?.cancel()
86
+ }
87
+ }
88
+ }
@@ -23,14 +23,13 @@ import static io.invertase.firebase.storage.ReactNativeFirebaseStorageCommon.STA
23
23
  import static io.invertase.firebase.storage.ReactNativeFirebaseStorageCommon.getExceptionCodeAndMessage;
24
24
 
25
25
  import android.util.Log;
26
- import android.util.SparseArray;
27
26
  import com.facebook.react.bridge.Arguments;
28
27
  import com.facebook.react.bridge.WritableMap;
29
28
  import com.google.firebase.storage.StorageReference;
30
29
  import com.google.firebase.storage.StorageTask;
31
30
  import javax.annotation.Nullable;
32
31
 
33
- class ReactNativeFirebaseStorageTask {
32
+ class ReactNativeFirebaseStorageTask implements StoragePendingHandle {
34
33
  static final String KEY_STATE = "state";
35
34
  static final String KEY_META_DATA = "metadata";
36
35
  static final String KEY_TOTAL_BYTES = "totalBytes";
@@ -39,8 +38,7 @@ class ReactNativeFirebaseStorageTask {
39
38
  private static final String KEY_CODE = "code";
40
39
  private static final String KEY_MESSAGE = "message";
41
40
  private static final String KEY_NATIVE_ERROR_MESSAGE = "nativeErrorMessage";
42
- private static final SparseArray<ReactNativeFirebaseStorageTask> PENDING_TASKS =
43
- new SparseArray<>();
41
+ static final RNFBStorageTaskRegistry PENDING_TASKS = new RNFBStorageTaskRegistry();
44
42
  private static final String TAG = "RNFBStorageTask";
45
43
  int taskId;
46
44
  String appName;
@@ -51,43 +49,15 @@ class ReactNativeFirebaseStorageTask {
51
49
  this.taskId = taskId;
52
50
  this.storageReference = storageReference;
53
51
  this.appName = appName;
54
- PENDING_TASKS.put(taskId, this);
55
52
  }
56
53
 
57
- static boolean pauseTaskById(int taskId) {
58
- ReactNativeFirebaseStorageTask reactNativeFirebaseStorageTask = PENDING_TASKS.get(taskId);
59
- if (reactNativeFirebaseStorageTask != null) {
60
- return reactNativeFirebaseStorageTask.pause();
61
- }
62
-
63
- return false;
64
- }
65
-
66
- static boolean resumeTaskById(int taskId) {
67
- ReactNativeFirebaseStorageTask reactNativeFirebaseStorageTask = PENDING_TASKS.get(taskId);
68
- if (reactNativeFirebaseStorageTask != null) {
69
- return reactNativeFirebaseStorageTask.resume();
70
- }
71
-
72
- return false;
54
+ /** Register after the native {@link StorageTask} exists; collision cancels this handle. */
55
+ boolean registerPending() {
56
+ return PENDING_TASKS.putOrDiscard(taskId, this);
73
57
  }
74
58
 
75
- static boolean cancelTaskById(int taskId) {
76
- ReactNativeFirebaseStorageTask reactNativeFirebaseStorageTask = PENDING_TASKS.get(taskId);
77
- if (reactNativeFirebaseStorageTask != null) {
78
- return reactNativeFirebaseStorageTask.cancel();
79
- }
80
-
81
- return false;
82
- }
83
-
84
- static void destroyAllTasks() {
85
- for (int i = 0, size = PENDING_TASKS.size(); i < size; i++) {
86
- int key = PENDING_TASKS.keyAt(i);
87
- ReactNativeFirebaseStorageTask reactNativeFirebaseStorageTask = PENDING_TASKS.get(key);
88
- reactNativeFirebaseStorageTask.cancel();
89
- }
90
- PENDING_TASKS.clear();
59
+ boolean hasStorageTask() {
60
+ return storageTask != null;
91
61
  }
92
62
 
93
63
  static WritableMap buildCancelledSnapshotMap(WritableMap snapshot) {
@@ -108,39 +78,48 @@ class ReactNativeFirebaseStorageTask {
108
78
  return taskMap;
109
79
  }
110
80
 
111
- private boolean pause() {
81
+ @Override
82
+ public boolean pause() {
112
83
  Log.d(TAG, "pausing task for " + storageReference.toString());
113
84
 
114
- if (!storageTask.isPaused() && storageTask.isInProgress()) {
85
+ if (storageTask != null && !storageTask.isPaused() && storageTask.isInProgress()) {
115
86
  return storageTask.pause();
116
87
  }
117
88
 
118
89
  return false;
119
90
  }
120
91
 
121
- private boolean resume() {
92
+ @Override
93
+ public boolean resume() {
122
94
  Log.d(TAG, "resuming task for " + storageReference.toString());
123
95
 
124
- if (storageTask.isPaused()) {
96
+ if (storageTask != null && storageTask.isPaused()) {
125
97
  return storageTask.resume();
126
98
  }
127
99
 
128
100
  return false;
129
101
  }
130
102
 
131
- private boolean cancel() {
103
+ @Override
104
+ public boolean cancel() {
132
105
  Log.d(TAG, "cancelling task for " + storageReference.toString());
133
106
 
134
- if (!storageTask.isCanceled() && storageTask.isInProgress()) {
135
- destroyTask();
136
- return storageTask.cancel();
107
+ if (storageTask != null
108
+ && !storageTask.isCanceled()
109
+ && !storageTask.isComplete()
110
+ && (storageTask.isInProgress() || storageTask.isPaused())) {
111
+ if (storageTask.cancel()) {
112
+ destroyTask();
113
+ return true;
114
+ }
137
115
  }
138
116
 
139
117
  return false;
140
118
  }
141
119
 
142
120
  void destroyTask() {
143
- PENDING_TASKS.remove(taskId);
121
+ // Identity-gated: cancel of a discarded collision must not evict another handle at taskId.
122
+ PENDING_TASKS.takeIf(taskId, handle -> handle == this);
144
123
 
145
124
  Log.d(TAG, "destroyed completed task for " + storageReference.toString());
146
125
  }
@@ -0,0 +1,30 @@
1
+ package io.invertase.firebase.storage;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ /**
21
+ * Pause / resume / cancel surface stored in {@link RNFBStorageTaskRegistry}. Invoked after {@code
22
+ * get}/{@code take}/{@code takeAll}, never under the HandleMap lock.
23
+ */
24
+ interface StoragePendingHandle {
25
+ boolean pause();
26
+
27
+ boolean resume();
28
+
29
+ boolean cancel();
30
+ }
@@ -0,0 +1,237 @@
1
+ package io.invertase.firebase.storage;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import static org.junit.Assert.assertEquals;
21
+ import static org.junit.Assert.assertFalse;
22
+ import static org.junit.Assert.assertNull;
23
+ import static org.junit.Assert.assertSame;
24
+ import static org.junit.Assert.assertTrue;
25
+ import static org.junit.Assert.fail;
26
+
27
+ import io.invertase.firebase.common.RNFBHandleCollisionException;
28
+ import org.junit.Test;
29
+
30
+ public class RNFBStorageTaskRegistryTest {
31
+
32
+ private static final class FakeHandle implements StoragePendingHandle {
33
+ int pauseCount;
34
+ int resumeCount;
35
+ int cancelCount;
36
+ boolean pauseResult = true;
37
+ boolean resumeResult = true;
38
+ boolean cancelResult = true;
39
+ Runnable onCancel;
40
+
41
+ @Override
42
+ public boolean pause() {
43
+ pauseCount++;
44
+ return pauseResult;
45
+ }
46
+
47
+ @Override
48
+ public boolean resume() {
49
+ resumeCount++;
50
+ return resumeResult;
51
+ }
52
+
53
+ @Override
54
+ public boolean cancel() {
55
+ cancelCount++;
56
+ if (onCancel != null) {
57
+ onCancel.run();
58
+ }
59
+ return cancelResult;
60
+ }
61
+ }
62
+
63
+ @Test
64
+ public void putGetTake_happyPath() throws Exception {
65
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
66
+ FakeHandle handle = new FakeHandle();
67
+ registry.put(1, handle);
68
+ assertSame(handle, registry.get(1));
69
+ assertSame(handle, registry.take(1));
70
+ assertNull(registry.get(1));
71
+ assertEquals(0, handle.cancelCount);
72
+ }
73
+
74
+ @Test
75
+ public void put_occupiedId_throwsCollision() throws Exception {
76
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
77
+ FakeHandle first = new FakeHandle();
78
+ registry.put(1, first);
79
+ try {
80
+ registry.put(1, new FakeHandle());
81
+ fail("expected RNFBHandleCollisionException");
82
+ } catch (RNFBHandleCollisionException e) {
83
+ assertTrue(e.getMessage().contains("1"));
84
+ assertSame(first, registry.get(1));
85
+ }
86
+ }
87
+
88
+ @Test
89
+ public void putOrDiscard_collision_cancelsIncoming() throws Exception {
90
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
91
+ FakeHandle first = new FakeHandle();
92
+ FakeHandle duplicate = new FakeHandle();
93
+ registry.put(2, first);
94
+ assertFalse(registry.putOrDiscard(2, duplicate));
95
+ assertEquals(1, duplicate.cancelCount);
96
+ assertEquals(0, first.cancelCount);
97
+ assertSame(first, registry.get(2));
98
+ }
99
+
100
+ @Test
101
+ public void putOrDiscard_storesWhenFree() {
102
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
103
+ FakeHandle handle = new FakeHandle();
104
+ assertTrue(registry.putOrDiscard(3, handle));
105
+ assertSame(handle, registry.get(3));
106
+ assertEquals(0, handle.cancelCount);
107
+ }
108
+
109
+ @Test
110
+ public void putOrDiscard_collision_nullIncoming_isNoOp() throws Exception {
111
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
112
+ FakeHandle first = new FakeHandle();
113
+ registry.put(4, first);
114
+ assertFalse(registry.putOrDiscard(4, null));
115
+ assertSame(first, registry.get(4));
116
+ }
117
+
118
+ @Test
119
+ public void takeIf_whenPredicateTrue_removesAndReturns() throws Exception {
120
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
121
+ FakeHandle handle = new FakeHandle();
122
+ registry.put(8, handle);
123
+ assertSame(handle, registry.takeIf(8, h -> h == handle));
124
+ assertNull(registry.get(8));
125
+ }
126
+
127
+ @Test
128
+ public void takeIf_whenPredicateFalse_leavesMapping() throws Exception {
129
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
130
+ FakeHandle handle = new FakeHandle();
131
+ FakeHandle other = new FakeHandle();
132
+ registry.put(9, handle);
133
+ assertNull(registry.takeIf(9, h -> h == other));
134
+ assertSame(handle, registry.get(9));
135
+ }
136
+
137
+ @Test
138
+ public void takeIf_whenMissing_returnsNull() {
139
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
140
+ assertNull(registry.takeIf(99, h -> true));
141
+ }
142
+
143
+ @Test
144
+ public void takeAndCancel_cancelsAfterTake() throws Exception {
145
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
146
+ FakeHandle handle = new FakeHandle();
147
+ registry.put(5, handle);
148
+ assertTrue(registry.takeAndCancel(5));
149
+ assertEquals(1, handle.cancelCount);
150
+ assertNull(registry.get(5));
151
+ }
152
+
153
+ @Test
154
+ public void takeAndCancel_missingKey_isNoOp() {
155
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
156
+ assertFalse(registry.takeAndCancel(99));
157
+ assertNull(registry.get(99));
158
+ }
159
+
160
+ @Test
161
+ public void takeAndCancel_whenCancelReturnsFalse_keepsMapping() throws Exception {
162
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
163
+ FakeHandle handle = new FakeHandle();
164
+ handle.cancelResult = false;
165
+ registry.put(6, handle);
166
+ assertFalse(registry.takeAndCancel(6));
167
+ assertEquals(1, handle.cancelCount);
168
+ assertSame(handle, registry.get(6));
169
+ }
170
+
171
+ /**
172
+ * Regression: production cancel may destroyTask (clear A) then a new put B can land at the same
173
+ * id before takeAndCancel's trailing unregister. Trailing take must be identity-gated so B stays.
174
+ */
175
+ @Test
176
+ public void takeAndCancel_replacementDuringCancel_leavesReplacement() throws Exception {
177
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
178
+ FakeHandle replacement = new FakeHandle();
179
+ FakeHandle original = new FakeHandle();
180
+ original.onCancel =
181
+ () -> {
182
+ // Simulate destroyTask clearing this handle, then a new put at the same id.
183
+ assertSame(original, registry.take(9));
184
+ try {
185
+ registry.put(9, replacement);
186
+ } catch (RNFBHandleCollisionException e) {
187
+ fail(e.getMessage());
188
+ }
189
+ };
190
+ registry.put(9, original);
191
+ assertTrue(registry.takeAndCancel(9));
192
+ assertEquals(1, original.cancelCount);
193
+ assertSame(replacement, registry.get(9));
194
+ assertEquals(0, replacement.cancelCount);
195
+ }
196
+
197
+ @Test
198
+ public void takeAllAndCancel_cancelsSnapshotAndLeavesEmpty() throws Exception {
199
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
200
+ FakeHandle a = new FakeHandle();
201
+ FakeHandle b = new FakeHandle();
202
+ registry.put(1, a);
203
+ registry.put(2, b);
204
+ registry.takeAllAndCancel();
205
+ assertEquals(1, a.cancelCount);
206
+ assertEquals(1, b.cancelCount);
207
+ assertNull(registry.get(1));
208
+ assertNull(registry.get(2));
209
+ }
210
+
211
+ @Test
212
+ public void takeAllAndCancel_empty_isNoOp() {
213
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
214
+ registry.takeAllAndCancel();
215
+ assertNull(registry.get(1));
216
+ }
217
+
218
+ @Test
219
+ public void takeAllAndCancel_nullHandle_isNoOp() throws Exception {
220
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
221
+ registry.put(6, null);
222
+ registry.takeAllAndCancel();
223
+ assertNull(registry.get(6));
224
+ }
225
+
226
+ @Test
227
+ public void put_afterTake_allowsReuse() throws Exception {
228
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
229
+ FakeHandle first = new FakeHandle();
230
+ FakeHandle second = new FakeHandle();
231
+ registry.put(1, first);
232
+ assertSame(first, registry.take(1));
233
+ registry.put(1, second);
234
+ assertSame(second, registry.get(1));
235
+ assertEquals(0, first.cancelCount);
236
+ }
237
+ }