diff --git a/src/api/client.ts b/src/api/client.ts index 870330d..f3acdb3 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -20,6 +20,21 @@ export class ApiError extends Error { } } +/** Error de red/tiempo (sin respuesta del servidor). `timedOut` indica timeout. */ +export class NetworkError extends Error { + constructor( + message: string, + public timedOut = false, + ) { + super(message); + this.name = 'NetworkError'; + } +} + +/** Timeout por defecto: peticiones JSON vs. subida de ficheros (multipart). */ +const DEFAULT_TIMEOUT_MS = 30_000; +const UPLOAD_TIMEOUT_MS = 120_000; + type TokenGetter = () => string | null; let getToken: TokenGetter = () => null; @@ -45,6 +60,8 @@ export interface RequestOptions { /** Si false, no adjunta el token (p.ej. /login). Por defecto true. */ auth?: boolean; signal?: AbortSignal; + /** Milisegundos hasta abortar por timeout. Por defecto 30s (120s si hay `form`). */ + timeoutMs?: number; } function buildUrl(path: string, query?: RequestOptions['query']): string { @@ -85,7 +102,39 @@ export async function request(path: string, opts: RequestOptions = {}): Promi body = JSON.stringify(json); } - const res = await fetch(buildUrl(path, query), { method, headers, body, signal }); + // Timeout con AbortController, combinado con la señal externa si la hay. + const timeoutMs = opts.timeoutMs ?? (form ? UPLOAD_TIMEOUT_MS : DEFAULT_TIMEOUT_MS); + const controller = new AbortController(); + const timedOut = { value: false }; + const timer = setTimeout(() => { + timedOut.value = true; + controller.abort(); + }, timeoutMs); + if (signal) { + if (signal.aborted) controller.abort(); + else signal.addEventListener('abort', () => controller.abort(), { once: true }); + } + + let res: Response; + try { + res = await fetch(buildUrl(path, query), { + method, + headers, + body, + signal: controller.signal, + }); + } catch (e) { + if (timedOut.value) { + throw new NetworkError(`Tiempo de espera agotado (${timeoutMs / 1000}s)`, true); + } + // Abort externo → propaga tal cual; resto → error de red genérico. + if (signal?.aborted) throw e; + throw new NetworkError( + e instanceof Error ? `Error de red: ${e.message}` : 'Error de red', + ); + } finally { + clearTimeout(timer); + } if (res.status === 401 && auth) { onUnauthorized?.(); diff --git a/src/screens/ProjectsScreen.tsx b/src/screens/ProjectsScreen.tsx index 6ca5915..54f8a38 100644 --- a/src/screens/ProjectsScreen.tsx +++ b/src/screens/ProjectsScreen.tsx @@ -12,9 +12,9 @@ import { import * as api from '../api/endpoints'; import { Project } from '../api/types'; import { useSession } from '../auth/session'; -import { getCursor, getProjects, saveProjectList, setActiveProjectId } from '../db/repositories'; +import { getProjects, saveProjectList, setActiveProjectId } from '../db/repositories'; import { isOnline } from '../net/connectivity'; -import { initialPull, runSync } from '../sync/engine'; +import { runSync } from '../sync/engine'; import { RootStackParamList } from '../navigation/types'; type Props = NativeStackScreenProps; @@ -57,12 +57,9 @@ export function ProjectsScreen({ navigation }: Props) { setOpening(p.id); try { await setActiveProjectId(p.id); - const online = await isOnline(); - if (online) { - const cursor = await getCursor(p.id); - if (cursor) await runSync(p.id); - else await initialPull(p.id); - } + // runSync ya resuelve ambos casos: sin cursor baja el snapshot completo, + // con cursor baja el delta (y vacía primero el outbox, vacío en la 1ª vez). + if (await isOnline()) await runSync(p.id); navigation.navigate('ProjectDetail', { projectId: p.id, name: p.name }); } finally { setOpening(null); diff --git a/src/sync/engine.ts b/src/sync/engine.ts index 2da12d1..72979eb 100644 --- a/src/sync/engine.ts +++ b/src/sync/engine.ts @@ -223,9 +223,3 @@ export async function runSync(projectId: number): Promise { return report; } - -/** Primera sincronización: snapshot completo (sin cursor). */ -export async function initialPull(projectId: number): Promise { - const bundle = await getBundle(projectId); - await applyBundle(bundle); -} diff --git a/src/sync/mutations.ts b/src/sync/mutations.ts index faa2ef7..9cf4081 100644 --- a/src/sync/mutations.ts +++ b/src/sync/mutations.ts @@ -16,17 +16,6 @@ import { IssueType, } from '../api/types'; -// ----- progress (append-only en servidor) ----- - -export function recordProgressUpdate(input: { - phase_id: number; - progress: number; - comment?: string; - location?: { lat: number; lng: number }; -}): Promise { - return enqueueOperation('progress_update', 'create', { ...input }); -} - // ----- feature.update (editable, last-write-wins) ----- export async function updateFeature(input: {