@uploadista/expo 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.
- package/LICENSE +21 -0
- package/README.md +567 -0
- package/expo-env.d.ts +3 -0
- package/package.json +45 -0
- package/src/client/create-uploadista-client.ts +67 -0
- package/src/client/index.ts +4 -0
- package/src/index.ts +77 -0
- package/src/services/abort-controller-factory.ts +32 -0
- package/src/services/base64-service.ts +29 -0
- package/src/services/checksum-service.ts +14 -0
- package/src/services/create-expo-services.ts +85 -0
- package/src/services/expo-file-system-provider.ts +227 -0
- package/src/services/file-reader-service.ts +184 -0
- package/src/services/fingerprint-service.ts +107 -0
- package/src/services/http-client.ts +235 -0
- package/src/services/id-generation-service.ts +14 -0
- package/src/services/index.ts +13 -0
- package/src/services/platform-service.ts +71 -0
- package/src/services/storage-service.ts +62 -0
- package/src/services/websocket-factory.ts +62 -0
- package/src/types/upload-input.ts +5 -0
- package/src/types.ts +116 -0
- package/src/utils/hash-util.ts +70 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { FingerprintService } from "@uploadista/client-core";
|
|
2
|
+
import * as Crypto from "expo-crypto";
|
|
3
|
+
import type { ExpoUploadInput } from "../types/upload-input";
|
|
4
|
+
import { computeblobSha256 } from "../utils/hash-util";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a FingerprintService for Expo environments
|
|
8
|
+
* Computes file fingerprints using SHA-256 hashing
|
|
9
|
+
* Supports Blob, File, and URI-based inputs
|
|
10
|
+
*/
|
|
11
|
+
export function createExpoFingerprintService(): FingerprintService<ExpoUploadInput> {
|
|
12
|
+
return {
|
|
13
|
+
computeFingerprint: async (input, _endpoint) => {
|
|
14
|
+
// Handle Blob/File objects directly
|
|
15
|
+
if (input instanceof Blob) {
|
|
16
|
+
return computeblobSha256(input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// For URI inputs (string or {uri: string}), we need to convert to Blob first
|
|
20
|
+
if (
|
|
21
|
+
typeof input === "string" ||
|
|
22
|
+
(input && typeof input === "object" && "uri" in input)
|
|
23
|
+
) {
|
|
24
|
+
const uri =
|
|
25
|
+
typeof input === "string" ? input : (input as { uri: string }).uri;
|
|
26
|
+
return computeFingerprintFromUri(uri);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Unsupported file input type for fingerprinting. Expected Blob, File, URI string, or {uri: string}",
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute fingerprint from a Expo file URI
|
|
38
|
+
* Uses Expo FileSystem to read the file and compute its SHA-256 hash
|
|
39
|
+
*/
|
|
40
|
+
async function computeFingerprintFromUri(uri: string): Promise<string> {
|
|
41
|
+
try {
|
|
42
|
+
// Use Expo FileSystem to read the file as base64
|
|
43
|
+
const FileSystem = await getExpoFileSystem();
|
|
44
|
+
const fileInfo = await FileSystem.getInfoAsync(uri);
|
|
45
|
+
|
|
46
|
+
if (!fileInfo.exists) {
|
|
47
|
+
throw new Error(`File does not exist at URI: ${uri}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read the entire file as base64
|
|
51
|
+
const base64String = await FileSystem.readAsStringAsync(uri, {
|
|
52
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Convert base64 to Uint8Array
|
|
56
|
+
const uint8Array = base64ToUint8Array(base64String);
|
|
57
|
+
|
|
58
|
+
// Compute SHA-256 hash directly on the Uint8Array
|
|
59
|
+
const hashBuffer = await Crypto.digest(
|
|
60
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
61
|
+
uint8Array,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Convert hash to hex string
|
|
65
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
66
|
+
const hashHex = hashArray
|
|
67
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
68
|
+
.join("");
|
|
69
|
+
|
|
70
|
+
return hashHex;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to compute fingerprint from URI ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Dynamically import Expo FileSystem
|
|
80
|
+
* This allows the service to work even if expo-file-system is not installed
|
|
81
|
+
*/
|
|
82
|
+
async function getExpoFileSystem() {
|
|
83
|
+
try {
|
|
84
|
+
return require("expo-file-system");
|
|
85
|
+
} catch (_error) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"expo-file-system is required for URI-based fingerprinting. " +
|
|
88
|
+
"Please install it with: npx expo install expo-file-system",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert base64 string to Uint8Array
|
|
95
|
+
* Uses js-base64 library for cross-platform compatibility
|
|
96
|
+
*/
|
|
97
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
98
|
+
// Use js-base64 for decoding (works in all environments)
|
|
99
|
+
const { fromBase64 } = require("js-base64");
|
|
100
|
+
const binaryString = fromBase64(base64);
|
|
101
|
+
|
|
102
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
103
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
104
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
105
|
+
}
|
|
106
|
+
return bytes;
|
|
107
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
* Expo-specific implementation of HttpClient using fetch API
|
|
15
|
+
* Expo's fetch is similar to browser fetch but optimized for React Native
|
|
16
|
+
*/
|
|
17
|
+
export function createExpoHttpClient(
|
|
18
|
+
config?: ConnectionPoolConfig,
|
|
19
|
+
): HttpClient {
|
|
20
|
+
return new ExpoHttpClient(config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Expo HTTP client implementation
|
|
25
|
+
*/
|
|
26
|
+
class ExpoHttpClient implements HttpClient {
|
|
27
|
+
private metrics: ConnectionMetrics;
|
|
28
|
+
private connectionTimes: number[] = [];
|
|
29
|
+
private requestCount = 0;
|
|
30
|
+
private errorCount = 0;
|
|
31
|
+
private timeoutCount = 0;
|
|
32
|
+
private retryCount = 0;
|
|
33
|
+
private startTime = Date.now();
|
|
34
|
+
|
|
35
|
+
constructor(_config: ConnectionPoolConfig = {}) {
|
|
36
|
+
// Configuration is stored for potential future use
|
|
37
|
+
// Currently Expo doesn't expose connection pool configuration
|
|
38
|
+
|
|
39
|
+
this.metrics = {
|
|
40
|
+
activeConnections: 0,
|
|
41
|
+
totalConnections: 0,
|
|
42
|
+
reuseRate: 0,
|
|
43
|
+
averageConnectionTime: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async request(
|
|
48
|
+
url: string,
|
|
49
|
+
options: HttpRequestOptions = {},
|
|
50
|
+
): Promise<HttpResponse> {
|
|
51
|
+
this.requestCount++;
|
|
52
|
+
|
|
53
|
+
const fetchOptions: RequestInit = {
|
|
54
|
+
method: options.method || "GET",
|
|
55
|
+
headers: options.headers,
|
|
56
|
+
// biome-ignore lint/suspicious/noExplicitAny: Expo's BodyInit type compatibility
|
|
57
|
+
body: options.body as any,
|
|
58
|
+
signal: options.signal as AbortSignal | undefined,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Add credentials if specified
|
|
62
|
+
if (options.credentials) {
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: Expo's RequestCredentials type compatibility
|
|
64
|
+
fetchOptions.credentials = options.credentials as any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Handle timeout
|
|
71
|
+
const timeoutPromise = options.timeout
|
|
72
|
+
? new Promise<never>((_, reject) => {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
this.timeoutCount++;
|
|
75
|
+
reject(new Error(`Request timeout after ${options.timeout}ms`));
|
|
76
|
+
}, options.timeout);
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
const fetchPromise = fetch(url, fetchOptions);
|
|
81
|
+
|
|
82
|
+
const response = timeoutPromise
|
|
83
|
+
? await Promise.race([fetchPromise, timeoutPromise])
|
|
84
|
+
: await fetchPromise;
|
|
85
|
+
|
|
86
|
+
const connectionTime = Date.now() - startTime;
|
|
87
|
+
this.connectionTimes.push(connectionTime);
|
|
88
|
+
this.metrics.totalConnections++;
|
|
89
|
+
this.updateMetrics();
|
|
90
|
+
|
|
91
|
+
// Convert fetch Response to HttpResponse
|
|
92
|
+
return this.adaptResponse(response);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.errorCount++;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private adaptResponse(response: Response): HttpResponse {
|
|
100
|
+
const headers: HeadersLike = {
|
|
101
|
+
get: (name: string) => response.headers.get(name),
|
|
102
|
+
has: (name: string) => response.headers.has(name),
|
|
103
|
+
forEach: (callback: (value: string, name: string) => void) => {
|
|
104
|
+
response.headers.forEach(callback);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
status: response.status,
|
|
110
|
+
statusText: response.statusText,
|
|
111
|
+
headers,
|
|
112
|
+
ok: response.ok,
|
|
113
|
+
json: () => response.json(),
|
|
114
|
+
text: () => response.text(),
|
|
115
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private updateMetrics(): void {
|
|
120
|
+
if (this.connectionTimes.length > 0) {
|
|
121
|
+
const sum = this.connectionTimes.reduce((a, b) => a + b, 0);
|
|
122
|
+
this.metrics.averageConnectionTime = sum / this.connectionTimes.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Estimate reuse rate based on connection times
|
|
126
|
+
// Faster connections are likely reused
|
|
127
|
+
const fastConnections = this.connectionTimes.filter(
|
|
128
|
+
(time) => time < 100,
|
|
129
|
+
).length;
|
|
130
|
+
this.metrics.reuseRate =
|
|
131
|
+
this.connectionTimes.length > 0
|
|
132
|
+
? fastConnections / this.connectionTimes.length
|
|
133
|
+
: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getMetrics(): ConnectionMetrics {
|
|
137
|
+
return { ...this.metrics };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getDetailedMetrics(): DetailedConnectionMetrics {
|
|
141
|
+
const uptime = Date.now() - this.startTime;
|
|
142
|
+
const requestsPerSecond =
|
|
143
|
+
uptime > 0 ? this.requestCount / (uptime / 1000) : 0;
|
|
144
|
+
const errorRate =
|
|
145
|
+
this.requestCount > 0 ? this.errorCount / this.requestCount : 0;
|
|
146
|
+
|
|
147
|
+
const fastConnections = this.connectionTimes.filter(
|
|
148
|
+
(time) => time < 100,
|
|
149
|
+
).length;
|
|
150
|
+
const slowConnections = this.connectionTimes.length - fastConnections;
|
|
151
|
+
|
|
152
|
+
const health = this.calculateHealth(errorRate);
|
|
153
|
+
|
|
154
|
+
const http2Info: Http2Info = {
|
|
155
|
+
supported: false, // Expo doesn't support HTTP/2
|
|
156
|
+
detected: false,
|
|
157
|
+
version: "h1.1",
|
|
158
|
+
multiplexingActive: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...this.metrics,
|
|
163
|
+
health,
|
|
164
|
+
requestsPerSecond,
|
|
165
|
+
errorRate,
|
|
166
|
+
timeouts: this.timeoutCount,
|
|
167
|
+
retries: this.retryCount,
|
|
168
|
+
fastConnections,
|
|
169
|
+
slowConnections,
|
|
170
|
+
http2Info,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private calculateHealth(errorRate: number): ConnectionHealth {
|
|
175
|
+
let status: "healthy" | "degraded" | "poor";
|
|
176
|
+
let score: number;
|
|
177
|
+
const issues: string[] = [];
|
|
178
|
+
const recommendations: string[] = [];
|
|
179
|
+
|
|
180
|
+
if (errorRate > 0.1) {
|
|
181
|
+
status = "poor";
|
|
182
|
+
score = 30;
|
|
183
|
+
issues.push(`High error rate: ${(errorRate * 100).toFixed(1)}%`);
|
|
184
|
+
recommendations.push("Check network connectivity");
|
|
185
|
+
} else if (errorRate > 0.05) {
|
|
186
|
+
status = "degraded";
|
|
187
|
+
score = 60;
|
|
188
|
+
issues.push(`Moderate error rate: ${(errorRate * 100).toFixed(1)}%`);
|
|
189
|
+
recommendations.push("Monitor connection stability");
|
|
190
|
+
} else {
|
|
191
|
+
status = "healthy";
|
|
192
|
+
score = 100;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.metrics.averageConnectionTime > 1000) {
|
|
196
|
+
issues.push(
|
|
197
|
+
`Slow connections: ${this.metrics.averageConnectionTime.toFixed(0)}ms avg`,
|
|
198
|
+
);
|
|
199
|
+
recommendations.push("Check network conditions");
|
|
200
|
+
score = Math.min(score, 70);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { status, score, issues, recommendations };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
reset(): void {
|
|
207
|
+
this.metrics = {
|
|
208
|
+
activeConnections: 0,
|
|
209
|
+
totalConnections: 0,
|
|
210
|
+
reuseRate: 0,
|
|
211
|
+
averageConnectionTime: 0,
|
|
212
|
+
};
|
|
213
|
+
this.connectionTimes = [];
|
|
214
|
+
this.requestCount = 0;
|
|
215
|
+
this.errorCount = 0;
|
|
216
|
+
this.timeoutCount = 0;
|
|
217
|
+
this.retryCount = 0;
|
|
218
|
+
this.startTime = Date.now();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async close(): Promise<void> {
|
|
222
|
+
// Expo fetch doesn't require explicit connection closing
|
|
223
|
+
this.reset();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async warmupConnections(urls: string[]): Promise<void> {
|
|
227
|
+
// Warmup by making HEAD requests to the URLs
|
|
228
|
+
const promises = urls.map((url) =>
|
|
229
|
+
this.request(url, { method: "HEAD" }).catch(() => {
|
|
230
|
+
// Ignore warmup errors
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
await Promise.all(promises);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IdGenerationService } from "@uploadista/client-core";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expo-specific implementation of IdGenerationService using uuid library
|
|
6
|
+
* crypto.randomUUID() is not available in Expo/React Native, so we use the uuid library
|
|
7
|
+
*/
|
|
8
|
+
export function createExpoIdGenerationService(): IdGenerationService {
|
|
9
|
+
return {
|
|
10
|
+
generate(): string {
|
|
11
|
+
return uuidv4();
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo service implementations for Uploadista client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createExpoBase64Service } from "./base64-service";
|
|
6
|
+
export {
|
|
7
|
+
createExpoServices,
|
|
8
|
+
type ExpoServiceOptions,
|
|
9
|
+
} from "./create-expo-services";
|
|
10
|
+
export { createExpoFileReaderService } from "./file-reader-service";
|
|
11
|
+
export { createExpoHttpClient } from "./http-client";
|
|
12
|
+
export { createExpoIdGenerationService } from "./id-generation-service";
|
|
13
|
+
export { createAsyncStorageService } from "./storage-service";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PlatformService, Timeout } from "@uploadista/client-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expo implementation of PlatformService
|
|
5
|
+
*/
|
|
6
|
+
export function createExpoPlatformService(): PlatformService {
|
|
7
|
+
return {
|
|
8
|
+
setTimeout: (callback: () => void, ms: number | undefined) => {
|
|
9
|
+
return globalThis.setTimeout(callback, ms);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
clearTimeout: (id: Timeout) => {
|
|
13
|
+
globalThis.clearTimeout(id as number);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
isBrowser: () => {
|
|
17
|
+
return false;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
isOnline: () => {
|
|
21
|
+
// Expo's NetInfo would need to be imported separately
|
|
22
|
+
// For now, assume online
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
isFileLike: (value: unknown) => {
|
|
27
|
+
// Check for blob-like interface or File-like object
|
|
28
|
+
return (
|
|
29
|
+
value !== null &&
|
|
30
|
+
typeof value === "object" &&
|
|
31
|
+
("uri" in value || "name" in value)
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getFileName: (file: unknown) => {
|
|
36
|
+
if (file !== null && typeof file === "object" && "name" in file) {
|
|
37
|
+
return (file as Record<string, unknown>).name as string | undefined;
|
|
38
|
+
}
|
|
39
|
+
if (file !== null && typeof file === "object" && "uri" in file) {
|
|
40
|
+
const uri = (file as Record<string, unknown>).uri as string | undefined;
|
|
41
|
+
if (uri) {
|
|
42
|
+
return uri.split("/").pop();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
getFileType: (file: unknown) => {
|
|
49
|
+
if (file !== null && typeof file === "object" && "type" in file) {
|
|
50
|
+
return (file as Record<string, unknown>).type as string | undefined;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
getFileSize: (file: unknown) => {
|
|
56
|
+
if (file !== null && typeof file === "object" && "size" in file) {
|
|
57
|
+
return (file as Record<string, unknown>).size as number | undefined;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
getFileLastModified: (file: unknown) => {
|
|
63
|
+
if (file !== null && typeof file === "object" && "lastModified" in file) {
|
|
64
|
+
return (file as Record<string, unknown>).lastModified as
|
|
65
|
+
| number
|
|
66
|
+
| undefined;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
+
import type { StorageService } from "@uploadista/client-core";
|
|
3
|
+
/**
|
|
4
|
+
* Expo-specific implementation of StorageService using AsyncStorage
|
|
5
|
+
* AsyncStorage is provided as an optional peer dependency and must be installed separately
|
|
6
|
+
*/
|
|
7
|
+
export function createAsyncStorageService(): StorageService {
|
|
8
|
+
const findEntries = async (
|
|
9
|
+
prefix: string,
|
|
10
|
+
): Promise<Record<string, string>> => {
|
|
11
|
+
const results: Record<string, string> = {};
|
|
12
|
+
|
|
13
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
14
|
+
for (const key in keys) {
|
|
15
|
+
if (key.startsWith(prefix)) {
|
|
16
|
+
const item = await AsyncStorage.getItem(key);
|
|
17
|
+
if (item) {
|
|
18
|
+
results[key] = item;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return results;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
async getItem(key: string): Promise<string | null> {
|
|
28
|
+
try {
|
|
29
|
+
return await AsyncStorage.getItem(key);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`AsyncStorage getItem error for key ${key}:`, error);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
await AsyncStorage.setItem(key, value);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`AsyncStorage setItem error for key ${key}:`, error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async removeItem(key: string): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
await AsyncStorage.removeItem(key);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`AsyncStorage removeItem error for key ${key}:`, error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async findAll(): Promise<Record<string, string>> {
|
|
55
|
+
return findEntries("");
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async find(prefix: string): Promise<Record<string, string>> {
|
|
59
|
+
return findEntries(prefix);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WebSocketFactory, WebSocketLike } from "@uploadista/client-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expo WebSocket implementation that wraps native WebSocket
|
|
5
|
+
* Expo provides a WebSocket API that is compatible with the browser WebSocket API
|
|
6
|
+
*/
|
|
7
|
+
class ExpoWebSocket implements WebSocketLike {
|
|
8
|
+
readonly CONNECTING = 0;
|
|
9
|
+
readonly OPEN = 1;
|
|
10
|
+
readonly CLOSING = 2;
|
|
11
|
+
readonly CLOSED = 3;
|
|
12
|
+
|
|
13
|
+
readyState: number;
|
|
14
|
+
onopen: (() => void) | null = null;
|
|
15
|
+
onclose: ((event: { code: number; reason: string }) => void) | null = null;
|
|
16
|
+
onerror: ((event: { message: string }) => void) | null = null;
|
|
17
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
18
|
+
|
|
19
|
+
private native: WebSocket;
|
|
20
|
+
|
|
21
|
+
constructor(url: string) {
|
|
22
|
+
this.native = new WebSocket(url);
|
|
23
|
+
this.readyState = this.native.readyState;
|
|
24
|
+
|
|
25
|
+
// Proxy event handlers
|
|
26
|
+
this.native.onopen = () => {
|
|
27
|
+
this.readyState = this.native.readyState;
|
|
28
|
+
this.onopen?.();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.native.onclose = (event) => {
|
|
32
|
+
this.readyState = this.native.readyState;
|
|
33
|
+
this.onclose?.({
|
|
34
|
+
code: event.code ?? 1000,
|
|
35
|
+
reason: event.reason ?? "undefined reason",
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.native.onerror = (event) => {
|
|
40
|
+
this.onerror?.(event);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.native.onmessage = (event) => {
|
|
44
|
+
this.onmessage?.({ data: event.data });
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
send(data: string | Uint8Array): void {
|
|
49
|
+
this.native.send(data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
close(code?: number, reason?: string): void {
|
|
53
|
+
this.native.close(code, reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Factory for creating Expo WebSocket connections
|
|
59
|
+
*/
|
|
60
|
+
export const createExpoWebSocketFactory = (): WebSocketFactory => ({
|
|
61
|
+
create: (url: string): WebSocketLike => new ExpoWebSocket(url),
|
|
62
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for Expo Uploadista client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for file picker operations
|
|
7
|
+
*/
|
|
8
|
+
export interface PickerOptions {
|
|
9
|
+
/** Allowed file types/MIME types */
|
|
10
|
+
allowedTypes?: string[];
|
|
11
|
+
/** Allow multiple selection */
|
|
12
|
+
allowMultiple?: boolean;
|
|
13
|
+
/** Maximum file size in bytes */
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for camera operations
|
|
19
|
+
*/
|
|
20
|
+
export interface CameraOptions {
|
|
21
|
+
/** Camera to use: 'front' or 'back' */
|
|
22
|
+
cameraType?: "front" | "back";
|
|
23
|
+
/** Image quality (0-1) */
|
|
24
|
+
quality?: number;
|
|
25
|
+
/** Maximum width for captured image */
|
|
26
|
+
maxWidth?: number;
|
|
27
|
+
/** Maximum height for captured image */
|
|
28
|
+
maxHeight?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result from a file pick operation
|
|
33
|
+
*/
|
|
34
|
+
export interface FilePickResult {
|
|
35
|
+
/** URI to the file (platform-specific format) */
|
|
36
|
+
uri: string;
|
|
37
|
+
/** File name with extension */
|
|
38
|
+
name: string;
|
|
39
|
+
/** File size in bytes */
|
|
40
|
+
size: number;
|
|
41
|
+
/** MIME type of the file (if available) */
|
|
42
|
+
mimeType?: string;
|
|
43
|
+
/** Local file path (if available) */
|
|
44
|
+
localPath?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Information about a file
|
|
49
|
+
*/
|
|
50
|
+
export interface FileInfo {
|
|
51
|
+
/** URI to the file */
|
|
52
|
+
uri: string;
|
|
53
|
+
/** File name */
|
|
54
|
+
name: string;
|
|
55
|
+
/** File size in bytes */
|
|
56
|
+
size: number;
|
|
57
|
+
/** MIME type (if available) */
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
/** Last modified timestamp */
|
|
60
|
+
modificationTime?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Interface for file system abstraction layer
|
|
65
|
+
* Provides pluggable access to file system APIs across different Expo environments
|
|
66
|
+
*/
|
|
67
|
+
export interface FileSystemProvider {
|
|
68
|
+
/**
|
|
69
|
+
* Opens a document picker for selecting files
|
|
70
|
+
* @param options - Configuration for the picker
|
|
71
|
+
* @returns Promise resolving to picked file information
|
|
72
|
+
*/
|
|
73
|
+
pickDocument(options?: PickerOptions): Promise<FilePickResult>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Opens an image picker for selecting images from gallery
|
|
77
|
+
* @param options - Configuration for the picker
|
|
78
|
+
* @returns Promise resolving to picked image information
|
|
79
|
+
*/
|
|
80
|
+
pickImage(options?: PickerOptions): Promise<FilePickResult>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Opens a video picker for selecting videos from gallery
|
|
84
|
+
* @param options - Configuration for the picker
|
|
85
|
+
* @returns Promise resolving to picked video information
|
|
86
|
+
*/
|
|
87
|
+
pickVideo(options?: PickerOptions): Promise<FilePickResult>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Captures a photo using the device camera
|
|
91
|
+
* @param options - Configuration for camera
|
|
92
|
+
* @returns Promise resolving to captured photo information
|
|
93
|
+
*/
|
|
94
|
+
pickCamera(options?: CameraOptions): Promise<FilePickResult>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets a URI for a document that can be read
|
|
98
|
+
* @param filePath - Path to the document
|
|
99
|
+
* @returns Promise resolving to accessible URI
|
|
100
|
+
*/
|
|
101
|
+
getDocumentUri(filePath: string): Promise<string>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reads file contents as ArrayBuffer
|
|
105
|
+
* @param uri - URI to read from
|
|
106
|
+
* @returns Promise resolving to file contents as ArrayBuffer
|
|
107
|
+
*/
|
|
108
|
+
readFile(uri: string): Promise<ArrayBuffer>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gets information about a file
|
|
112
|
+
* @param uri - URI of the file
|
|
113
|
+
* @returns Promise resolving to file information
|
|
114
|
+
*/
|
|
115
|
+
getFileInfo(uri: string): Promise<FileInfo>;
|
|
116
|
+
}
|