@@ -346,7 +346,10 @@ Future<List<ServerConversation>> sendStorageToBackend(File file, String sdCardDa
346346Future <SyncLocalFilesResponse > syncLocalFiles (List <File > files, {UploadProgressCallback ? onUploadProgress}) async {
347347 try {
348348 var response = await makeMultipartApiCall (
349- url: '${Env .apiBaseUrl }v1/sync-local-files' , files: files, onUploadProgress: onUploadProgress);
349+ url: '${Env .apiBaseUrl }v1/sync-local-files' ,
350+ files: files,
351+ onUploadProgress: onUploadProgress,
352+ );
350353
351354 if (response.statusCode == 200 || response.statusCode == 207 ) {
352355 var result = SyncLocalFilesResponse .fromJson (jsonDecode (response.body));
@@ -374,6 +377,106 @@ Future<SyncLocalFilesResponse> syncLocalFiles(List<File> files, {UploadProgressC
374377 }
375378}
376379
380+ /// v2 async sync: POST files → 202 with job_id, then poll until terminal.
381+ /// Returns the same SyncLocalFilesResponse as v1 once processing is confirmed complete.
382+ typedef SyncJobPollCallback = void Function (SyncJobStatusResponse status);
383+
384+ Future <SyncLocalFilesResponse > syncLocalFilesV2 (
385+ List <File > files, {
386+ UploadProgressCallback ? onUploadProgress,
387+ SyncJobPollCallback ? onPollProgress,
388+ }) async {
389+ try {
390+ // Step 1: Submit files
391+ var response = await makeMultipartApiCall (
392+ url: '${Env .apiBaseUrl }v2/sync-local-files' ,
393+ files: files,
394+ onUploadProgress: onUploadProgress,
395+ );
396+
397+ // Fast-path responses (no async job created)
398+ if (response.statusCode == 200 ) {
399+ return SyncLocalFilesResponse .fromJson (jsonDecode (response.body));
400+ }
401+
402+ if (response.statusCode != 202 ) {
403+ if (response.statusCode == 400 ) {
404+ throw Exception ('Audio file could not be processed by server' );
405+ } else if (response.statusCode == 413 ) {
406+ throw Exception ('Audio file is too large to upload' );
407+ } else if (response.statusCode == 429 ) {
408+ throw Exception ('Rate limited or budget exhausted' );
409+ } else if (response.statusCode >= 500 ) {
410+ throw Exception ('Server is temporarily unavailable' );
411+ } else {
412+ throw Exception ('Upload failed unexpectedly' );
413+ }
414+ }
415+
416+ // Step 2: Poll for completion
417+ var startResponse = SyncJobStartResponse .fromJson (jsonDecode (response.body));
418+ var jobId = startResponse.jobId;
419+ var pollInterval = Duration (milliseconds: startResponse.pollAfterMs);
420+
421+ const maxPolls = 120 ; // 120 x 3s = 6 minutes max
422+ for (var i = 0 ; i < maxPolls; i++ ) {
423+ await Future .delayed (pollInterval);
424+
425+ var pollResponse = await makeApiCall (
426+ url: '${Env .apiBaseUrl }v2/sync-local-files/$jobId ' ,
427+ headers: {},
428+ method: 'GET' ,
429+ body: '' ,
430+ );
431+
432+ if (pollResponse == null ) {
433+ Logger .debug ('syncLocalFilesV2 poll failed: null response' );
434+ continue ; // Retry on transient errors
435+ }
436+
437+ // Terminal errors — don't retry
438+ if (pollResponse.statusCode == 404 ) {
439+ throw Exception ('Sync job not found or expired' );
440+ }
441+ if (pollResponse.statusCode == 403 ) {
442+ throw Exception ('Not authorized to view this sync job' );
443+ }
444+ if (pollResponse.statusCode != 200 ) {
445+ Logger .debug ('syncLocalFilesV2 poll failed: ${pollResponse .statusCode }' );
446+ continue ; // Retry on transient errors
447+ }
448+
449+ var jobStatus = SyncJobStatusResponse .fromJson (jsonDecode (pollResponse.body));
450+
451+ // Report poll progress to caller for UI updates
452+ onPollProgress? .call (jobStatus);
453+
454+ if (jobStatus.isTerminal) {
455+ // All segments failed → throw to match v1's 500 behavior (WAL stays retryable)
456+ if (jobStatus.status == 'failed' ) {
457+ throw Exception (jobStatus.error ?? 'Sync job failed' );
458+ }
459+ // Success or partial failure → return result
460+ if (jobStatus.result != null ) {
461+ return jobStatus.result! ;
462+ }
463+ return SyncLocalFilesResponse (
464+ newConversationIds: [],
465+ updatedConversationIds: [],
466+ failedSegments: jobStatus.failedSegments,
467+ totalSegments: jobStatus.totalSegments,
468+ );
469+ }
470+ }
471+
472+ // Polling timed out — don't mark as synced
473+ throw Exception ('Sync job timed out waiting for results' );
474+ } catch (e) {
475+ Logger .debug ('syncLocalFilesV2 error: $e ' );
476+ rethrow ;
477+ }
478+ }
479+
377480Future <(List <ServerConversation >, int , int )> searchConversationsServer (
378481 String query, {
379482 int ? page,
0 commit comments