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.
- package/android/AGENTS.md +74 -0
- package/android/build.gradle +34 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobModule.kt +274 -0
- package/android/src/main/java/com/bufferedblob/BufferedBlobPackage.kt +32 -0
- package/android/src/main/java/com/bufferedblob/HandleRegistry.kt +84 -0
- package/android/src/main/java/com/bufferedblob/StreamingBridge.kt +211 -0
- package/cpp/AGENTS.md +71 -0
- package/cpp/AndroidPlatformBridge.cpp +437 -0
- package/cpp/AndroidPlatformBridge.h +79 -0
- package/cpp/BufferedBlobStreamingHostObject.cpp +344 -0
- package/cpp/BufferedBlobStreamingHostObject.h +118 -0
- package/cpp/CMakeLists.txt +49 -0
- package/cpp/jni_onload.cpp +32 -0
- package/ios/AGENTS.md +76 -0
- package/ios/BufferedBlobModule.h +44 -0
- package/ios/BufferedBlobModule.m +433 -0
- package/ios/BufferedBlobModule.mm +192 -0
- package/ios/BufferedBlobStreamingBridge.h +21 -0
- package/ios/BufferedBlobStreamingBridge.mm +442 -0
- package/ios/HandleRegistry.h +29 -0
- package/ios/HandleRegistry.m +67 -0
- package/ios/HandleTypes.h +83 -0
- package/ios/HandleTypes.m +333 -0
- package/lib/module/AGENTS.md +70 -0
- package/lib/module/NativeBufferedBlob.js +5 -0
- package/lib/module/NativeBufferedBlob.js.map +1 -0
- package/lib/module/api/AGENTS.md +62 -0
- package/lib/module/api/download.js +40 -0
- package/lib/module/api/download.js.map +1 -0
- package/lib/module/api/fileOps.js +70 -0
- package/lib/module/api/fileOps.js.map +1 -0
- package/lib/module/api/hash.js +13 -0
- package/lib/module/api/hash.js.map +1 -0
- package/lib/module/api/readFile.js +23 -0
- package/lib/module/api/readFile.js.map +1 -0
- package/lib/module/api/writeFile.js +18 -0
- package/lib/module/api/writeFile.js.map +1 -0
- package/lib/module/errors.js +45 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +25 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/module.js +19 -0
- package/lib/module/module.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/paths.js +32 -0
- package/lib/module/paths.js.map +1 -0
- package/lib/module/types.js +15 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/wrappers.js +107 -0
- package/lib/module/wrappers.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts +37 -0
- package/lib/typescript/src/NativeBufferedBlob.d.ts.map +1 -0
- package/lib/typescript/src/api/download.d.ts +13 -0
- package/lib/typescript/src/api/download.d.ts.map +1 -0
- package/lib/typescript/src/api/fileOps.d.ts +9 -0
- package/lib/typescript/src/api/fileOps.d.ts.map +1 -0
- package/lib/typescript/src/api/hash.d.ts +3 -0
- package/lib/typescript/src/api/hash.d.ts.map +1 -0
- package/lib/typescript/src/api/readFile.d.ts +3 -0
- package/lib/typescript/src/api/readFile.d.ts.map +1 -0
- package/lib/typescript/src/api/writeFile.d.ts +3 -0
- package/lib/typescript/src/api/writeFile.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +25 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/module.d.ts +23 -0
- package/lib/typescript/src/module.d.ts.map +1 -0
- package/lib/typescript/src/paths.d.ts +11 -0
- package/lib/typescript/src/paths.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/wrappers.d.ts +14 -0
- package/lib/typescript/src/wrappers.d.ts.map +1 -0
- package/package.json +114 -0
- package/react-native-buffered-blob.podspec +37 -0
- package/react-native.config.js +10 -0
- package/src/AGENTS.md +70 -0
- package/src/NativeBufferedBlob.ts +54 -0
- package/src/api/AGENTS.md +62 -0
- package/src/api/download.ts +46 -0
- package/src/api/fileOps.ts +83 -0
- package/src/api/hash.ts +14 -0
- package/src/api/readFile.ts +37 -0
- package/src/api/writeFile.ts +24 -0
- package/src/errors.ts +50 -0
- package/src/index.ts +28 -0
- package/src/module.ts +48 -0
- package/src/paths.ts +35 -0
- package/src/types.ts +42 -0
- 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
|