feat(nakui-ui): snapshot/compaction durante runtime cada N writes

El compact ya no es sólo en boot: en runtime, después de cada write
efectivo (commit_seed Created/Updated, commit_morphism, commit_delete),
incrementamos un contador en memoria y disparamos maybe_compact_log
cuando alcanza el threshold (mismo NAKUI_SNAPSHOT_THRESHOLD del
startup). El log no crece sin tope en sesiones largas.

- Nuevos fields en MetaUi: snap_path, snapshot_threshold,
  writes_since_compact (los dos primeros cacheados en new()).
- Nuevo método tick_runtime_compact: increment + check + maybe
  compact + reset. Si compact falla, counter NO se resetea (próximo
  write reintenta). Threshold 0 desactiva.
- Helper append_compact_msg concatena el msg de compact al toast
  del op original con "; " separator.
- Wireado en los 3 callsites de write. NoChange (edit sin cambios)
  no cuenta — preserva "1 write = 1 log entry = 1 tick".

2 tests nuevos: format del helper, ciclo de 7 writes con
threshold=3 verifica 2 compacts + counter residual + log final con
1 anchor + 1 write.

37 tests verdes (+2). Workspace build verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-09 22:02:00 +00:00
parent 9951307fd9
commit 613f4f299e
2 changed files with 236 additions and 6 deletions
+64
View File
@@ -6,6 +6,70 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(nakui-ui): snapshot/compaction durante runtime cada N writes
Cierra el último pending del round de persistencia. Antes el compact
sólo corría al startup — para una sesión larga con muchas escrituras,
el log crecía sin tope hasta el próximo restart, y el siguiente boot
pagaba el costo lineal del replay.
Cambios:
- **Nuevos fields en `MetaUi`**:
- `snap_path: PathBuf` — cacheado del init para que el tick no
tenga que recomputarlo.
- `snapshot_threshold: usize` — leído del env en `new()` y
cacheado. `0` desactiva runtime compact (mismo env y
semantic que el threshold de startup).
- `writes_since_compact: u64` — contador que incrementa por cada
write efectivo y se resetea cuando el threshold dispara
`maybe_compact_log`.
- **Nuevo método `tick_runtime_compact()`**:
- Early return si `threshold == 0`.
- Increment + check vs threshold.
- Si cruza: lock log + store, llama `maybe_compact_log`.
- **Si compactó OK**: counter = 0, devuelve msg.
- **Si `maybe_compact_log` returned None** (counter dijo "go"
pero entries < 2): counter = 0 (no re-entrar cada write).
- **Si error**: counter NO se resetea (próximo write reintenta),
devuelve el error.
- **Nuevo helper `append_compact_msg(base, opt)`**: concatena el
msg del compact al toast del op original con `";"` separator.
- **Wireup en 3 callsites de write efectivo**:
- `apply_action::SeedEntity`: tick si outcome != NoChange.
- `apply_action::Morphism`: tick siempre que Ok.
- Click handler `[Confirmar]` del delete modal: tick si commit_delete Ok.
- **NoChange no cuenta**: un edit que no cambia nada no escribe al
log, así que tampoco debería avanzar el counter — preserva la
semantic "1 write = 1 log entry = 1 tick".
2 tests nuevos:
- `append_compact_msg_handles_both_branches` — base solo vs base
+ compact, formato del separator.
- `runtime_compact_cycle_resets_counter_after_threshold` — E2E
estilo simulación: 7 writes con threshold=3 → 2 compacts (en
write 3 y 6), counter residual = 1, log final con 2 entries
(1 anchor + 1 write residual). Reproduce el algoritmo del tick
sin GPUI cx; si la lógica del método cambia, se rompe como signal.
37 tests verdes (+2). Workspace build verde.
Trade-offs:
- **Counter en memoria, no persistido**: si la app crashea entre
compacts, al próximo boot el counter parte de 0. El startup
compact (basado en entry_count del log file) compensa esto:
si quedó mucho post-último-compact, se compacta al boot.
- **Lock orden**: tick toma log lock primero, store lock después.
Misma orden que `commit_seed` y `commit_morphism`, no debería
haber deadlock.
- **Costo del tick**: 1 increment + 1 compare por write. Cuando
cruza threshold, 1 read del log (entries) + 1 snapshot write +
1 compact. Para threshold=50 es ~1 fsync cada 50 writes —
amortiza bien.
Pendientes restantes:
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
- **Validación cross-field** (UUID del EntityRef existe en la
entity referida).
### feat(nakui-ui): atajo Esc para cancelar el modal de delete
Cierra otro pendiente de UX. El banner de confirmación de delete
ya tenía botones [Cancelar] / [Confirmar], pero la acción más