@react-native-firebase/storage 26.3.2 → 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.
@@ -0,0 +1,285 @@
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.assertNotNull;
23
+ import static org.junit.Assert.assertNull;
24
+ import static org.junit.Assert.assertSame;
25
+ import static org.junit.Assert.assertTrue;
26
+ import static org.mockito.ArgumentMatchers.anyString;
27
+ import static org.mockito.Mockito.mock;
28
+ import static org.mockito.Mockito.mockStatic;
29
+ import static org.mockito.Mockito.when;
30
+
31
+ import android.util.Log;
32
+ import com.google.firebase.storage.StorageReference;
33
+ import com.google.firebase.storage.StorageTask;
34
+ import org.junit.After;
35
+ import org.junit.Test;
36
+ import org.mockito.MockedStatic;
37
+
38
+ public class ReactNativeFirebaseStorageTaskTest {
39
+
40
+ private static final class FakeHandle implements StoragePendingHandle {
41
+ int cancelCount;
42
+
43
+ @Override
44
+ public boolean pause() {
45
+ return false;
46
+ }
47
+
48
+ @Override
49
+ public boolean resume() {
50
+ return false;
51
+ }
52
+
53
+ @Override
54
+ public boolean cancel() {
55
+ cancelCount++;
56
+ return true;
57
+ }
58
+ }
59
+
60
+ private static final class RecordingTask extends ReactNativeFirebaseStorageTask {
61
+ int beginCount;
62
+
63
+ RecordingTask(int taskId) {
64
+ super(taskId, null, "app");
65
+ }
66
+
67
+ void begin() {
68
+ beginCount++;
69
+ }
70
+ }
71
+
72
+ @After
73
+ public void tearDown() {
74
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(11);
75
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(12);
76
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(13);
77
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(14);
78
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(15);
79
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(16);
80
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(17);
81
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(18);
82
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(19);
83
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(20);
84
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(21);
85
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.take(22);
86
+ }
87
+
88
+ private static StorageReference stubReference() {
89
+ StorageReference ref = mock(StorageReference.class);
90
+ when(ref.toString()).thenReturn("gs://bucket/path");
91
+ return ref;
92
+ }
93
+
94
+ private static ReactNativeFirebaseStorageTask registeredTask(int taskId) {
95
+ ReactNativeFirebaseStorageTask task =
96
+ new ReactNativeFirebaseStorageTask(taskId, stubReference(), "app");
97
+ assertTrue(task.registerPending());
98
+ return task;
99
+ }
100
+
101
+ @Test
102
+ public void constructor_uniqueId_registersPendingTask() {
103
+ RecordingTask task = new RecordingTask(11);
104
+ assertTrue(task.registerPending());
105
+ assertSame(task, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(11));
106
+ task.begin();
107
+ assertEquals(1, task.beginCount);
108
+ }
109
+
110
+ @Test
111
+ public void registerPending_occupiedId_cancelsIncomingAndKeepsExisting() throws Exception {
112
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
113
+ when(Log.d(anyString(), anyString())).thenReturn(0);
114
+ FakeHandle first = new FakeHandle();
115
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.put(12, first);
116
+ ReactNativeFirebaseStorageTask second =
117
+ new ReactNativeFirebaseStorageTask(12, stubReference(), "app");
118
+ assertFalse(second.registerPending());
119
+ assertSame(first, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(12));
120
+ assertEquals(0, first.cancelCount);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Regression: when the incoming handle has a live StorageTask whose cancel() succeeds,
126
+ * destroyTask must not evict the existing mapping at the same taskId (identity-gated take).
127
+ */
128
+ @Test
129
+ public void registerPending_occupiedId_successfulCancelDoesNotEvictExisting() {
130
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
131
+ when(Log.d(anyString(), anyString())).thenReturn(0);
132
+ ReactNativeFirebaseStorageTask existing = registeredTask(22);
133
+
134
+ ReactNativeFirebaseStorageTask incoming =
135
+ new ReactNativeFirebaseStorageTask(22, stubReference(), "app");
136
+ StorageTask<?> storageTask = mock(StorageTask.class);
137
+ incoming.setStorageTask(storageTask);
138
+ when(storageTask.isCanceled()).thenReturn(false);
139
+ when(storageTask.isComplete()).thenReturn(false);
140
+ when(storageTask.isPaused()).thenReturn(false);
141
+ when(storageTask.isInProgress()).thenReturn(true);
142
+ when(storageTask.cancel()).thenReturn(true);
143
+
144
+ assertFalse(incoming.registerPending());
145
+ assertSame(existing, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(22));
146
+ }
147
+ }
148
+
149
+ @Test
150
+ public void pauseResumeCancel_whenStorageTaskNull_returnFalseWithoutNpe() {
151
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
152
+ when(Log.d(anyString(), anyString())).thenReturn(0);
153
+ ReactNativeFirebaseStorageTask task = registeredTask(13);
154
+ assertSame(task, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(13));
155
+ assertFalse(task.pause());
156
+ assertFalse(task.resume());
157
+ assertFalse(task.cancel());
158
+ assertSame(task, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(13));
159
+ }
160
+ }
161
+
162
+ @Test
163
+ public void cancelIfPresent_whenStorageTaskNull_doesNotTakeMapping() {
164
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
165
+ when(Log.d(anyString(), anyString())).thenReturn(0);
166
+ ReactNativeFirebaseStorageTask task = registeredTask(14);
167
+ assertFalse(ReactNativeFirebaseStorageTask.PENDING_TASKS.takeAndCancel(14));
168
+ assertSame(task, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(14));
169
+ assertFalse(task.pause());
170
+ }
171
+ }
172
+
173
+ @Test
174
+ public void setTaskStatusCancel_whenStorageTaskNull_doesNotTakeMapping() {
175
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
176
+ when(Log.d(anyString(), anyString())).thenReturn(0);
177
+ registeredTask(15);
178
+ assertFalse(ReactNativeFirebaseStorageTask.PENDING_TASKS.takeAndCancel(15));
179
+ assertNotNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(15));
180
+ }
181
+ }
182
+
183
+ @Test
184
+ public void destroyTask_takesPendingMapping() {
185
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
186
+ when(Log.d(anyString(), anyString())).thenReturn(0);
187
+ ReactNativeFirebaseStorageTask task = registeredTask(16);
188
+ task.destroyTask();
189
+ assertNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(16));
190
+ }
191
+ }
192
+
193
+ @Test
194
+ public void invalidate_whenStorageTaskNull_takeAllThenCancelWithoutNpe() {
195
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
196
+ when(Log.d(anyString(), anyString())).thenReturn(0);
197
+ registeredTask(17);
198
+ ReactNativeFirebaseStorageTask.PENDING_TASKS.takeAllAndCancel();
199
+ assertNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(17));
200
+ }
201
+ }
202
+
203
+ @Test
204
+ public void pauseResumeCancel_whenStorageTaskPresent_delegateToSdkTask() {
205
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
206
+ when(Log.d(anyString(), anyString())).thenReturn(0);
207
+ ReactNativeFirebaseStorageTask task = registeredTask(18);
208
+ StorageTask<?> storageTask = mock(StorageTask.class);
209
+ task.setStorageTask(storageTask);
210
+
211
+ when(storageTask.isPaused()).thenReturn(false);
212
+ when(storageTask.isInProgress()).thenReturn(true);
213
+ when(storageTask.pause()).thenReturn(true);
214
+ assertTrue(task.pause());
215
+
216
+ when(storageTask.isPaused()).thenReturn(true);
217
+ when(storageTask.resume()).thenReturn(true);
218
+ assertTrue(task.resume());
219
+
220
+ when(storageTask.isPaused()).thenReturn(false);
221
+ when(storageTask.isCanceled()).thenReturn(false);
222
+ when(storageTask.isInProgress()).thenReturn(true);
223
+ when(storageTask.cancel()).thenReturn(true);
224
+ assertTrue(task.cancel());
225
+ assertNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(18));
226
+ }
227
+ }
228
+
229
+ @Test
230
+ public void cancel_whenPaused_delegatesToSdkTask() {
231
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
232
+ when(Log.d(anyString(), anyString())).thenReturn(0);
233
+ ReactNativeFirebaseStorageTask task = registeredTask(19);
234
+ StorageTask<?> storageTask = mock(StorageTask.class);
235
+ task.setStorageTask(storageTask);
236
+
237
+ when(storageTask.isCanceled()).thenReturn(false);
238
+ when(storageTask.isComplete()).thenReturn(false);
239
+ when(storageTask.isPaused()).thenReturn(true);
240
+ when(storageTask.isInProgress()).thenReturn(false);
241
+ when(storageTask.cancel()).thenReturn(true);
242
+
243
+ assertTrue(task.cancel());
244
+ assertNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(19));
245
+ }
246
+ }
247
+
248
+ @Test
249
+ public void takeAndCancel_whenPausedCancelSucceeds_takesMapping() {
250
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
251
+ when(Log.d(anyString(), anyString())).thenReturn(0);
252
+ ReactNativeFirebaseStorageTask task = registeredTask(20);
253
+ StorageTask<?> storageTask = mock(StorageTask.class);
254
+ task.setStorageTask(storageTask);
255
+
256
+ when(storageTask.isCanceled()).thenReturn(false);
257
+ when(storageTask.isComplete()).thenReturn(false);
258
+ when(storageTask.isPaused()).thenReturn(true);
259
+ when(storageTask.isInProgress()).thenReturn(false);
260
+ when(storageTask.cancel()).thenReturn(true);
261
+
262
+ assertTrue(ReactNativeFirebaseStorageTask.PENDING_TASKS.takeAndCancel(20));
263
+ assertNull(ReactNativeFirebaseStorageTask.PENDING_TASKS.get(20));
264
+ }
265
+ }
266
+
267
+ @Test
268
+ public void cancel_whenSdkCancelReturnsFalse_keepsMapping() {
269
+ try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
270
+ when(Log.d(anyString(), anyString())).thenReturn(0);
271
+ ReactNativeFirebaseStorageTask task = registeredTask(21);
272
+ StorageTask<?> storageTask = mock(StorageTask.class);
273
+ task.setStorageTask(storageTask);
274
+
275
+ when(storageTask.isCanceled()).thenReturn(false);
276
+ when(storageTask.isComplete()).thenReturn(false);
277
+ when(storageTask.isPaused()).thenReturn(true);
278
+ when(storageTask.isInProgress()).thenReturn(false);
279
+ when(storageTask.cancel()).thenReturn(false);
280
+
281
+ assertFalse(task.cancel());
282
+ assertSame(task, ReactNativeFirebaseStorageTask.PENDING_TASKS.get(21));
283
+ }
284
+ }
285
+ }
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  // Generated by genversion.
4
- export const version = '26.3.2';
4
+ export const version = '26.4.0';
5
5
  //# sourceMappingURL=version.js.map
@@ -1,7 +1,7 @@
1
1
  import type { FirebaseApp } from '@react-native-firebase/app';
2
2
  import './types/internal';
3
3
  import type { EmulatorMockTokenOptions, FirebaseStorage, FullMetadata, ListOptions, ListResult, SettableMetadata, StorageReference, Task, UploadMetadata, UploadResult, UploadTask } from './types/storage';
4
- export declare const SDK_VERSION = "26.3.2";
4
+ export declare const SDK_VERSION = "26.4.0";
5
5
  export declare function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage;
6
6
  export type * from './types/storage';
7
7
  export { StringFormat, TaskEvent, TaskState } from './StorageStatics';
@@ -1,2 +1,2 @@
1
- export declare const version = "26.3.2";
1
+ export declare const version = "26.4.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -28,6 +28,7 @@
28
28
  #import "RNFBRCTEventEmitter.h"
29
29
  #import "RNFBStorageCommon.h"
30
30
  #import "RNFBStorageHelper.h"
31
+ #import "RNFBStorageTaskRegistry.h"
31
32
 
32
33
  static NSString *const RNFB_STORAGE_EVENT = @"storage_event";
33
34
  static NSString *const RNFB_STORAGE_STATE_CHANGED = @"state_changed";
@@ -36,7 +37,7 @@ static NSString *const RNFB_STORAGE_UPLOAD_FAILURE = @"upload_failure";
36
37
  static NSString *const RNFB_STORAGE_DOWNLOAD_SUCCESS = @"download_success";
37
38
  static NSString *const RNFB_STORAGE_DOWNLOAD_FAILURE = @"download_failure";
38
39
 
39
- static NSMutableDictionary *PENDING_TASKS;
40
+ static RNFBStorageTaskRegistry *pendingTasks;
40
41
 
41
42
  // The iOS SDK has a short memory on settings, store these globally and set them in each time
42
43
  static NSString *emulatorHost = nil;
@@ -54,16 +55,14 @@ static NSTimeInterval maxOperationRetryTime = 120;
54
55
  + (void)initializeSharedStateOnce {
55
56
  static dispatch_once_t onceToken;
56
57
  dispatch_once(&onceToken, ^{
57
- PENDING_TASKS = [[NSMutableDictionary alloc] init];
58
+ pendingTasks = [[RNFBStorageTaskRegistry alloc] init];
58
59
  emulatorConfigs = [[NSMutableDictionary alloc] init];
59
60
  });
60
61
  }
61
62
 
62
63
  + (void)invalidate {
63
64
  [self initializeSharedStateOnce];
64
- for (NSString *key in [PENDING_TASKS allKeys]) {
65
- [PENDING_TASKS removeObjectForKey:key];
66
- }
65
+ [pendingTasks cancelAll];
67
66
  }
68
67
 
69
68
  #pragma mark -
@@ -283,7 +282,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
283
282
  downloadTask = [storageReference writeToFile:localFile];
284
283
  });
285
284
 
286
- PENDING_TASKS[taskIdNumber] = downloadTask;
285
+ if (![pendingTasks putOrDiscard:taskIdNumber value:downloadTask]) {
286
+ reject(@"internal", @"Handle id already registered", nil);
287
+ return;
288
+ }
287
289
 
288
290
  [downloadTask
289
291
  observeStatus:FIRStorageTaskStatusResume
@@ -324,7 +326,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
324
326
  [downloadTask
325
327
  observeStatus:FIRStorageTaskStatusSuccess
326
328
  handler:^(FIRStorageTaskSnapshot *snapshot) {
327
- [PENDING_TASKS removeObjectForKey:taskIdNumber];
329
+ [pendingTasks takeIf:taskIdNumber
330
+ when:^BOOL(id value) {
331
+ return value == downloadTask;
332
+ }];
328
333
 
329
334
  NSDictionary *stateChangedEventBody =
330
335
  [RNFBStorageCommon getDownloadTaskAsDictionary:snapshot];
@@ -349,7 +354,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
349
354
  [downloadTask
350
355
  observeStatus:FIRStorageTaskStatusFailure
351
356
  handler:^(FIRStorageTaskSnapshot *snapshot) {
352
- [PENDING_TASKS removeObjectForKey:taskIdNumber];
357
+ [pendingTasks takeIf:taskIdNumber
358
+ when:^BOOL(id value) {
359
+ return value == downloadTask;
360
+ }];
353
361
 
354
362
  NSDictionary *stateChangedEventBody =
355
363
  [RNFBStorageCommon getDownloadTaskAsDictionary:snapshot];
@@ -501,7 +509,7 @@ static NSTimeInterval maxOperationRetryTime = 120;
501
509
  + (NSNumber *)setTaskStatus:(NSString *)appName taskId:(double)taskId status:(double)status {
502
510
  [self initializeSharedStateOnce];
503
511
  NSNumber *taskIdNumber = @(taskId);
504
- id task = PENDING_TASKS[taskIdNumber];
512
+ id task = [pendingTasks get:taskIdNumber];
505
513
  if (task == nil) {
506
514
  return @NO;
507
515
  }
@@ -546,7 +554,15 @@ static NSTimeInterval maxOperationRetryTime = 120;
546
554
  (currentStatus == FIRStorageTaskStatusResume ||
547
555
  currentStatus == FIRStorageTaskStatusProgress ||
548
556
  currentStatus == FIRStorageTaskStatusPause)) {
549
- [PENDING_TASKS removeObjectForKey:taskIdNumber];
557
+ // Identity take then cancel so a replacement at taskId is not stolen.
558
+ // Ordering differs from registry takeAndCancel: (takeIf → cancel) vs Android
559
+ // (get → cancel → takeIf). Both are identity-safe; FIRStorage cancel is void.
560
+ if ([pendingTasks takeIf:taskIdNumber
561
+ when:^BOOL(id value) {
562
+ return value == task;
563
+ }] == nil) {
564
+ return @NO;
565
+ }
550
566
  if ([task isKindOfClass:[FIRStorageDownloadTask class]]) {
551
567
  [(FIRStorageDownloadTask *)task cancel];
552
568
  } else {
@@ -595,7 +611,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
595
611
  resolver:(RCTPromiseResolveBlock)resolve
596
612
  rejecter:(RCTPromiseRejectBlock)reject {
597
613
  [self initializeSharedStateOnce];
598
- PENDING_TASKS[taskId] = uploadTask;
614
+ if (![pendingTasks putOrDiscard:taskId value:uploadTask]) {
615
+ reject(@"internal", @"Handle id already registered", nil);
616
+ return;
617
+ }
599
618
 
600
619
  [uploadTask
601
620
  observeStatus:FIRStorageTaskStatusResume
@@ -635,7 +654,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
635
654
 
636
655
  [uploadTask observeStatus:FIRStorageTaskStatusSuccess
637
656
  handler:^(FIRStorageTaskSnapshot *snapshot) {
638
- [PENDING_TASKS removeObjectForKey:taskId];
657
+ [pendingTasks takeIf:taskId
658
+ when:^BOOL(id value) {
659
+ return value == uploadTask;
660
+ }];
639
661
 
640
662
  NSDictionary *eventBody =
641
663
  [RNFBStorageCommon getUploadTaskAsDictionary:snapshot];
@@ -661,7 +683,10 @@ static NSTimeInterval maxOperationRetryTime = 120;
661
683
  [uploadTask
662
684
  observeStatus:FIRStorageTaskStatusFailure
663
685
  handler:^(FIRStorageTaskSnapshot *snapshot) {
664
- [PENDING_TASKS removeObjectForKey:taskId];
686
+ [pendingTasks takeIf:taskId
687
+ when:^BOOL(id value) {
688
+ return value == uploadTask;
689
+ }];
665
690
 
666
691
  NSMutableDictionary *taskSnapshotDict =
667
692
  [RNFBStorageCommon getUploadTaskAsDictionary:snapshot];
@@ -239,13 +239,12 @@ RCT_EXPORT_MODULE(NativeRNFBTurboStorage);
239
239
  #pragma mark -
240
240
  #pragma mark Constants
241
241
 
242
- - (facebook::react::ModuleConstants<JS::NativeRNFBTurboStorage::Constants::Builder>)
243
- constantsToExport {
242
+ - (facebook::react::ModuleConstants<JS::NativeRNFBTurboStorage::Constants>)constantsToExport {
244
243
  return [_RCTTypedModuleConstants
245
244
  newWithUnsafeDictionary:[RNFBStorageHelper storageConstantsDictionary]];
246
245
  }
247
246
 
248
- - (facebook::react::ModuleConstants<JS::NativeRNFBTurboStorage::Constants::Builder>)getConstants {
247
+ - (facebook::react::ModuleConstants<JS::NativeRNFBTurboStorage::Constants>)getConstants {
249
248
  return [self constantsToExport];
250
249
  }
251
250
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ */
17
+
18
+ #import <Foundation/Foundation.h>
19
+
20
+ NS_ASSUME_NONNULL_BEGIN
21
+
22
+ /**
23
+ * Pending upload/download task map. Unique `put`; callers `take` or `cancelAll` then cancel
24
+ * outside the HandleMap lock. Stored values are expected to respond to `cancel`.
25
+ */
26
+ @interface RNFBStorageTaskRegistry : NSObject
27
+
28
+ - (BOOL)put:(id)key value:(id)value error:(NSError *_Nullable *_Nullable)error;
29
+ /// Unique put. On collision, `cancel`s the incoming value and leaves the existing mapping.
30
+ - (BOOL)putOrDiscard:(id)key value:(id)value;
31
+ - (nullable id)get:(id)key;
32
+ - (nullable id)take:(id)key;
33
+ /// Identity-gated take; see RNFBHandleMap `takeIf:when:`.
34
+ - (nullable id)takeIf:(id)key when:(BOOL (^)(id value))condition;
35
+ /// get → cancel → identity take (Android shape). FIRStorage cancel is void (no keep-on-false).
36
+ /// Production setTaskStatus cancel uses takeIf then cancel; see RNFBStorageHelper.
37
+ - (BOOL)takeAndCancel:(id)key;
38
+ - (void)cancelAll;
39
+
40
+ @end
41
+
42
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ */
17
+
18
+ #import "RNFBStorageTaskRegistry.h"
19
+
20
+ #if __has_include("RNFBHandleMap.h")
21
+ #import "RNFBHandleMap.h"
22
+ #else
23
+ #import "RNFBApp/RNFBHandleMap.h"
24
+ #endif
25
+
26
+ @interface RNFBStorageTaskRegistry ()
27
+ @property(nonatomic, strong) RNFBHandleMap *map;
28
+ @end
29
+
30
+ @implementation RNFBStorageTaskRegistry
31
+
32
+ - (instancetype)init {
33
+ self = [super init];
34
+ if (self) {
35
+ _map = [[RNFBHandleMap alloc] init];
36
+ }
37
+ return self;
38
+ }
39
+
40
+ - (void)rnfb_cancelHandle:(id)handle {
41
+ if (handle && [handle respondsToSelector:@selector(cancel)]) {
42
+ [handle cancel];
43
+ }
44
+ }
45
+
46
+ - (BOOL)put:(id)key value:(id)value error:(NSError **)error {
47
+ return [self.map put:key value:value error:error];
48
+ }
49
+
50
+ - (BOOL)putOrDiscard:(id)key value:(id)value {
51
+ if ([self.map putIfAbsent:key value:value]) {
52
+ return YES;
53
+ }
54
+ [self rnfb_cancelHandle:value];
55
+ return NO;
56
+ }
57
+
58
+ - (id)get:(id)key {
59
+ return [self.map get:key];
60
+ }
61
+
62
+ - (id)take:(id)key {
63
+ return [self.map take:key];
64
+ }
65
+
66
+ - (id)takeIf:(id)key when:(BOOL (^)(id))condition {
67
+ return [self.map takeIf:key when:condition];
68
+ }
69
+
70
+ - (BOOL)takeAndCancel:(id)key {
71
+ // Align with Android RNFBStorageTaskRegistry.takeAndCancel: get → cancel → identity take.
72
+ // FIRStorage*Task cancel is void (no BOOL), so there is no keep-on-false path; always
73
+ // identity-take after cancel. Production setTaskStatus cancel uses takeIf then cancel
74
+ // (RNFBStorageHelper) — identity-safe; this helper matches Android ordering for registry/tests.
75
+ id handle = [self.map get:key];
76
+ if (handle == nil) {
77
+ return NO;
78
+ }
79
+ [self rnfb_cancelHandle:handle];
80
+ [self.map takeIf:key
81
+ when:^BOOL(id value) {
82
+ return value == handle;
83
+ }];
84
+ return YES;
85
+ }
86
+
87
+ - (void)cancelAll {
88
+ NSArray *handlers = [self.map takeAll];
89
+ for (id handler in handlers) {
90
+ [self rnfb_cancelHandle:handler];
91
+ }
92
+ }
93
+
94
+ @end