Skip to content

Commit 72ae53f

Browse files
committed
refactor: leverage arraybuffer on native read/write stream
1 parent 694af94 commit 72ae53f

File tree

5 files changed

+70
-191
lines changed

5 files changed

+70
-191
lines changed

android/src/main/java/com/margelo/nitro/fs2/Fs2Stream.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ class Fs2Stream(): HybridFs2StreamSpec() {
161161
if (state.job == null) {
162162
state.job = streamScope.launch {
163163
val bufferSize = state.options?.bufferSize ?: 8192
164-
val encoding = state.options?.encoding ?: Encoding.ARRAYBUFFER
165164
val start = state.options?.start ?: 0L
166165
val end = state.options?.end
167166
var position = start
@@ -195,8 +194,7 @@ class Fs2Stream(): HybridFs2StreamSpec() {
195194
streamId = streamId,
196195
data = ArrayBuffer.copy(java.nio.ByteBuffer.wrap(data)),
197196
chunk = chunk,
198-
position = position,
199-
encoding = encoding
197+
position = position
200198
)
201199
)
202200

@@ -302,22 +300,6 @@ class Fs2Stream(): HybridFs2StreamSpec() {
302300
}
303301
}
304302

305-
override fun writeStringToStream(streamId: String, data: String): Promise<Unit> {
306-
return Promise.async {
307-
val impl = writeStreams[streamId] ?: throw Exception("ENOENT: No such write stream: $streamId")
308-
if (!impl.state.isActive) throw Exception("EPIPE: Write stream is not active: $streamId")
309-
val encoding = impl.state.options?.encoding ?: Encoding.UTF8
310-
val bytes = when (encoding) {
311-
Encoding.UTF8 -> data.toByteArray(Charsets.UTF_8)
312-
Encoding.ASCII -> data.toByteArray(Charsets.US_ASCII)
313-
Encoding.BASE64 -> android.util.Base64.decode(data, android.util.Base64.DEFAULT)
314-
else -> data.toByteArray()
315-
}
316-
impl.queue.add(WriteRequest(bytes, isString = true))
317-
impl.state.job?.let { if (!it.isActive) throw Exception("EPIPE: Write job is not active") }
318-
}
319-
}
320-
321303
override fun flushWriteStream(streamId: String): Promise<Unit> {
322304
return Promise.async {
323305
val impl = writeStreams[streamId] ?: throw Exception("ENOENT: No such write stream: $streamId")

docs/FILE_STREAM.md

Lines changed: 39 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ File streams are ideal for handling large files where loading entire content int
1212
- **Event-Driven**: Use Nitro callbacks for real-time progress updates
1313
- **Cross-Platform**: Consistent API across iOS and Android
1414

15+
> **Note:** The stream API is now binary-only. All encoding/decoding (e.g., UTF-8, Base64) must be handled in JavaScript. Native code only receives and returns `ArrayBuffer`.
16+
1517
## Read Stream API
1618

1719
### `createReadStream(path: string, options?: ReadStreamOptions): Promise<ReadStreamHandle>`
@@ -29,12 +31,9 @@ Creates a read stream for efficiently reading large files in chunks.
2931
```typescript
3032
interface ReadStreamOptions {
3133
bufferSize?: number; // Buffer size in bytes (default: 4096)
32-
encoding?: StreamEncoding; // 'utf8' | 'ascii' | 'base64' | 'arraybuffer' (default: 'arraybuffer')
3334
start?: bigint; // Start position in bytes (default: 0)
3435
end?: bigint; // End position in bytes (default: file end)
3536
}
36-
37-
type StreamEncoding = 'utf8' | 'ascii' | 'base64' | 'arraybuffer';
3837
```
3938

4039
#### ReadStreamHandle
@@ -96,7 +95,6 @@ interface ReadStreamDataEvent {
9695
data: ArrayBuffer; // Raw data chunk
9796
chunk: bigint; // Chunk number (0-based)
9897
position: bigint; // Current position in file
99-
encoding: StreamEncoding;
10098
}
10199

102100
interface ReadStreamProgressEvent {
@@ -122,39 +120,37 @@ interface ReadStreamErrorEvent {
122120
### Read Stream Example
123121

124122
```typescript
125-
import { Fs2 } from 'react-native-fs2-nitro';
123+
import { Fs2, concatenateArrayBuffers, listenToReadStreamData, listenToReadStreamProgress, listenToReadStreamEnd, listenToReadStreamError } from 'react-native-fs2-nitro';
126124

127125
async function readLargeFile() {
128126
try {
129127
// Create read stream
130128
const stream = await Fs2.createReadStream('/path/to/large-file.dat', {
131-
bufferSize: 8192, // 8KB chunks
132-
encoding: 'arraybuffer'
129+
bufferSize: 8192 // 8KB chunks
133130
});
134131

135132
let totalData = new ArrayBuffer(0);
136133

137134
// Listen for data chunks
138-
const unsubscribeData = Fs2.listenToReadStreamData(
135+
const unsubscribeData = listenToReadStreamData(
139136
stream.streamId,
140137
(event) => {
141138
console.log(`Received chunk ${event.chunk}, ${event.data.byteLength} bytes`);
142-
143139
// Accumulate data (for small files) or process chunk immediately
144140
totalData = concatenateArrayBuffers(totalData, event.data);
145141
}
146142
);
147143

148144
// Listen for progress updates
149-
const unsubscribeProgress = Fs2.listenToReadStreamProgress(
145+
const unsubscribeProgress = listenToReadStreamProgress(
150146
stream.streamId,
151147
(event) => {
152-
console.log(`Progress: ${event.progress.toFixed(1)}% (${event.bytesRead}/${event.totalBytes})`);
148+
console.log(`Progress: ${(event.progress * 100).toFixed(1)}% (${event.bytesRead}/${event.totalBytes})`);
153149
}
154150
);
155151

156152
// Listen for completion
157-
const unsubscribeEnd = Fs2.listenToReadStreamEnd(
153+
const unsubscribeEnd = listenToReadStreamEnd(
158154
stream.streamId,
159155
(event) => {
160156
console.log('Stream finished successfully');
@@ -165,7 +161,7 @@ async function readLargeFile() {
165161
);
166162

167163
// Listen for errors
168-
const unsubscribeError = Fs2.listenToReadStreamError(
164+
const unsubscribeError = listenToReadStreamError(
169165
stream.streamId,
170166
(event) => {
171167
console.error('Stream error:', event.error);
@@ -202,7 +198,6 @@ Creates a write stream for efficiently writing large files in chunks.
202198
```typescript
203199
interface WriteStreamOptions {
204200
append?: boolean; // Append to existing file (default: false)
205-
encoding?: StreamEncoding; // 'utf8' | 'ascii' | 'base64' | 'arraybuffer' (default: 'arraybuffer')
206201
bufferSize?: number; // Internal buffer size (default: 4096)
207202
createDirectories?: boolean; // Create parent directories if needed (default: true)
208203
}
@@ -217,9 +212,6 @@ interface WriteStreamHandle {
217212
// Write data chunk to stream
218213
write(data: ArrayBuffer): Promise<void>;
219214

220-
// Write string data (when encoding is utf8 or base64)
221-
writeString(data: string): Promise<void>;
222-
223215
// Flush any buffered data
224216
flush(): Promise<void>;
225217

@@ -281,27 +273,26 @@ interface WriteStreamErrorEvent {
281273
### Write Stream Example
282274

283275
```typescript
284-
import { Fs2 } from 'react-native-fs2-nitro';
276+
import { Fs2, listenToWriteStreamProgress, listenToWriteStreamFinish, listenToWriteStreamError } from 'react-native-fs2-nitro';
285277

286278
async function writeLargeFile() {
287279
try {
288280
// Create write stream
289281
const stream = await Fs2.createWriteStream('/path/to/output-file.dat', {
290282
append: false,
291-
encoding: 'binary',
292283
createDirectories: true
293284
});
294285

295286
// Listen for progress
296-
const unsubscribeProgress = Fs2.listenToWriteStreamProgress(
287+
const unsubscribeProgress = listenToWriteStreamProgress(
297288
stream.streamId,
298289
(event) => {
299290
console.log(`Written: ${event.bytesWritten} bytes`);
300291
}
301292
);
302293

303294
// Listen for completion
304-
const unsubscribeFinish = Fs2.listenToWriteStreamFinish(
295+
const unsubscribeFinish = listenToWriteStreamFinish(
305296
stream.streamId,
306297
(event) => {
307298
console.log('Write completed:', event.bytesWritten, 'bytes');
@@ -311,7 +302,7 @@ async function writeLargeFile() {
311302
);
312303

313304
// Listen for errors
314-
const unsubscribeError = Fs2.listenToWriteStreamError(
305+
const unsubscribeError = listenToWriteStreamError(
315306
stream.streamId,
316307
(event) => {
317308
console.error('Write error:', event.error);
@@ -341,110 +332,45 @@ async function writeLargeFile() {
341332

342333
## Utility Functions
343334

344-
### Text Processing with Streams
335+
### Text and Binary Processing with Streams
345336

346337
```typescript
347-
// Read text file in chunks with specific encoding
348-
async function readTextStream(filePath: string, encoding: 'utf8' = 'utf8') {
349-
const stream = await Fs2.createReadStream(filePath, { encoding });
350-
let content = '';
351-
352-
return new Promise<string>((resolve, reject) => {
353-
const unsubscribeData = Fs2.listenToReadStreamData(stream.streamId, (event) => {
354-
// Convert ArrayBuffer to string based on encoding
355-
const chunk = arrayBufferToString(event.data, encoding);
356-
content += chunk;
357-
});
338+
import { readStream, writeStream } from 'react-native-fs2-nitro';
358339

359-
const unsubscribeEnd = Fs2.listenToReadStreamEnd(stream.streamId, () => {
360-
unsubscribeData();
361-
unsubscribeEnd();
362-
resolve(content);
363-
});
340+
// Read a file as a string (text mode)
341+
async function readTextFile(filePath: string, encoding: 'utf8' = 'utf8') {
342+
const content = await readStream(filePath, encoding);
343+
// content is a string
344+
}
364345

365-
const unsubscribeError = Fs2.listenToReadStreamError(stream.streamId, (event) => {
366-
unsubscribeData();
367-
unsubscribeEnd();
368-
unsubscribeError();
369-
reject(new Error(event.error));
370-
});
346+
// Read a file as binary (default)
347+
async function readBinaryFile(filePath: string) {
348+
const buffer = await readStream(filePath); // default is 'arraybuffer'
349+
// buffer is an ArrayBuffer
350+
}
371351

372-
stream.start();
373-
});
352+
// Write a string to a file (text mode)
353+
async function writeTextFile(filePath: string, text: string, encoding: 'utf8' = 'utf8') {
354+
await writeStream(filePath, text, encoding);
374355
}
375356

376-
// Write text with streaming
377-
async function writeTextStream(filePath: string, text: string, encoding: 'utf8' = 'utf8') {
378-
const stream = await Fs2.createWriteStream(filePath, { encoding });
379-
380-
const data = stringToArrayBuffer(text, encoding);
381-
await stream.write(data);
382-
await stream.close();
357+
// Write binary data to a file (default)
358+
async function writeBinaryFile(filePath: string, buffer: ArrayBuffer) {
359+
await writeStream(filePath, buffer); // default is 'arraybuffer'
383360
}
384361
```
385362

386363
### File Copy with Progress
387364

388365
```typescript
389-
async function copyFileWithProgress(
366+
import { copyFileWithProgress } from 'react-native-fs2-nitro';
367+
368+
async function copyFileWithProgressExample(
390369
sourcePath: string,
391370
destPath: string,
392371
onProgress?: (progress: number) => void
393372
) {
394-
const readStream = await Fs2.createReadStream(sourcePath, { bufferSize: 16384 });
395-
const writeStream = await Fs2.createWriteStream(destPath, { bufferSize: 16384 });
396-
397-
return new Promise<void>((resolve, reject) => {
398-
let unsubscribeData: (() => void) | null = null;
399-
let unsubscribeProgress: (() => void) | null = null;
400-
let unsubscribeEnd: (() => void) | null = null;
401-
let unsubscribeError: (() => void) | null = null;
402-
403-
const cleanup = () => {
404-
unsubscribeData?.();
405-
unsubscribeProgress?.();
406-
unsubscribeEnd?.();
407-
unsubscribeError?.();
408-
};
409-
410-
// Forward read data to write stream
411-
unsubscribeData = Fs2.listenToReadStreamData(readStream.streamId, async (event) => {
412-
try {
413-
await writeStream.write(event.data);
414-
} catch (error) {
415-
cleanup();
416-
reject(error);
417-
}
418-
});
419-
420-
// Track progress
421-
if (onProgress) {
422-
unsubscribeProgress = Fs2.listenToReadStreamProgress(readStream.streamId, (event) => {
423-
onProgress(event.progress);
424-
});
425-
}
426-
427-
// Handle completion
428-
unsubscribeEnd = Fs2.listenToReadStreamEnd(readStream.streamId, async () => {
429-
try {
430-
await writeStream.close();
431-
cleanup();
432-
resolve();
433-
} catch (error) {
434-
cleanup();
435-
reject(error);
436-
}
437-
});
438-
439-
// Handle errors
440-
unsubscribeError = Fs2.listenToReadStreamError(readStream.streamId, (event) => {
441-
cleanup();
442-
reject(new Error(event.error));
443-
});
444-
445-
// Start the copy
446-
readStream.start();
447-
});
373+
await copyFileWithProgress(sourcePath, destPath, { bufferSize: 16384, onProgress });
448374
}
449375
```
450376

@@ -461,19 +387,7 @@ async function copyFileWithProgress(
461387

462388
4. **Threading**: Stream operations run on background threads, keeping the UI responsive
463389

464-
5. **Encoding**: Use 'arraybuffer' encoding for maximum performance when dealing with raw data
465-
466-
## Platform Differences
467-
468-
### iOS
469-
- Uses `NSInputStream` and `NSOutputStream` for efficient native streaming
470-
- Supports all encoding types natively
471-
- File system permissions handled automatically
472-
473-
### Android
474-
- Uses `FileInputStream` and `FileOutputStream` with buffer management
475-
- UTF-8 and Base64 encoding handled efficiently
476-
- Respects Android scoped storage requirements
390+
5. **Encoding**: All encoding/decoding is handled in JavaScript. Native code only deals with `ArrayBuffer`.
477391

478392
## Error Codes
479393

@@ -501,13 +415,15 @@ ReactNativeBlobUtil.fs.readStream(path, encoding, bufferSize)
501415
});
502416

503417
// fs2-nitro style
504-
const stream = await Fs2.createReadStream(path, { encoding, bufferSize });
418+
const stream = await Fs2.createReadStream(path, { bufferSize });
505419
const unsubscribeData = Fs2.listenToReadStreamData(stream.streamId, event => {
506420
/* handle event.data */
507421
});
422+
508423
const unsubscribeEnd = Fs2.listenToReadStreamEnd(stream.streamId, () => {
509424
/* handle end */
510425
});
426+
511427
await stream.start();
512428
```
513429

ios/Fs2Stream.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ class Fs2Stream: HybridFs2StreamSpec {
144144
state.isActive = true
145145

146146
let bufferSize = Int(state.options?.bufferSize ?? 8192)
147-
let encoding = state.options?.encoding ?? .arraybuffer
148147
let start = state.options?.start ?? 0
149148
let end = state.options?.end
150149
var position = start
@@ -181,7 +180,7 @@ class Fs2Stream: HybridFs2StreamSpec {
181180
let data = try state.fileHandle.read(upToCount: bytesToRead) ?? Data()
182181
if data.isEmpty { break }
183182
let arrayBuffer = try ArrayBufferHolder.copy(data: data)
184-
self.readStreamDataListeners[streamId]?(ReadStreamDataEvent(streamId: streamId, data: arrayBuffer, chunk: chunk, position: position, encoding: encoding))
183+
self.readStreamDataListeners[streamId]?(ReadStreamDataEvent(streamId: streamId, data: arrayBuffer, chunk: chunk, position: position))
185184
position += Int64(data.count)
186185
state.position = position
187186
bytesReadTotal += Int64(data.count)
@@ -267,25 +266,6 @@ class Fs2Stream: HybridFs2StreamSpec {
267266
}
268267
}
269268

270-
func writeStringToStream(streamId: String, data: String) throws -> NitroModules.Promise<Void> {
271-
return Promise.async {
272-
guard let state = self.writeStreams[streamId] else {
273-
throw NSError(domain: "Fs2Stream", code: 0, userInfo: [NSLocalizedDescriptionKey: "ENOENT: No such write stream: \(streamId)"])
274-
}
275-
if !state.isActive { throw NSError(domain: "Fs2Stream", code: 0, userInfo: [NSLocalizedDescriptionKey: "EPIPE: Write stream is not active: \(streamId)"]) }
276-
let encoding = state.options?.encoding ?? .utf8
277-
let bytes: Data
278-
switch encoding {
279-
case .utf8: bytes = data.data(using: .utf8) ?? Data()
280-
case .ascii: bytes = data.data(using: .ascii) ?? Data()
281-
case .base64: bytes = Data(base64Encoded: data) ?? Data()
282-
default: bytes = data.data(using: .utf8) ?? Data()
283-
}
284-
state.writeBufferContinuation?.yield((bytes, true))
285-
if let task = state.task, task.isCancelled { throw NSError(domain: "Fs2Stream", code: 0, userInfo: [NSLocalizedDescriptionKey: "EPIPE: Write job is not active"]) }
286-
}
287-
}
288-
289269
func flushWriteStream(streamId: String) throws -> NitroModules.Promise<Void> {
290270
return Promise.async {
291271
guard let state = self.writeStreams[streamId] else {

0 commit comments

Comments
 (0)