@uploadista/react-native-bare 0.0.3

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.
@@ -0,0 +1,144 @@
1
+ import type {
2
+ FileReaderService,
3
+ FileSource,
4
+ SliceResult,
5
+ } from "@uploadista/client-core";
6
+ import type { ReactNativeUploadInput } from "@/types/upload-input";
7
+
8
+ /**
9
+ * React Native-specific implementation of FileReaderService
10
+ * Handles Blob, File, and URI-based file inputs
11
+ */
12
+ export function createReactNativeFileReaderService(): FileReaderService<ReactNativeUploadInput> {
13
+ return {
14
+ async openFile(input, _chunkSize: number): Promise<FileSource> {
15
+ // Handle Blob/File objects
16
+ if (input instanceof Blob) {
17
+ return createBlobFileSource(input);
18
+ }
19
+
20
+ // Handle URI strings or URI objects
21
+ if (
22
+ typeof input === "string" ||
23
+ (input && typeof input === "object" && "uri" in input)
24
+ ) {
25
+ const uri =
26
+ typeof input === "string" ? input : (input as { uri: string }).uri;
27
+ return createUriFileSource(uri);
28
+ }
29
+
30
+ throw new Error(
31
+ "Unsupported file input type for React Native. Expected Blob, File, URI string, or {uri: string}",
32
+ );
33
+ },
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Create a FileSource from a Blob object
39
+ */
40
+ function createBlobFileSource(blob: Blob): FileSource {
41
+ return {
42
+ input: blob,
43
+ size: blob.size,
44
+ async slice(start: number, end: number): Promise<SliceResult> {
45
+ const chunk = blob.slice(start, end);
46
+
47
+ // React Native Blob may not have arrayBuffer() method
48
+ let arrayBuffer: ArrayBuffer;
49
+ if (typeof chunk.arrayBuffer === "function") {
50
+ arrayBuffer = await chunk.arrayBuffer();
51
+ } else {
52
+ // Fallback: use FileReader for React Native
53
+ arrayBuffer = await blobToArrayBuffer(chunk);
54
+ }
55
+
56
+ const done = end >= blob.size;
57
+
58
+ return {
59
+ done,
60
+ value: new Uint8Array(arrayBuffer),
61
+ size: chunk.size,
62
+ };
63
+ },
64
+ close() {
65
+ // No cleanup needed for Blob
66
+ },
67
+ name: null,
68
+ type: null,
69
+ lastModified: null,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Convert Blob to ArrayBuffer using FileReader (fallback for React Native)
75
+ */
76
+ function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
77
+ return new Promise((resolve, reject) => {
78
+ const reader = new FileReader();
79
+ reader.onload = () => {
80
+ if (reader.result instanceof ArrayBuffer) {
81
+ resolve(reader.result);
82
+ } else {
83
+ reject(new Error("FileReader result is not an ArrayBuffer"));
84
+ }
85
+ };
86
+ reader.onerror = () => reject(reader.error);
87
+ reader.readAsArrayBuffer(blob);
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Create a FileSource from a URI
93
+ * This is a simplified implementation - you may need to use react-native-fs
94
+ * or another library for more advanced file operations
95
+ */
96
+ function createUriFileSource(uri: string): FileSource {
97
+ // For URIs, we need to fetch the file first
98
+ // This implementation assumes the URI can be fetched as a blob
99
+ let cachedBlob: Blob | null = null;
100
+ let cachedSize: number | null = null;
101
+
102
+ return {
103
+ input: uri,
104
+ size: cachedSize,
105
+ async slice(start: number, end: number): Promise<SliceResult> {
106
+ // Fetch the blob if not cached
107
+ if (!cachedBlob) {
108
+ try {
109
+ const response = await fetch(uri);
110
+ cachedBlob = await response.blob();
111
+ cachedSize = cachedBlob.size;
112
+ } catch (error) {
113
+ throw new Error(`Failed to fetch file from URI ${uri}: ${error}`);
114
+ }
115
+ }
116
+
117
+ const chunk = cachedBlob.slice(start, end);
118
+
119
+ // React Native Blob may not have arrayBuffer() method
120
+ let arrayBuffer: ArrayBuffer;
121
+ if (typeof chunk.arrayBuffer === "function") {
122
+ arrayBuffer = await chunk.arrayBuffer();
123
+ } else {
124
+ arrayBuffer = await blobToArrayBuffer(chunk);
125
+ }
126
+
127
+ const done = end >= cachedBlob.size;
128
+
129
+ return {
130
+ done,
131
+ value: new Uint8Array(arrayBuffer),
132
+ size: chunk.size,
133
+ };
134
+ },
135
+ close() {
136
+ // Clear cached blob
137
+ cachedBlob = null;
138
+ cachedSize = null;
139
+ },
140
+ lastModified: null,
141
+ name: uri,
142
+ type: null,
143
+ };
144
+ }
@@ -0,0 +1,242 @@
1
+ import type {
2
+ ConnectionHealth,
3
+ ConnectionMetrics,
4
+ ConnectionPoolConfig,
5
+ DetailedConnectionMetrics,
6
+ HeadersLike,
7
+ Http2Info,
8
+ HttpClient,
9
+ HttpRequestOptions,
10
+ HttpResponse,
11
+ } from "@uploadista/client-core";
12
+
13
+ /**
14
+ * React Native-specific implementation of HttpClient using fetch API
15
+ * React Native's fetch is similar to browser fetch but may have different performance characteristics
16
+ */
17
+ export function createReactNativeHttpClient(
18
+ config?: ConnectionPoolConfig,
19
+ ): HttpClient {
20
+ return new ReactNativeHttpClient(config);
21
+ }
22
+
23
+ /**
24
+ * React Native HTTP client implementation
25
+ */
26
+ class ReactNativeHttpClient implements HttpClient {
27
+ private config: ConnectionPoolConfig;
28
+ private metrics: ConnectionMetrics;
29
+ private connectionTimes: number[] = [];
30
+ private requestCount = 0;
31
+ private connectionCount = 0;
32
+ private errorCount = 0;
33
+ private timeoutCount = 0;
34
+ private retryCount = 0;
35
+ private startTime = Date.now();
36
+
37
+ constructor(config: ConnectionPoolConfig = {}) {
38
+ this.config = {
39
+ maxConnectionsPerHost: config.maxConnectionsPerHost ?? 6,
40
+ connectionTimeout: config.connectionTimeout ?? 30000,
41
+ keepAliveTimeout: config.keepAliveTimeout ?? 60000,
42
+ enableHttp2: config.enableHttp2 ?? false, // HTTP/2 not supported in RN
43
+ retryOnConnectionError: config.retryOnConnectionError ?? true,
44
+ };
45
+
46
+ this.metrics = {
47
+ activeConnections: 0,
48
+ totalConnections: 0,
49
+ reuseRate: 0,
50
+ averageConnectionTime: 0,
51
+ };
52
+ }
53
+
54
+ async request(
55
+ url: string,
56
+ options: HttpRequestOptions = {},
57
+ ): Promise<HttpResponse> {
58
+ this.requestCount++;
59
+
60
+ const fetchOptions: RequestInit = {
61
+ method: options.method || "GET",
62
+ headers: options.headers,
63
+ body: options.body as any, // React Native's BodyInit type
64
+ signal: options.signal as AbortSignal | undefined,
65
+ };
66
+
67
+ // Add credentials if specified
68
+ if (options.credentials) {
69
+ fetchOptions.credentials = options.credentials as any; // React Native's RequestCredentials type
70
+ }
71
+
72
+ const startTime = Date.now();
73
+
74
+ try {
75
+ // Handle timeout
76
+ const timeoutPromise = options.timeout
77
+ ? new Promise<never>((_, reject) => {
78
+ setTimeout(() => {
79
+ this.timeoutCount++;
80
+ reject(new Error(`Request timeout after ${options.timeout}ms`));
81
+ }, options.timeout);
82
+ })
83
+ : null;
84
+
85
+ const fetchPromise = fetch(url, fetchOptions);
86
+
87
+ const response = timeoutPromise
88
+ ? await Promise.race([fetchPromise, timeoutPromise])
89
+ : await fetchPromise;
90
+
91
+ const connectionTime = Date.now() - startTime;
92
+ this.connectionTimes.push(connectionTime);
93
+ this.connectionCount++;
94
+ this.metrics.totalConnections++;
95
+ this.updateMetrics();
96
+
97
+ // Convert fetch Response to HttpResponse
98
+ return this.adaptResponse(response);
99
+ } catch (error) {
100
+ this.errorCount++;
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ private adaptResponse(response: Response): HttpResponse {
106
+ const headers: HeadersLike = {
107
+ get: (name: string) => response.headers.get(name),
108
+ has: (name: string) => response.headers.has(name),
109
+ forEach: (callback: (value: string, name: string) => void) => {
110
+ response.headers.forEach(callback);
111
+ },
112
+ };
113
+
114
+ return {
115
+ status: response.status,
116
+ statusText: response.statusText,
117
+ headers,
118
+ ok: response.ok,
119
+ json: () => response.json(),
120
+ text: () => response.text(),
121
+ arrayBuffer: () => response.arrayBuffer(),
122
+ };
123
+ }
124
+
125
+ private updateMetrics(): void {
126
+ if (this.connectionTimes.length > 0) {
127
+ const sum = this.connectionTimes.reduce((a, b) => a + b, 0);
128
+ this.metrics.averageConnectionTime = sum / this.connectionTimes.length;
129
+ }
130
+
131
+ // Estimate reuse rate based on connection times
132
+ // Faster connections are likely reused
133
+ const fastConnections = this.connectionTimes.filter(
134
+ (time) => time < 100,
135
+ ).length;
136
+ this.metrics.reuseRate =
137
+ this.connectionTimes.length > 0
138
+ ? fastConnections / this.connectionTimes.length
139
+ : 0;
140
+ }
141
+
142
+ getMetrics(): ConnectionMetrics {
143
+ return { ...this.metrics };
144
+ }
145
+
146
+ getDetailedMetrics(): DetailedConnectionMetrics {
147
+ const uptime = Date.now() - this.startTime;
148
+ const requestsPerSecond =
149
+ uptime > 0 ? this.requestCount / (uptime / 1000) : 0;
150
+ const errorRate =
151
+ this.requestCount > 0 ? this.errorCount / this.requestCount : 0;
152
+
153
+ const fastConnections = this.connectionTimes.filter(
154
+ (time) => time < 100,
155
+ ).length;
156
+ const slowConnections = this.connectionTimes.length - fastConnections;
157
+
158
+ const health = this.calculateHealth(errorRate);
159
+
160
+ const http2Info: Http2Info = {
161
+ supported: false, // React Native doesn't support HTTP/2
162
+ detected: false,
163
+ version: "h1.1",
164
+ multiplexingActive: false,
165
+ };
166
+
167
+ return {
168
+ ...this.metrics,
169
+ health,
170
+ requestsPerSecond,
171
+ errorRate,
172
+ timeouts: this.timeoutCount,
173
+ retries: this.retryCount,
174
+ fastConnections,
175
+ slowConnections,
176
+ http2Info,
177
+ };
178
+ }
179
+
180
+ private calculateHealth(errorRate: number): ConnectionHealth {
181
+ let status: "healthy" | "degraded" | "poor";
182
+ let score: number;
183
+ const issues: string[] = [];
184
+ const recommendations: string[] = [];
185
+
186
+ if (errorRate > 0.1) {
187
+ status = "poor";
188
+ score = 30;
189
+ issues.push(`High error rate: ${(errorRate * 100).toFixed(1)}%`);
190
+ recommendations.push("Check network connectivity");
191
+ } else if (errorRate > 0.05) {
192
+ status = "degraded";
193
+ score = 60;
194
+ issues.push(`Moderate error rate: ${(errorRate * 100).toFixed(1)}%`);
195
+ recommendations.push("Monitor connection stability");
196
+ } else {
197
+ status = "healthy";
198
+ score = 100;
199
+ }
200
+
201
+ if (this.metrics.averageConnectionTime > 1000) {
202
+ issues.push(
203
+ `Slow connections: ${this.metrics.averageConnectionTime.toFixed(0)}ms avg`,
204
+ );
205
+ recommendations.push("Check network conditions");
206
+ score = Math.min(score, 70);
207
+ }
208
+
209
+ return { status, score, issues, recommendations };
210
+ }
211
+
212
+ reset(): void {
213
+ this.metrics = {
214
+ activeConnections: 0,
215
+ totalConnections: 0,
216
+ reuseRate: 0,
217
+ averageConnectionTime: 0,
218
+ };
219
+ this.connectionTimes = [];
220
+ this.requestCount = 0;
221
+ this.connectionCount = 0;
222
+ this.errorCount = 0;
223
+ this.timeoutCount = 0;
224
+ this.retryCount = 0;
225
+ this.startTime = Date.now();
226
+ }
227
+
228
+ async close(): Promise<void> {
229
+ // React Native fetch doesn't require explicit connection closing
230
+ this.reset();
231
+ }
232
+
233
+ async warmupConnections(urls: string[]): Promise<void> {
234
+ // Warmup by making HEAD requests to the URLs
235
+ const promises = urls.map((url) =>
236
+ this.request(url, { method: "HEAD" }).catch(() => {
237
+ // Ignore warmup errors
238
+ }),
239
+ );
240
+ await Promise.all(promises);
241
+ }
242
+ }
@@ -0,0 +1,14 @@
1
+ import type { IdGenerationService } from "@uploadista/client-core";
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ /**
5
+ * React Native-specific implementation of IdGenerationService using uuid library
6
+ * crypto.randomUUID() is not available in React Native, so we use the uuid library
7
+ */
8
+ export function createReactNativeIdGenerationService(): IdGenerationService {
9
+ return {
10
+ generate(): string {
11
+ return uuidv4();
12
+ },
13
+ };
14
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * React Native service implementations for Uploadista client
3
+ */
4
+
5
+ export { createReactNativeBase64Service } from "./base64-service";
6
+ export {
7
+ createReactNativeServices,
8
+ type ReactNativeServiceOptions,
9
+ } from "./create-react-native-services";
10
+
11
+ export { createReactNativeFileReaderService } from "./file-reader-service";
12
+
13
+ export { createReactNativeHttpClient } from "./http-client";
14
+ export { createReactNativeIdGenerationService } from "./id-generation-service";
15
+ export { createAsyncStorageService } from "./storage-service";
@@ -0,0 +1,247 @@
1
+ import type {
2
+ CameraOptions,
3
+ FileInfo,
4
+ FilePickResult,
5
+ FileSystemProvider,
6
+ PickerOptions,
7
+ } from "@uploadista/react-native-core";
8
+
9
+ import * as DocumentPicker from "react-native-document-picker";
10
+ import * as ImagePicker from "react-native-image-picker";
11
+ import RNFetchBlob from "rn-fetch-blob";
12
+
13
+ /**
14
+ * File system provider implementation for bare React Native environment
15
+ * Uses react-native-image-picker, react-native-fs, and native APIs
16
+ */
17
+ export class NativeFileSystemProvider implements FileSystemProvider {
18
+ async pickDocument(options?: PickerOptions): Promise<FilePickResult> {
19
+ try {
20
+ const result = await DocumentPicker.pick({
21
+ type: options?.allowedTypes || [DocumentPicker.types.allFiles],
22
+ ...(options?.allowMultiple && { isMultiple: true }),
23
+ });
24
+
25
+ const file = Array.isArray(result) ? result[0] : result;
26
+
27
+ if (!file) {
28
+ throw new Error("No document selected");
29
+ }
30
+
31
+ return {
32
+ uri: file.uri,
33
+ name: file.name || "document",
34
+ size: file.size || 0,
35
+ mimeType: file.type || undefined,
36
+ localPath: file.fileCopyUri || undefined,
37
+ };
38
+ } catch (error) {
39
+ if (
40
+ error instanceof Error &&
41
+ (error.message.includes("cancelled") ||
42
+ error.message.includes("Cancelled"))
43
+ ) {
44
+ throw error;
45
+ }
46
+ throw new Error(
47
+ `Failed to pick document: ${error instanceof Error ? error.message : String(error)}`,
48
+ );
49
+ }
50
+ }
51
+
52
+ async pickImage(options?: PickerOptions): Promise<FilePickResult> {
53
+ try {
54
+ return new Promise((resolve, reject) => {
55
+ ImagePicker.launchImageLibrary(
56
+ {
57
+ mediaType: "photo",
58
+ selectionLimit: options?.allowMultiple ? 0 : 1,
59
+ quality: 1,
60
+ // biome-ignore lint/suspicious/noExplicitAny: react-native-image-picker type mismatch
61
+ } as any,
62
+ (response: unknown) => {
63
+ const res = response as {
64
+ didCancel?: boolean;
65
+ errorCode?: string;
66
+ assets?: Array<{
67
+ uri: string;
68
+ fileName?: string;
69
+ fileSize?: number;
70
+ type?: string;
71
+ }>;
72
+ };
73
+
74
+ if (res.didCancel) {
75
+ reject(new Error("Image picker was cancelled"));
76
+ } else if (res.errorCode) {
77
+ reject(new Error(`Image picker error: ${res.errorCode}`));
78
+ } else if (res.assets && res.assets.length > 0) {
79
+ const asset = res.assets[0];
80
+ if (asset) {
81
+ resolve({
82
+ uri: asset.uri,
83
+ name: asset.fileName || `image-${Date.now()}.jpg`,
84
+ size: asset.fileSize || 0,
85
+ mimeType: asset.type || "image/jpeg",
86
+ });
87
+ } else {
88
+ reject(new Error("No image selected"));
89
+ }
90
+ } else {
91
+ reject(new Error("No image selected"));
92
+ }
93
+ },
94
+ );
95
+ });
96
+ } catch (error) {
97
+ throw new Error(
98
+ `Failed to pick image: ${error instanceof Error ? error.message : String(error)}`,
99
+ );
100
+ }
101
+ }
102
+
103
+ async pickVideo(options?: PickerOptions): Promise<FilePickResult> {
104
+ try {
105
+ return new Promise((resolve, reject) => {
106
+ ImagePicker.launchImageLibrary(
107
+ {
108
+ mediaType: "video",
109
+ selectionLimit: options?.allowMultiple ? 0 : 1,
110
+ },
111
+ (response: unknown) => {
112
+ const res = response as {
113
+ didCancel?: boolean;
114
+ errorCode?: string;
115
+ assets?: Array<{
116
+ uri: string;
117
+ fileName?: string;
118
+ fileSize?: number;
119
+ type?: string;
120
+ }>;
121
+ };
122
+
123
+ if (res.didCancel) {
124
+ reject(new Error("Video picker was cancelled"));
125
+ } else if (res.errorCode) {
126
+ reject(new Error(`Video picker error: ${res.errorCode}`));
127
+ } else if (res.assets && res.assets.length > 0) {
128
+ const asset = res.assets[0];
129
+ if (asset) {
130
+ resolve({
131
+ uri: asset.uri,
132
+ name: asset.fileName || `video-${Date.now()}.mp4`,
133
+ size: asset.fileSize || 0,
134
+ mimeType: asset.type || "video/mp4",
135
+ });
136
+ } else {
137
+ reject(new Error("No video selected"));
138
+ }
139
+ } else {
140
+ reject(new Error("No video selected"));
141
+ }
142
+ },
143
+ );
144
+ });
145
+ } catch (error) {
146
+ throw new Error(
147
+ `Failed to pick video: ${error instanceof Error ? error.message : String(error)}`,
148
+ );
149
+ }
150
+ }
151
+
152
+ async pickCamera(options?: CameraOptions): Promise<FilePickResult> {
153
+ try {
154
+ return new Promise((resolve, reject) => {
155
+ ImagePicker.launchCamera(
156
+ {
157
+ mediaType: "photo",
158
+ cameraType: options?.cameraType === "front" ? "front" : "back",
159
+ quality: options?.quality ?? 1,
160
+ // biome-ignore lint/suspicious/noExplicitAny: react-native-image-picker type mismatch
161
+ } as any,
162
+ (response: unknown) => {
163
+ const res = response as {
164
+ didCancel?: boolean;
165
+ errorCode?: string;
166
+ assets?: Array<{
167
+ uri: string;
168
+ fileName?: string;
169
+ fileSize?: number;
170
+ type?: string;
171
+ }>;
172
+ };
173
+
174
+ if (res.didCancel) {
175
+ reject(new Error("Camera was cancelled"));
176
+ } else if (res.errorCode) {
177
+ reject(new Error(`Camera error: ${res.errorCode}`));
178
+ } else if (res.assets && res.assets.length > 0) {
179
+ const asset = res.assets[0];
180
+ if (asset) {
181
+ resolve({
182
+ uri: asset.uri,
183
+ name: asset.fileName || `photo-${Date.now()}.jpg`,
184
+ size: asset.fileSize || 0,
185
+ mimeType: asset.type || "image/jpeg",
186
+ });
187
+ } else {
188
+ reject(new Error("No photo captured"));
189
+ }
190
+ } else {
191
+ reject(new Error("No photo captured"));
192
+ }
193
+ },
194
+ );
195
+ });
196
+ } catch (error) {
197
+ throw new Error(
198
+ `Failed to capture photo: ${error instanceof Error ? error.message : String(error)}`,
199
+ );
200
+ }
201
+ }
202
+
203
+ async readFile(uri: string): Promise<ArrayBuffer> {
204
+ try {
205
+ // Read file as base64
206
+ const base64Data = await RNFetchBlob.fs.readFile(uri, "base64");
207
+
208
+ // Convert base64 to ArrayBuffer
209
+ const binaryString = atob(base64Data);
210
+ const bytes = new Uint8Array(binaryString.length);
211
+ for (let i = 0; i < binaryString.length; i++) {
212
+ bytes[i] = binaryString.charCodeAt(i);
213
+ }
214
+ return bytes.buffer;
215
+ } catch (error) {
216
+ throw new Error(
217
+ `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
218
+ );
219
+ }
220
+ }
221
+
222
+ async getDocumentUri(filePath: string): Promise<string> {
223
+ // In bare RN with native modules, the path is typically already a URI
224
+ return filePath;
225
+ }
226
+
227
+ async getFileInfo(uri: string): Promise<FileInfo> {
228
+ try {
229
+ const stat = await RNFetchBlob.fs.stat(uri);
230
+
231
+ return {
232
+ uri,
233
+ name: stat.filename || uri.split("/").pop() || "unknown",
234
+ size: stat.size || 0,
235
+ modificationTime: stat.lastModified
236
+ ? typeof stat.lastModified === "string"
237
+ ? parseInt(stat.lastModified, 10)
238
+ : stat.lastModified
239
+ : undefined,
240
+ };
241
+ } catch (error) {
242
+ throw new Error(
243
+ `Failed to get file info: ${error instanceof Error ? error.message : String(error)}`,
244
+ );
245
+ }
246
+ }
247
+ }