Saltar al contenido principal

migration-plan

Ver en Git


Plan de Migración — Sync Offline (App Vieja → App Nueva)

Contexto

La app mobile se actualiza manteniendo el mismo package_name. La app vieja persiste requests pendientes en SQLite (key "offline", tabla U{userId}). La app nueva usa FileSystem con archivos individuales JSON en el directorio sync-requests/. La migración la ejecuta el shell una única vez al primer arranque.

Formato origen (SQLite)

// Tabla: U{userId}, Key: "offline"
interface OfflineData {
requestQueue: { [sender: string]: Array<OldRequest> };
removedQueue: { [sender: string]: Array<OldRequest> };
transactionId: number;
}

interface OldRequest {
id: number;
api: string; // endpoint relativo (ej: "/scouting/scout")
body: string | { [key: string]: any };
method: 'POST' | 'POSTF' | 'PUT' | 'PUTF' | 'GET' | 'DELETE' | 'PATCH';
opts: {
body?: { [key: string]: any };
params: { [key: string]: any };
headers: { [key: string]: string }; // Content-Type, X-Version (sin Authorization)
};
eventData: { [key: string]: any };
latestError?: { error: any; timestamp: number };
deleted?: number; // timestamp, solo en removedQueue
attempts: number;
date: number; // Date.now() al encolar
userId: string;
}

Formato destino (FileSystem)

Cada request se guarda como archivo individual: sync-requests/{uuid}.json

interface PostApiRequest {
request_endpoint: string; // endpoint relativo
request_body: BodyInit; // body serializado (string JSON o url-encoded)
request_date: string; // ISO 8601 (ej: "2024-01-15T10:30:00Z")
request_headers: StringValueObject; // { "Content-Type": "...", "X-Version": "..." }
request_id: string; // UUID v4 generado en migración
request_method: 'POST' | 'PUT' | 'DELETE' | 'PATCH';
formdata?: IFormdataItem[]; // solo para PUTF
}

Archivos de imágenes (PUTF) se guardan como base64 en: sync-files/{uuid}

Estrategia

Precondiciones

  • El shell tiene acceso a SQLite (lectura) y al FileSystem (escritura).
  • La migración se ejecuta antes de instanciar el APIManager.
  • Se usa un flag de migración en FileSystem para no re-ejecutar.

Flujo

App inicia (primer arranque post-actualización)


¿Existe flag "migration-done"? ── Sí → Skip, continuar arranque normal
│ No

Leer todas las tablas U{userId} de SQLite


Para cada userId: leer key "offline" → OfflineData


Transformar requestQueue + removedQueue → PostApiRequest[]


Escribir cada PostApiRequest como archivo en sync-requests/


Migrar archivos PUTF (si existen en filesystem viejo)


Escribir flag "migration-done"


Continuar arranque normal (instanciar APIManager)

Transformación por campo

Campo origen (OldRequest)Campo destino (PostApiRequest)Transformación
apirequest_endpointCopiar tal cual
bodyrequest_bodySi es object → JSON.stringify(body). Si es string → copiar
daterequest_datenew Date(date).toISOString().slice(0, -5) + 'Z'
opts.headersrequest_headersCopiar (ya no incluye Authorization)
request_idGenerar nuevo UUID v4
methodrequest_methodMapear (ver sección PUTF)
opts.paramsSerializar como query string y appendear a request_endpoint si tiene valores

Mapeo de métodos

Método viejoMétodo nuevoNota
POSTPOSTDirecto
PUTPUTDirecto
PATCHPATCHDirecto
DELETEDELETEDirecto
PUTFPUTSe convierte a request con formdata (ver sección PUTF)
POSTFPOSTIdem PUTF pero con POST (no se usa actualmente)
GETNo migrar (no debería existir en cola offline)

Manejo de PUTF (archivos)

Las requests PUTF en la app vieja referencian un archivo local por path (en opts.params). El flujo:

  1. Leer el archivo desde el path original del filesystem del dispositivo.
  2. Convertir a base64.
  3. Guardar en sync-files/{uuid} (donde uuid es el que venga en opts.params.uuid, o generar uno nuevo).
  4. Generar el PostApiRequest con campo formdata:
const formdataItems: IFormdataItem[] = [
{
key: 'file',
isFile: true,
value: {
uuid: fileUuid,
filename: originalFilename,
type: mimetype // de opts.params.mimetype
}
}
];
  1. Si el archivo original no existe (fue borrado por la actualización), marcar la request como no migrable y loguear.

Datos de PUTF en la app vieja

Los params típicos de PUTF contienen:

  • uuid: identificador del recurso
  • path: ruta local del archivo en el dispositivo
  • mimetype / type: MIME type del archivo

Manejo de removedQueue

Las requests en removedQueue se migran igual que las de requestQueue. Se distinguen con metadata adicional:

interface MigratedPostApiRequest extends PostApiRequest {
_migrated_removed?: boolean;
_migrated_deleted_at?: number; // timestamp original de "deleted"
_migrated_sender?: string; // sender original para contexto
_migrated_error?: any; // latestError original
}

Esto permite que la UI de sync de la nueva app las identifique como eliminadas si necesita mostrarlas.

Manejo de múltiples usuarios

SQLite tiene tablas por usuario (U{userId}). La migración debe:

  1. Listar todas las tablas que matcheen el patrón U*.
  2. Para cada una, leer la key "offline".
  3. Migrar todas las requests preservando el userId original en la metadata.

El nuevo sistema no segrega por usuario en el filesystem. Todas las requests van al mismo directorio sync-requests/. Si se necesita segregar, usar subdirectorios: sync-requests/{userId}/{uuid}.json.

Ordenamiento

El nuevo sistema ordena por request_date al leer las requests pendientes (sort by request_date DESC). Para respetar la jerarquía de senders del sistema viejo al generar las fechas:

  1. Ordenar primero por jerarquía del sender (field=2 primero, luego jerarquía 1, luego 0).
  2. Dentro de cada jerarquía, ordenar por date original (FIFO).
  3. Asignar request_date secuenciales que respeten este orden global.
// Jerarquías conocidas
const SENDER_HIERARCHY: Record<string, number> = {
field: 2,
scouting: 1,
sampling: 1, // incluye sub-senders sampling-*
mzone: 1,
'mzone-img': 1,
tags: 1,
evtSDK: 1,
apps: 1,
weather: 0,
lang: 0
}

function getSenderHierarchy(sender: string): number {
return SENDER_HIERARCHY[sender] ?? SENDER_HIERARCHY[sender.split('-')[0]] ?? 0
}

// Ordenar: mayor jerarquía primero, luego por date ASC dentro de cada grupo
const sorted = allRequests.sort((a, b) => {
const hierDiff = getSenderHierarchy(b.sender) - getSenderHierarchy(a.sender)
if (hierDiff !== 0) return hierDiff
return a.date - b.date
})

// Asignar request_date secuenciales
let baseDate = new Date(sorted[0].date)
for (const req of sorted) {
req.request_date = baseDate.toISOString().slice(0, -5) + 'Z'
baseDate.setSeconds(baseDate.getSeconds() + 1)
}

Flag de migración

await filesystem.write(
filesystem.getPath('last-sync') + '_migration_done',
{
migratedAt: new Date().toISOString(),
requestsMigrated: totalMigrated,
requestsFailed: totalFailed,
users: userIds
}
)

Al iniciar, verificar si este archivo existe. Si existe → skip migración.

Manejo de errores

EscenarioAcción
SQLite no existe o está vacíoSkip migración, escribir flag
Tabla de usuario no tiene key "offline"Skip ese usuario
requestQueue y removedQueue vacíasSkip ese usuario
Archivo PUTF no encontradoLoguear warning, migrar request sin formdata (se perderá el archivo)
Error de escritura en FileSystemReintentar 1 vez. Si falla, loguear y continuar con siguiente request
Error inesperado generalLoguear, NO escribir flag (permitir re-ejecución en próximo arranque)

Validación post-migración

Después de escribir todos los archivos:

  1. Leer sync-requests/ y contar archivos.
  2. Comparar con el total esperado (sum de requests en todas las colas).
  3. Si hay discrepancia, loguear warning pero continuar (no bloquear el arranque).

Dependencias del shell para implementar

DependenciaUso
SQLite plugin (lectura)Acceder a tablas U{userId}, leer key "offline"
FileSystem (misma impl que APIManager)Escribir en sync-requests/ y sync-files/
uuid (v4)Generar IDs de requests migradas
Acceso al filesystem nativoLeer archivos PUTF del path original

Checklist de implementación

  • Crear servicio MigrationService en el shell
  • Implementar lectura de SQLite (listar tablas U*, leer key "offline")
  • Implementar transformación OldRequest → PostApiRequest
  • Implementar migración de archivos PUTF (leer archivo → base64 → sync-files/)
  • Implementar migración de removedQueue con metadata
  • Implementar ordenamiento por jerarquía + fecha
  • Implementar flag de migración (lectura y escritura)
  • Implementar logging/reporte de migración
  • Implementar validación post-migración
  • Test: migrar requestQueue con requests JSON
  • Test: migrar requestQueue con PUTF (archivo existe)
  • Test: migrar requestQueue con PUTF (archivo no existe)
  • Test: migrar removedQueue
  • Test: múltiples usuarios
  • Test: SQLite vacío / inexistente
  • Test: idempotencia (re-ejecutar no duplica si flag no se escribió)