@react-native-firebase/storage 26.3.3 → 26.4.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
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
+ ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **storage:** use RNFBHandleMap for pending tasks ([acf77f1](https://github.com/invertase/react-native-firebase/commit/acf77f1ff1680f1ce9c65f72cf0056df7b998aa5))
11
+
6
12
  ## [26.3.3](https://github.com/invertase/react-native-firebase/compare/v26.3.2...v26.3.3) (2026-09-01)
7
13
 
8
14
  ### Bug Fixes
@@ -33,7 +33,7 @@ Pod::Spec.new do |s|
33
33
  'ios/RNFBStorage/*.h',
34
34
  'ios/generated/**/*.h',
35
35
  ]
36
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
36
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**'
37
37
 
38
38
  # Must be set before install_modules_dependencies so RN can append use_frameworks
39
39
  # HEADER_SEARCH_PATHS (React-debug etc.). Assigning after overwrites those paths
@@ -93,6 +93,9 @@ dependencies {
93
93
  api appProject
94
94
  implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}")
95
95
  implementation "com.google.firebase:firebase-storage"
96
+
97
+ testImplementation "junit:junit:4.13.2"
98
+ testImplementation "org.mockito:mockito-core:5.14.2"
96
99
  }
97
100
 
98
101
  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,93 @@
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.List;
23
+ import java.util.function.Predicate;
24
+
25
+ /**
26
+ * Pending upload/download task map. Unique {@link #put}; callers {@link #take} or {@link
27
+ * #takeAllAndCancel} then cancel outside the HandleMap lock.
28
+ */
29
+ final class RNFBStorageTaskRegistry {
30
+ private final RNFBHandleMap<Integer, StoragePendingHandle> map = new RNFBHandleMap<>();
31
+
32
+ void put(int taskId, StoragePendingHandle handle) throws RNFBHandleCollisionException {
33
+ map.put(taskId, handle);
34
+ }
35
+
36
+ /**
37
+ * Unique put. On collision, {@code cancel()} the incoming handle and leave the existing mapping.
38
+ *
39
+ * @return true if stored
40
+ */
41
+ boolean putOrDiscard(int taskId, StoragePendingHandle handle) {
42
+ if (map.putIfAbsent(taskId, handle)) {
43
+ return true;
44
+ }
45
+ if (handle != null) {
46
+ handle.cancel();
47
+ }
48
+ return false;
49
+ }
50
+
51
+ StoragePendingHandle get(int taskId) {
52
+ return map.get(taskId);
53
+ }
54
+
55
+ StoragePendingHandle take(int taskId) {
56
+ return map.take(taskId);
57
+ }
58
+
59
+ StoragePendingHandle takeIf(int taskId, Predicate<StoragePendingHandle> shouldTake) {
60
+ return map.takeIf(taskId, shouldTake);
61
+ }
62
+
63
+ /**
64
+ * Cancel outside the HandleMap lock; remove the mapping only when cancel succeeds. Returns {@code
65
+ * false} when no mapping existed or cancel failed. Trailing unregister is identity-gated so a
66
+ * replacement put after {@code cancel()} (e.g. production {@code destroyTask}) is not stolen.
67
+ *
68
+ * <p>Shape: get → cancel → identity takeIf on success (keep mapping when cancel returns false).
69
+ * iOS registry mirrors get → cancel → identity takeIf, but FIRStorage cancel is void so there is
70
+ * no keep-on-false path — see {@code RNFBStorageTaskRegistry.takeAndCancel:} / helper {@code
71
+ * setTaskStatus}.
72
+ */
73
+ boolean takeAndCancel(int taskId) {
74
+ StoragePendingHandle handle = map.get(taskId);
75
+ if (handle == null) {
76
+ return false;
77
+ }
78
+ boolean cancelled = handle.cancel();
79
+ if (cancelled) {
80
+ map.takeIf(taskId, h -> h == handle);
81
+ }
82
+ return cancelled;
83
+ }
84
+
85
+ void takeAllAndCancel() {
86
+ List<StoragePendingHandle> remaining = map.takeAll();
87
+ for (StoragePendingHandle handle : remaining) {
88
+ if (handle != null) {
89
+ handle.cancel();
90
+ }
91
+ }
92
+ }
93
+ }
@@ -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,212 @@
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 takeAndCancel_cancelsAfterTake() throws Exception {
120
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
121
+ FakeHandle handle = new FakeHandle();
122
+ registry.put(5, handle);
123
+ assertTrue(registry.takeAndCancel(5));
124
+ assertEquals(1, handle.cancelCount);
125
+ assertNull(registry.get(5));
126
+ }
127
+
128
+ @Test
129
+ public void takeAndCancel_missingKey_isNoOp() {
130
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
131
+ assertFalse(registry.takeAndCancel(99));
132
+ assertNull(registry.get(99));
133
+ }
134
+
135
+ @Test
136
+ public void takeAndCancel_whenCancelReturnsFalse_keepsMapping() throws Exception {
137
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
138
+ FakeHandle handle = new FakeHandle();
139
+ handle.cancelResult = false;
140
+ registry.put(6, handle);
141
+ assertFalse(registry.takeAndCancel(6));
142
+ assertEquals(1, handle.cancelCount);
143
+ assertSame(handle, registry.get(6));
144
+ }
145
+
146
+ /**
147
+ * Regression: production cancel may destroyTask (clear A) then a new put B can land at the same
148
+ * id before takeAndCancel's trailing unregister. Trailing take must be identity-gated so B stays.
149
+ */
150
+ @Test
151
+ public void takeAndCancel_replacementDuringCancel_leavesReplacement() throws Exception {
152
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
153
+ FakeHandle replacement = new FakeHandle();
154
+ FakeHandle original = new FakeHandle();
155
+ original.onCancel =
156
+ () -> {
157
+ // Simulate destroyTask clearing this handle, then a new put at the same id.
158
+ assertSame(original, registry.take(9));
159
+ try {
160
+ registry.put(9, replacement);
161
+ } catch (RNFBHandleCollisionException e) {
162
+ fail(e.getMessage());
163
+ }
164
+ };
165
+ registry.put(9, original);
166
+ assertTrue(registry.takeAndCancel(9));
167
+ assertEquals(1, original.cancelCount);
168
+ assertSame(replacement, registry.get(9));
169
+ assertEquals(0, replacement.cancelCount);
170
+ }
171
+
172
+ @Test
173
+ public void takeAllAndCancel_cancelsSnapshotAndLeavesEmpty() throws Exception {
174
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
175
+ FakeHandle a = new FakeHandle();
176
+ FakeHandle b = new FakeHandle();
177
+ registry.put(1, a);
178
+ registry.put(2, b);
179
+ registry.takeAllAndCancel();
180
+ assertEquals(1, a.cancelCount);
181
+ assertEquals(1, b.cancelCount);
182
+ assertNull(registry.get(1));
183
+ assertNull(registry.get(2));
184
+ }
185
+
186
+ @Test
187
+ public void takeAllAndCancel_empty_isNoOp() {
188
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
189
+ registry.takeAllAndCancel();
190
+ assertNull(registry.get(1));
191
+ }
192
+
193
+ @Test
194
+ public void takeAllAndCancel_nullHandle_isNoOp() throws Exception {
195
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
196
+ registry.put(6, null);
197
+ registry.takeAllAndCancel();
198
+ assertNull(registry.get(6));
199
+ }
200
+
201
+ @Test
202
+ public void put_afterTake_allowsReuse() throws Exception {
203
+ RNFBStorageTaskRegistry registry = new RNFBStorageTaskRegistry();
204
+ FakeHandle first = new FakeHandle();
205
+ FakeHandle second = new FakeHandle();
206
+ registry.put(1, first);
207
+ assertSame(first, registry.take(1));
208
+ registry.put(1, second);
209
+ assertSame(second, registry.get(1));
210
+ assertEquals(0, first.cancelCount);
211
+ }
212
+ }