feat: enhance offline support with improved network management and sync handling

This commit is contained in:
Mo Tarbin
2026-06-06 00:58:18 -04:00
parent 24118e0dc3
commit 4e0b5d79df
9 changed files with 219 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
import { Preferences } from '@capacitor/preferences'
import { API_URL } from '../Config'
import { networkManager } from '../hooks/NetworkManager'
import { logout, RefreshToken } from './Fetcher'
import {
clearAllTokens,
@@ -146,15 +147,22 @@ class ApiClient {
async request(endpoint, options = {}) {
await this.init()
const url = `${this.customServerURL}${endpoint}`
// Abort after 10s so a dead/unreachable server doesn't hang the UI
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10_000)
const config = {
// credentials: 'include',
...options,
headers: this.getHeaders(options.headers),
signal: options.signal ?? controller.signal,
}
try {
// 1. Initial Request
let response = await fetch(url, config)
clearTimeout(timeoutId)
// 2. Check for 401 (Unauthorized)
if (response.status === 401) {
@@ -222,6 +230,9 @@ class ApiClient {
return response
} catch (error) {
clearTimeout(timeoutId)
// fetch() threw = network-level failure or timeout — mark server unreachable
networkManager.setServerUnreachable()
console.error('Request failed', error)
throw error
}

View File

@@ -26,17 +26,32 @@ class SQLiteBackend {
constructor() {
this.db = null
this.initialized = false
this._initPromise = null
}
async init() {
if (this.initialized) return
// Return the in-flight promise if init is already underway (prevents double createConnection)
if (this._initPromise) return this._initPromise
this.db = await CapacitorSQLite.createConnection({
database: DB_NAME,
version: DB_VERSION,
encrypted: false,
mode: 'no-encryption',
this._initPromise = this._doInit().finally(() => {
this._initPromise = null
})
return this._initPromise
}
async _doInit() {
try {
this.db = await CapacitorSQLite.createConnection({
database: DB_NAME,
version: DB_VERSION,
encrypted: false,
mode: 'no-encryption',
})
} catch (err) {
// Connection already open (e.g. React StrictMode double-mount) — reuse it
if (!err?.message?.includes('already exists')) throw err
}
await CapacitorSQLite.open({ database: DB_NAME })
await CapacitorSQLite.execute({

View File

@@ -55,6 +55,8 @@ class SyncEngine {
// Step 3: Delta sync from server
await this._deltaSync()
// Sync succeeded — server is reachable (only sync success restores online status)
networkManager.setServerReachable()
this._notify({ syncing: false, lastSync: Date.now() })
return true
} catch (err) {
@@ -182,7 +184,7 @@ class SyncEngine {
let hasMore = true
let currentCursor = cursor
while (hasMore && networkManager.isOnline) {
while (hasMore && networkManager.deviceOnline) {
// Use apiClient.get which handles auth and returns a fetch Response
const response = await apiClient.get(
`/sync/changes?since=${currentCursor}`,