@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.
- package/CHANGELOG.md +21 -0
- package/RNFBStorage.podspec +9 -4
- package/android/build.gradle +6 -0
- package/android/src/main/java/io/invertase/firebase/storage/NativeRNFBTurboStorage.java +24 -4
- package/android/src/main/java/io/invertase/firebase/storage/RNFBStorageTaskRegistry.kt +88 -0
- package/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageTask.java +25 -46
- package/android/src/main/java/io/invertase/firebase/storage/StoragePendingHandle.java +30 -0
- package/android/src/test/java/io/invertase/firebase/storage/RNFBStorageTaskRegistryTest.java +237 -0
- package/android/src/test/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageTaskTest.java +285 -0
- package/dist/module/index.js +1 -2
- package/dist/module/index.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/lib/index.d.ts +1 -1
- package/dist/typescript/lib/types/internal.d.ts +2 -3
- package/dist/typescript/lib/types/internal.d.ts.map +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/ios/RNFBStorage/RNFBStorageCommon.m +17 -98
- package/ios/RNFBStorage/RNFBStorageErrorCodeMapper.swift +122 -0
- package/ios/RNFBStorage/RNFBStorageErrorSnapshotDictBuilder.swift +70 -0
- package/ios/RNFBStorage/RNFBStorageHelper.m +38 -13
- package/ios/RNFBStorage/RNFBStorageTaskRegistry.h +42 -0
- package/ios/RNFBStorage/RNFBStorageTaskRegistry.m +87 -0
- package/ios/RNFBStorage/RNFBStorageTaskRegistryStorage.swift +87 -0
- package/ios/RNFBStorage/RNFBStorageTaskStatusMapper.swift +57 -0
- package/ios/RNFBStorage.xcodeproj/project.pbxproj +26 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageErrorCodeMapperFacadeTests.m +91 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageErrorCodeMapperTests.swift +211 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageErrorSnapshotDictBuilderFacadeTests.m +125 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageTaskRegistryStorageTests.swift +154 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageTaskRegistryTests.m +225 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageTaskStatusMapperFacadeTests.m +52 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageTaskStatusMapperTests.swift +58 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageUnitTests.xcodeproj/project.pbxproj +301 -0
- package/ios/RNFBStorageUnitTests/RNFBStorageUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBStorageUnitTests.xcscheme +69 -0
- package/lib/index.ts +1 -1
- package/lib/types/internal.ts +5 -4
- package/lib/version.ts +1 -1
- 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
|
package/RNFBStorage.podspec
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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}'"
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
340
|
+
{
|
|
341
|
+
StoragePendingHandle handle = tasks.get(id);
|
|
342
|
+
return handle != null && handle.pause();
|
|
343
|
+
}
|
|
327
344
|
case 1:
|
|
328
|
-
|
|
345
|
+
{
|
|
346
|
+
StoragePendingHandle handle = tasks.get(id);
|
|
347
|
+
return handle != null && handle.resume();
|
|
348
|
+
}
|
|
329
349
|
case 2:
|
|
330
|
-
return
|
|
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
|
+
}
|
package/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageTask.java
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
@Override
|
|
104
|
+
public boolean cancel() {
|
|
132
105
|
Log.d(TAG, "cancelling task for " + storageReference.toString());
|
|
133
106
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
+
}
|