jueves, 25 de junio de 2026

Sincronización y persistencia de odontograma

 Quiero que me preguntes qué requerimientos estoy necesitando cubrir para recomendarme una solución de sincronización frontend-backend, con lectura y grabado en base de datos.

Perfecto. Antes de recomendar una arquitectura (REST, Inertia, Server Actions, WebSockets, polling, CQRS, cache local, sincronización optimista, etc.), necesito entender algunos aspectos de tu caso de uso.

Respondé lo que puedas:

1. Frecuencia de actualización

  • ¿Los datos cambian sólo cuando el usuario hace acciones?
  • ¿O pueden cambiar por otros usuarios/procesos mientras la pantalla está abierta?

2. Tiempo real

  • ¿Necesitás que los cambios aparezcan casi instantáneamente?
  • ¿Cuántos segundos de demora serían aceptables?

3. Volumen de datos

  • ¿Cuántos registros suele manejar una pantalla?
    • decenas
    • cientos
    • miles
    • decenas de miles

4. Patrón de uso

  • ¿Hay muchas más lecturas que escrituras?
  • ¿O las escrituras son frecuentes?

5. Consistencia

  • ¿Es crítico que el usuario vea siempre el estado más reciente de la base de datos?
  • ¿O tolerás que vea datos levemente desactualizados durante algunos segundos?

6. Conflictos

  • ¿Puede ocurrir que dos usuarios modifiquen simultáneamente la misma entidad?
  • Si ocurre, ¿qué debería pasar?

7. UX

  • ¿Querés que las modificaciones se reflejen instantáneamente en la UI (optimistic updates)?
  • ¿O preferís esperar la confirmación del backend?

8. Navegación

  • ¿Tu aplicación es principalmente:
    • formularios
    • CRUD administrativo
    • dashboard
    • editor complejo
    • tiempo real colaborativo
    • otra cosa?

9. Stack actual

  • ¿Qué usás exactamente?
    • React
    • Inertia
    • Laravel
    • Vite
    • TypeScript
    • otra cosa

10. Estado frontend

  • ¿Actualmente usás algo como:
    • Context
    • Zustand
    • Redux
    • React Query / TanStack Query
    • SWR
    • nada

11. Offline

  • ¿La aplicación debe seguir funcionando sin conexión y sincronizar después?

12. Prioridades

Ordená estas prioridades:

  • simplicidad
  • rendimiento
  • consistencia
  • tiempo real
  • facilidad de mantenimiento
  • escalabilidad

13. Caso concreto

Describime una pantalla representativa:

"Tengo un odontograma. El usuario modifica piezas dentales, se guardan en Laravel y otros usuarios pueden verlo."

o el caso que corresponda.

Con esas respuestas puedo recomendarte una estrategia concreta de sincronización frontend ↔ backend ↔ base de datos en lugar de una lista genérica de tecnologías.



1. Los datos cambian sólo cuando el usuario hace acciones. 
2. Puedo tolerar 5 segundos de demora, siempre y cuando haya un indicador en pantalla mientras todavía no están los nuevoa datos listos para mostrar. 
3. El volumen de datos de una pantalla es un JSON como el siguiente: 
export default
{
version: 1,
/* protesis: dentadura, enlaces, soluciones
* extraccion: dentadura, soluciones
* implante: dentadura, soluciones
* ortodoncia: enlaces, soluciones
*/
dentadura: [
/* tipo: protesis | muela | muela-leche | diente | diente-leche | edentulismo | extraccion |
* | implante |
*/
{ tipo: 'protesis', numero: '17', casillero: 4 },
{ tipo: 'protesis', numero: '16', casillero: 6 },
{ tipo: 'protesis', numero: '15', casillero: 8 },
{ tipo: 'muela', numero: '14(10)', casillero: 10 },
{ tipo: 'muela', casillero: 11},
//...
],
marcas_dentales: [
/* codigo: caries | obturacion-pendiente | obturacion-realizada
*
*/
{ codigo: 'obturacion-pendiente', casillero: 10, superficie: 1, presupuestado: false },
{ codigo: 'obturacion-realizada', casillero: 12, superficie: 2, presupuestado: true },
//...
],
problemas : [
/* tipo: desgaste | desplazamiento | discromia | ectopia | erupcion | extrusion
* | fractura | impacto | intrusion | macrodoncia | microdoncia | necrosis
* | remanente-radicular | rotacion | semi-impacto |
*/
{ tipo: 'remanente-radicular', casillero: 10 },
{ tipo: 'extrusion', casillero: 10 },
{ tipo: 'macrodoncia', casillero: 10 },
//...
],
// Array ordenado por primer_casillero
// de enlaces entre casilleros
enlaces : [
/* tipo: diastema | fusion | ortodoncia | protesis | transposicion
*
*/
{ tipo: 'protesis', primer_casillero: 4, ultimo_casillero: 8 },
{ tipo: 'protesis', primer_casillero: 12, ultimo_casillero: 16 },
// ...
],
soluciones: [
/* tipo: extraccion | implante | ortodoncia | perno-corona | protesis | tratamiento-conducto
*
*/
{ tipo: 'protesis', casillero: 4, presupuestado: true, realizado: true },
{ tipo: 'extraccion', casillero: 10, presupuestado: true, realizado: false },
// ...
],
}
4. Hay una lectura inicial y una baja frecuencia de escrituras. 
5. Es crítico que el usuario vea siempre el estado más reciente de la base de datos. 
6. Si 2 usuarios intentan modificar la misma entidad no hay problema en bloquear al segundo en ingresar. 
7. Prefiero esperar a la confirmación del backend. 
8. Editor complejo. Es un odontograma interactivo. Adjunto imagen. 
9. React, Inertia, Laravel, Vite, Typescript. 
10. Nada. 
11. No hace falta que siga funcionando offline. 
12. 1) simplicidad; 2) facilidad de mantenimiento; 3); consistencia; 4) rendimiento; 5) escalabilidad. 
13. Tengo un odontograma. El usuario modifica piezas dentales, patologías, tratamientos. Lo hace mediante un diálogo interactivo en pantalla. En el frontend todo esto se representa en un JSON como el que te compartí en mi respuesta #3.

Con lo que describís, yo no usaría nada sofisticado (WebSockets, Zustand, Redux, CQRS, sincronización offline, etc.). Estarías pagando complejidad sin obtener beneficios.

Tu caso tiene varias características muy favorables:

  • El odontograma completo es un único agregado lógico.
  • El JSON es pequeño (decenas o cientos de KB como mucho).
  • Las escrituras son poco frecuentes.
  • No necesitás tiempo real.
  • Priorizás simplicidad y mantenimiento.
  • Preferís esperar la confirmación del backend.
  • Querés consistencia fuerte.

Lo que implementaría

Backend

Una tabla:

odontogramas
-----------
id
version
contenido_json
updated_at

donde contenido_json es esencialmente el JSON que mostraste.


Frontend

Estado local:

const [odontograma, setOdontograma] =
useState<Odontograma>(props.odontograma)

const [guardando, setGuardando] =
useState(false)

Guardado

Cuando el usuario confirma una modificación:

setGuardando(true)

await router.post(
route('odontograma.guardar'),
odontograma
)

await recargar()

setGuardando(false)

donde:

async function recargar() {
router.reload({
only: ['odontograma']
})
}

o equivalente con fetch.


Lo importante: versionado optimista

Veo un posible problema:

"si dos usuarios intentan modificar la misma entidad no hay problema en bloquear al segundo"

Entonces agregaría:

{
version: 37,
...
}

al JSON.


Al guardar:

if ($request->version !== $odontograma->version) {
abort(409);
}

Si alguien guardó antes:

Usuario A abre v37
Usuario B abre v37

Usuario A guarda
→ v38

Usuario B guarda
→ 409 Conflict

y mostrás:

El odontograma fue modificado por otro usuario.
Recargue la pantalla.

Mejor todavía: lock pesimista

Como dijiste:

"no hay problema en bloquear al segundo"

haría esto.

Al abrir:

odontograma_locks
-----------------
odontograma_id
usuario_id
expires_at

El primer usuario adquiere el lock.

El segundo ve:

El odontograma está siendo editado por Juan Pérez.

y entra en modo lectura.


Qué NO usaría

No usaría:

  • Redux
  • Zustand
  • MobX
  • WebSockets
  • SSE
  • React Query
  • SWR
  • Event Sourcing

porque ninguna de esas herramientas resuelve un problema que realmente tengas.


Lo que sí me preocuparía

Al mirar el JSON veo que el odontograma es básicamente un documento.

Yo evitaría hacer:

POST agregar-caries
POST borrar-caries
POST agregar-fusion
POST agregar-diastema
POST agregar-protesis

etc.

Terminarías con decenas de endpoints.

Preferiría:

PUT /odontograma/:id

enviando el documento completo.

await axios.put(
`/odontograma/${id}`,
odontograma
)

El backend valida y persiste.

La complejidad queda concentrada en un único lugar.


Para tu stack (Laravel + Inertia + React + TS) mi recomendación concreta sería:

  1. Mantener el odontograma completo como un único documento JSON.
  2. Estado local React simple (useState o useRef según corresponda).
  3. Guardado explícito al confirmar cambios.
  4. Recarga desde backend después del guardado exitoso.
  5. Lock pesimista de edición por usuario.
  6. Campo version para detectar inconsistencias si el lock falla.

Eso te da una solución muy consistente y extremadamente simple de mantener.

lunes, 22 de junio de 2026

Type predicates en Typescript

Dado el siguiente código:

const factoriaDeSimbolos = {
transposicion: (es_superior:boolean) =>
<g transform={`translate(15 ${es_superior ? '0': '58'})`}>
<Transposicion/>
</g>
}

enlaces.filter(
x =>
['diastema', 'fusion', 'transposicion'].includes(x.tipo)
).forEach(x => {
const indice_de_referencia = String(sequence.next())
dft[esSuperior(x.primer_casillero) ? 0 : 1].push(
<Insercion casillero={(x.primer_casillero+x.ultimo_casillero)/2} key={indice_de_referencia}>
{factoriaDeSimbolos[x.tipo](esSuperior(x.primer_casillero))}
</Insercion>
)
})

 

 Dentro del contexto del callback de forEach, quiero ser explícito en que el tipo de dato de x está modificado por el callback de la función filter en que la propiedad tipo pertenece a la enumeración de valores permitidos.

La forma idiomática en TypeScript es que el callback de filter sea un type predicate.

Supongamos:

type TipoEnlace =
| 'diastema'
| 'fusion'
| 'transposicion'
| 'otro';

interface Enlace {
tipo: TipoEnlace;
primer_casillero: number;
ultimo_casillero: number;
}

Podés definir:

type EnlaceConInsercion =
Enlace & {
tipo: 'diastema' | 'fusion' | 'transposicion';
};

function esEnlaceConInsercion(
enlace: Enlace
): enlace is EnlaceConInsercion {
return ['diastema', 'fusion', 'transposicion']
.includes(enlace.tipo);
}

y luego:

enlaces
.filter(esEnlaceConInsercion)
.forEach(x => {
// x.tipo es:
// 'diastema' | 'fusion' | 'transposicion'

const indice_de_referencia = String(sequence.next());

dft[esSuperior(x.primer_casillero) ? 0 : 1].push(
<Insercion
casillero={(x.primer_casillero + x.ultimo_casillero) / 2}
key={indice_de_referencia}
>
{factoriaDeSimbolos[x.tipo](esSuperior(x.primer_casillero))}
</Insercion>
);
});

Si no querés crear una función aparte, podés usar la sobrecarga de filter directamente:

enlaces
.filter(
(
x
): x is Enlace & {
tipo: 'diastema' | 'fusion' | 'transposicion';
} =>
['diastema', 'fusion', 'transposicion']
.includes(x.tipo)
)
.forEach(x => {
// x.tipo ya está refinado
});

Sin embargo, para código reutilizable y legible, la primera opción (esEnlaceConInsercion) suele ser bastante mejor. Además, TypeScript propagará automáticamente el refinamiento de tipos hasta el forEach.

jueves, 18 de junio de 2026

Understanding Stepping Differences in Chrome DevTools Debugger

 Chrome devtools Step into next function call - F11, Ctrl+; vs Step - F9

The primary difference between these Chrome DevTools actions is how they handle asynchronous code (like setTimeout, promises, or fetch requests) and how strictly they step line-by-line. [1, 2]
Here is a direct breakdown of how Step into next function call (F11 / Ctrl+;) compares to Step (F9). [1, 2, 3]
Direct Comparison
Feature [1, 2, 3, 4, 5]Step into next function call (F11 / Ctrl+;)Step (F9)
Primary ActionEnters inside the very next function call.Executes the next chronological line of code.
Async HandlingEnters inside asynchronous code blocks (e.g., setTimeout callbacks).Skips over async callbacks, treating them like a normal step.
No Function ScenarioBehaves like a standard line-by-line step if no function is present.Moves directly to the absolute next line.

📥 Step Into Next Function Call (F11 or Ctrl+;) [1]
This action tells the debugger to dive deep into whatever code is firing next. [1, 2]
  • Synchronous Behavior: If you are paused on calculateTotal(), pressing F11 moves the debugger directly inside the first line of the calculateTotal function definition.
  • Asynchronous Power: It is built to track async boundaries. If you step into a line containing a setTimeout(() => { console.log('hi') }, 1000), DevTools will actually hand off and pause execution inside that arrow function when the timer fires, rather than letting it run invisibly in the background. [1, 2, 3, 4]
➡️ Step (F9)
This command is a chronological, linear walk through your script. [1, 2]
  • Line-by-line Execution: It executes the next statement strictly as it appears in the script timeline.
  • Async Ignorance: It explicitly ignores async orchestration. If it hits a setTimeout or a microtask callback hook, it schedules it normally but keeps moving down your primary execution thread. It behaves much like a traditional "Step Over" command for asynchronous operations, ensuring you stay focused on the current script execution sequence. [1, 2, 3]
If you are debugging complex asynchronous architectures, let me know if you are working with Promises, async/await, or event listeners. I can provide tailored tips for navigating those call stacks efficiently!