@oxyhq/services 5.16.4 → 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.
Files changed (82) hide show
  1. package/lib/commonjs/core/OxyServices.base.js +3 -1
  2. package/lib/commonjs/core/OxyServices.base.js.map +1 -1
  3. package/lib/commonjs/core/mixins/OxyServices.assets.js +20 -330
  4. package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
  5. package/lib/commonjs/index.js +156 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +45 -7
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/mutations/index.js +60 -20
  10. package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +230 -1
  12. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/queries/index.js +96 -30
  14. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
  16. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +75 -1
  18. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +50 -2
  20. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useAssets.js +8 -29
  22. package/lib/commonjs/ui/hooks/useAssets.js.map +1 -1
  23. package/lib/commonjs/ui/screens/FileManagementScreen.js +14 -10
  24. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  25. package/lib/module/core/OxyServices.base.js +3 -1
  26. package/lib/module/core/OxyServices.base.js.map +1 -1
  27. package/lib/module/core/mixins/OxyServices.assets.js +20 -331
  28. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
  29. package/lib/module/index.js +17 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/ui/context/OxyContext.js +45 -7
  32. package/lib/module/ui/context/OxyContext.js.map +1 -1
  33. package/lib/module/ui/hooks/mutations/index.js +12 -3
  34. package/lib/module/ui/hooks/mutations/index.js.map +1 -1
  35. package/lib/module/ui/hooks/mutations/useAccountMutations.js +227 -0
  36. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  37. package/lib/module/ui/hooks/queries/index.js +15 -4
  38. package/lib/module/ui/hooks/queries/index.js.map +1 -1
  39. package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
  40. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  41. package/lib/module/ui/hooks/queries/useAccountQueries.js +73 -0
  42. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  43. package/lib/module/ui/hooks/queries/useServicesQueries.js +50 -2
  44. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  45. package/lib/module/ui/hooks/useAssets.js +8 -29
  46. package/lib/module/ui/hooks/useAssets.js.map +1 -1
  47. package/lib/module/ui/screens/FileManagementScreen.js +12 -10
  48. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  49. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
  50. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +1 -70
  51. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  52. package/lib/typescript/core/mixins/index.d.ts +4 -14
  53. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -0
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  57. package/lib/typescript/ui/hooks/mutations/index.d.ts +8 -2
  58. package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
  59. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +19 -0
  60. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  61. package/lib/typescript/ui/hooks/queries/index.d.ts +9 -3
  62. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
  63. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +4 -0
  64. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  65. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +6 -0
  66. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  67. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  68. package/lib/typescript/ui/hooks/useAssets.d.ts.map +1 -1
  69. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/core/OxyServices.base.ts +5 -1
  72. package/src/core/mixins/OxyServices.assets.ts +21 -338
  73. package/src/index.ts +49 -2
  74. package/src/ui/context/OxyContext.tsx +49 -7
  75. package/src/ui/hooks/mutations/index.ts +24 -3
  76. package/src/ui/hooks/mutations/useAccountMutations.ts +205 -0
  77. package/src/ui/hooks/queries/index.ts +29 -4
  78. package/src/ui/hooks/queries/queryKeys.ts +6 -0
  79. package/src/ui/hooks/queries/useAccountQueries.ts +69 -0
  80. package/src/ui/hooks/queries/useServicesQueries.ts +49 -2
  81. package/src/ui/hooks/useAssets.ts +8 -28
  82. 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, // Don't cache file lists - always get fresh data
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
- // Calculate SHA256
390
- let sha256: string;
391
- try {
392
- sha256 = await this.calculateSHA256(file);
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
- // Initialize upload
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
- // Try presigned URL first, fallback to direct upload
406
- try {
407
- await this.uploadToPresignedUrl(initResponse.uploadUrl, file, onProgress);
408
- } catch (uploadError: any) {
409
- // Fallback: direct upload via API to avoid CORS issues
410
- try {
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
- // Complete upload
429
- try {
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, // 5 minutes cache
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, // 10 minutes cache for URLs
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, // default 10 minutes cache for URLs
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 components and hooks
115
+ // UI hooks - Stores
116
116
  export { useAuthStore } from './ui/stores/authStore';
117
- export { useAssetStore, useAssets as useAssetsStore, useAsset, useUploadProgress, useAssetLoading, useAssetErrors, useAssetsByApp, useAssetsByEntity, useAssetUsageCount, useIsAssetLinked } from './ui/stores/assetStore';
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 checkInterval: NodeJS.Timeout | null = null;
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
- return;
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 (checkInterval) {
428
- clearInterval(checkInterval);
469
+ if (checkTimeout) {
470
+ clearTimeout(checkTimeout);
429
471
  }
430
472
  };
431
473
  }, [oxyServices, storage, syncIdentity, logger]);