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,344 @@
1
+ #include "BufferedBlobStreamingHostObject.h"
2
+ #include <ReactCommon/TurboModuleUtils.h>
3
+ #include <cmath>
4
+ #include <string>
5
+ #include <utility>
6
+
7
+ namespace bufferedblob {
8
+
9
+ static int safeHandleId(const facebook::jsi::Value& val) {
10
+ double d = val.asNumber();
11
+ if (std::isnan(d) || std::isinf(d) || d < 0 || d > 2147483647.0) {
12
+ return -1; // Invalid handle ID, will be not-found in registry
13
+ }
14
+ return static_cast<int>(d);
15
+ }
16
+
17
+ using namespace facebook;
18
+
19
+ // --- OwnedMutableBuffer ---
20
+
21
+ OwnedMutableBuffer::OwnedMutableBuffer(std::vector<uint8_t> data)
22
+ : data_(std::move(data)) {}
23
+
24
+ size_t OwnedMutableBuffer::size() const {
25
+ return data_.size();
26
+ }
27
+
28
+ uint8_t* OwnedMutableBuffer::data() {
29
+ return data_.data();
30
+ }
31
+
32
+ // --- BufferedBlobStreamingHostObject ---
33
+
34
+ BufferedBlobStreamingHostObject::BufferedBlobStreamingHostObject(
35
+ jsi::Runtime& runtime,
36
+ std::shared_ptr<react::CallInvoker> callInvoker,
37
+ std::shared_ptr<PlatformBridge> bridge)
38
+ : runtime_(runtime),
39
+ callInvoker_(std::move(callInvoker)),
40
+ bridge_(std::move(bridge)),
41
+ alive_(std::make_shared<std::atomic<bool>>(true)) {}
42
+
43
+ BufferedBlobStreamingHostObject::~BufferedBlobStreamingHostObject() {
44
+ *alive_ = false;
45
+ }
46
+
47
+ std::vector<jsi::PropNameID> BufferedBlobStreamingHostObject::getPropertyNames(
48
+ jsi::Runtime& rt) {
49
+ std::vector<jsi::PropNameID> names;
50
+ names.push_back(jsi::PropNameID::forAscii(rt, "readNextChunk"));
51
+ names.push_back(jsi::PropNameID::forAscii(rt, "write"));
52
+ names.push_back(jsi::PropNameID::forAscii(rt, "flush"));
53
+ names.push_back(jsi::PropNameID::forAscii(rt, "close"));
54
+ names.push_back(jsi::PropNameID::forAscii(rt, "startDownload"));
55
+ names.push_back(jsi::PropNameID::forAscii(rt, "cancelDownload"));
56
+ names.push_back(jsi::PropNameID::forAscii(rt, "getReaderInfo"));
57
+ names.push_back(jsi::PropNameID::forAscii(rt, "getWriterInfo"));
58
+ return names;
59
+ }
60
+
61
+ jsi::Value BufferedBlobStreamingHostObject::get(
62
+ jsi::Runtime& rt,
63
+ const jsi::PropNameID& name) {
64
+ auto propName = name.utf8(rt);
65
+
66
+ // --- readNextChunk(handleId): Promise<ArrayBuffer | null> ---
67
+ if (propName == "readNextChunk") {
68
+ return jsi::Function::createFromHostFunction(
69
+ rt, name, 1,
70
+ [this](jsi::Runtime& rt, const jsi::Value&,
71
+ const jsi::Value* args, size_t count) -> jsi::Value {
72
+ if (count < 1) {
73
+ throw jsi::JSError(rt, "readNextChunk requires 1 argument");
74
+ }
75
+ int handleId = safeHandleId(args[0]);
76
+ auto callInvoker = callInvoker_;
77
+ auto bridge = bridge_;
78
+ auto alive = alive_;
79
+
80
+ return react::createPromiseAsJSIValue(
81
+ rt,
82
+ [handleId, callInvoker, bridge, alive](
83
+ jsi::Runtime& rt2,
84
+ std::shared_ptr<react::Promise> promise) {
85
+ bridge->readNextChunk(
86
+ handleId,
87
+ // onSuccess: data available
88
+ [callInvoker, promise, rtPtr = &rt2, alive](std::vector<uint8_t> data) {
89
+ callInvoker->invokeAsync(
90
+ [promise, rtPtr, data = std::move(data), alive]() mutable {
91
+ if (!*alive) return;
92
+ auto buffer = std::make_shared<OwnedMutableBuffer>(
93
+ std::move(data));
94
+ auto arrayBuffer = jsi::ArrayBuffer(
95
+ *rtPtr, std::move(buffer));
96
+ promise->resolve(std::move(arrayBuffer));
97
+ });
98
+ },
99
+ // onEOF: no more data
100
+ [callInvoker, promise, alive]() {
101
+ callInvoker->invokeAsync([promise, alive]() {
102
+ if (!*alive) return;
103
+ promise->resolve(jsi::Value::null());
104
+ });
105
+ },
106
+ // onError
107
+ [callInvoker, promise, alive](std::string error) {
108
+ callInvoker->invokeAsync(
109
+ [promise, error = std::move(error), alive]() {
110
+ if (!*alive) return;
111
+ promise->reject(error);
112
+ });
113
+ });
114
+ });
115
+ });
116
+ }
117
+
118
+ // --- write(handleId, data): Promise<number> ---
119
+ if (propName == "write") {
120
+ return jsi::Function::createFromHostFunction(
121
+ rt, name, 2,
122
+ [this](jsi::Runtime& rt, const jsi::Value&,
123
+ const jsi::Value* args, size_t count) -> jsi::Value {
124
+ if (count < 2) {
125
+ throw jsi::JSError(rt, "write requires 2 arguments");
126
+ }
127
+ int handleId = safeHandleId(args[0]);
128
+ auto arrayBuffer =
129
+ args[1].asObject(rt).getArrayBuffer(rt);
130
+ auto dataPtr = arrayBuffer.data(rt);
131
+ auto dataSize = arrayBuffer.size(rt);
132
+
133
+ // Copy data since the ArrayBuffer may be GC'd
134
+ std::vector<uint8_t> dataCopy(dataPtr, dataPtr + dataSize);
135
+
136
+ auto callInvoker = callInvoker_;
137
+ auto bridge = bridge_;
138
+ auto alive = alive_;
139
+
140
+ return react::createPromiseAsJSIValue(
141
+ rt,
142
+ [handleId, callInvoker, bridge, alive,
143
+ dataCopy = std::move(dataCopy)](
144
+ jsi::Runtime& rt2,
145
+ std::shared_ptr<react::Promise> promise) {
146
+ bridge->write(
147
+ handleId, std::move(dataCopy),
148
+ [callInvoker, promise, alive](int bytesWritten) {
149
+ callInvoker->invokeAsync(
150
+ [promise, bytesWritten, alive]() {
151
+ if (!*alive) return;
152
+ promise->resolve(jsi::Value(static_cast<double>(bytesWritten)));
153
+ });
154
+ },
155
+ [callInvoker, promise, alive](std::string error) {
156
+ callInvoker->invokeAsync(
157
+ [promise, error = std::move(error), alive]() {
158
+ if (!*alive) return;
159
+ promise->reject(error);
160
+ });
161
+ });
162
+ });
163
+ });
164
+ }
165
+
166
+ // --- flush(handleId): Promise<void> ---
167
+ if (propName == "flush") {
168
+ return jsi::Function::createFromHostFunction(
169
+ rt, name, 1,
170
+ [this](jsi::Runtime& rt, const jsi::Value&,
171
+ const jsi::Value* args, size_t count) -> jsi::Value {
172
+ if (count < 1) {
173
+ throw jsi::JSError(rt, "flush requires 1 argument");
174
+ }
175
+ int handleId = safeHandleId(args[0]);
176
+ auto callInvoker = callInvoker_;
177
+ auto bridge = bridge_;
178
+ auto alive = alive_;
179
+
180
+ return react::createPromiseAsJSIValue(
181
+ rt,
182
+ [handleId, callInvoker, bridge, alive](
183
+ jsi::Runtime& rt2,
184
+ std::shared_ptr<react::Promise> promise) {
185
+ bridge->flush(
186
+ handleId,
187
+ [callInvoker, promise, alive]() {
188
+ callInvoker->invokeAsync([promise, alive]() {
189
+ if (!*alive) return;
190
+ promise->resolve(jsi::Value::undefined());
191
+ });
192
+ },
193
+ [callInvoker, promise, alive](std::string error) {
194
+ callInvoker->invokeAsync(
195
+ [promise, error = std::move(error), alive]() {
196
+ if (!*alive) return;
197
+ promise->reject(error);
198
+ });
199
+ });
200
+ });
201
+ });
202
+ }
203
+
204
+ // --- close(handleId): void (synchronous) ---
205
+ if (propName == "close") {
206
+ return jsi::Function::createFromHostFunction(
207
+ rt, name, 1,
208
+ [this](jsi::Runtime& rt, const jsi::Value&,
209
+ const jsi::Value* args, size_t count) -> jsi::Value {
210
+ if (count < 1) {
211
+ throw jsi::JSError(rt, "close requires 1 argument");
212
+ }
213
+ int handleId = safeHandleId(args[0]);
214
+ bridge_->close(handleId);
215
+ return jsi::Value::undefined();
216
+ });
217
+ }
218
+
219
+ // --- startDownload(handleId, onProgress): Promise<void> ---
220
+ if (propName == "startDownload") {
221
+ return jsi::Function::createFromHostFunction(
222
+ rt, name, 2,
223
+ [this](jsi::Runtime& rt, const jsi::Value&,
224
+ const jsi::Value* args, size_t count) -> jsi::Value {
225
+ if (count < 2) {
226
+ throw jsi::JSError(rt, "startDownload requires 2 arguments");
227
+ }
228
+ int handleId = safeHandleId(args[0]);
229
+ auto progressFn =
230
+ std::make_shared<jsi::Function>(args[1].asObject(rt).asFunction(rt));
231
+ auto callInvoker = callInvoker_;
232
+ auto bridge = bridge_;
233
+ auto alive = alive_;
234
+ // Capture runtime pointer for use in progress callback.
235
+ // Safe because invokeAsync runs on JS thread where runtime is valid.
236
+ jsi::Runtime* rtPtr = &rt;
237
+
238
+ return react::createPromiseAsJSIValue(
239
+ rt,
240
+ [handleId, callInvoker, bridge, progressFn, rtPtr, alive](
241
+ jsi::Runtime& rt2,
242
+ std::shared_ptr<react::Promise> promise) {
243
+ bridge->startDownload(
244
+ handleId,
245
+ // onProgress callback - invoke on JS thread
246
+ [callInvoker, progressFn, rtPtr, alive](
247
+ double bytesDownloaded, double totalBytes,
248
+ double progress) {
249
+ callInvoker->invokeAsync(
250
+ [progressFn, rtPtr, bytesDownloaded, totalBytes,
251
+ progress, alive]() {
252
+ if (!*alive) return;
253
+ progressFn->call(
254
+ *rtPtr,
255
+ jsi::Value(bytesDownloaded),
256
+ jsi::Value(totalBytes),
257
+ jsi::Value(progress));
258
+ });
259
+ },
260
+ // onSuccess
261
+ [callInvoker, promise, alive]() {
262
+ callInvoker->invokeAsync([promise, alive]() {
263
+ if (!*alive) return;
264
+ promise->resolve(jsi::Value::undefined());
265
+ });
266
+ },
267
+ // onError
268
+ [callInvoker, promise, alive](std::string error) {
269
+ callInvoker->invokeAsync(
270
+ [promise, error = std::move(error), alive]() {
271
+ if (!*alive) return;
272
+ promise->reject(error);
273
+ });
274
+ });
275
+ });
276
+ });
277
+ }
278
+
279
+ // --- cancelDownload(handleId): void (synchronous) ---
280
+ if (propName == "cancelDownload") {
281
+ return jsi::Function::createFromHostFunction(
282
+ rt, name, 1,
283
+ [this](jsi::Runtime& rt, const jsi::Value&,
284
+ const jsi::Value* args, size_t count) -> jsi::Value {
285
+ if (count < 1) {
286
+ throw jsi::JSError(rt, "cancelDownload requires 1 argument");
287
+ }
288
+ int handleId = safeHandleId(args[0]);
289
+ bridge_->cancelDownload(handleId);
290
+ return jsi::Value::undefined();
291
+ });
292
+ }
293
+
294
+ // --- getReaderInfo(handleId): { fileSize, bytesRead, isEOF } (synchronous) ---
295
+ if (propName == "getReaderInfo") {
296
+ return jsi::Function::createFromHostFunction(
297
+ rt, name, 1,
298
+ [this](jsi::Runtime& rt, const jsi::Value&,
299
+ const jsi::Value* args, size_t count) -> jsi::Value {
300
+ if (count < 1) {
301
+ throw jsi::JSError(rt, "getReaderInfo requires 1 argument");
302
+ }
303
+ int handleId = safeHandleId(args[0]);
304
+ auto info = bridge_->getReaderInfo(handleId);
305
+ auto obj = jsi::Object(rt);
306
+ obj.setProperty(rt, "fileSize", info.fileSize);
307
+ obj.setProperty(rt, "bytesRead", info.bytesRead);
308
+ obj.setProperty(rt, "isEOF", info.isEOF);
309
+ return obj;
310
+ });
311
+ }
312
+
313
+ // --- getWriterInfo(handleId): { bytesWritten } (synchronous) ---
314
+ if (propName == "getWriterInfo") {
315
+ return jsi::Function::createFromHostFunction(
316
+ rt, name, 1,
317
+ [this](jsi::Runtime& rt, const jsi::Value&,
318
+ const jsi::Value* args, size_t count) -> jsi::Value {
319
+ if (count < 1) {
320
+ throw jsi::JSError(rt, "getWriterInfo requires 1 argument");
321
+ }
322
+ int handleId = safeHandleId(args[0]);
323
+ auto info = bridge_->getWriterInfo(handleId);
324
+ auto obj = jsi::Object(rt);
325
+ obj.setProperty(rt, "bytesWritten", info.bytesWritten);
326
+ return obj;
327
+ });
328
+ }
329
+
330
+ return jsi::Value::undefined();
331
+ }
332
+
333
+ void BufferedBlobStreamingHostObject::install(
334
+ jsi::Runtime& runtime,
335
+ std::shared_ptr<react::CallInvoker> callInvoker,
336
+ std::shared_ptr<PlatformBridge> bridge) {
337
+ auto hostObject = std::make_shared<BufferedBlobStreamingHostObject>(
338
+ runtime, std::move(callInvoker), std::move(bridge));
339
+ auto object = jsi::Object::createFromHostObject(runtime, hostObject);
340
+ runtime.global().setProperty(
341
+ runtime, "__BufferedBlobStreaming", std::move(object));
342
+ }
343
+
344
+ } // namespace bufferedblob
@@ -0,0 +1,118 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+ #include <ReactCommon/CallInvoker.h>
5
+ #include <memory>
6
+ #include <functional>
7
+ #include <vector>
8
+ #include <cstdint>
9
+
10
+ namespace bufferedblob {
11
+
12
+ using namespace facebook;
13
+
14
+ /**
15
+ * Platform bridge abstraction.
16
+ * Each platform (Android/iOS) implements this interface to provide
17
+ * native streaming operations that the JSI HostObject delegates to.
18
+ */
19
+ struct PlatformBridge {
20
+ virtual ~PlatformBridge() = default;
21
+
22
+ // Reader operations
23
+ virtual void readNextChunk(
24
+ int handleId,
25
+ std::function<void(std::vector<uint8_t>)> onSuccess,
26
+ std::function<void()> onEOF,
27
+ std::function<void(std::string)> onError
28
+ ) = 0;
29
+
30
+ // Writer operations
31
+ virtual void write(
32
+ int handleId,
33
+ std::vector<uint8_t> data,
34
+ std::function<void(int)> onSuccess,
35
+ std::function<void(std::string)> onError
36
+ ) = 0;
37
+
38
+ virtual void flush(
39
+ int handleId,
40
+ std::function<void()> onSuccess,
41
+ std::function<void(std::string)> onError
42
+ ) = 0;
43
+
44
+ // Close (sync)
45
+ virtual void close(int handleId) = 0;
46
+
47
+ // Download operations
48
+ virtual void startDownload(
49
+ int handleId,
50
+ std::function<void(double, double, double)> onProgress,
51
+ std::function<void()> onSuccess,
52
+ std::function<void(std::string)> onError
53
+ ) = 0;
54
+
55
+ virtual void cancelDownload(int handleId) = 0;
56
+
57
+ // Reader info (sync)
58
+ struct ReaderInfo {
59
+ double fileSize;
60
+ double bytesRead;
61
+ bool isEOF;
62
+ };
63
+ virtual ReaderInfo getReaderInfo(int handleId) = 0;
64
+
65
+ // Writer info (sync)
66
+ struct WriterInfo {
67
+ double bytesWritten;
68
+ };
69
+ virtual WriterInfo getWriterInfo(int handleId) = 0;
70
+ };
71
+
72
+ /**
73
+ * JSI HostObject that exposes streaming operations to JavaScript.
74
+ * Installed on global.__BufferedBlobStreaming by the install() method.
75
+ */
76
+ class BufferedBlobStreamingHostObject : public jsi::HostObject {
77
+ public:
78
+ BufferedBlobStreamingHostObject(
79
+ jsi::Runtime& runtime,
80
+ std::shared_ptr<react::CallInvoker> callInvoker,
81
+ std::shared_ptr<PlatformBridge> bridge
82
+ );
83
+
84
+ ~BufferedBlobStreamingHostObject() override;
85
+
86
+ jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override;
87
+ std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override;
88
+
89
+ /**
90
+ * Install the HostObject on the given runtime as global.__BufferedBlobStreaming.
91
+ */
92
+ static void install(
93
+ jsi::Runtime& runtime,
94
+ std::shared_ptr<react::CallInvoker> callInvoker,
95
+ std::shared_ptr<PlatformBridge> bridge
96
+ );
97
+
98
+ private:
99
+ jsi::Runtime& runtime_;
100
+ std::shared_ptr<react::CallInvoker> callInvoker_;
101
+ std::shared_ptr<PlatformBridge> bridge_;
102
+ std::shared_ptr<std::atomic<bool>> alive_;
103
+ };
104
+
105
+ /**
106
+ * MutableBuffer subclass that owns its data for zero-copy ArrayBuffer creation.
107
+ */
108
+ class OwnedMutableBuffer : public jsi::MutableBuffer {
109
+ public:
110
+ explicit OwnedMutableBuffer(std::vector<uint8_t> data);
111
+ size_t size() const override;
112
+ uint8_t* data() override;
113
+
114
+ private:
115
+ std::vector<uint8_t> data_;
116
+ };
117
+
118
+ } // namespace bufferedblob
@@ -0,0 +1,49 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(bufferedblobstreaming)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ # When included via app autolinking (REACTNATIVE_MERGED_SO), include the
8
+ # codegen-generated CMakeLists.txt so react_codegen_BufferedBlobSpec is built.
9
+ if(REACTNATIVE_MERGED_SO)
10
+ set(CODEGEN_JNI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../android/build/generated/source/codegen/jni")
11
+ if(EXISTS "${CODEGEN_JNI_DIR}/CMakeLists.txt")
12
+ add_subdirectory(${CODEGEN_JNI_DIR} codegen_build)
13
+ endif()
14
+ endif()
15
+
16
+ add_library(${PROJECT_NAME} SHARED
17
+ BufferedBlobStreamingHostObject.cpp
18
+ AndroidPlatformBridge.cpp
19
+ jni_onload.cpp
20
+ )
21
+
22
+ target_include_directories(${PROJECT_NAME} PRIVATE
23
+ ${CMAKE_CURRENT_SOURCE_DIR}
24
+ )
25
+
26
+ # When included via app autolinking (REACTNATIVE_MERGED_SO), use the merged
27
+ # reactnative/jsi/fbjni aliases set up by ReactNative-application.cmake.
28
+ # When built standalone by the library's build.gradle, use prefab targets.
29
+ if(REACTNATIVE_MERGED_SO)
30
+ target_link_libraries(${PROJECT_NAME}
31
+ jsi
32
+ reactnative
33
+ fbjni
34
+ android
35
+ log
36
+ )
37
+ else()
38
+ find_package(ReactAndroid REQUIRED CONFIG)
39
+ find_package(fbjni REQUIRED CONFIG)
40
+ target_link_libraries(${PROJECT_NAME}
41
+ ReactAndroid::jsi
42
+ ReactAndroid::react_nativemodule_core
43
+ ReactAndroid::turbomodulejsijni
44
+ ReactAndroid::reactnativejni
45
+ fbjni::fbjni
46
+ android
47
+ log
48
+ )
49
+ endif()
@@ -0,0 +1,32 @@
1
+ #include <fbjni/fbjni.h>
2
+ #include <jsi/jsi.h>
3
+ #include <ReactCommon/CallInvokerHolder.h>
4
+ #include "BufferedBlobStreamingHostObject.h"
5
+ #include "AndroidPlatformBridge.h"
6
+
7
+ using namespace facebook;
8
+
9
+ extern "C" JNIEXPORT void JNICALL
10
+ Java_com_bufferedblob_BufferedBlobModule_nativeInstall(
11
+ JNIEnv* env,
12
+ jobject thiz,
13
+ jlong jsiPtr,
14
+ jobject callInvokerHolder) {
15
+ auto& runtime = *reinterpret_cast<jsi::Runtime*>(jsiPtr);
16
+
17
+ auto callInvokerHolderRef =
18
+ jni::make_local(reinterpret_cast<react::CallInvokerHolder::javaobject>(
19
+ callInvokerHolder));
20
+ auto callInvoker = callInvokerHolderRef->cthis()->getCallInvoker();
21
+
22
+ auto bridge = std::make_shared<bufferedblob::AndroidPlatformBridge>(env);
23
+
24
+ bufferedblob::BufferedBlobStreamingHostObject::install(
25
+ runtime, callInvoker, bridge);
26
+ }
27
+
28
+ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
29
+ return jni::initialize(vm, [] {
30
+ // No native methods to register via fbjni - we use raw JNI above
31
+ });
32
+ }
package/ios/AGENTS.md ADDED
@@ -0,0 +1,76 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-02-15 -->
3
+
4
+ # ios/
5
+
6
+ iOS platform bridge: ObjC++ Turbo Module, Swift implementation, JSI wiring, and handle registry.
7
+
8
+ ## Purpose
9
+
10
+ Implement BufferedBlob for iOS:
11
+ - **BufferedBlobModule.mm**: ObjC++ Turbo Module bridge; wires JSI HostObject via install()
12
+ - **BufferedBlobModule.swift**: Swift implementation of handle factories, FS operations, hashing with CommonCrypto
13
+ - **BufferedBlobStreamingBridge.h/mm**: IOSPlatformBridge; async dispatch via dispatch_async, NSURLSession downloads
14
+ - **HandleRegistry.swift**: Thread-safe handle storage (NSLock), maps numeric IDs to reader/writer/downloader handles
15
+ - **HandleTypes.swift**: ReaderHandleIOS, WriterHandleIOS, DownloaderHandleIOS classes
16
+ - **BufferedBlob-Bridging-Header.h**: Swift/ObjC++ interop
17
+
18
+ ## Key Files
19
+
20
+ | File | Description |
21
+ |------|-------------|
22
+ | `BufferedBlobModule.mm` | RCT_EXPORT_MODULE, RCT_EXPORT_METHOD, install() calls installBufferedBlobStreaming() |
23
+ | `BufferedBlobModule.swift` | Handle factories (openRead, openWrite, createDownload), FS ops, hashFile with CommonCrypto |
24
+ | `BufferedBlobStreamingBridge.h` | IOSPlatformBridge class header; inherits from C++ PlatformBridge |
25
+ | `BufferedBlobStreamingBridge.mm` | IOSPlatformBridge implementation: dispatch_async, NSURLSession, InputStream/OutputStream |
26
+ | `HandleRegistry.swift` | Singleton: register(handle), remove(id), get(id); thread-safe with NSLock |
27
+ | `HandleTypes.swift` | ReaderHandleIOS (InputStream, fileSize, bytesRead), WriterHandleIOS (OutputStream, bytesWritten), DownloaderHandleIOS (URLSessionDataTask, URL) |
28
+ | `BufferedBlob-Bridging-Header.h` | Swift bridging header for ObjC++ classes (C++ PlatformBridge, etc.) |
29
+
30
+ ## For AI Agents
31
+
32
+ ### Working In This Directory
33
+
34
+ 1. **ObjC++ bridge pattern**: BufferedBlobModule.mm exports methods via RCT_EXPORT_METHOD/RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD. Delegates to Swift implementation via _swiftModule.
35
+ 2. **Bridged vs. Bridgeless**: install() detects RN 0.74+ bridgeless mode; falls back to RCTBridge.runtime. Both paths supported.
36
+ 3. **Handle lifecycle**: Turbo Module returns numeric handle IDs. Platform bridge stores actual handle objects (reader/writer/downloader). Always close handles.
37
+ 4. **Async dispatch**: File ops and downloads use `DispatchQueue.global(qos: .userInitiated)` to avoid blocking main thread. Promises resolve on main thread.
38
+ 5. **CommonCrypto hashing**: Streams file in 8192-byte chunks; supports SHA256, MD5. No full file load.
39
+ 6. **NSURLSession downloads**: Async, supports progress callback, cancellation. Data task delegates to DownloaderHandleIOS.
40
+
41
+ ### Testing Requirements
42
+
43
+ - **Module loading**: Verify install() succeeds and JSI HostObject is accessible
44
+ - **Handle factories**: Test openRead/openWrite/createDownload with valid/invalid paths
45
+ - **File operations**: Test exists, stat, mkdir, ls, cp, mv, unlink on temp files
46
+ - **Hashing**: Verify SHA256/MD5 output matches known values (use openssl dgst to verify)
47
+ - **Streaming**: Test readNextChunk, write, flush, close; verify file contents
48
+ - **Download**: Test with HTTP server, verify progress callback, verify file written
49
+ - **Cleanup**: Verify handles are released (NSLock does not deadlock, no dangling FileHandles)
50
+
51
+ ### Common Patterns
52
+
53
+ 1. **Create reader**: `openRead(path, bufferSize)` → ReaderHandleIOS(path, bufferSize) → register(handle) → return handleId
54
+ 2. **Stream read**: Bridge readNextChunk(handleId) → get handle → read chunk → convert to ArrayBuffer → resolve Promise
55
+ 3. **Download with progress**: `createDownload(url, destPath)` → DownloaderHandleIOS → URLSession dataTask → progress callback → finalize
56
+ 4. **Error handling**: Catch Foundation errors; wrap in [error localizedDescription]; reject Promise
57
+
58
+ ## Dependencies
59
+
60
+ ### Internal
61
+ - C++ PlatformBridge (abstract interface from cpp/)
62
+ - HandleRegistry, HandleTypes (shared state across ObjC++ and Swift)
63
+
64
+ ### External
65
+ - **React Native** >= 0.76.0
66
+ - `<React/RCTBridgeModule.h>` — RCT_EXPORT_MODULE, RCT_EXPORT_METHOD
67
+ - `<React/RCTBridge+Private.h>` — Bridge.runtime, jsCallInvoker
68
+ - `<ReactCommon/RCTTurboModule.h>` — TurboModule protocol
69
+ - `<jsi/jsi.h>` — JSI runtime (passed to install())
70
+ - **Apple frameworks**
71
+ - `Foundation/FileManager` — File operations
72
+ - `CommonCrypto/CommonDigest.h` — SHA256, MD5 hashing
73
+ - `Foundation/URLSession` — Downloads
74
+ - `Foundation/InputStream, OutputStream` — Streaming
75
+
76
+ <!-- MANUAL: Document CommonCrypto vs. CryptoKit trade-offs, iOS version support (min 12.0+), URLSession configuration -->
@@ -0,0 +1,44 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ @class HandleRegistry;
6
+
7
+ /**
8
+ * Core BufferedBlob module implementation.
9
+ * Provides handle factories for readers/writers/downloaders and
10
+ * common filesystem operations (exists, stat, unlink, mkdir, ls, cp, mv, hash).
11
+ *
12
+ * All async FS operations are dispatched to global concurrent queues.
13
+ * FileManager.default is documented as safe for concurrent use from multiple queues
14
+ * for basic operations (exists, stat, unlink, mkdir, ls, cp, mv).
15
+ */
16
+ @interface BufferedBlobModule : NSObject
17
+
18
+ + (NSString *)moduleName;
19
+ + (BOOL)requiresMainQueueSetup;
20
+
21
+ - (NSDictionary *)constantsToExport;
22
+
23
+ // Handle factories
24
+ - (NSNumber *)openRead:(NSString *)path bufferSize:(double)bufferSize;
25
+ - (NSNumber *)openWrite:(NSString *)path append:(BOOL)append;
26
+ - (NSNumber *)createDownload:(NSString *)url destPath:(NSString *)destPath headers:(NSDictionary *)headers;
27
+ - (void)closeHandle:(double)handleId;
28
+
29
+ // FS operations (async with promise callbacks)
30
+ typedef void (^RCTPromiseResolveBlock)(id _Nullable result);
31
+ typedef void (^RCTPromiseRejectBlock)(NSString * _Nullable code,
32
+ NSString * _Nullable message,
33
+ NSError * _Nullable error);
34
+
35
+ - (void)exists:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
36
+ - (void)stat:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
37
+ - (void)unlink:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
38
+ - (void)mkdir:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
39
+ - (void)ls:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
40
+ - (void)cp:(NSString *)srcPath destPath:(NSString *)destPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
41
+ - (void)mv:(NSString *)srcPath destPath:(NSString *)destPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
42
+ - (void)hashFile:(NSString *)path algorithm:(NSString *)algorithm resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
43
+
44
+ @end