migration-plan
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 |
|---|---|---|
api | request_endpoint | Copiar tal cual |
body | request_body | Si es object → JSON.stringify(body). Si es string → copiar |
date | request_date | new Date(date).toISOString().slice(0, -5) + 'Z' |
opts.headers | request_headers | Copiar (ya no incluye Authorization) |
| — | request_id | Generar nuevo UUID v4 |
method | request_method | Mapear (ver sección PUTF) |
opts.params | — | Serializar como query string y appendear a request_endpoint si tiene valores |
Mapeo de métodos
| Método viejo | Método nuevo | Nota |
|---|---|---|
POST | POST | Directo |
PUT | PUT | Directo |
PATCH | PATCH | Directo |
DELETE | DELETE | Directo |
PUTF | PUT | Se convierte a request con formdata (ver sección PUTF) |
POSTF | POST | Idem PUTF pero con POST (no se usa actualmente) |
GET | — | No 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:
- Leer el archivo desde el path original del filesystem del dispositivo.
- Convertir a base64.
- Guardar en
sync-files/{uuid}(donde uuid es el que venga enopts.params.uuid, o generar uno nuevo). - Generar el
PostApiRequestcon campoformdata:
const formdataItems: IFormdataItem[] = [
{
key: 'file',
isFile: true,
value: {
uuid: fileUuid,
filename: originalFilename,
type: mimetype // de opts.params.mimetype
}
}
];
- 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 recursopath: ruta local del archivo en el dispositivomimetype/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:
- Listar todas las tablas que matcheen el patrón
U*. - Para cada una, leer la key
"offline". - Migrar todas las requests preservando el
userIdoriginal 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:
- Ordenar primero por jerarquía del sender (field=2 primero, luego jerarquía 1, luego 0).
- Dentro de cada jerarquía, ordenar por
dateoriginal (FIFO). - Asignar
request_datesecuenciales 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
| Escenario | Acción |
|---|---|
| SQLite no existe o está vacío | Skip migración, escribir flag |
| Tabla de usuario no tiene key "offline" | Skip ese usuario |
requestQueue y removedQueue vacías | Skip ese usuario |
| Archivo PUTF no encontrado | Loguear warning, migrar request sin formdata (se perderá el archivo) |
| Error de escritura en FileSystem | Reintentar 1 vez. Si falla, loguear y continuar con siguiente request |
| Error inesperado general | Loguear, NO escribir flag (permitir re-ejecución en próximo arranque) |
Validación post-migración
Después de escribir todos los archivos:
- Leer
sync-requests/y contar archivos. - Comparar con el total esperado (sum de requests en todas las colas).
- Si hay discrepancia, loguear warning pero continuar (no bloquear el arranque).
Dependencias del shell para implementar
| Dependencia | Uso |
|---|---|
| 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 nativo | Leer archivos PUTF del path original |
Checklist de implementación
- Crear servicio
MigrationServiceen 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ó)