2025-12-25 12:24:19 +00:00
/ * *
* Offline Request Queue Service
* FE - API - 015 : Queue requests when offline and replay when back online
2026-01-13 18:47:57 +00:00
*
2025-12-25 12:24:19 +00:00
* Stores failed requests due to network issues and replays them when connection is restored
* /
2025-12-28 15:07:02 +00:00
import { AxiosRequestConfig } from 'axios' ;
2025-12-25 12:24:19 +00:00
import { apiClient } from './api/client' ;
import { logger } from '@/utils/logger' ;
/ * *
* Queued request with metadata
* /
export interface QueuedRequest {
id : string ;
config : AxiosRequestConfig ;
timestamp : number ;
retryCount : number ;
priority : 'high' | 'normal' | 'low' ;
}
/ * *
* Options for queueing requests
* /
export interface QueueOptions {
priority ? : 'high' | 'normal' | 'low' ;
maxRetries? : number ;
retryDelay? : number ;
}
/ * *
* Offline Queue Service
* Manages a queue of failed requests to be retried when connection is restored
* /
class OfflineQueueService {
private queue : QueuedRequest [ ] = [ ] ;
private isProcessing = false ;
private maxQueueSize = 100 ; // Maximum number of queued requests
private defaultMaxRetries = 3 ;
private defaultRetryDelay = 1000 ; // 1 second
/ * *
* Initialize the service
* /
constructor ( ) {
// Load queue from storage on initialization
this . loadQueue ( ) ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// Listen for online events to process queue
if ( typeof window !== 'undefined' ) {
window . addEventListener ( 'online' , ( ) = > {
logger . info ( '[OfflineQueue] Connection restored, processing queue' ) ;
this . processQueue ( ) ;
} ) ;
2026-01-18 12:55:28 +00:00
// FIX: Also process queue if we're already online when the service initializes
// This handles the case where requests were queued while offline but we're now online
if ( navigator . onLine && this . queue . length > 0 ) {
// Small delay to ensure everything is initialized
setTimeout ( ( ) = > {
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:constructor' , message : 'Auto-processing queue on init (already online)' , data : { queueLength :this.queue.length } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
this . processQueue ( ) ;
} , 1000 ) ;
}
2025-12-25 12:24:19 +00:00
}
}
/ * *
* Check if we ' re currently offline
* /
private isOffline ( ) : boolean {
if ( typeof navigator === 'undefined' ) {
return false ;
}
return ! navigator . onLine ;
}
/ * *
* Generate a unique ID for a request
* /
private generateRequestId ( ) : string {
return ` req_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
}
/ * *
* Add a request to the queue
* /
async queueRequest (
config : AxiosRequestConfig ,
options : QueueOptions = { } ,
) : Promise < string > {
2026-01-13 18:47:57 +00:00
const { priority = 'normal' } = options ;
2025-12-25 12:24:19 +00:00
// Check queue size limit
if ( this . queue . length >= this . maxQueueSize ) {
// Remove oldest low-priority request
2026-01-13 18:47:57 +00:00
const lowPriorityIndex = this . queue . findIndex (
( req ) = > req . priority === 'low' ,
) ;
2025-12-25 12:24:19 +00:00
if ( lowPriorityIndex !== - 1 ) {
this . queue . splice ( lowPriorityIndex , 1 ) ;
} else {
// Remove oldest request if no low-priority found
this . queue . shift ( ) ;
}
}
const queuedRequest : QueuedRequest = {
id : this.generateRequestId ( ) ,
config ,
timestamp : Date.now ( ) ,
retryCount : 0 ,
priority ,
} ;
// Insert based on priority (high first, then normal, then low)
const priorityOrder = { high : 0 , normal : 1 , low : 2 } ;
const insertIndex = this . queue . findIndex (
( req ) = > priorityOrder [ req . priority ] > priorityOrder [ priority ] ,
) ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
if ( insertIndex === - 1 ) {
this . queue . push ( queuedRequest ) ;
} else {
this . queue . splice ( insertIndex , 0 , queuedRequest ) ;
}
// Save to storage
await this . saveQueue ( ) ;
2026-01-13 18:47:57 +00:00
logger . info (
` [OfflineQueue] Request queued: ${ config . method ? . toUpperCase ( ) } ${ config . url } ` ,
{
requestId : queuedRequest.id ,
priority ,
queueSize : this.queue.length ,
} ,
) ;
2025-12-25 12:24:19 +00:00
return queuedRequest . id ;
}
/ * *
* Process the queue when back online
* /
async processQueue ( ) : Promise < void > {
2026-01-18 12:55:28 +00:00
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:137' , message : 'processQueue called' , data : { isProcessing :this.isProcessing , isOffline :this.isOffline ( ) , queueLength :this.queue.length } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
2025-12-25 12:24:19 +00:00
if ( this . isProcessing || this . isOffline ( ) || this . queue . length === 0 ) {
2026-01-18 12:55:28 +00:00
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:139' , message : 'processQueue early return' , data : { reason :this.isProcessing? 'processing' : this . isOffline ( ) ? 'offline' : 'empty' } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
2025-12-25 12:24:19 +00:00
return ;
}
this . isProcessing = true ;
2026-01-13 18:47:57 +00:00
logger . info (
` [OfflineQueue] Processing ${ this . queue . length } queued requests ` ,
) ;
2026-01-18 12:55:28 +00:00
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:142' , message : 'Starting queue processing' , data : { queueLength :this.queue.length } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
2025-12-25 12:24:19 +00:00
// Process requests in order (high priority first)
while ( this . queue . length > 0 && ! this . isOffline ( ) ) {
const request = this . queue [ 0 ] ;
2026-01-18 12:55:28 +00:00
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:149' , message : 'Processing request' , data : { requestId :request.id , method :request.config.method , url :request.config.url , retryCount :request.retryCount } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
try {
// Retry the request
2025-12-28 15:07:02 +00:00
await apiClient . request ( request . config ) ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// Success - remove from queue
this . queue . shift ( ) ;
await this . saveQueue ( ) ;
2026-01-13 18:47:57 +00:00
logger . info (
` [OfflineQueue] Request succeeded: ${ request . config . method ? . toUpperCase ( ) } ${ request . config . url } ` ,
{
requestId : request.id ,
} ,
) ;
2025-12-25 12:24:19 +00:00
// Small delay between requests to avoid overwhelming the server
await new Promise ( ( resolve ) = > setTimeout ( resolve , 100 ) ) ;
} catch ( error ) {
// Check if we should retry
request . retryCount ++ ;
const maxRetries = this . defaultMaxRetries ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
if ( request . retryCount >= maxRetries ) {
// Max retries reached - remove from queue
2026-01-13 18:47:57 +00:00
logger . error (
` [OfflineQueue] Request failed after ${ maxRetries } retries: ${ request . config . method ? . toUpperCase ( ) } ${ request . config . url } ` ,
{
requestId : request.id ,
error ,
} ,
) ;
2025-12-25 12:24:19 +00:00
this . queue . shift ( ) ;
await this . saveQueue ( ) ;
} else {
// Move to end of queue for retry
this . queue . shift ( ) ;
this . queue . push ( request ) ;
await this . saveQueue ( ) ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// Wait before retrying
2026-01-13 18:47:57 +00:00
await new Promise ( ( resolve ) = >
setTimeout ( resolve , this . defaultRetryDelay * request . retryCount ) ,
) ;
2025-12-25 12:24:19 +00:00
}
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// If we went offline again, stop processing
if ( this . isOffline ( ) ) {
2026-01-13 18:47:57 +00:00
logger . warn (
'[OfflineQueue] Connection lost, stopping queue processing' ,
) ;
2025-12-25 12:24:19 +00:00
break ;
}
}
}
this . isProcessing = false ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
if ( this . queue . length > 0 ) {
2026-01-13 18:47:57 +00:00
logger . info (
` [OfflineQueue] Queue processing complete, ${ this . queue . length } requests remaining ` ,
) ;
2025-12-25 12:24:19 +00:00
} else {
logger . info ( '[OfflineQueue] All queued requests processed successfully' ) ;
}
}
/ * *
* Get the current queue size
* /
getQueueSize ( ) : number {
2026-01-18 12:55:28 +00:00
const size = this . queue . length ;
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:220' , message : 'getQueueSize called' , data : { size , isProcessing :this.isProcessing , isOffline :this.isOffline ( ) } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
return size ;
2025-12-25 12:24:19 +00:00
}
/ * *
* Get all queued requests ( for debugging / monitoring )
* /
getQueue ( ) : QueuedRequest [ ] {
return [ . . . this . queue ] ;
}
/ * *
* Clear the queue
* /
async clearQueue ( ) : Promise < void > {
2026-01-18 12:55:28 +00:00
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'offlineQueue.ts:234' , message : 'clearQueue called' , data : { queueLength :this.queue.length } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
2025-12-25 12:24:19 +00:00
this . queue = [ ] ;
await this . saveQueue ( ) ;
logger . info ( '[OfflineQueue] Queue cleared' ) ;
}
/ * *
* Remove a specific request from the queue
* /
async removeRequest ( requestId : string ) : Promise < boolean > {
const index = this . queue . findIndex ( ( req ) = > req . id === requestId ) ;
if ( index !== - 1 ) {
this . queue . splice ( index , 1 ) ;
await this . saveQueue ( ) ;
logger . info ( ` [OfflineQueue] Request removed from queue: ${ requestId } ` ) ;
return true ;
}
return false ;
}
/ * *
* Save queue to localStorage
* /
private async saveQueue ( ) : Promise < void > {
try {
if ( typeof window !== 'undefined' && window . localStorage ) {
const serialized = JSON . stringify ( this . queue ) ;
localStorage . setItem ( 'veza_offline_queue' , serialized ) ;
}
} catch ( error ) {
2026-01-13 18:47:57 +00:00
logger . error ( '[OfflineQueue] Failed to save queue to localStorage' , {
error ,
} ) ;
2025-12-25 12:24:19 +00:00
}
}
/ * *
* Load queue from localStorage
* /
private async loadQueue ( ) : Promise < void > {
try {
if ( typeof window !== 'undefined' && window . localStorage ) {
const serialized = localStorage . getItem ( 'veza_offline_queue' ) ;
if ( serialized ) {
const parsed = JSON . parse ( serialized ) ;
// Validate and filter out old requests (older than 24 hours)
const oneDayAgo = Date . now ( ) - 24 * 60 * 60 * 1000 ;
2026-01-13 18:47:57 +00:00
this . queue = parsed . filter (
( req : QueuedRequest ) = > req . timestamp > oneDayAgo ,
) ;
2025-12-25 12:24:19 +00:00
if ( this . queue . length !== parsed . length ) {
await this . saveQueue ( ) ;
}
2026-01-13 18:47:57 +00:00
logger . info (
` [OfflineQueue] Loaded ${ this . queue . length } requests from storage ` ,
) ;
2025-12-25 12:24:19 +00:00
}
}
} catch ( error ) {
2026-01-13 18:47:57 +00:00
logger . error ( '[OfflineQueue] Failed to load queue from localStorage' , {
error ,
} ) ;
2025-12-25 12:24:19 +00:00
this . queue = [ ] ;
}
}
/ * *
* Check if a request should be queued
* Some requests ( like GET ) might not need to be queued
* /
shouldQueueRequest ( config : AxiosRequestConfig ) : boolean {
const method = config . method ? . toUpperCase ( ) ;
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// Don't queue GET requests (they can be retried fresh)
if ( method === 'GET' ) {
return false ;
}
2026-01-13 18:47:57 +00:00
2025-12-25 12:24:19 +00:00
// Queue mutation requests (POST, PUT, DELETE, PATCH)
return [ 'POST' , 'PUT' , 'DELETE' , 'PATCH' ] . includes ( method || '' ) ;
}
}
// Singleton instance
export const offlineQueue = new OfflineQueueService ( ) ;