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
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-buffered-blob",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "No more OOM, just use buffered-blob",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"react-native-buffered-blob.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!**/__tests__",
|
|
24
|
+
"!**/__fixtures__",
|
|
25
|
+
"!**/__mocks__",
|
|
26
|
+
"!**/.*"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepare": "bob build",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "jest",
|
|
32
|
+
"test:watch": "jest --watch",
|
|
33
|
+
"test:coverage": "jest --coverage",
|
|
34
|
+
"release": "release-it"
|
|
35
|
+
},
|
|
36
|
+
"codegenConfig": {
|
|
37
|
+
"name": "BufferedBlobSpec",
|
|
38
|
+
"type": "modules",
|
|
39
|
+
"jsSrcsDir": "src",
|
|
40
|
+
"android": {
|
|
41
|
+
"javaPackageName": "com.bufferedblob"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react-native",
|
|
46
|
+
"ios",
|
|
47
|
+
"android",
|
|
48
|
+
"blob",
|
|
49
|
+
"file"
|
|
50
|
+
],
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/l2hyunwoo/react-native-nitro-blob.git",
|
|
54
|
+
"directory": "packages/react-native-buffered-blob"
|
|
55
|
+
},
|
|
56
|
+
"author": "HyunWoo Lee <l2hyunwoo@gmail.com> (https://github.com/l2hyunwoo)",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": "*",
|
|
60
|
+
"react-native": ">=0.76.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"react": "19.2.0",
|
|
64
|
+
"react-native": "0.83.0",
|
|
65
|
+
"react-native-builder-bob": "^0.40.18",
|
|
66
|
+
"typescript": "^5.9.2"
|
|
67
|
+
},
|
|
68
|
+
"react-native-builder-bob": {
|
|
69
|
+
"source": "src",
|
|
70
|
+
"output": "lib",
|
|
71
|
+
"targets": [
|
|
72
|
+
[
|
|
73
|
+
"module",
|
|
74
|
+
{
|
|
75
|
+
"esm": true
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"typescript",
|
|
80
|
+
{
|
|
81
|
+
"project": "tsconfig.build.json"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"release-it": {
|
|
87
|
+
"git": {
|
|
88
|
+
"commitMessage": "chore: release react-native-buffered-blob ${version}",
|
|
89
|
+
"tagName": "react-native-buffered-blob@${version}",
|
|
90
|
+
"requireCleanWorkingDir": true
|
|
91
|
+
},
|
|
92
|
+
"npm": {
|
|
93
|
+
"publish": true,
|
|
94
|
+
"publishPath": "."
|
|
95
|
+
},
|
|
96
|
+
"github": {
|
|
97
|
+
"release": true,
|
|
98
|
+
"releaseName": "react-native-buffered-blob@${version}"
|
|
99
|
+
},
|
|
100
|
+
"hooks": {
|
|
101
|
+
"before:init": [
|
|
102
|
+
"yarn prepare"
|
|
103
|
+
],
|
|
104
|
+
"after:bump": "yarn prepare"
|
|
105
|
+
},
|
|
106
|
+
"plugins": {
|
|
107
|
+
"@release-it/conventional-changelog": {
|
|
108
|
+
"preset": {
|
|
109
|
+
"name": "angular"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
3
|
+
|
|
4
|
+
Pod::Spec.new do |s|
|
|
5
|
+
s.name = "react-native-buffered-blob"
|
|
6
|
+
s.version = package["version"]
|
|
7
|
+
s.summary = package["description"]
|
|
8
|
+
s.homepage = package["repository"]["url"]
|
|
9
|
+
s.license = package["license"]
|
|
10
|
+
s.authors = package["author"]
|
|
11
|
+
s.platforms = { :ios => '15.1' }
|
|
12
|
+
s.source = { :git => package["repository"]["url"], :tag => s.version }
|
|
13
|
+
s.source_files = [
|
|
14
|
+
"ios/**/*.{h,m,mm}",
|
|
15
|
+
"cpp/**/*.{h,hpp,cpp}",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Exclude Android-only files from the iOS build.
|
|
19
|
+
s.exclude_files = [
|
|
20
|
+
"cpp/jni_onload.cpp",
|
|
21
|
+
"cpp/AndroidPlatformBridge.{h,cpp}",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Keep C++ headers out of the modulemap so the Clang module builder
|
|
25
|
+
# does not try to parse them in C mode (which cannot resolve C++
|
|
26
|
+
# standard library includes like <functional>).
|
|
27
|
+
s.private_header_files = [
|
|
28
|
+
"cpp/**/*.{h,hpp}",
|
|
29
|
+
"ios/BufferedBlobStreamingBridge.h",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
s.pod_target_xcconfig = {
|
|
33
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
install_modules_dependencies(s)
|
|
37
|
+
end
|
package/src/AGENTS.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-15 -->
|
|
3
|
+
|
|
4
|
+
# src/
|
|
5
|
+
|
|
6
|
+
TypeScript API layer: Turbo Module spec, native module interface, streaming proxy, and high-level file operations.
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Provides the JavaScript interface to the native BufferedBlob module:
|
|
11
|
+
- **Turbo Module specification** (NativeBufferedBlob.ts) — codegen input for handle factories and FS ops
|
|
12
|
+
- **Streaming proxy** (module.ts) — JSI HostObject accessor, install() wiring
|
|
13
|
+
- **Type wrappers** (types.ts) — BlobReader/BlobWriter interfaces with property proxying
|
|
14
|
+
- **Error handling** (errors.ts) — ErrorCode enum and wrapError() utility
|
|
15
|
+
- **Paths** (paths.ts) — Dirs constants and path utilities
|
|
16
|
+
|
|
17
|
+
## Key Files
|
|
18
|
+
|
|
19
|
+
| File | Description |
|
|
20
|
+
|------|-------------|
|
|
21
|
+
| `NativeBufferedBlob.ts` | Turbo Module spec: install(), handle factories, FS ops, hashFile. Codegen input. |
|
|
22
|
+
| `module.ts` | NativeModule (TurboModuleRegistry), StreamingProxy interface, getStreamingProxy() |
|
|
23
|
+
| `types.ts` | HashAlgorithm, FileType enums; FileInfo, BlobReader, BlobWriter interfaces; wrapReader/wrapWriter |
|
|
24
|
+
| `errors.ts` | BlobError class, ErrorCode enum, wrapError() helper |
|
|
25
|
+
| `paths.ts` | Dirs constants (documentDir, cacheDir, tempDir, downloadDir); join(), dirname(), basename(), extname() |
|
|
26
|
+
| `index.ts` | Barrel exports for public API |
|
|
27
|
+
|
|
28
|
+
## Subdirectories
|
|
29
|
+
|
|
30
|
+
| Directory | Purpose |
|
|
31
|
+
|-----------|---------|
|
|
32
|
+
| `api/` | High-level file operations, download, hashing, streaming wrappers |
|
|
33
|
+
|
|
34
|
+
## For AI Agents
|
|
35
|
+
|
|
36
|
+
### Working In This Directory
|
|
37
|
+
|
|
38
|
+
1. **Turbo Module spec**: Changes to `NativeBufferedBlob.ts` trigger codegen. After editing, run `yarn prepare` to regenerate native module headers.
|
|
39
|
+
2. **Streaming proxy access**: `getStreamingProxy()` returns the JSI HostObject installed by native code. Always call `NativeModule.install()` first (done in module.ts).
|
|
40
|
+
3. **Handle pattern**: Turbo Module returns numeric handles. Pass to streaming proxy methods. Always close handles to release native resources.
|
|
41
|
+
4. **Type safety**: Use strict TypeScript. BlobReader/BlobWriter are interface wrappers; do NOT spread HostObject properties (getters would be lost).
|
|
42
|
+
5. **Error propagation**: Catch errors from native calls; use `wrapError()` to normalize error messages.
|
|
43
|
+
|
|
44
|
+
### Testing Requirements
|
|
45
|
+
|
|
46
|
+
- **Type checking**: Verify no TypeScript errors in strict mode
|
|
47
|
+
- **Module loading**: Verify `install()` succeeds and JSI HostObject is wired
|
|
48
|
+
- **Handle cleanup**: Verify handles are closed; no dangling native resources
|
|
49
|
+
- **Error cases**: Test file not found, invalid paths, invalid buffer sizes, cancelled operations
|
|
50
|
+
|
|
51
|
+
### Common Patterns
|
|
52
|
+
|
|
53
|
+
1. **Create reader**: `const handle = NativeModule.openRead(path, bufferSize)` -> `const reader = wrapReader(handle, proxy)` -> `reader.readNextChunk()` -> `reader.close()`
|
|
54
|
+
2. **Create writer**: `const handle = NativeModule.openWrite(path, append)` -> `const writer = wrapWriter(handle, proxy)` -> `writer.write(data)` -> `writer.flush()` -> `writer.close()`
|
|
55
|
+
3. **File ops**: `exists()`, `stat()`, `mkdir()`, `ls()`, `cp()`, `mv()`, `unlink()` all return Promises
|
|
56
|
+
4. **Hash file**: `hashFile(path, 'sha256' | 'md5')` streams file without loading into memory
|
|
57
|
+
5. **Download with progress**: Use `download({ url, destPath, onProgress })` API; streaming proxy handles details
|
|
58
|
+
|
|
59
|
+
## Dependencies
|
|
60
|
+
|
|
61
|
+
### Internal
|
|
62
|
+
- Turbo Module spec generated from `NativeBufferedBlob.ts` at build time
|
|
63
|
+
- `module.ts` calls `install()` on first import (side effect)
|
|
64
|
+
- `api/` layer depends on NativeModule and getStreamingProxy()
|
|
65
|
+
|
|
66
|
+
### External
|
|
67
|
+
- **React Native** >= 0.76.0 (TurboModuleRegistry, JSI)
|
|
68
|
+
- **TypeScript** ^5.9.2 (type checking, codegen)
|
|
69
|
+
|
|
70
|
+
<!-- MANUAL: Document API changes, Turbo Module version bumps, deprecations -->
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
// Called once to install JSI HostObject on global.__BufferedBlobStreaming
|
|
6
|
+
install(): boolean;
|
|
7
|
+
|
|
8
|
+
// --- Stream Handle Factories ---
|
|
9
|
+
// Return numeric handle IDs that reference native objects in an internal registry.
|
|
10
|
+
// The handle IDs are passed to the JSI HostObject for streaming operations.
|
|
11
|
+
openRead(path: string, bufferSize: number): number;
|
|
12
|
+
openWrite(path: string, append: boolean): number;
|
|
13
|
+
createDownload(url: string, destPath: string, headers: Object): number;
|
|
14
|
+
|
|
15
|
+
// --- Handle Cleanup ---
|
|
16
|
+
// Close a handle by ID. Safe to call multiple times.
|
|
17
|
+
closeHandle(handleId: number): void;
|
|
18
|
+
|
|
19
|
+
// --- File System Operations ---
|
|
20
|
+
exists(path: string): Promise<boolean>;
|
|
21
|
+
stat(path: string): Promise<{
|
|
22
|
+
path: string;
|
|
23
|
+
name: string;
|
|
24
|
+
size: number;
|
|
25
|
+
type: string;
|
|
26
|
+
lastModified: number;
|
|
27
|
+
}>;
|
|
28
|
+
unlink(path: string): Promise<void>;
|
|
29
|
+
mkdir(path: string): Promise<void>;
|
|
30
|
+
ls(path: string): Promise<
|
|
31
|
+
Array<{
|
|
32
|
+
path: string;
|
|
33
|
+
name: string;
|
|
34
|
+
size: number;
|
|
35
|
+
type: string;
|
|
36
|
+
lastModified: number;
|
|
37
|
+
}>
|
|
38
|
+
>;
|
|
39
|
+
cp(srcPath: string, destPath: string): Promise<void>;
|
|
40
|
+
mv(srcPath: string, destPath: string): Promise<void>;
|
|
41
|
+
|
|
42
|
+
// --- Hashing ---
|
|
43
|
+
hashFile(path: string, algorithm: string): Promise<string>;
|
|
44
|
+
|
|
45
|
+
// --- Constants ---
|
|
46
|
+
getConstants(): {
|
|
47
|
+
documentDir: string;
|
|
48
|
+
cacheDir: string;
|
|
49
|
+
tempDir: string;
|
|
50
|
+
downloadDir: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('BufferedBlob');
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-02-15 -->
|
|
3
|
+
|
|
4
|
+
# src/api/
|
|
5
|
+
|
|
6
|
+
High-level convenience APIs for file operations, streaming, hashing, and downloads.
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Provide idiomatic TypeScript wrappers around Turbo Module calls and JSI HostObject streaming:
|
|
11
|
+
- **File operations**: exists, stat, mkdir, ls, cp, mv, unlink
|
|
12
|
+
- **Streaming**: readFile (deprecated), writeFile (deprecated)
|
|
13
|
+
- **Hashing**: hashFile with streaming (SHA256, MD5)
|
|
14
|
+
- **Downloads**: download with progress callback
|
|
15
|
+
|
|
16
|
+
## Key Files
|
|
17
|
+
|
|
18
|
+
| File | Description |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| `fileOps.ts` | FS operations: exists, stat, mkdir, ls, cp, mv, unlink. All return Promises. |
|
|
21
|
+
| `hash.ts` | hashFile(path, algorithm) → Promise<string>. Streams file; supports 'sha256', 'md5'. |
|
|
22
|
+
| `download.ts` | download(options) → Promise<void>. Returns DownloadOptions interface, handles progress callback. |
|
|
23
|
+
| `readFile.ts` | DEPRECATED: createReader(path, bufferSize) → BlobReader. Use openRead() + wrapReader() directly. |
|
|
24
|
+
| `writeFile.ts` | DEPRECATED: createWriter(path, append) → BlobWriter. Use openWrite() + wrapWriter() directly. |
|
|
25
|
+
|
|
26
|
+
## For AI Agents
|
|
27
|
+
|
|
28
|
+
### Working In This Directory
|
|
29
|
+
|
|
30
|
+
1. **File operations**: All async. Return Promises that resolve/reject with platform-specific error codes.
|
|
31
|
+
2. **Streaming shortcuts**: readFile/writeFile are deprecated convenience wrappers. New code should use `openRead()` + `wrapReader()` directly for explicit handle management.
|
|
32
|
+
3. **Download pattern**: `download()` creates handle, calls JSI streaming startDownload, manages progress callback, cleans up handle in finally block.
|
|
33
|
+
4. **Error handling**: Use `wrapError()` from parent module to normalize native errors.
|
|
34
|
+
5. **Handle lifecycle**: Always close streaming handles (readFile/writeFile do this implicitly; direct handle usage requires explicit close).
|
|
35
|
+
|
|
36
|
+
### Testing Requirements
|
|
37
|
+
|
|
38
|
+
- **File ops**: Create temp files, test exists/stat/mkdir/ls/cp/mv/unlink
|
|
39
|
+
- **Hashing**: Verify hash output against known digests (sha256, md5)
|
|
40
|
+
- **Download**: Test with HTTP server, verify progress callback fires, verify file written
|
|
41
|
+
- **Error cases**: Test file not found, permission denied, invalid paths, cancelled downloads
|
|
42
|
+
|
|
43
|
+
### Common Patterns
|
|
44
|
+
|
|
45
|
+
1. **Check file exists**: `const exists = await exists(path)`
|
|
46
|
+
2. **Get file info**: `const info = await stat(path)` → {path, name, size, type, lastModified}
|
|
47
|
+
3. **List directory**: `const files = await ls(dirPath)` → FileInfo[]
|
|
48
|
+
4. **Copy file**: `await cp(srcPath, destPath)`
|
|
49
|
+
5. **Hash file**: `const hash = await hashFile(path, 'sha256')`
|
|
50
|
+
6. **Download with progress**: `await download({ url, destPath, headers: {}, onProgress: (p) => console.log(p.progress) })`
|
|
51
|
+
|
|
52
|
+
## Dependencies
|
|
53
|
+
|
|
54
|
+
### Internal
|
|
55
|
+
- `../module.ts` — NativeModule, getStreamingProxy()
|
|
56
|
+
- `../types.ts` — BlobReader, BlobWriter, FileInfo, DownloadProgress, HashAlgorithm
|
|
57
|
+
- `../errors.ts` — wrapError()
|
|
58
|
+
|
|
59
|
+
### External
|
|
60
|
+
- **React Native** >= 0.76.0 (TurboModuleRegistry for NativeModule calls)
|
|
61
|
+
|
|
62
|
+
<!-- MANUAL: Document deprecated APIs, migration guides for readFile/writeFile users -->
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NativeModule, getStreamingProxy } from '../module';
|
|
2
|
+
import { wrapError } from '../errors';
|
|
3
|
+
import type { DownloadProgress } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface DownloadOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
destPath: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
onProgress?: (progress: DownloadProgress) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DownloadHandle {
|
|
13
|
+
promise: Promise<void>;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function download(options: DownloadOptions): DownloadHandle {
|
|
18
|
+
const { url, destPath, headers = {}, onProgress } = options;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const handleId = NativeModule.createDownload(url, destPath, headers);
|
|
22
|
+
const streaming = getStreamingProxy();
|
|
23
|
+
|
|
24
|
+
const progressCallback = onProgress
|
|
25
|
+
? (bytesDownloaded: number, totalBytes: number, progress: number) => {
|
|
26
|
+
onProgress({ bytesDownloaded, totalBytes, progress });
|
|
27
|
+
}
|
|
28
|
+
: (_b: number, _t: number, _p: number) => {};
|
|
29
|
+
|
|
30
|
+
const promise = (async () => {
|
|
31
|
+
try {
|
|
32
|
+
await streaming.startDownload(handleId, progressCallback);
|
|
33
|
+
} finally {
|
|
34
|
+
NativeModule.closeHandle(handleId);
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
const cancel = () => {
|
|
39
|
+
streaming.cancelDownload(handleId);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return { promise, cancel };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw wrapError(e, destPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NativeModule } from '../module';
|
|
2
|
+
import { wrapError } from '../errors';
|
|
3
|
+
import type { FileInfo } from '../types';
|
|
4
|
+
import { FileType } from '../types';
|
|
5
|
+
|
|
6
|
+
function toFileType(raw: string): FileType {
|
|
7
|
+
const values: string[] = Object.values(FileType);
|
|
8
|
+
return values.includes(raw) ? (raw as FileType) : FileType.UNKNOWN;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mapFileInfo(raw: {
|
|
12
|
+
path: string;
|
|
13
|
+
name: string;
|
|
14
|
+
size: number;
|
|
15
|
+
type: string;
|
|
16
|
+
lastModified: number;
|
|
17
|
+
}): FileInfo {
|
|
18
|
+
return {
|
|
19
|
+
path: raw.path,
|
|
20
|
+
name: raw.name,
|
|
21
|
+
size: raw.size,
|
|
22
|
+
type: toFileType(raw.type),
|
|
23
|
+
lastModified: raw.lastModified,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function exists(path: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
return await NativeModule.exists(path);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw wrapError(e, path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function stat(path: string): Promise<FileInfo> {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await NativeModule.stat(path);
|
|
38
|
+
return mapFileInfo(raw);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw wrapError(e, path);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function unlink(path: string): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await NativeModule.unlink(path);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw wrapError(e, path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function mkdir(path: string): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
await NativeModule.mkdir(path);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw wrapError(e, path);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function ls(path: string): Promise<FileInfo[]> {
|
|
61
|
+
try {
|
|
62
|
+
const rawList = await NativeModule.ls(path);
|
|
63
|
+
return rawList.map(mapFileInfo);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
throw wrapError(e, path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function cp(srcPath: string, destPath: string): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
await NativeModule.cp(srcPath, destPath);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
throw wrapError(e, srcPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function mv(srcPath: string, destPath: string): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await NativeModule.mv(srcPath, destPath);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
throw wrapError(e, srcPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/api/hash.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NativeModule } from '../module';
|
|
2
|
+
import { wrapError } from '../errors';
|
|
3
|
+
import { HashAlgorithm } from '../types';
|
|
4
|
+
|
|
5
|
+
export async function hashFile(
|
|
6
|
+
path: string,
|
|
7
|
+
algorithm: HashAlgorithm = HashAlgorithm.SHA256
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
try {
|
|
10
|
+
return await NativeModule.hashFile(path, algorithm);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw wrapError(e, path);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NativeModule, getStreamingProxy } from '../module';
|
|
2
|
+
import { wrapError, BlobError, ErrorCode } from '../errors';
|
|
3
|
+
import { wrapReader } from '../wrappers';
|
|
4
|
+
import type { BlobReader } from '../types';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BUFFER_SIZE = 65536; // 64KB
|
|
7
|
+
|
|
8
|
+
export function createReader(
|
|
9
|
+
path: string,
|
|
10
|
+
bufferSize: number = DEFAULT_BUFFER_SIZE
|
|
11
|
+
): BlobReader {
|
|
12
|
+
try {
|
|
13
|
+
if (
|
|
14
|
+
!Number.isFinite(bufferSize) ||
|
|
15
|
+
bufferSize < 4096 ||
|
|
16
|
+
bufferSize > 4194304
|
|
17
|
+
) {
|
|
18
|
+
throw new BlobError(
|
|
19
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
20
|
+
`bufferSize must be between 4096 and 4194304, got ${bufferSize}`,
|
|
21
|
+
path
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const handleId = NativeModule.openRead(path, bufferSize);
|
|
25
|
+
if (handleId < 0) {
|
|
26
|
+
throw new BlobError(
|
|
27
|
+
ErrorCode.IO_ERROR,
|
|
28
|
+
'Failed to open file for reading',
|
|
29
|
+
path
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const streaming = getStreamingProxy();
|
|
33
|
+
return wrapReader(handleId, streaming);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
throw wrapError(e, path);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NativeModule, getStreamingProxy } from '../module';
|
|
2
|
+
import { wrapError, BlobError, ErrorCode } from '../errors';
|
|
3
|
+
import { wrapWriter } from '../wrappers';
|
|
4
|
+
import type { BlobWriter } from '../types';
|
|
5
|
+
|
|
6
|
+
export function createWriter(
|
|
7
|
+
path: string,
|
|
8
|
+
append: boolean = false
|
|
9
|
+
): BlobWriter {
|
|
10
|
+
try {
|
|
11
|
+
const handleId = NativeModule.openWrite(path, append);
|
|
12
|
+
if (handleId < 0) {
|
|
13
|
+
throw new BlobError(
|
|
14
|
+
ErrorCode.IO_ERROR,
|
|
15
|
+
'Failed to open file for writing',
|
|
16
|
+
path
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const streaming = getStreamingProxy();
|
|
20
|
+
return wrapWriter(handleId, streaming);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw wrapError(e, path);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export enum ErrorCode {
|
|
2
|
+
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
|
|
3
|
+
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
|
4
|
+
FILE_ALREADY_EXISTS = 'FILE_ALREADY_EXISTS',
|
|
5
|
+
NOT_A_FILE = 'NOT_A_FILE',
|
|
6
|
+
NOT_A_DIRECTORY = 'NOT_A_DIRECTORY',
|
|
7
|
+
DIRECTORY_NOT_EMPTY = 'DIRECTORY_NOT_EMPTY',
|
|
8
|
+
IO_ERROR = 'IO_ERROR',
|
|
9
|
+
INVALID_ARGUMENT = 'INVALID_ARGUMENT',
|
|
10
|
+
DOWNLOAD_FAILED = 'DOWNLOAD_FAILED',
|
|
11
|
+
DOWNLOAD_CANCELLED = 'DOWNLOAD_CANCELLED',
|
|
12
|
+
READER_CLOSED = 'READER_CLOSED',
|
|
13
|
+
WRITER_CLOSED = 'WRITER_CLOSED',
|
|
14
|
+
UNKNOWN = 'UNKNOWN',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class BlobError extends Error {
|
|
18
|
+
public readonly code: ErrorCode;
|
|
19
|
+
public readonly path?: string;
|
|
20
|
+
|
|
21
|
+
constructor(code: ErrorCode, message: string, path?: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'BlobError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.path = path;
|
|
26
|
+
Object.setPrototypeOf(this, BlobError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse native error with format "[ERROR_CODE] message" into BlobError.
|
|
32
|
+
*/
|
|
33
|
+
export function wrapError(e: unknown, path?: string): BlobError {
|
|
34
|
+
if (e instanceof BlobError) {
|
|
35
|
+
return e;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
39
|
+
const match = /^\[([A-Z_]+)\]\s*(.*)$/.exec(message);
|
|
40
|
+
|
|
41
|
+
if (match) {
|
|
42
|
+
const [, code, msg] = match;
|
|
43
|
+
const errorCode = Object.values(ErrorCode).includes(code as ErrorCode)
|
|
44
|
+
? (code as ErrorCode)
|
|
45
|
+
: ErrorCode.UNKNOWN;
|
|
46
|
+
return new BlobError(errorCode, msg || message, path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new BlobError(ErrorCode.UNKNOWN, message, path);
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Errors
|
|
2
|
+
export { ErrorCode, BlobError, wrapError } from './errors';
|
|
3
|
+
|
|
4
|
+
// Paths
|
|
5
|
+
export { Dirs, join, dirname, basename, extname } from './paths';
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
FileInfo,
|
|
10
|
+
DownloadProgress,
|
|
11
|
+
BlobReader,
|
|
12
|
+
BlobWriter,
|
|
13
|
+
} from './types';
|
|
14
|
+
export { HashAlgorithm, FileType } from './types';
|
|
15
|
+
|
|
16
|
+
// API - Streaming
|
|
17
|
+
export { createReader } from './api/readFile';
|
|
18
|
+
export { createWriter } from './api/writeFile';
|
|
19
|
+
|
|
20
|
+
// API - File Operations
|
|
21
|
+
export { exists, stat, unlink, mkdir, ls, cp, mv } from './api/fileOps';
|
|
22
|
+
|
|
23
|
+
// API - Hashing
|
|
24
|
+
export { hashFile } from './api/hash';
|
|
25
|
+
|
|
26
|
+
// API - Download
|
|
27
|
+
export { download } from './api/download';
|
|
28
|
+
export type { DownloadOptions, DownloadHandle } from './api/download';
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import NativeModule from './NativeBufferedBlob';
|
|
2
|
+
|
|
3
|
+
// Install JSI HostObject on first import
|
|
4
|
+
const installed = NativeModule.install();
|
|
5
|
+
if (!installed) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
'[BufferedBlob] Failed to install JSI streaming bridge. Ensure the native module is linked.'
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StreamingProxy {
|
|
12
|
+
readNextChunk(handleId: number): Promise<ArrayBuffer | null>;
|
|
13
|
+
write(handleId: number, data: ArrayBuffer): Promise<number>;
|
|
14
|
+
flush(handleId: number): Promise<void>;
|
|
15
|
+
close(handleId: number): void;
|
|
16
|
+
startDownload(
|
|
17
|
+
handleId: number,
|
|
18
|
+
onProgress: (
|
|
19
|
+
bytesDownloaded: number,
|
|
20
|
+
totalBytes: number,
|
|
21
|
+
progress: number
|
|
22
|
+
) => void
|
|
23
|
+
): Promise<void>;
|
|
24
|
+
cancelDownload(handleId: number): void;
|
|
25
|
+
getReaderInfo(handleId: number): {
|
|
26
|
+
fileSize: number;
|
|
27
|
+
bytesRead: number;
|
|
28
|
+
isEOF: boolean;
|
|
29
|
+
};
|
|
30
|
+
getWriterInfo(handleId: number): { bytesWritten: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare global {
|
|
34
|
+
var __BufferedBlobStreaming: StreamingProxy | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Access the JSI HostObject installed by native
|
|
38
|
+
function getStreamingProxy(): StreamingProxy {
|
|
39
|
+
const proxy = globalThis.__BufferedBlobStreaming;
|
|
40
|
+
if (!proxy) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'[BufferedBlob] Streaming proxy not available. Make sure install() was called.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return proxy as StreamingProxy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { NativeModule, getStreamingProxy };
|