wiki-Offline-Features-Integration
Offline Features Integration
Online Features Integration
El objetivo de esta wiki es que sirva de guía para la integración del funcionamiento offline en los features de la plataforma en dispositivos mobile.
Disclaimer: Con el fin de que esta guía sea lo mas practica posible, se van a obviar algunos detalles del funcionamiento interno del EntityManager y se va a hacer foco en que necesitamos para hacer funcionar un feature de manera offline. Esto no es una documentación precisa del proyecto EntityManager y no fue escrita con tal propósito.
Para que un feature funcione de manera offline, nos vamos a apoyar en las carpetas internas del dispositivo las cuales cumplirían la función de "base de datos" mientras no contemos con conexión a internet.
Requisitos previos para el desarrollo:
- En android studio levantar un dispositivo virtual (utilizar API 34, se encontraron algunos problemas de compatibilidad con API 35), el cual va a servir para comprobar el funcionamiento offline y acceder a las carpetas internas.
- En el proyecto architecture, utilizar el comando npm run sync android, para levantar en nuestro dispositivo la aplicación de auravant. Es importante que tengamos la versión del feature que queremos desarrollar actualizada en el entorno del cual la arquitectura se va a servir mediante el proyectId.
- Para acceder a la consola, en nuestro browser, abrir una nueva pestaña con la la url: chrome://inspect, donde vamos a poder observar los dispositivos que se encuentran corriendo, luego hacer click en inspeccionar.

- Por el momento, cada vez que queramos ver los cambios realizados en nuestro feature, en el dispositivo virtual, tendremos que deployar el feature y activar la nueva versión en el administrador interno.
EntityManager
Asumiendo que nuestro feature objetivo necesita de la creación de nuevas entidades que luego sirvan para guardar la información que proviene del backend de manera local, a continuación se detalla como hacerlo:
- Abrir el proyecto EntityManager y en el directorio src/interfaces/entities, crear una nueva interfaz con el formato que corresponde al GET del set de datos con el queremos interactuar. Ejemplo, si queremos que el feature personas funcione de manera offline, debemos primero identificar con que formato recibimos el GET de la api personas. Y crear una interfaz que respete dicho formato, siguiendo el ejemplo de personas, la interfaz se vería de la siguiente manera:
export interface Person {
address: string
application: number[]
created_at: string
cuit: string
deleted_at: string
email: string
enterprises: ResourceEnterprise[]
farms_uuid: string[]
is_owner: boolean
last_name: string
name: string
note: string
person_id: number
phone_number: string
province: string
representation: string
rol_id: number[]
farms_id: number[]
ropo: string
staff_relation_id: number
staff_uuid: string
town: string
user_id: number
zipcode: string
}
- Porque el formato debe corresponderse con el GET que habitualmente obtenemos como respuesta cuando trabajamos de manera online?. Es importante que el formato de los datos guardados en el POST offline corresponda con el formato que habitualmente obtenemos como respuesta en un GET cuando trabajamos online. Esto se debe a que, en condiciones normales, al realizar un POST, enviamos al backend solo el payload con la información estrictamente necesaria. Luego, el backend puede procesar, formatear o enriquecer estos datos antes de devolverlos en un GET, que incluye información adicional o en un formato específico. Sin embargo, en un entorno offline, no contamos con el backend para hacer este procesamiento. Por eso, al guardar la información de manera local, necesitamos que el formato sea lo más parecido posible a lo que recibiríamos de un GET online, garantizando así consistencia y facilitando la sincronización cuando necesitemos acceder a estos datos.
- Ahora que ya tenemos creada la interfaz, debemos ir al directorio src/classes/Entity.ts, y agregar una nueva clase. Ejemplo: public staff = new EntityManager<Person[]>('staff').
- Deployar y actualizar la version del EntityManager en el admin para disponibilizar nuestros cambios.
Diferencias entre EntityManager y EntityManagerByUuid: En el ejemplo de personas, utilizamos la clase EntityManager, y cuando accedamos de manera offline a nuestra "base de datos" llamada staff, encontraremos un arreglo con todas las personas que fueron agregadas, pero que pasa si necesitamos que un set de datos estee atado a por ejemplo un campo. En ese caso utilizaremos EntityManagerByUuid, que nos permite agrupar los datos bajo un mismo criterio y de esta manera tenerlos disponibles en forma mas organizada. Ejemplo: public activities = new EntityManagerByUuid<Activity[]>('activities'), esto va a permitir que podamos guardar un arreglo de actividades agrupadas por un mismo uuid, que puede ser de un campo o de otro agrupador que consideremos necesario.
Agregando funcionamiento offline a un feature
En el feature objetivo, creamos una carpeta llamada entity, donde vamos a definir las funciones para la creación/modificación, de nuestra base de datos interna.
DEFINICIÓN POST
export default function postEntity (uuid: string, formData: MyMachine): void {
const entity = getEntityManager()
entity.machines.update({
callback: read => ({
data: read.data.concat([{
...formData,
created_at: '',
deleted_at: '',
enterprises: [],
farms_uuid: [],
fuel_consumption: formData.fuel_consumption ?? 0,
fuel_consumption_unit: formData.fuel_consumption_unit ?? '',
is_owner: true,
machine_id: 0,
property_name: '',
purchase_cost: formData.purchase_cost ?? 0,
purchase_cost_unit: formData.purchase_cost_unit,
status_name: '',
user_id: 0,
machine_uuid: uuid,
// brand: formData.brand ?? '',
farms_id: [],
license: '',
machine_type_id: 1,
roma: 0
}])
})
}).catch(logError)
}
Hacemos un entity.machines.update(), para agregar este nuevo objecto al arreglo de maquinarias ya existente, pero ademas notece como se agregan de forma manual otras propiedades características de lo que normalmente obtenemos en un get de maquinarias (como se explico en el apartado del funcionamiento del entitymanager).
DEFINICIÓN PATCH
export default function patchEntity (uuid: string, formData: MyMachine): void {
const entity = getEntityManager()
entity.machines.update({
callback: read => {
const machine = read.data.find(d => d.machine_uuid === uuid)
if (machine !== undefined) {
for (const key of Object.keys(formData)) {
machine[key] = formData[key]
}
}
return read
}
}).catch(logError)
}
Nuevamente hacemos un update sobre nuestra "base de datos" machines, pero esta vez utilizamos el uuid para identificar a que elemento queremos realizar la modificación.
DEFINICIÓN DELETE
export default function deleteEntity (uuid: string): void {
const entity = getEntityManager()
entity.machines.update({
callback: read => ({ data: read.data.filter(d => d.machine_uuid !== uuid) })
}).catch(logError)
}
Implementando las funciones del entity:
GET
Ahora que ya tenemos todo lo necesario para agregar el funcionamiento offline al feature objetivo, vamos a identificar los gets que utilizamos de manera online para servirnos de datos, y vamos a modificar la petición, para que también se cree en nuestro dispositivo, el set de datos correspondiente para funcionar sin una conexión a internet.
Ejemplo de petición get sin utilizar el entity manager:
useEffect(() => {
API.get({ url: '/api/resources/machine' }))
.then((res: { data: Machine[] }) => {
setMachines(res.data.length > 0 ? res.data : [])
})
.catch(err => {
logError(err)
setMachines([])
})
}, [])
Ejemplo de petición utilizando el entity manager:
useEffect(() => {
entity.machines.get()
.then(read => setMachines(read.data))
.catch(logError)
.then(async () => await API.get({ url: '/api/resources/machine' }))
.then((res: { data: Machine[] }) => {
entity.machines.save({ data: { data: res.data } }).catch(logError)
setMachines(res.data.length > 0 ? res.data : [])
})
.catch(err => {
logError(err)
setMachines(prev => prev ?? [])
})
}, [])
Como observamos en el ejemplo, primero accedemos y seteamos la información proveniente del dispositivo meditante entity.machines.get(), despues intentamos acceder a la información de manera online, si no tenemos éxito en la petición, vamos a continuar al catch, y nos vamos a quedar con la información que obtuvimos del dispositivo. En el caso de que la petición online tenga éxito, vamos a utilizar esta información para volver a setear la entidad de maquinarias mediante entity.machines.save(), de esta manera nos aseguramos de tener siempre una version local actualizada en el dispositivo.
POST
A tener en cuenta es que en el ejemplo que se muestra a continuación a la hora de implementar nuestra función postEntity, utilizaremos un uuid creado por nosotros en el frontend.
Es importante que asignemos nosotros mismos un UUID en lugar de dejar que el backend lo genere automáticamente. Cuando no hay conexión a internet, las peticiones fallidas que gestiona el APIManager se sincronizarán y se procesarán cuando se reanude la conexión, y estos datos se convertirán en los "oficiales". Si no asignamos un UUID al objeto creado offline, podríamos terminar con dos objetos duplicados: el que se creó offline, sin UUID, y el que se genera automáticamente al reanudar la conexión y sincronizar los datos. Para evitar esto, debemos asegurarnos de que nuestras APIs soporten la creación de datos con un UUID definido por el cliente.
const uuid = v4()
API.post({ url: '/api/resources/machine', body: JSON.stringify({ ...formdata, machine_uuid: uuid }) })
.then(() => {
postEntity(uuid, formdata)
setLoading(false)
navigate('/')
toast.success({
description: t('notification.machine.success.creation'),
duration: 0
})
})
.catch((err: Error) => {
logError(err)
if (err.message !== 'backend') {
postEntity(uuid, formdata)
}
navigate('/')
toast.error({
description: t('notification.machine.error.creation'),
duration: 0
})
})
Notece que tanto en el then como en el catch, ejecutamos nuestra función de postEntity, de esta manera mantenemos actualizada la base de datos del dispositivo a pesar de contar con una conexión a internet.
PATCH
La implementación en este caso es la misma que en el post, pero en vez de utilizar el uuid generado por nosotros, vamos a utilizar el uuid del dato que estamos queriendo modificar.
DELETE
Ejecutamos nuestra función deleteEntity, enviando el uuid del objeto a eliminar.
Ejemplo:
API.delete({ url: `/api/resources/machine/${machine.machine_uuid}`, body: '', options: { fullResponse: true } })
.then(() => {
setOverlay('')
successDelete()
setMachines(prev => (prev ?? []).filter(p => p.machine_uuid !== machine.machine_uuid))
deleteEntity(machine.machine_uuid)
})
.catch((err: Error) => {
logError(err)
if (err.message !== 'backend') {
setOverlay('')
setMachines(prev => (prev ?? []).filter(p => p.machine_uuid !== machine.machine_uuid))
deleteEntity(machine.machine_uuid)
}
})
Probar el funcionamiento offline
Para probar el funcionamiento offline, debemos desde el dispositivo virtual, desconectarnos de internet y utilizar el feature normalmente para que se ejecuten nuestras peticiones, y comprobar que se creen/modifiquen en nuestra base de datos local, y que al reanudar la conexión los datos se visualizen correctamente.
Carpetas internas del dispositivo
Para validar que nuestra base de datos interna se mantiene actualizada y responde a nuestras peticiones, vamos a inspeccionar las carpetas internas del dispositivo. En android estudio, acceder a "Device Manager", y luego a "Device Explorer", aquí podremos ver todas las carpetas internas del dispositivo virtual, el directorio donde encontraremos los archivos del entitymanager es: /data/data/com.auravant/files/users/84621/entities. En este directorio podemos ver y comprobar si nuestros cambios aplicados se ven reflejados en los datos que se guardan en el dispositivo.