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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user