react-native-buffered-blob 1.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 (93) hide show
  1. package/android/AGENTS.md +74 -0
  2. package/android/build.gradle +34 -0
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/com/bufferedblob/BufferedBlobModule.kt +274 -0
  5. package/android/src/main/java/com/bufferedblob/BufferedBlobPackage.kt +32 -0
  6. package/android/src/main/java/com/bufferedblob/HandleRegistry.kt +84 -0
  7. package/android/src/main/java/com/bufferedblob/StreamingBridge.kt +211 -0
  8. package/cpp/AGENTS.md +71 -0
  9. package/cpp/AndroidPlatformBridge.cpp +437 -0
  10. package/cpp/AndroidPlatformBridge.h +79 -0
  11. package/cpp/BufferedBlobStreamingHostObject.cpp +344 -0
  12. package/cpp/BufferedBlobStreamingHostObject.h +118 -0
  13. package/cpp/CMakeLists.txt +49 -0
  14. package/cpp/jni_onload.cpp +32 -0
  15. package/ios/AGENTS.md +76 -0
  16. package/ios/BufferedBlobModule.h +44 -0
  17. package/ios/BufferedBlobModule.m +433 -0
  18. package/ios/BufferedBlobModule.mm +192 -0
  19. package/ios/BufferedBlobStreamingBridge.h +21 -0
  20. package/ios/BufferedBlobStreamingBridge.mm +442 -0
  21. package/ios/HandleRegistry.h +29 -0
  22. package/ios/HandleRegistry.m +67 -0
  23. package/ios/HandleTypes.h +83 -0
  24. package/ios/HandleTypes.m +333 -0
  25. package/lib/module/AGENTS.md +70 -0
  26. package/lib/module/NativeBufferedBlob.js +5 -0
  27. package/lib/module/NativeBufferedBlob.js.map +1 -0
  28. package/lib/module/api/AGENTS.md +62 -0
  29. package/lib/module/api/download.js +40 -0
  30. package/lib/module/api/download.js.map +1 -0
  31. package/lib/module/api/fileOps.js +70 -0
  32. package/lib/module/api/fileOps.js.map +1 -0
  33. package/lib/module/api/hash.js +13 -0
  34. package/lib/module/api/hash.js.map +1 -0
  35. package/lib/module/api/readFile.js +23 -0
  36. package/lib/module/api/readFile.js.map +1 -0
  37. package/lib/module/api/writeFile.js +18 -0
  38. package/lib/module/api/writeFile.js.map +1 -0
  39. package/lib/module/errors.js +45 -0
  40. package/lib/module/errors.js.map +1 -0
  41. package/lib/module/index.js +25 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/module.js +19 -0
  44. package/lib/module/module.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/paths.js +32 -0
  47. package/lib/module/paths.js.map +1 -0
  48. package/lib/module/types.js +15 -0
  49. package/lib/module/types.js.map +1 -0
  50. package/lib/module/wrappers.js +107 -0
  51. package/lib/module/wrappers.js.map +1 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/NativeBufferedBlob.d.ts +37 -0
  54. package/lib/typescript/src/NativeBufferedBlob.d.ts.map +1 -0
  55. package/lib/typescript/src/api/download.d.ts +13 -0
  56. package/lib/typescript/src/api/download.d.ts.map +1 -0
  57. package/lib/typescript/src/api/fileOps.d.ts +9 -0
  58. package/lib/typescript/src/api/fileOps.d.ts.map +1 -0
  59. package/lib/typescript/src/api/hash.d.ts +3 -0
  60. package/lib/typescript/src/api/hash.d.ts.map +1 -0
  61. package/lib/typescript/src/api/readFile.d.ts +3 -0
  62. package/lib/typescript/src/api/readFile.d.ts.map +1 -0
  63. package/lib/typescript/src/api/writeFile.d.ts +3 -0
  64. package/lib/typescript/src/api/writeFile.d.ts.map +1 -0
  65. package/lib/typescript/src/errors.d.ts +25 -0
  66. package/lib/typescript/src/errors.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +11 -0
  68. package/lib/typescript/src/index.d.ts.map +1 -0
  69. package/lib/typescript/src/module.d.ts +23 -0
  70. package/lib/typescript/src/module.d.ts.map +1 -0
  71. package/lib/typescript/src/paths.d.ts +11 -0
  72. package/lib/typescript/src/paths.d.ts.map +1 -0
  73. package/lib/typescript/src/types.d.ts +37 -0
  74. package/lib/typescript/src/types.d.ts.map +1 -0
  75. package/lib/typescript/src/wrappers.d.ts +14 -0
  76. package/lib/typescript/src/wrappers.d.ts.map +1 -0
  77. package/package.json +114 -0
  78. package/react-native-buffered-blob.podspec +37 -0
  79. package/react-native.config.js +10 -0
  80. package/src/AGENTS.md +70 -0
  81. package/src/NativeBufferedBlob.ts +54 -0
  82. package/src/api/AGENTS.md +62 -0
  83. package/src/api/download.ts +46 -0
  84. package/src/api/fileOps.ts +83 -0
  85. package/src/api/hash.ts +14 -0
  86. package/src/api/readFile.ts +37 -0
  87. package/src/api/writeFile.ts +24 -0
  88. package/src/errors.ts +50 -0
  89. package/src/index.ts +28 -0
  90. package/src/module.ts +48 -0
  91. package/src/paths.ts +35 -0
  92. package/src/types.ts +42 -0
  93. package/src/wrappers.ts +123 -0
@@ -0,0 +1,74 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-02-15 -->
3
+
4
+ # android/
5
+
6
+ Android platform bridge: Kotlin Turbo Module, JNI bridge, streaming operations, and handle registry.
7
+
8
+ ## Purpose
9
+
10
+ Implement BufferedBlob for Android:
11
+ - **BufferedBlobModule.kt**: Turbo Module class extending NativeBufferedBlobSpec; handle factories, FS operations, hashing
12
+ - **BufferedBlobPackage.kt**: TurboReactPackage provider for React Native
13
+ - **HandleRegistry.kt**: Thread-safe handle storage (ConcurrentHashMap), numeric IDs via AtomicInteger
14
+ - **StreamingBridge.kt**: Static JNI-callable methods for read/write/flush/download; OkHttp enqueue() for async downloads
15
+ - **build.gradle**: CMake linking, OkHttp 4.12.0 dependency
16
+
17
+ ## Key Files
18
+
19
+ | File | Description |
20
+ |------|-------------|
21
+ | `src/main/java/com/bufferedblob/BufferedBlobModule.kt` | NativeBufferedBlobSpec impl: install(), openRead, openWrite, createDownload, FS ops, hashFile |
22
+ | `src/main/java/com/bufferedblob/BufferedBlobPackage.kt` | TurboReactPackage, ReactModuleInfoProvider; returns BufferedBlobModule |
23
+ | `src/main/java/com/bufferedblob/HandleRegistry.kt` | Singleton: register(handle), remove(id), get(id); AtomicInteger counter, ConcurrentHashMap storage |
24
+ | `src/main/java/com/bufferedblob/StreamingBridge.kt` | Static JNI-callable methods: readNextChunk, write, flush, close, startDownload, cancelDownload, getReaderInfo, getWriterInfo |
25
+ | `build.gradle` | Dependencies: react-android, OkHttp; CMake pointing to cpp/CMakeLists.txt |
26
+
27
+ ## For AI Agents
28
+
29
+ ### Working In This Directory
30
+
31
+ 1. **NativeBufferedBlobSpec**: Generated from Turbo Module spec (NativeBufferedBlob.ts). BufferedBlobModule extends it; implement all abstract methods.
32
+ 2. **Coroutine scope**: Module has private CoroutineScope(Dispatchers.IO + SupervisorJob()). All async FS ops launch() on this scope. Module.invalidate() cancels scope.
33
+ 3. **Handle lifecycle**: Turbo Module returns numeric handles. JNI and Kotlin static methods access handles via HandleRegistry.get(handleId).
34
+ 4. **JNI bridge**: nativeInstall(jsiPtr, callInvokerHolder) is called from install(). Calls C++ install() which creates AndroidPlatformBridge.
35
+ 5. **OkHttp downloads**: StreamingBridge.startDownload() uses OkHttp client.newCall(request).enqueue(callback). Calls JNI progress callback and resolve/reject.
36
+ 6. **Error codes**: Return -1 for handle errors; throw RuntimeException with "[CODE]" prefix for validation errors.
37
+
38
+ ### Testing Requirements
39
+
40
+ - **Module loading**: Verify install() returns true; verify system.loadLibrary("bufferedblobstreaming") succeeds
41
+ - **Handle factories**: Test openRead/openWrite/createDownload with valid/invalid paths, buffer sizes
42
+ - **File operations**: Test exists, stat, mkdir, ls, cp, mv, unlink on temp files via scope.launch
43
+ - **Hashing**: Verify SHA256/MD5 via MessageDigest.getInstance()
44
+ - **Streaming**: Test readNextChunk, write, flush, close; verify file IO
45
+ - **Download**: Test with HTTP server, verify OkHttp progress callback, verify file written
46
+ - **Cleanup**: Verify module.invalidate() cancels scope and clears handles
47
+ - **Thread safety**: Test concurrent handle access via ConcurrentHashMap
48
+
49
+ ### Common Patterns
50
+
51
+ 1. **Coroutine file op**: `scope.launch { try { /* do work */ promise.resolve(result) } catch (e: Exception) { promise.reject("ERR_FS", e.message) } }`
52
+ 2. **Create reader**: `openRead(path, bufferSize)` → validate buffer size → FileInputStream(file) → ReaderHandle → register → return handleId
53
+ 3. **Stream read**: JNI readNextChunk(handleId) → get handle → read chunk → call progress callback → call resolve callback
54
+ 4. **Download**: `createDownload(url, destPath)` → DownloaderHandle(url, destPath) → register → StreamingBridge.startDownload() → OkHttp enqueue → callback chain
55
+ 5. **Error codes**: Buffer size validation throws with "[INVALID_ARGUMENT]"; file not found with "[FILE_NOT_FOUND]"
56
+
57
+ ## Dependencies
58
+
59
+ ### Internal
60
+ - `HandleRegistry.kt` — Singleton handle storage
61
+ - `StreamingBridge.kt` — JNI static methods (called from C++)
62
+
63
+ ### External
64
+ - **React Native** >= 0.76.0
65
+ - Codegen: NativeBufferedBlobSpec (generated from src/NativeBufferedBlob.ts)
66
+ - TurboModuleRegistry, Promise, ReactMethod, ReadableMap
67
+ - **Android SDK**
68
+ - `java.io.File, FileInputStream, FileOutputStream` — File IO
69
+ - `java.security.MessageDigest` — SHA256, MD5 hashing
70
+ - `android.os.Environment` — DIRECTORY_DOWNLOADS
71
+ - **OkHttp** 4.12.0 — HTTP downloads
72
+ - **Kotlin Coroutines** — Dispatchers.IO, CoroutineScope, SupervisorJob
73
+
74
+ <!-- MANUAL: Document OkHttp configuration (timeouts, certificates), Coroutine scope lifecycle on module invalidate, JNI callback performance -->
@@ -0,0 +1,34 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ }
6
+
7
+ apply plugin: "com.android.library"
8
+ apply plugin: "kotlin-android"
9
+ apply plugin: "com.facebook.react"
10
+
11
+ android {
12
+ namespace "com.bufferedblob"
13
+ compileSdk safeExtGet("compileSdkVersion", 35)
14
+
15
+ defaultConfig {
16
+ minSdk safeExtGet("minSdkVersion", 24)
17
+ }
18
+
19
+ buildFeatures {
20
+ buildConfig true
21
+ prefab true
22
+ }
23
+
24
+ sourceSets {
25
+ main {
26
+ java.srcDirs += ['src/main/java']
27
+ }
28
+ }
29
+ }
30
+
31
+ dependencies {
32
+ implementation "com.facebook.react:react-android"
33
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
34
+ }
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.bufferedblob">
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ </manifest>
@@ -0,0 +1,274 @@
1
+ package com.bufferedblob
2
+
3
+ import android.os.Environment
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.WritableNativeArray
9
+ import com.facebook.react.bridge.WritableNativeMap
10
+ import java.io.File
11
+ import java.io.FileInputStream
12
+ import java.io.FileOutputStream
13
+ import java.security.MessageDigest
14
+ import kotlinx.coroutines.CoroutineScope
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.SupervisorJob
17
+ import kotlinx.coroutines.cancel
18
+ import kotlinx.coroutines.launch
19
+
20
+ class BufferedBlobModule(reactContext: ReactApplicationContext)
21
+ : NativeBufferedBlobSpec(reactContext) {
22
+
23
+ companion object {
24
+ const val NAME = "BufferedBlob"
25
+ private const val MIN_BUFFER_SIZE = 4096
26
+ private const val MAX_BUFFER_SIZE = 4194304 // 4MB
27
+ private const val HASH_CHUNK_SIZE = 8192
28
+ }
29
+
30
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
31
+
32
+ override fun getName(): String = NAME
33
+
34
+ override fun install(): Boolean {
35
+ return try {
36
+ val jsContext = reactApplicationContext.javaScriptContextHolder?.get() ?: return false
37
+ if (jsContext == 0L) return false
38
+ val callInvokerHolder = reactApplicationContext.jsCallInvokerHolder ?: return false
39
+ nativeInstall(jsContext, callInvokerHolder)
40
+ true
41
+ } catch (e: Exception) {
42
+ false
43
+ }
44
+ }
45
+
46
+ private external fun nativeInstall(jsiPtr: Long, callInvokerHolder: Any)
47
+
48
+ init {
49
+ System.loadLibrary("bufferedblobstreaming")
50
+ }
51
+
52
+ @ReactMethod(isBlockingSynchronousMethod = true)
53
+ override fun openRead(path: String, bufferSize: Double): Double {
54
+ val size = bufferSize.toInt()
55
+ if (size < MIN_BUFFER_SIZE || size > MAX_BUFFER_SIZE) {
56
+ throw RuntimeException("[INVALID_ARGUMENT] Buffer size must be $MIN_BUFFER_SIZE-$MAX_BUFFER_SIZE: $size")
57
+ }
58
+ val file = File(path)
59
+ if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
60
+ if (!file.isFile) throw RuntimeException("[INVALID_ARGUMENT] Path is not a file: $path")
61
+ val stream = try {
62
+ FileInputStream(file)
63
+ } catch (e: java.io.FileNotFoundException) {
64
+ throw RuntimeException("[FILE_NOT_FOUND] File was removed during open: $path")
65
+ }
66
+ val reader = ReaderHandle(stream, size, file.length())
67
+ return HandleRegistry.register(reader).toDouble()
68
+ }
69
+
70
+ @ReactMethod(isBlockingSynchronousMethod = true)
71
+ override fun openWrite(path: String, append: Boolean): Double {
72
+ val file = File(path)
73
+ file.parentFile?.mkdirs()
74
+ val stream = FileOutputStream(file, append)
75
+ val writer = WriterHandle(stream)
76
+ return HandleRegistry.register(writer).toDouble()
77
+ }
78
+
79
+ @ReactMethod(isBlockingSynchronousMethod = true)
80
+ override fun createDownload(url: String, destPath: String, headers: ReadableMap): Double {
81
+ val headerMap = mutableMapOf<String, String>()
82
+ val iter = headers.keySetIterator()
83
+ while (iter.hasNextKey()) {
84
+ val key = iter.nextKey()
85
+ headerMap[key] = headers.getString(key) ?: ""
86
+ }
87
+ val handle = DownloaderHandle(url, destPath, headerMap)
88
+ return HandleRegistry.register(handle).toDouble()
89
+ }
90
+
91
+ @ReactMethod(isBlockingSynchronousMethod = true)
92
+ override fun closeHandle(handleId: Double) {
93
+ HandleRegistry.remove(handleId.toInt())
94
+ }
95
+
96
+ override fun getTypedExportedConstants(): Map<String, Any> {
97
+ return mapOf(
98
+ "documentDir" to reactApplicationContext.filesDir.absolutePath,
99
+ "cacheDir" to reactApplicationContext.cacheDir.absolutePath,
100
+ "tempDir" to (System.getProperty("java.io.tmpdir") ?: reactApplicationContext.cacheDir.absolutePath),
101
+ "downloadDir" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
102
+ )
103
+ }
104
+
105
+ override fun exists(path: String, promise: Promise) {
106
+ scope.launch {
107
+ try {
108
+ promise.resolve(File(path).exists())
109
+ } catch (e: Exception) {
110
+ promise.reject("ERR_FS", e.message, e)
111
+ }
112
+ }
113
+ }
114
+
115
+ override fun stat(path: String, promise: Promise) {
116
+ scope.launch {
117
+ try {
118
+ val file = File(path)
119
+ if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
120
+ val type = when {
121
+ file.isFile -> "file"
122
+ file.isDirectory -> "directory"
123
+ else -> "unknown"
124
+ }
125
+ val map = WritableNativeMap().apply {
126
+ putString("path", file.absolutePath)
127
+ putString("name", file.name)
128
+ putDouble("size", (if (file.isFile) file.length() else 0L).toDouble())
129
+ putString("type", type)
130
+ putDouble("lastModified", file.lastModified().toDouble())
131
+ }
132
+ promise.resolve(map)
133
+ } catch (e: Exception) {
134
+ promise.reject("ERR_FS", e.message, e)
135
+ }
136
+ }
137
+ }
138
+
139
+ override fun unlink(path: String, promise: Promise) {
140
+ scope.launch {
141
+ try {
142
+ val file = File(path)
143
+ if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
144
+ if (file.isDirectory) {
145
+ if (!file.deleteRecursively()) throw RuntimeException("[IO_ERROR] Failed to delete directory: $path")
146
+ } else {
147
+ if (!file.delete()) throw RuntimeException("[IO_ERROR] Failed to delete: $path")
148
+ }
149
+ promise.resolve(null)
150
+ } catch (e: Exception) {
151
+ promise.reject("ERR_FS", e.message, e)
152
+ }
153
+ }
154
+ }
155
+
156
+ override fun mkdir(path: String, promise: Promise) {
157
+ scope.launch {
158
+ try {
159
+ val file = File(path)
160
+ if (file.exists()) {
161
+ if (!file.isDirectory) {
162
+ throw RuntimeException("[INVALID_ARGUMENT] Path exists and is not a directory: $path")
163
+ }
164
+ promise.resolve(null)
165
+ return@launch
166
+ }
167
+ if (!file.mkdirs()) throw RuntimeException("[IO_ERROR] Failed to create directory: $path")
168
+ promise.resolve(null)
169
+ } catch (e: Exception) {
170
+ promise.reject("ERR_FS", e.message, e)
171
+ }
172
+ }
173
+ }
174
+
175
+ override fun ls(path: String, promise: Promise) {
176
+ scope.launch {
177
+ try {
178
+ val dir = File(path)
179
+ if (!dir.exists()) throw RuntimeException("[FILE_NOT_FOUND] Directory does not exist: $path")
180
+ if (!dir.isDirectory) throw RuntimeException("[NOT_A_DIRECTORY] Path is not a directory: $path")
181
+ val arr = WritableNativeArray()
182
+ (dir.listFiles() ?: emptyArray()).forEach { file ->
183
+ val type = when {
184
+ file.isFile -> "file"
185
+ file.isDirectory -> "directory"
186
+ else -> "unknown"
187
+ }
188
+ arr.pushMap(WritableNativeMap().apply {
189
+ putString("path", file.absolutePath)
190
+ putString("name", file.name)
191
+ putDouble("size", (if (file.isFile) file.length() else 0L).toDouble())
192
+ putString("type", type)
193
+ putDouble("lastModified", file.lastModified().toDouble())
194
+ })
195
+ }
196
+ promise.resolve(arr)
197
+ } catch (e: Exception) {
198
+ promise.reject("ERR_FS", e.message, e)
199
+ }
200
+ }
201
+ }
202
+
203
+ override fun cp(srcPath: String, destPath: String, promise: Promise) {
204
+ scope.launch {
205
+ try {
206
+ val src = File(srcPath)
207
+ if (!src.exists()) throw RuntimeException("[FILE_NOT_FOUND] Source does not exist: $srcPath")
208
+ if (!src.isFile) throw RuntimeException("[INVALID_ARGUMENT] Source is not a file: $srcPath")
209
+ val dest = File(destPath)
210
+ dest.parentFile?.mkdirs()
211
+ src.copyTo(dest, overwrite = true)
212
+ promise.resolve(null)
213
+ } catch (e: Exception) {
214
+ promise.reject("ERR_FS", e.message, e)
215
+ }
216
+ }
217
+ }
218
+
219
+ override fun mv(srcPath: String, destPath: String, promise: Promise) {
220
+ scope.launch {
221
+ try {
222
+ val src = File(srcPath)
223
+ if (!src.exists()) throw RuntimeException("[FILE_NOT_FOUND] Source does not exist: $srcPath")
224
+ val dest = File(destPath)
225
+ dest.parentFile?.mkdirs()
226
+ if (!src.renameTo(dest)) {
227
+ if (src.isFile) {
228
+ src.copyTo(dest, overwrite = true)
229
+ if (!src.delete()) {
230
+ throw RuntimeException("[IO_ERROR] Move partially failed: copied but could not delete source: $srcPath")
231
+ }
232
+ } else {
233
+ throw RuntimeException("[IO_ERROR] Failed to move: $srcPath")
234
+ }
235
+ }
236
+ promise.resolve(null)
237
+ } catch (e: Exception) {
238
+ promise.reject("ERR_FS", e.message, e)
239
+ }
240
+ }
241
+ }
242
+
243
+ override fun hashFile(path: String, algorithm: String, promise: Promise) {
244
+ scope.launch {
245
+ try {
246
+ val file = File(path)
247
+ if (!file.exists()) throw RuntimeException("[FILE_NOT_FOUND] File does not exist: $path")
248
+ if (!file.isFile) throw RuntimeException("[INVALID_ARGUMENT] Path is not a file: $path")
249
+ val algoName = when (algorithm) {
250
+ "sha256" -> "SHA-256"
251
+ "md5" -> "MD5"
252
+ else -> throw RuntimeException("[INVALID_ARGUMENT] Unknown algorithm: $algorithm")
253
+ }
254
+ val digest = MessageDigest.getInstance(algoName)
255
+ FileInputStream(file).use { stream ->
256
+ val buffer = ByteArray(HASH_CHUNK_SIZE)
257
+ var bytesRead: Int
258
+ while (stream.read(buffer).also { bytesRead = it } != -1) {
259
+ digest.update(buffer, 0, bytesRead)
260
+ }
261
+ }
262
+ promise.resolve(digest.digest().joinToString("") { "%02x".format(it) })
263
+ } catch (e: Exception) {
264
+ promise.reject("ERR_FS", e.message, e)
265
+ }
266
+ }
267
+ }
268
+
269
+ override fun invalidate() {
270
+ scope.cancel()
271
+ HandleRegistry.clear()
272
+ super.invalidate()
273
+ }
274
+ }
@@ -0,0 +1,32 @@
1
+ package com.bufferedblob
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class BufferedBlobPackage : TurboReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return if (name == BufferedBlobModule.NAME) {
12
+ BufferedBlobModule(reactContext)
13
+ } else {
14
+ null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19
+ return ReactModuleInfoProvider {
20
+ mapOf(
21
+ BufferedBlobModule.NAME to ReactModuleInfo(
22
+ BufferedBlobModule.NAME,
23
+ BufferedBlobModule::class.java.name,
24
+ false,
25
+ false,
26
+ false,
27
+ true
28
+ )
29
+ )
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,84 @@
1
+ package com.bufferedblob
2
+
3
+ import java.io.Closeable
4
+ import java.io.FileInputStream
5
+ import java.io.FileOutputStream
6
+ import java.util.concurrent.ConcurrentHashMap
7
+ import java.util.concurrent.atomic.AtomicBoolean
8
+ import java.util.concurrent.atomic.AtomicInteger
9
+
10
+ object HandleRegistry {
11
+ private val nextId = AtomicInteger(1)
12
+ private val handles = ConcurrentHashMap<Int, Any>()
13
+
14
+ fun register(obj: Any): Int {
15
+ while (true) {
16
+ val id = nextId.getAndUpdate { current ->
17
+ if (current >= Int.MAX_VALUE) 1 else current + 1
18
+ }
19
+ if (handles.putIfAbsent(id, obj) == null) return id
20
+ }
21
+ }
22
+
23
+ @Suppress("UNCHECKED_CAST")
24
+ fun <T> get(id: Int): T? = handles[id] as? T
25
+
26
+ fun remove(id: Int): Any? {
27
+ val obj = handles.remove(id)
28
+ if (obj is Closeable) {
29
+ try { obj.close() } catch (_: Exception) {}
30
+ }
31
+ return obj
32
+ }
33
+
34
+ fun clear() {
35
+ handles.keys.toList().forEach { remove(it) }
36
+ }
37
+ }
38
+
39
+ data class ReaderHandle(
40
+ val stream: FileInputStream,
41
+ val bufferSize: Int,
42
+ val fileSize: Long,
43
+ @Volatile var bytesRead: Long = 0L,
44
+ @Volatile var isEOF: Boolean = false
45
+ ) : Closeable {
46
+ private val closed = AtomicBoolean(false)
47
+ val isClosed: Boolean get() = closed.get()
48
+ override fun close() {
49
+ if (closed.compareAndSet(false, true)) {
50
+ try { stream.close() } catch (_: Exception) {}
51
+ }
52
+ }
53
+ }
54
+
55
+ data class WriterHandle(
56
+ val stream: FileOutputStream,
57
+ @Volatile var bytesWritten: Long = 0L
58
+ ) : Closeable {
59
+ private val closed = AtomicBoolean(false)
60
+ val isClosed: Boolean get() = closed.get()
61
+ override fun close() {
62
+ if (closed.compareAndSet(false, true)) {
63
+ try { stream.flush(); stream.close() } catch (_: Exception) {}
64
+ }
65
+ }
66
+ }
67
+
68
+ data class DownloaderHandle(
69
+ val url: String,
70
+ val destPath: String,
71
+ val headers: Map<String, String>,
72
+ @Volatile var isCancelled: Boolean = false,
73
+ @Volatile var call: okhttp3.Call? = null,
74
+ @Volatile var bytesDownloaded: Long = 0L,
75
+ @Volatile var totalBytes: Long = -1L
76
+ ) : Closeable {
77
+ fun cancel() {
78
+ isCancelled = true
79
+ call?.cancel()
80
+ }
81
+ override fun close() {
82
+ cancel()
83
+ }
84
+ }