@oxyhq/services 5.16.3 → 5.16.7
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/lib/commonjs/core/OxyServices.base.js +3 -1
- package/lib/commonjs/core/OxyServices.base.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.assets.js +20 -330
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/commonjs/index.js +156 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +45 -7
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/index.js +60 -20
- package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +230 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/index.js +96 -30
- package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +75 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +50 -2
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAssets.js +8 -29
- package/lib/commonjs/ui/hooks/useAssets.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +14 -10
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/core/OxyServices.base.js +3 -1
- package/lib/module/core/OxyServices.base.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.assets.js +20 -331
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/module/index.js +17 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +45 -7
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/index.js +12 -3
- package/lib/module/ui/hooks/mutations/index.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +227 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/index.js +15 -4
- package/lib/module/ui/hooks/queries/index.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +73 -0
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/useServicesQueries.js +50 -2
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/module/ui/hooks/useAssets.js +8 -29
- package/lib/module/ui/hooks/useAssets.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +12 -10
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +1 -70
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +4 -14
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/index.d.ts +8 -2
- package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +19 -0
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/index.d.ts +9 -3
- package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +6 -0
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useAssets.d.ts.map +1 -1
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/OxyServices.base.ts +5 -1
- package/src/core/mixins/OxyServices.assets.ts +21 -338
- package/src/index.ts +49 -2
- package/src/ui/context/OxyContext.tsx +49 -7
- package/src/ui/hooks/mutations/index.ts +24 -3
- package/src/ui/hooks/mutations/useAccountMutations.ts +205 -0
- package/src/ui/hooks/queries/index.ts +29 -4
- package/src/ui/hooks/queries/queryKeys.ts +6 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +69 -0
- package/src/ui/hooks/queries/useServicesQueries.ts +49 -2
- package/src/ui/hooks/useAssets.ts +8 -28
- package/src/ui/hooks/useSessionManagement.ts +1 -1
- package/src/ui/screens/FileManagementScreen.tsx +10 -11
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
* Asset & File Methods Mixin
|
|
3
|
-
*/
|
|
4
|
-
import type { AccountStorageUsageResponse, AssetInitResponse, AssetUrlResponse, AssetVariant } from '../../models/interfaces';
|
|
1
|
+
import type { AccountStorageUsageResponse, AssetUrlResponse, AssetVariant } from '../../models/interfaces';
|
|
5
2
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
6
|
-
import { File } from 'expo-file-system';
|
|
7
3
|
|
|
8
4
|
export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
9
5
|
return class extends Base {
|
|
10
6
|
constructor(...args: any[]) {
|
|
11
7
|
super(...(args as [any]));
|
|
12
8
|
}
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// FILE METHODS (Convenience wrappers using Asset Service)
|
|
15
|
-
// ============================================================================
|
|
16
9
|
|
|
17
10
|
/**
|
|
18
11
|
* Delete file
|
|
19
12
|
*/
|
|
20
13
|
async deleteFile(fileId: string): Promise<any> {
|
|
21
14
|
try {
|
|
22
|
-
// Central Asset Service delete with force=true behavior controlled by caller via assetDelete
|
|
23
15
|
return await this.makeRequest('DELETE', `/api/assets/${encodeURIComponent(fileId)}`, undefined, { cache: false });
|
|
24
16
|
} catch (error) {
|
|
25
17
|
throw this.handleError(error);
|
|
@@ -28,8 +20,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
28
20
|
|
|
29
21
|
/**
|
|
30
22
|
* Get file download URL (synchronous - uses stream endpoint for images to avoid ORB blocking)
|
|
31
|
-
* The stream endpoint serves images directly with proper CORS headers, avoiding browser ORB blocking
|
|
32
|
-
* For better performance with signed URLs, use getFileDownloadUrlAsync when possible
|
|
33
23
|
*/
|
|
34
24
|
getFileDownloadUrl(fileId: string, variant?: string, expiresIn?: number): string {
|
|
35
25
|
const base = this.getBaseURL();
|
|
@@ -40,16 +30,12 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
40
30
|
const token = this.getClient().getAccessToken();
|
|
41
31
|
if (token) params.set('token', token);
|
|
42
32
|
|
|
43
|
-
// Use stream endpoint which serves images directly with proper CORS headers
|
|
44
|
-
// This avoids ERR_BLOCKED_BY_ORB errors that occur with redirect-based endpoints
|
|
45
33
|
const qs = params.toString();
|
|
46
34
|
return `${base}/api/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
/**
|
|
50
38
|
* Get file download URL asynchronously (returns signed URL directly from CDN)
|
|
51
|
-
* This is more efficient than the synchronous version as it avoids redirects
|
|
52
|
-
* Use this when you can handle async operations (e.g., in useEffect, useMemo with async)
|
|
53
39
|
*/
|
|
54
40
|
async getFileDownloadUrlAsync(fileId: string, variant?: string, expiresIn?: number): Promise<string> {
|
|
55
41
|
try {
|
|
@@ -62,7 +48,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
62
48
|
|
|
63
49
|
return url || this.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
64
50
|
} catch (error) {
|
|
65
|
-
// Fallback to synchronous method on error
|
|
66
51
|
return this.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
67
52
|
}
|
|
68
53
|
}
|
|
@@ -76,7 +61,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
76
61
|
if (limit) paramsObj.limit = limit;
|
|
77
62
|
if (offset) paramsObj.offset = offset;
|
|
78
63
|
return await this.makeRequest('GET', '/api/assets', paramsObj, {
|
|
79
|
-
cache: false,
|
|
64
|
+
cache: false,
|
|
80
65
|
});
|
|
81
66
|
} catch (error) {
|
|
82
67
|
throw this.handleError(error);
|
|
@@ -85,8 +70,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
85
70
|
|
|
86
71
|
/**
|
|
87
72
|
* Get account storage usage (server-side usage aggregated from assets)
|
|
88
|
-
*
|
|
89
|
-
* NOTE: This is NOT the same as `getStorage()` from the language mixin (which returns local storage).
|
|
90
73
|
*/
|
|
91
74
|
async getAccountStorageUsage(): Promise<AccountStorageUsageResponse> {
|
|
92
75
|
try {
|
|
@@ -142,7 +125,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
142
125
|
|
|
143
126
|
/**
|
|
144
127
|
* Get batch access to multiple files
|
|
145
|
-
* Returns URLs and access status for each file
|
|
146
128
|
*/
|
|
147
129
|
async getBatchFileAccess(fileIds: string[], context?: string): Promise<Record<string, any>> {
|
|
148
130
|
try {
|
|
@@ -161,7 +143,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
161
143
|
async getFileDownloadUrls(fileIds: string[], context?: string): Promise<Record<string, string>> {
|
|
162
144
|
const response: any = await this.getBatchFileAccess(fileIds, context);
|
|
163
145
|
const urls: Record<string, string> = {};
|
|
164
|
-
// response.results is the map
|
|
165
146
|
const results = response.results || {};
|
|
166
147
|
for (const [id, result] of Object.entries(results as Record<string, any>)) {
|
|
167
148
|
if (result.allowed && result.url) {
|
|
@@ -175,281 +156,46 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
175
156
|
* Upload raw file data
|
|
176
157
|
*/
|
|
177
158
|
async uploadRawFile(file: File | Blob, visibility?: 'private' | 'public' | 'unlisted', metadata?: Record<string, any>): Promise<any> {
|
|
178
|
-
// Switch to Central Asset Service upload flow
|
|
179
159
|
return this.assetUpload(file as File, visibility, metadata);
|
|
180
160
|
}
|
|
181
161
|
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// CENTRAL ASSET SERVICE METHODS
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Get base64 string from file - uses expo-file-system when URI is available
|
|
188
|
-
* Returns base64 string directly for use with expo-crypto
|
|
189
|
-
*/
|
|
190
|
-
async getFileBase64(file: File | Blob): Promise<string> {
|
|
191
|
-
// Check for URI from DocumentPicker (Expo 54)
|
|
192
|
-
const uri = (file as any).uri;
|
|
193
|
-
if (uri && typeof uri === 'string') {
|
|
194
|
-
// Use Expo 54 FileSystem API
|
|
195
|
-
try {
|
|
196
|
-
const fileInstance = new File(uri);
|
|
197
|
-
return await fileInstance.base64();
|
|
198
|
-
} catch (error: any) {
|
|
199
|
-
throw new Error(`Failed to read file from URI: ${error.message || 'Unknown error'}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// For files without URI (web Blobs), convert to base64 using fetch
|
|
204
|
-
const blobUrl = URL.createObjectURL(file);
|
|
205
|
-
try {
|
|
206
|
-
const response = await fetch(blobUrl);
|
|
207
|
-
if (!response.ok) {
|
|
208
|
-
throw new Error(`Failed to fetch file: ${response.statusText || 'HTTP ' + response.status}`);
|
|
209
|
-
}
|
|
210
|
-
const buffer = await response.arrayBuffer();
|
|
211
|
-
// Convert ArrayBuffer to base64 using chunked approach to avoid stack overflow
|
|
212
|
-
return this.arrayBufferToBase64Safe(buffer);
|
|
213
|
-
} finally {
|
|
214
|
-
URL.revokeObjectURL(blobUrl);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Convert File or Blob to ArrayBuffer - uses expo-file-system when URI available
|
|
220
|
-
*/
|
|
221
|
-
async fileToArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
|
222
|
-
// Check for URI from DocumentPicker (Expo 54)
|
|
223
|
-
const uri = (file as any).uri;
|
|
224
|
-
if (uri && typeof uri === 'string') {
|
|
225
|
-
// Use Expo 54 FileSystem API
|
|
226
|
-
try {
|
|
227
|
-
const fileInstance = new File(uri);
|
|
228
|
-
const bytes = await fileInstance.bytes();
|
|
229
|
-
return bytes.buffer;
|
|
230
|
-
} catch (error: any) {
|
|
231
|
-
throw new Error(`Failed to read file from URI: ${error.message || 'Unknown error'}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// For files without URI, use native arrayBuffer if available, else fetch
|
|
236
|
-
if (typeof (file as File).arrayBuffer === 'function') {
|
|
237
|
-
return await (file as File).arrayBuffer();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Fallback: fetch via blob URL
|
|
241
|
-
const blobUrl = URL.createObjectURL(file);
|
|
242
|
-
try {
|
|
243
|
-
const response = await fetch(blobUrl);
|
|
244
|
-
if (!response.ok) {
|
|
245
|
-
throw new Error(`Failed to fetch file: ${response.statusText || 'HTTP ' + response.status}`);
|
|
246
|
-
}
|
|
247
|
-
return await response.arrayBuffer();
|
|
248
|
-
} finally {
|
|
249
|
-
URL.revokeObjectURL(blobUrl);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Convert ArrayBuffer to hex string
|
|
255
|
-
*/
|
|
256
|
-
arrayBufferToHex(buffer: ArrayBuffer): string {
|
|
257
|
-
const bytes = new Uint8Array(buffer);
|
|
258
|
-
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Convert base64 string to ArrayBuffer (safe for large files)
|
|
263
|
-
*/
|
|
264
|
-
base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
265
|
-
const binaryString = atob(base64);
|
|
266
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
267
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
268
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
269
|
-
}
|
|
270
|
-
return bytes.buffer;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Convert binary string to base64 (manual implementation for Node.js when btoa is not available)
|
|
275
|
-
*/
|
|
276
|
-
binaryToBase64(binary: string): string {
|
|
277
|
-
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
278
|
-
let result = '';
|
|
279
|
-
let i = 0;
|
|
280
|
-
|
|
281
|
-
while (i < binary.length) {
|
|
282
|
-
const a = binary.charCodeAt(i++);
|
|
283
|
-
const b = i < binary.length ? binary.charCodeAt(i++) : 0;
|
|
284
|
-
const c = i < binary.length ? binary.charCodeAt(i++) : 0;
|
|
285
|
-
|
|
286
|
-
const bitmap = (a << 16) | (b << 8) | c;
|
|
287
|
-
|
|
288
|
-
result += base64Chars.charAt((bitmap >> 18) & 63);
|
|
289
|
-
result += base64Chars.charAt((bitmap >> 12) & 63);
|
|
290
|
-
result += i - 2 < binary.length ? base64Chars.charAt((bitmap >> 6) & 63) : '=';
|
|
291
|
-
result += i - 1 < binary.length ? base64Chars.charAt(bitmap & 63) : '=';
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return result;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Convert ArrayBuffer to base64 string (safe chunked approach to avoid stack overflow)
|
|
299
|
-
*/
|
|
300
|
-
arrayBufferToBase64Safe(buffer: ArrayBuffer): string {
|
|
301
|
-
const bytes = new Uint8Array(buffer);
|
|
302
|
-
const chunkSize = 8192; // Process in chunks to avoid stack overflow
|
|
303
|
-
|
|
304
|
-
// Use chunked approach for large buffers
|
|
305
|
-
if (bytes.length > chunkSize) {
|
|
306
|
-
let binary = '';
|
|
307
|
-
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
308
|
-
const chunk = bytes.slice(i, i + chunkSize);
|
|
309
|
-
binary += String.fromCharCode.apply(null, Array.from(chunk));
|
|
310
|
-
}
|
|
311
|
-
// Use btoa if available (browser/React Native), otherwise use manual encoding
|
|
312
|
-
return typeof btoa !== 'undefined' ? btoa(binary) : this.binaryToBase64(binary);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Small buffers can use direct conversion
|
|
316
|
-
const binary = String.fromCharCode.apply(null, Array.from(bytes));
|
|
317
|
-
return typeof btoa !== 'undefined' ? btoa(binary) : this.binaryToBase64(binary);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Calculate SHA256 hash of file content - uses expo-crypto (Expo 54 unified API)
|
|
322
|
-
*/
|
|
323
|
-
async calculateSHA256(file: File | Blob): Promise<string> {
|
|
324
|
-
// Use expo-crypto (works on all platforms with Expo 54)
|
|
325
|
-
const CryptoModule = await import('expo-crypto' as any).catch(() => null);
|
|
326
|
-
if (!CryptoModule) {
|
|
327
|
-
throw new Error('expo-crypto is not available. Install it with: npx expo install expo-crypto');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const Crypto = CryptoModule.default || CryptoModule;
|
|
331
|
-
if (!Crypto?.digestStringAsync) {
|
|
332
|
-
throw new Error('expo-crypto.digestStringAsync is not available');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
// Read file as base64 (uses expo-file-system if URI available, else converts)
|
|
337
|
-
const base64 = await this.getFileBase64(file);
|
|
338
|
-
const algorithm = (Crypto as any).CryptoDigestAlgorithm?.SHA256 || 'SHA256';
|
|
339
|
-
const encoding = (Crypto as any).CryptoEncoding?.BASE64 || 'base64';
|
|
340
|
-
return await Crypto.digestStringAsync(algorithm, base64, { encoding });
|
|
341
|
-
} catch (error: any) {
|
|
342
|
-
const errorMessage = error?.message || String(error) || 'Unknown error';
|
|
343
|
-
throw new Error(`Failed to calculate SHA256 hash: ${errorMessage}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Initialize asset upload - returns pre-signed URL and file ID
|
|
349
|
-
*/
|
|
350
|
-
async assetInit(sha256: string, size: number, mime: string): Promise<AssetInitResponse> {
|
|
351
|
-
try {
|
|
352
|
-
return await this.makeRequest<AssetInitResponse>('POST', '/api/assets/init', {
|
|
353
|
-
sha256,
|
|
354
|
-
size,
|
|
355
|
-
mime
|
|
356
|
-
}, { cache: false });
|
|
357
|
-
} catch (error) {
|
|
358
|
-
throw this.handleError(error);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Complete asset upload - commit metadata and trigger variant generation
|
|
364
|
-
*/
|
|
365
|
-
async assetComplete(fileId: string, originalName: string, size: number, mime: string, visibility?: 'private' | 'public' | 'unlisted', metadata?: Record<string, any>): Promise<any> {
|
|
366
|
-
try {
|
|
367
|
-
return await this.makeRequest('POST', '/api/assets/complete', {
|
|
368
|
-
fileId,
|
|
369
|
-
originalName,
|
|
370
|
-
size,
|
|
371
|
-
mime,
|
|
372
|
-
visibility,
|
|
373
|
-
metadata
|
|
374
|
-
}, { cache: false });
|
|
375
|
-
} catch (error) {
|
|
376
|
-
throw this.handleError(error);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
162
|
/**
|
|
381
163
|
* Upload file using Central Asset Service
|
|
382
164
|
*/
|
|
383
165
|
async assetUpload(file: File, visibility?: 'private' | 'public' | 'unlisted', metadata?: Record<string, any>, onProgress?: (progress: number) => void): Promise<any> {
|
|
384
166
|
const fileName = file.name || 'unknown';
|
|
385
167
|
const fileSize = file.size;
|
|
386
|
-
const fileType = file.type || 'application/octet-stream';
|
|
387
168
|
|
|
388
169
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
} catch (error: any) {
|
|
394
|
-
throw new Error(`Failed to calculate file hash for "${fileName}": ${error.message || 'Unknown error'}`);
|
|
170
|
+
const formData = new FormData();
|
|
171
|
+
formData.append('file', file);
|
|
172
|
+
if (visibility) {
|
|
173
|
+
formData.append('visibility', visibility);
|
|
395
174
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
let initResponse: AssetInitResponse;
|
|
399
|
-
try {
|
|
400
|
-
initResponse = await this.assetInit(sha256, fileSize, fileType);
|
|
401
|
-
} catch (error: any) {
|
|
402
|
-
throw new Error(`Failed to initialize upload for "${fileName}": ${error.message || 'Unknown error'}`);
|
|
175
|
+
if (metadata) {
|
|
176
|
+
formData.append('metadata', JSON.stringify(metadata));
|
|
403
177
|
}
|
|
404
178
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const fd = new FormData();
|
|
412
|
-
fd.append('file', file);
|
|
413
|
-
await this.getClient().request({
|
|
414
|
-
method: 'POST',
|
|
415
|
-
url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
|
|
416
|
-
data: fd,
|
|
417
|
-
cache: false,
|
|
418
|
-
});
|
|
419
|
-
} catch (directUploadError: any) {
|
|
420
|
-
throw new Error(
|
|
421
|
-
`Failed to upload file "${fileName}" (${(fileSize / 1024 / 1024).toFixed(2)}MB): ` +
|
|
422
|
-
`Presigned URL failed: ${uploadError.message || 'Unknown error'}. ` +
|
|
423
|
-
`Direct upload failed: ${directUploadError.message || 'Unknown error'}`
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
179
|
+
const response = await this.getClient().request<{ file: any }>({
|
|
180
|
+
method: 'POST',
|
|
181
|
+
url: '/api/assets/upload',
|
|
182
|
+
data: formData,
|
|
183
|
+
cache: false,
|
|
184
|
+
});
|
|
427
185
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return await this.assetComplete(
|
|
431
|
-
initResponse.fileId,
|
|
432
|
-
fileName,
|
|
433
|
-
fileSize,
|
|
434
|
-
fileType,
|
|
435
|
-
visibility,
|
|
436
|
-
metadata
|
|
437
|
-
);
|
|
438
|
-
} catch (error: any) {
|
|
439
|
-
throw new Error(`Failed to complete upload for "${fileName}": ${error.message || 'Unknown error'}`);
|
|
186
|
+
if (onProgress && response) {
|
|
187
|
+
onProgress(100);
|
|
440
188
|
}
|
|
189
|
+
|
|
190
|
+
return response;
|
|
441
191
|
} catch (error) {
|
|
442
|
-
// Log the original error for debugging
|
|
443
192
|
console.error('File upload error:', error);
|
|
444
193
|
|
|
445
|
-
// Preserve original error message before passing to handleError
|
|
446
|
-
// This ensures we don't lose the error details
|
|
447
194
|
let errorMessage = 'File upload failed';
|
|
448
195
|
|
|
449
196
|
if (error instanceof Error) {
|
|
450
197
|
errorMessage = error.message || errorMessage;
|
|
451
198
|
} else if (error && typeof error === 'object') {
|
|
452
|
-
// Try to extract message from various error formats
|
|
453
199
|
if ('message' in error) {
|
|
454
200
|
errorMessage = String((error as any).message) || errorMessage;
|
|
455
201
|
} else if ('error' in error && typeof (error as any).error === 'string') {
|
|
@@ -461,66 +207,28 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
461
207
|
errorMessage = String(error) || errorMessage;
|
|
462
208
|
}
|
|
463
209
|
|
|
464
|
-
// Add file context to error for better debugging
|
|
465
210
|
const contextError = error as Error & { fileContext?: Record<string, unknown> };
|
|
466
211
|
if (!contextError.fileContext) {
|
|
467
212
|
contextError.fileContext = {
|
|
468
213
|
fileName,
|
|
469
214
|
fileSize,
|
|
470
|
-
fileType,
|
|
471
215
|
};
|
|
472
216
|
}
|
|
473
217
|
|
|
474
|
-
// If the error already has a message, preserve it
|
|
475
218
|
if (error instanceof Error && error.message) {
|
|
476
|
-
// Pass through handleError but ensure message is preserved
|
|
477
219
|
const handledError = this.handleError(contextError);
|
|
478
|
-
// If handleError stripped the message, restore it
|
|
479
220
|
if (!handledError.message || handledError.message.trim() === 'An unexpected error occurred') {
|
|
480
221
|
handledError.message = errorMessage;
|
|
481
222
|
}
|
|
482
223
|
throw handledError;
|
|
483
224
|
}
|
|
484
225
|
|
|
485
|
-
// For non-Error objects, create a new Error with the message
|
|
486
226
|
const newError = new Error(errorMessage);
|
|
487
227
|
(newError as any).fileContext = contextError.fileContext;
|
|
488
228
|
throw this.handleError(newError);
|
|
489
229
|
}
|
|
490
230
|
}
|
|
491
231
|
|
|
492
|
-
/**
|
|
493
|
-
* Upload file to pre-signed URL
|
|
494
|
-
*/
|
|
495
|
-
public async uploadToPresignedUrl(url: string, file: File, onProgress?: (progress: number) => void): Promise<void> {
|
|
496
|
-
return new Promise((resolve, reject) => {
|
|
497
|
-
const xhr = new XMLHttpRequest();
|
|
498
|
-
|
|
499
|
-
xhr.upload.addEventListener('progress', (event) => {
|
|
500
|
-
if (event.lengthComputable && onProgress) {
|
|
501
|
-
const progress = (event.loaded / event.total) * 100;
|
|
502
|
-
onProgress(progress);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
xhr.addEventListener('load', () => {
|
|
507
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
508
|
-
resolve();
|
|
509
|
-
} else {
|
|
510
|
-
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
xhr.addEventListener('error', () => {
|
|
515
|
-
reject(new Error('Upload failed'));
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
xhr.open('PUT', url);
|
|
519
|
-
xhr.setRequestHeader('Content-Type', file.type);
|
|
520
|
-
xhr.send(file);
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
232
|
/**
|
|
525
233
|
* Link asset to an entity
|
|
526
234
|
*/
|
|
@@ -557,7 +265,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
557
265
|
try {
|
|
558
266
|
return await this.makeRequest('GET', `/api/assets/${fileId}`, undefined, {
|
|
559
267
|
cache: true,
|
|
560
|
-
cacheTTL: 5 * 60 * 1000,
|
|
268
|
+
cacheTTL: 5 * 60 * 1000,
|
|
561
269
|
});
|
|
562
270
|
} catch (error) {
|
|
563
271
|
throw this.handleError(error);
|
|
@@ -575,7 +283,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
575
283
|
|
|
576
284
|
return await this.makeRequest<AssetUrlResponse>('GET', `/api/assets/${fileId}/url`, params, {
|
|
577
285
|
cache: true,
|
|
578
|
-
cacheTTL: 10 * 60 * 1000,
|
|
286
|
+
cacheTTL: 10 * 60 * 1000,
|
|
579
287
|
});
|
|
580
288
|
} catch (error) {
|
|
581
289
|
throw this.handleError(error);
|
|
@@ -619,9 +327,6 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
619
327
|
|
|
620
328
|
/**
|
|
621
329
|
* Update asset visibility
|
|
622
|
-
* @param fileId - The file ID
|
|
623
|
-
* @param visibility - New visibility level ('private', 'public', or 'unlisted')
|
|
624
|
-
* @returns Updated asset information
|
|
625
330
|
*/
|
|
626
331
|
async assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<any> {
|
|
627
332
|
try {
|
|
@@ -633,42 +338,20 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
633
338
|
}
|
|
634
339
|
}
|
|
635
340
|
|
|
636
|
-
/**
|
|
637
|
-
* Helper: Upload and link avatar with automatic public visibility
|
|
638
|
-
* @param file - The avatar file
|
|
639
|
-
* @param userId - User ID to link to
|
|
640
|
-
* @param app - App name (defaults to 'profiles')
|
|
641
|
-
* @returns The uploaded and linked asset
|
|
642
|
-
*/
|
|
643
341
|
async uploadAvatar(file: File, userId: string, app: string = 'profiles'): Promise<any> {
|
|
644
342
|
try {
|
|
645
|
-
// Upload as public
|
|
646
343
|
const asset = await this.assetUpload(file, 'public');
|
|
647
|
-
|
|
648
|
-
// Link to user profile as avatar
|
|
649
344
|
await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
|
|
650
|
-
|
|
651
345
|
return asset;
|
|
652
346
|
} catch (error) {
|
|
653
347
|
throw this.handleError(error);
|
|
654
348
|
}
|
|
655
349
|
}
|
|
656
350
|
|
|
657
|
-
/**
|
|
658
|
-
* Helper: Upload and link profile banner with automatic public visibility
|
|
659
|
-
* @param file - The banner file
|
|
660
|
-
* @param userId - User ID to link to
|
|
661
|
-
* @param app - App name (defaults to 'profiles')
|
|
662
|
-
* @returns The uploaded and linked asset
|
|
663
|
-
*/
|
|
664
351
|
async uploadProfileBanner(file: File, userId: string, app: string = 'profiles'): Promise<any> {
|
|
665
352
|
try {
|
|
666
|
-
// Upload as public
|
|
667
353
|
const asset = await this.assetUpload(file, 'public');
|
|
668
|
-
|
|
669
|
-
// Link to user profile as banner
|
|
670
354
|
await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
|
|
671
|
-
|
|
672
355
|
return asset;
|
|
673
356
|
} catch (error) {
|
|
674
357
|
throw this.handleError(error);
|
|
@@ -696,7 +379,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
696
379
|
Object.keys(params).length ? params : undefined,
|
|
697
380
|
{
|
|
698
381
|
cache: true,
|
|
699
|
-
cacheTTL: cacheTTL ?? 10 * 60 * 1000,
|
|
382
|
+
cacheTTL: cacheTTL ?? 10 * 60 * 1000,
|
|
700
383
|
}
|
|
701
384
|
);
|
|
702
385
|
|
package/src/index.ts
CHANGED
|
@@ -112,13 +112,60 @@ export type {
|
|
|
112
112
|
MinimalUserData
|
|
113
113
|
} from './models/session';
|
|
114
114
|
|
|
115
|
-
// UI
|
|
115
|
+
// UI hooks - Stores
|
|
116
116
|
export { useAuthStore } from './ui/stores/authStore';
|
|
117
|
-
export {
|
|
117
|
+
export {
|
|
118
|
+
useAssetStore,
|
|
119
|
+
useAssets as useAssetsStore,
|
|
120
|
+
useAsset,
|
|
121
|
+
useUploadProgress,
|
|
122
|
+
useAssetLoading,
|
|
123
|
+
useAssetErrors,
|
|
124
|
+
useAssetsByApp,
|
|
125
|
+
useAssetsByEntity,
|
|
126
|
+
useAssetUsageCount,
|
|
127
|
+
useIsAssetLinked
|
|
128
|
+
} from './ui/stores/assetStore';
|
|
129
|
+
|
|
130
|
+
// UI hooks - Custom hooks
|
|
118
131
|
export { useSessionSocket } from './ui/hooks/useSessionSocket';
|
|
119
132
|
export { useAssets, setOxyAssetInstance } from './ui/hooks/useAssets';
|
|
120
133
|
export { useFileDownloadUrl, setOxyFileUrlInstance } from './ui/hooks/useFileDownloadUrl';
|
|
121
134
|
|
|
135
|
+
// UI hooks - Query hooks (TanStack Query)
|
|
136
|
+
export {
|
|
137
|
+
// Account queries
|
|
138
|
+
useUserProfile,
|
|
139
|
+
useUserProfiles,
|
|
140
|
+
useCurrentUser,
|
|
141
|
+
useUserById,
|
|
142
|
+
useUserByUsername,
|
|
143
|
+
useUsersBySessions,
|
|
144
|
+
usePrivacySettings,
|
|
145
|
+
// Service queries
|
|
146
|
+
useSessions,
|
|
147
|
+
useSession,
|
|
148
|
+
useDeviceSessions,
|
|
149
|
+
useUserDevices,
|
|
150
|
+
useSecurityInfo,
|
|
151
|
+
} from './ui/hooks/queries';
|
|
152
|
+
|
|
153
|
+
// UI hooks - Mutation hooks (TanStack Query)
|
|
154
|
+
export {
|
|
155
|
+
// Account mutations
|
|
156
|
+
useUpdateProfile,
|
|
157
|
+
useUploadAvatar,
|
|
158
|
+
useUpdateAccountSettings,
|
|
159
|
+
useUpdatePrivacySettings,
|
|
160
|
+
useUploadFile,
|
|
161
|
+
// Service mutations
|
|
162
|
+
useSwitchSession,
|
|
163
|
+
useLogoutSession,
|
|
164
|
+
useLogoutAll,
|
|
165
|
+
useUpdateDeviceName,
|
|
166
|
+
useRemoveDevice,
|
|
167
|
+
} from './ui/hooks/mutations';
|
|
168
|
+
|
|
122
169
|
// UI components
|
|
123
170
|
export { OxySignInButton } from './ui/components/OxySignInButton';
|
|
124
171
|
export { OxyLogo, FollowButton } from './ui';
|
|
@@ -380,16 +380,40 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
380
380
|
if (!storage) return;
|
|
381
381
|
|
|
382
382
|
let wasOffline = false;
|
|
383
|
-
let
|
|
383
|
+
let checkTimeout: NodeJS.Timeout | null = null;
|
|
384
|
+
|
|
385
|
+
// Circuit breaker and exponential backoff state
|
|
386
|
+
const stateRef = {
|
|
387
|
+
consecutiveFailures: 0,
|
|
388
|
+
currentInterval: 10000, // Start with 10 seconds
|
|
389
|
+
baseInterval: 10000, // Base interval in milliseconds
|
|
390
|
+
maxInterval: 60000, // Maximum interval (60 seconds)
|
|
391
|
+
maxFailures: 5, // Circuit breaker threshold
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const scheduleNextCheck = () => {
|
|
395
|
+
if (checkTimeout) {
|
|
396
|
+
clearTimeout(checkTimeout);
|
|
397
|
+
}
|
|
398
|
+
checkTimeout = setTimeout(() => {
|
|
399
|
+
checkNetworkAndSync();
|
|
400
|
+
}, stateRef.currentInterval);
|
|
401
|
+
};
|
|
384
402
|
|
|
385
403
|
const checkNetworkAndSync = async () => {
|
|
386
404
|
try {
|
|
387
405
|
// Try a lightweight health check to see if we're online
|
|
388
406
|
await oxyServices.healthCheck().catch(() => {
|
|
389
407
|
wasOffline = true;
|
|
390
|
-
|
|
408
|
+
throw new Error('Health check failed');
|
|
391
409
|
});
|
|
392
410
|
|
|
411
|
+
// Health check succeeded - reset circuit breaker and backoff
|
|
412
|
+
if (stateRef.consecutiveFailures > 0) {
|
|
413
|
+
stateRef.consecutiveFailures = 0;
|
|
414
|
+
stateRef.currentInterval = stateRef.baseInterval;
|
|
415
|
+
}
|
|
416
|
+
|
|
393
417
|
// If we were offline and now we're online, sync identity if needed
|
|
394
418
|
if (wasOffline) {
|
|
395
419
|
if (__DEV__ && logger) {
|
|
@@ -414,18 +438,36 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
414
438
|
} catch (error) {
|
|
415
439
|
// Network check failed - we're offline
|
|
416
440
|
wasOffline = true;
|
|
441
|
+
|
|
442
|
+
// Increment failure count and apply exponential backoff
|
|
443
|
+
stateRef.consecutiveFailures++;
|
|
444
|
+
|
|
445
|
+
// Calculate new interval with exponential backoff, capped at maxInterval
|
|
446
|
+
const backoffMultiplier = Math.min(
|
|
447
|
+
Math.pow(2, stateRef.consecutiveFailures - 1),
|
|
448
|
+
stateRef.maxInterval / stateRef.baseInterval
|
|
449
|
+
);
|
|
450
|
+
stateRef.currentInterval = Math.min(
|
|
451
|
+
stateRef.baseInterval * backoffMultiplier,
|
|
452
|
+
stateRef.maxInterval
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// If we hit the circuit breaker threshold, use max interval
|
|
456
|
+
if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
|
|
457
|
+
stateRef.currentInterval = stateRef.maxInterval;
|
|
458
|
+
}
|
|
459
|
+
} finally {
|
|
460
|
+
// Always schedule next check (will use updated interval)
|
|
461
|
+
scheduleNextCheck();
|
|
417
462
|
}
|
|
418
463
|
};
|
|
419
464
|
|
|
420
465
|
// Check immediately
|
|
421
466
|
checkNetworkAndSync();
|
|
422
467
|
|
|
423
|
-
// Check periodically (every 10 seconds when app is active)
|
|
424
|
-
checkInterval = setInterval(checkNetworkAndSync, 10000);
|
|
425
|
-
|
|
426
468
|
return () => {
|
|
427
|
-
if (
|
|
428
|
-
|
|
469
|
+
if (checkTimeout) {
|
|
470
|
+
clearTimeout(checkTimeout);
|
|
429
471
|
}
|
|
430
472
|
};
|
|
431
473
|
}, [oxyServices, storage, syncIdentity, logger]);
|