Metadata-Version: 2.4
Name: megadbx
Version: 1.1.4
Summary: Motor de base de datos embebida, rapida y robusta para Python.
Author: MegaStar
License: ISC
Keywords: database,db,embedded,json,megadb,jsondb
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Topic :: Database
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: certifi>=2023.7.22
Provides-Extra: xxhash
Requires-Dist: xxhash>=3.0.0; extra == "xxhash"
Provides-Extra: panel
Requires-Dist: flask>=2.0; extra == "panel"
Requires-Dist: bcrypt>=4.0; extra == "panel"
Provides-Extra: all
Requires-Dist: xxhash>=3.0.0; extra == "all"
Requires-Dist: flask>=2.0; extra == "all"
Requires-Dist: bcrypt>=4.0; extra == "all"

# MEGADBX (Python)

`megadbx` es un motor de base de datos embebida para Python, diseñado para ser rápido y confiable. Combina la flexibilidad de un **document store** con características avanzadas como integridad de datos, concurrencia segura, compresión, control de versiones, índices optimizados y alta robustez, todo en un paquete ligero y eficiente.

Este paquete es una adaptacion **completa y síncrona** del proyecto original `megadbx` para Node.js: misma lógica, mismos algoritmos, mismo formato de archivos en disco, la única diferencia real es que en node.js todo es `async/await` y aquí todo es **síncrono** (llamadas normales, sin `await`, sin loop de eventos).

## Novedades 1.1.0 => 1.1.4
* Se añadio nuevas opciones a [MegaDBCloud](#megadbcloud) tanto en la clase como en el panel administrativo.
* Se añadio la opcion para desactivar el intervalo de tiempo de la creacion de backups.
* Se arreglaron bugs menores.

## Novedades 1.1.0

* Se añadio la posibilidad de almacenar datos en nube usando MegaDBCloud, todo mediante una interfaz rapida, sencilla y segura.
* Ya se cuenta con un servidor de discord para hacer consultas, dudas o sugerencias, **https://discord.gg/rXngzDpAHf** 

---
## Características principales

- **Persistencia optimizada en archivos JSON comprimidos**
  - Escritura atómica en disco con compresión opcional.
  - Mirror y Journal para recuperación segura.

- **Integridad de datos avanzada**
  - Hash **SHA-256** por bloque.
  - **CRC32** por páginas de datos.
  - **xxHash** para detección rápida de corrupción.
  - **Checksum por documento** (no solo por bloque) si un documento puntual se corrompe, se detecta y se recupera solo, sin invalidar el resto del bloque (ver [Checksums por documento y recuperación automática](#checksums-por-documento-y-recuperación-automática)).
  - **WAL con checksum por línea** detecta corrupción silenciosa (bitrot), no solo cortes a mitad de escritura.
  - **Backups verificados**: `restoreBackup()` valida el checksum del backup ANTES de aplicar nada a la base de datos.

- **Compresión selectiva de campos** (`gzip` o `lz4`), con **umbral de tamaño mínimo** (`compressMinSize`) no comprime valores chicos donde comprimir sale más caro que dejarlos tal cual.

- **Búsquedas rápidas**
  - **Bloom Filters** por bloque para aceleración de consultas.
  - **LRU Cache** para acceso inmediato a datos recientes.
  - Precarga de bloques en `find()`.

- **Automatización**
  - `flush()` periódico configurable (corre solo, en un hilo de fondo).
  - `createBackup()` automático con intervalos programados.

- **Snapshots & Backups**
  - Crear y restaurar snapshots completos.
  - Backups comprimidos con rotación flexible (por **contador** o **timestamp**), y **verificados por checksum antes de restaurar**.

- **Coordinación multiproceso** (`multiProcess: True`) para cuando varios procesos Python (por ejemplo, varios workers de `gunicorn`/`multiprocessing`) comparten la misma carpeta de base de datos: invalidación de caché entre procesos + locks por bloque, sin necesidad de un servidor central.

- **Transacciones ACID reales entre múltiples bases de datos** commit de 2 fases (prepare + apply) con recuperación automática si el proceso se cae a mitad de un commit.

- **TTL por documento** documentos que expiran solos, con expiración perezosa (`get()`) y un barrido activo periódico.

- **Modo seguro y multiproceso** (*MegaDB*, *MegaDBSafe*, *MegaDBFull*)
  - **WAL (Write-Ahead Log)** para consistencia y recuperación.
  - Concurrencia segura con **cola FIFO interna** (basada en `threading.RLock`).
  - Consultas con filtros avanzados (`$gt`, `$lt`, `$in`, `$regex`, etc.).
  - **Cursor de streaming** (`stream()`, `entries()`) generadores Python normales, para recorrer colecciones grandes sin cargarlas enteras en RAM.

- **Extensiones avanzadas** (*MegaDBFull*)
  - Índices: secundarios, compuestos, **trie** (prefijos) y **skiplist** (rangos) todos con **modo paginado en disco opcional**, para colecciones grandes.
  - `rebuildAllIndexes()` reconstruye todos los índices en streaming (memoria acotada), ideal para activarlos sobre datos que ya existían.
  - **MVCC**: múltiples versiones de registros sin bloqueos pesados.
  - **Append-only log (AOF)** para almacenamiento auditable.
  - **Merkle Tree** para verificación criptográfica de integridad.

- **Schema opcional** (`required`, `type`, `enum`, `unique`) por colección.

- **Transacciones entre múltiples bases de datos.**
- **Panel de administración web** (`AdminPanel`, basado en Flask) login, CRUD completo con bloqueo optimista, gestión de índices/backups/transacciones pendientes, auditoría.
- ...y mucho más.
---
## Qué persistencia usa megadbx actualmente al guardar los datos

megadbx implementa un sistema de persistencia diseñado para garantizar integridad de datos, resistencia a fallos y consistencia en disco, incluso en casos de caída del proceso o del sistema operativo.

El ciclo de escritura y persistencia combina varias capas de protección:

### 1. Escritura atómica
Cada modificación en un bloque se guarda mediante un proceso *atomic write*:
- Se escribe primero en un archivo temporal.
- El archivo temporal se sincroniza en disco (`fsync`).
- Luego se reemplaza el archivo original (`os.replace`), operación atómica en sistemas POSIX.

Esto asegura que nunca exista un archivo a medio escribir: o queda el archivo anterior, o el nuevo completo.

### 2. Journal de respaldo
Antes de sobrescribir un bloque, megadbx crea un archivo journal (`.journal`) que contiene la nueva versión.
- Si ocurre un fallo en mitad de la escritura, el journal queda disponible como copia.
- Una vez que la escritura finaliza con éxito, el journal se elimina.

Este patrón es similar al *Write-Ahead Logging (WAL)*.

### 3. Mirror de bloques
Además del archivo principal, cada bloque se replica en un directorio espejo (mirror).
- El mirror mantiene una segunda copia sincronizada de cada bloque.
- Si el archivo principal se corrompe (ejemplo: fallo de disco, corte de energía durante flush), megadbx puede recuperarlo automáticamente desde el mirror en el próximo arranque.

### 4. Checksums e integridad
Cada bloque almacenado en disco incluye:
- `crcPages`: sumas CRC32 por páginas, para verificar integridad parcial.
- `xxhash`: un hash fuerte del bloque completo.
- `keyCrc`: checksums de subdocumentos individuales.
- `docChecksums`: un checksum **por documento raíz** (no solo del bloque entero) ver la sección siguiente.

Con esto se puede detectar y reparar corrupción de datos a varios niveles.

### Checksums por documento y recuperación automática
Además del checksum de bloque completo, cada documento raíz tiene su propio checksum guardado junto a el (`docChecksums`). Esto importa porque un checksum de bloque entero solo detecta que "algo en este bloque cambió" mas no te dice *que* documento, y una corrupción de un solo documento invalidaría la lectura de todos los demás que viven en el mismo bloque si solo tuvieras el checksum global.

Cuando `get()` detecta que el checksum de un documento no coincide, dispara una cadena de recuperación en este orden, probando cada fuente hasta encontrar una copia íntegra:

1. **Espejo (mirror)** la copia espejo del bloque, escrita de forma atómica e independiente. Cubre el caso más común: bitrot de disco en una sola de las dos copias.
2. **Backups** del más reciente al más viejo, cada uno con su propio checksum verificado antes de confiar en él. Se extrae solo el documento puntual, no se restaura la colección entera.
3. **WAL** disponible en `MegaDBSafe`/`MegaDBFull`: se reconstruye el último valor conocido reaplicando en orden las operaciones registradas para esa clave.
4. **Cuarentena** si ninguna fuente tenía una copia íntegra, la copia cruda (posiblemente corrupta) se mueve a una carpeta `_quarantine/` y lanza un `MegaDBError` explícito, en vez de devolver datos potencialmente corruptos silenciosamente.

### Índice global
- `useGlobalIndex: True` mantiene un archivo `index.json` (mapa clave raíz => bloque) para acelerar la carga tras reiniciar.
- Si `index.json` falta o está dañado, megadbx puede reconstruir el índice completo a partir de los datos de los bloques.

---
En conjunto, estas técnicas permiten:
- **Atomicidad**: nunca quedan datos parciales.
- **Durabilidad**: los cambios confirmados sobreviven a fallos del proceso o del sistema operativo.
- **Recuperación rápida**: con journal y mirror, la base de datos se repara sola si encuentra corrupción.
- **Seguridad extra**: snapshots y backups permiten volver atrás en caso de error humano.
- Evita lecturas repetitivas de JSON en disco.
- Mantiene la mayoría de las operaciones de consulta en memoria.
- Ofrece tiempos de respuesta consistentes incluso en bases con muchos datos.
---

## Instalación

```bash
pip install megadbx

# Opcional, para checksums xxh64 (si no está, cae a sha256 truncado):
pip install megadbx[xxhash]

# Opcional, si usas compressAlg='lz4':
pip install lz4

# Opcional, si usas la clase AdminPanel (ver sección más abajo):
pip install megadbx[panel]
```

---

# Notas generales sobre parámetros y uso de palabras.
* En la documentacion se usan palabras como:
  - `coleccion`: Toda la base de datos (db) => {key1: value1, key2: value2, etc} => {documento1, documento2, etc}
  - `documento(s)`: Lo que se guardó en la base de datos => {key: value}
  - `clave/key`: La clave con la que se guardo el documento => key
  - `value/valor`: El valor guardado en el documento => value
  - `query`: diccionario de filtro usando operadores relacionales => { "edad": { "$gt": 18 } } etc.
  - `path`: Ruta dentro del documento ("usuario1") o coleccion (si usa wildcard)
  - `wildcard`: Símbolo especial para acceder a toda la coleccion o sub documentos de un documento. "*"
  - `dot notation`: Símbolo especial para acceder anidadamente a una ruta dentro de un documento '.'

> **Nota sobre `None` vs valores ausentes**: JavaScript distingue `undefined` (ausencia de valor) de `null` (valor nulo explícito). Python no tiene un equivalente nativo de `undefined`, así que en este port **ambos casos se representan con `None`** el comportamiento esperable de un `.get()` de Python sobre una clave inexistente.

# Operadores relacionales y textuales

* Existen operadores que nos ayudan a la hora de filtrar o buscar documentos, estos operadores siguen una regla estricta:
  - `$gt`: Mayor que: **valor > $gt**
  - `$gte`: Mayor o igual que: **valor >= $gte**
  - `$lt`: Menor que: **valor < $lt**
  - `$lte`: Menor o igual que: **valor <= $lte**
  - `$eq`: Igualdad: **valor == $eq**
  - `$ne`: Distinto: **valor != $ne**
  - `$in`: Pertenece a la lista - $in debe ser una lista: **valor in $in**
  - `$nin`: No pertenece a la lista - $nin debe ser una lista: **valor not in $nin**
  - `$between`: Rango inclusivo [min, max] - Esta dentro del rango min y max: **valor >= min and valor <= max**
  - `$prefix`: Coincidencia de prefijo de string: **str(valor).startswith(...)**
  - `$regex`: Expresión regular (string), opcional `$flags` (`i`, `s`, `m`): **re.search($regex, str(valor), flags)**


# Clases principales

## (MegaDB) Constructor y métodos:
* [MegaDB](#megadb)
  * [set](#set)
  * [has](#has)
  * [get](#get)
  * [delete](#delete)
  * [all](#all)
  * [stream](#stream)
  * [entries](#entries)
  * [keys](#keys)
  * [values](#values)
  * [count](#count)
  * [find](#find)
  * [aggregate](#aggregate)
  * [update](#update)
  * [watch](#watch)
  * [stats](#stats)
  * [flush](#flush)
  * [close](#close)
  * [createBackup](#createbackup)
  * [restoreBackup](#restorebackup)
  * [listBackups](#listbackups)
  * [createSnapshot](#createsnapshot)
  * [restoreSnapshot](#restoresnapshot)
* Conceptos relacionados: 
  * [Schema](#schema) 
  * [Umbral de compresión](#umbral-de-compresión) 
  * [Multiproceso](#multiproceso) 
  * [TTL por documento](#ttl-por-documento) 
  * [Checksums por documento y recuperación automática](#checksums-por-documento-y-recuperación-automática)
---
## (MegaDBSafe) Constructor y métodos:
* [MegaDBSafe](#megadbsafe)
  * [ready](#ready)
  * [set](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [has](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
  * [get](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [delete](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [all](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
  * [stream](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
  * [entries](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [keys](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [values](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [count](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [find](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [aggregate](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [update](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [watch](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch) 
  * [stats](#stats-1) 
  * [flush](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot) 
  * [close](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot) 
  * [createBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
  * [restoreBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
  * [listBackups](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot) 
  * [createSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot) 
  * [restoreSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
---
## (MegaDBFull) Constructor y métodos:
* [MegaDBFull](#megadbfull)
  * [ready](#ready-1)
  * [set](#set-1) 
  * [has](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [get](#get-1) 
  * [delete](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [all](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [stream](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [entries](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [keys](#has--all--stream--entries--delete--keys--values--count--find--aggregate) 
  * [values](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [count](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [find](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [aggregate](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
  * [update](#update-1) 
  * [watch](#watch-1) 
  * [history](#history) 
  * [stats](#stats-2) 
  * [flush](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
  * [close](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
  * [createBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1) 
  * [restoreBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
  * [listBackups](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
  * [createSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1) 
  * [restoreSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1) 
  * [verifyIntegrityFast](#verifyintegrityfast)  
  * [rebuildAllIndexes](#rebuildallindexes)
* Conceptos relacionados: 
  * [Índices paginados en disco](#índices-paginados-en-disco-compositeindexmanager-trieindexmanager-y-skiplistindexmanager)
---
## MegaDBCloud Constructor y metodos:
* [MegaDBCloud](#megadbcloud)
  * [`get`, `set`, `delete`, `find`,`count`, `update`, `aggregate`, `stats`](#metodos-cloud)
---
## (Transaction) Constructor y métodos:
* [Transaction](#transaction)
  * `tx.<db>.get(path)` 
  * `.set(path, value)`
  * `.has(path)` 
  * `.delete(path)`
* [commit](#commit)
* [rollback](#rollback)
* [Transaction.recoverPending](#transactionrecoverpending-estático)
---
## (AdminPanel) Constructor y métodos:
* [AdminPanel](#adminpanel)
  * [start](#start)
  * [stop](#stop)
---

## Dato importante, [leer esto después de entender cómo funciona MegaDB/MegaDBSafe/MegaDBFull](#leer-después-de-entender-megadb-megadbsafe-y-megadbfull)



# 1. `MegaDB`

## MegaDB
```python
from megadbx import MegaDB
db = MegaDB(collection_name, options={})
```

- **collection_name** (str): nombre de la colección (carpeta en `./db/`).
- **options** (dict, opcional):


| Opción                 | Default                           | Explicación                                               |
| ---------------------- | ---------------------------------- | ----------------------------------------------------------|
| `dir`                  | directorio del script principal    | Directorio donde se creará la carpeta del db/             |
| `mirrorDirName`        | `mirror`                           | Carpeta para guardar los 'espejos' del db                 |
| `blockSize`            | `100`                              | Cantidad de claves por bloque.                            |
| `flushInterval`        | `30000` ms                         | Cada cuánto guardar datos a disco.                        |
| `crcPageSize`          | `4096` bytes                       | Tamaño de página para CRC.                                |
| `schema`               | `None`                             | Validación (`required`, `type`, `enum`, `unique`) ver [Schema](#schema). |
| `secondaryIndexes`     | `[]`                               | Campos indexados.                                         |
| `rebuildIndexOnLoad`   | `False`                            | Reconstruye los campos indexados al inicializar el db.    |
| `compressFields`       | `[]`                               | Campos a comprimir.                                       |
| `compressMinSize`      | `256` bytes                        | No comprime valores por debajo de este tamaño, ver [Umbral de compresión](#umbral-de-compresión). |
| `bloomK`               | `4`                                 | Hashes en BloomFilter.                                     |
| `expectedKeysPerBlock` | `1000`                             | Estimación de cuántas claves tendrá cada bloque.           |
| `bloomM`               | `expectedKeysPerBlock * 10`        | Tamaño (en bits) del Bloom filter por bloque.               |
| `lruCap`               | `128`                               | Tamaño de caché LRU.                                        |
| `integrityOnRead`      | `True`                              | Verificar integridad en lectura.                            |
| `integrityCooldownMs`  | `180000` ms                        | Tiempo entre verificaciones de integridad.                  |
| `backupMode`           | `counter`                           | Nombres de backup (`counter` o `timestamp`).                |
| `backupInterval`       | `3600000` ms                        | Intervalo entre backups, 0 para desactivarlo.                                     |
| `maxBackups`           | `10`                                 | Backups a conservar.                                         |
| `compressAlg`          | `gzip`                               | Algoritmo de compresión (`gzip` o `lz4`).                    |
| `useGlobalIndex`       | `False`                              | Mantiene y persiste un índice global simple.                 |
| `indexFlushInterval`   | `1800000` ms                        | Cada cuánto se guarda cuando `useGlobalIndex` está en `True`. |
| `keyCacheCap`          | `1024`                               | Tamaño de una caché de claves-valor para acelerar `get()`.   |
| `multiProcess`         | `False`                              | Coordinación entre procesos que comparten la misma carpeta `db/`, ver [Multiproceso](#multiproceso). |
| `manifestPollIntervalMs` | `3000` ms                          | Cada cuánto revisa el manifiesto de versiones en modo `multiProcess`. |
| `lockTimeoutMs`        | `30000` ms                           | Tiempo máximo esperando un lock de bloque en modo `multiProcess`. |
| `lockTtlMs`            | `30000` ms                           | TTL del lock de escritura (para detectar locks huérfanos si un proceso muere sosteniéndolo). |
| `ttlSweepIntervalMs`   | `flushInterval`                      | Cada cuánto corre el barrido activo de documentos vencidos, ver [TTL por documento](#ttl-por-documento). |


**(Explicación detallada)**

`dir`: Directorio donde se creará la base de datos; la carpeta será `[dir]/db/[collection_name]`. Si no se especifica, se usa el directorio del script principal que arrancó el proceso Python.
```python
from megadbx import MegaDB
import os

"""
  Estructura actual de la carpeta global:
  (estoy usando el archivo db.py)

  📂 proyecto
    📂 base_de_datos
      📄 handler.py
      📄 db.py
    📂 comandos
      📄 comando1.py
"""

# Crear la db a la altura de la carpeta base_de_datos/comandos.
db = MegaDB('usuarios')  # sin opciones, usa el default.
"""
  📂 proyecto
    📂 base_de_datos
      📄 handler.py
      📄 db.py
    📂 comandos
      📄 comando1.py
    📂 db
      📂 usuarios
"""
# o también usando dir explícito:
db = MegaDB('usuarios', {
    "dir": os.path.join(os.path.dirname(__file__), "..")
})
```

`mirrorDirName`: Carpeta donde se crean los respaldos "espejos", para recuperar archivos si algo falla al escribir (default: `'mirror'`).

`blockSize`: Tamaño de bloque en cantidad de claves (default: 100).
  - Divide los datos en varios bloques para no tener un JSON gigante.
  - **megadbx** no guarda toda la colección en un solo archivo (como haría un `usuarios.json` enorme).
  - En lugar de eso, divide la colección en varios bloques (archivos pequeños).
  - **blockSize** define el número aproximado de claves que se guardan en cada bloque.
  - Por defecto es 100: cada archivo (`block_0001.json`, `block_0002.json`, etc.) contendrá alrededor de 100 claves.
```python
# Supongamos que guardas 500 usuarios en la colección "usuarios":
for i in range(1, 501):
    db.set(f"u{i}", {"nombre": f"Persona{i}"})
```
```
Caso 1 — blockSize: 100 (default)
db/usuarios/
 ├── block_0001.json   ← contiene ~100 usuarios
 ├── block_0002.json   ← contiene ~100 usuarios
 ├── block_0003.json   ← contiene ~100 usuarios
 ├── block_0004.json   ← contiene ~100 usuarios
 ├── block_0005.json   ← contiene ~100 usuarios
```
- 500 usuarios = 5 bloques.
- Bloques más pequeños → más archivos, más fragmentación.
- Ideal si quieres minimizar pérdidas (solo se daña un bloque chiquito).

Valores pequeños (ej. 50):
 - ✅ Más seguro (si se daña un bloque, pierdes menos).
 - ❌ Más archivos que manejar, un poco más lento para recorrer todo.

Valores grandes (ej. 500 o 1000):
 - ✅ Menos archivos, búsquedas más rápidas en algunos casos.
 - ❌ Si se corrompe un bloque, pierdes más datos.

Valor por defecto (100):
 - Equilibrio entre seguridad y rendimiento.

No te preocupes: hay métodos avanzados de seguridad/respaldo, tanto internos automáticos como otros a tu criterio, para reducir esto prácticamente a cero.

`flushInterval`: Cada cuánto (ms) se guardan los cambios automáticamente en disco (se escriben a disco los cambios pendientes) en segundo plano.
 - Más corto = menos pérdida posible si se cae el proceso, pero más I/O.
 - Default: 30000 ms => 30s.
 - Los valores se reflejan en disco al hacer flush; en memoria se actualiza en tiempo real.
 - Internamente corre en un hilo daemon (`threading.Timer`), sin que tengas que hacer nada.

`crcPageSize`: Tamaño de "página" para calcular CRC por fragmentos del archivo; ayuda a detectar corrupción y reparar desde el espejo (mirror) (default: 4096 = 4KB).

## Schema
`schema`: Validación opcional por colección al hacer `set()`.

```python
db = MegaDB("usuarios", {
    "schema": {
        "fields": {
            "nombre": {"type": "string", "required": True},
            "edad":   {"type": "number"},
            "email":  {"type": "string", "required": True, "unique": True},
            "rol":    {"type": "string", "enum": ["admin", "user", "guest"]},
        }
    }
})

db.set("u1", {"nombre": "dix", "edad": 30, "email": "dix@x.com"})  # OK

db.set("u2", {"edad": 20})
# lanza MegaDBError: Documento invalido para "u2": campo requerido
# faltante: "nombre"; campo requerido faltante: "email"

db.set("u3", {"nombre": "Bob", "email": "dix@x.com"})
# lanza MegaDBError: Valor duplicado para campo unico "email" en "u3": 'dix@x.com'
```

Reglas soportadas por campo:
- `type`: `'string' | 'number' | 'boolean' | 'object' | 'array'`.
- `required`: si `True`, el campo no puede faltar.
- `enum`: lista de valores permitidos.
- `unique`: no puede haber dos documentos con el mismo valor en ese campo. El índice de unicidad se reconstruye solo (escaneando lo existente una vez) la primera vez que se necesita, así que también funciona si activas `unique` sobre datos que ya tenías guardados.

**Compatibilidad**: si le pasas el formato "plano" (`{campo: 'tipo'}`), se normaliza automáticamente al formato con `fields`:
```python
# formato plano, también funciona (se normaliza solo):
schema = {"edad": "number", "nombre": "string"}
```

**Alcance (a propósito, para mantenerlo simple y predecible)**: solo valida cuando se escribe un documento RAÍZ completo (`db.set('u1', {...})`), no en escrituras de rutas anidadas (`db.set('u1.direccion.ciudad', 'Lima')`). Validar en escrituras parciales requeriría leer + mergear + revalidar el documento entero en cada escritura anidada.

En `MegaDBSafe`/`MegaDBFull`, la validación corre **antes** de tocar el WAL/AOF/MVCC, si el documento es inválido, la operación nunca llega a loguearse.

`secondaryIndexes`: Lista de campos para indexar y acelerar búsquedas por igualdad en `find()`, usando filtros `$eq`/`$in`/`$ne`/`$nin`/`$gt`/`$lt`/`$gte`/`$lte`/`$between`/`$prefix`/`$regex` sobre el valor.

- Guarda en un diccionario inverso las rutas de las claves asociadas a un valor.
- Esto evita tener que recorrer todos los datos ya que directamente accede a la ruta.
- Soporta rutas anidadas usando *dot notation* (el punto `.`).
- Muy útil cuando tienes miles de datos. Ej:

```python
# Este ejemplo fue testeado con 50,000 usuarios añadidos al db

# usando propiedades planas al igual que dot notation
secondary_indexes = ["nombre", "perfil.id", "perfil.edad"]
db = MegaDB("usuarios", {"secondaryIndexes": secondary_indexes})

db.set("u1", {"nombre": "mega", "email": "mega@gmail.com"})
db.set("u2", {"nombre": "ratsa", "email": "ratsa@gmail.com"})

db.set("u3", {"perfil": {"id": "dix", "edad": 25}})
db.set("u4", {"perfil": {"id": "Luis", "edad": 30}})
db.set("u5", {"perfil": {"id": "dix", "edad": 40}})

# usamos find sin el beneficio del indice (comparación conceptual):
import time
t0 = time.perf_counter()
data = db.find("*", {"nombre": {"$eq": "mega"}})
print(time.perf_counter() - t0)
print(data)  # [{'nombre': 'mega', 'email': 'mega@gmail.com'}]

t0 = time.perf_counter()
data2 = db.find("*", {"perfil.id": {"$eq": "dix"}})
print(time.perf_counter() - t0)
print(data2)
# [{'perfil': {'id': 'dix', 'edad': 25}}, {'perfil': {'id': 'dix', 'edad': 40}}]
```
**DATO IMPORTANTE:**

*En MegaDB:*

  - `secondaryIndexes`, al guardar en memoria, solo guarda los datos que se hicieron posteriores a la inicialización del db, si tenías datos agregados anteriormente, solo se indexan los nuevos hasta que el db se reinicie (desventaja por tenerlo solo en memoria).
  - Existe la opción de revertir esto: al iniciar el db, se cargan todos los datos y se reconstruye el `secondaryIndexes` en memoria en base a tu db actual (ver `rebuildIndexOnLoad`).

*En [MegaDBSafe](#megadbsafe) o [MegaDBFull](#megadbfull)*

  - `secondaryIndexes` persiste en disco (se guarda en `indexes.json`), así que al iniciar el db solo se toman los datos actuales del archivo, mejorando flexibilidad y rapidez.
  - Para sincronizar datos preexistentes con `indexes.json`, usa `rebuildIndexOnLoad`, puedes usarlo una vez para sincronizar y luego deshabilitarlo.

`rebuildIndexOnLoad`: Reconstruye los campos indexados del `secondaryIndexes` tomando los datos ya existentes de los bloques (default: `False`).

`compressFields`: Lista de rutas de campos (soporta dot notation `.` y wildcards `*`) a comprimir con gzip o lz4 al persistir en disco. Al leer se descomprime solo. Útil para textos grandes:
```python
db = MegaDB("datos", {"compressFields": [
    "user1",             # campo top-level, comprime todo el subárbol
    "user2.descripcion", # anidado dot notation .
    "*.descripcion",     # wildcard * , aplica a todas las claves raíz con subcampo descripcion
    "datos.*.prof",      # dot notation + wildcard, aplica a todas las claves de datos con subcampo prof
]})

db.set('user1', {"titulo": "Ejemplo", "descripcion": "Texto muuuuuuuy largo....."})
db.set('user2', {"titulo": "Ejemplo", "descripcion": "Texto muuuuuuuy largo....."})

db.set('datos', {
    "mario": {"prof": "programador", "edad": 20},
    "pedro": {"prof": "agronomo", "edad": 25},
    "juan": False,
})  # aplica "datos.*.prof" a mario y pedro
```
- Puedes combinar wildcards y dot notation incluso más de una vez: `data.*.*.obj1`, `*.*.obj2`, etc.

`bloomK`: Número de hashes del Bloom filter por bloque. Más alto = menos falsos positivos, más CPU (default: 4).

`expectedKeysPerBlock`: Estimación de cuántas claves tendrá cada bloque; se usa para dimensionar el Bloom filter si no fijas `bloomM` (default: 1000).

`bloomM`: Tamaño (en bits) del Bloom filter por bloque; si no se pasa, se calcula como `expectedKeysPerBlock * 10`.

- Tip rápido: si quieres bajar falsos positivos, sube `bloomM` o `bloomK`.

`integrityOnRead`: Activa verificaciones de integridad (CRC por páginas + hash) de forma periódica/al leer los bloques. Si detecta corrupción intenta auto-recuperar desde el `mirror` (default: `True`).

`integrityCooldownMs`: Tiempo mínimo entre verificaciones de integridad (default: 180000 ms => 3 minutos, para no "castigar" el disco).

`backupMode`: `"counter"` o `"timestamp"`. Define cómo nombrar los backups.

`backupInterval`: Cada cuánto (ms) se crea un backup automático comprimido de todos los bloques, rotado según `maxBackups` (default: 1 hora, 0 para desactivarlo).

`maxBackups`: Cuántos backups guardar antes de borrar los viejos (default: 10).

`compressAlg`: Algoritmo de compresión para bloques/campos, y también para snapshots/backups (default: `gzip`).

| Característica                 | **Gzip**                                          | **LZ4**                                                    |
| ------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
| **Compresión**                  | Alta (archivos más pequeños)                       | Media (archivos más grandes que gzip)                        |
| **Velocidad de compresión**     | Lenta (más CPU)                                    | Muy rápida                                                    |
| **Velocidad de descompresión**  | Buena, pero más lenta que LZ4                      | Extremadamente rápida                                         |
| **CPU requerida**               | Mayor                                              | Mucho menor                                                    |
| **Soporte nativo en Python**    | Sí (`gzip`, stdlib)                                | No (se instala `lz4`)                                          |
| **Instalación**                 | No requiere nada                                   | `pip install lz4` (puede requerir compilador C)                |

`useGlobalIndex`: Mantiene y persiste un índice global simple en `index.json` (mapea claves raíz → bloque), para acelerar la carga/consultas tras reiniciar (default: `False`).

`indexFlushInterval`: Cada cuánto se persiste `index.json` cuando `useGlobalIndex` está en `True`; si lo pones en 0 no hay intervalo periódico (default: 1800000 ms => 30 minutos).

`lruCap`: Capacidad de la caché LRU de bloques en memoria (default: 128); más grande = menos lecturas de disco, más RAM.

`keyCacheCap`: Tamaño máximo de la caché LRU para claves individuales, controla cuántas claves recientes se guardan en memoria para acelerar lecturas repetidas (default: 1024). Cuando llamas a `get(path)`, la base de datos guarda en memoria los últimos resultados para que futuras lecturas sean instantáneas, sin:
  - Leer el bloque desde disco.
  - Descomprimir el campo (si estaba en `compressFields`).
  - Pasar por validaciones de integridad.

```python
db = MegaDB("users", {"keyCacheCap": 2})  # solo guardará 2 claves en caché

db.set("user.1", {"name": "mega", "age": 13})
db.set("user.2", {"name": "ratsa", "age": 14})
db.set("user.3", {"name": "Charlie", "age": 40})

print(db.get("user.1"))  # primera lectura: va a disco y guarda en caché
print(db.get("user.1"))  # segunda lectura: desde caché, mucho más rápida
print(db.get("user.3"))  # al llegar a 3 claves, la más antigua se expulsa
```

## Umbral de compresión
`compressMinSize`: Con `compressFields`, no comprime valores cuyo tamaño serializado (JSON) esté por debajo de este umbral (default: 256 bytes). Comprimir un valor chico es contraproducente: el header de gzip por sí solo son ~18 bytes, y sumale el costo de CPU, para valores chicos, terminas gastando más de lo que ahorras.

```python
db = MegaDB("usuarios", {
    "compressFields": ["bio"],
    "compressMinSize": 256,  # default
})

db.set("u1", {"bio": "hola"})              # ~15 bytes: NO se comprime
db.set("u2", {"bio": "x" * 2000})          # 2000+ bytes: SI se comprime
```

## Multiproceso
`multiProcess`: Coordina esta instancia con OTROS procesos Python que comparten la misma carpeta `db/`, el caso típico son varios workers de un servidor WSGI (`gunicorn`, `uwsgi`) o `multiprocessing`, donde cada worker es un proceso de sistema operativo separado con su propia copia en memoria de `blocksCache` (default: `False`).

**El problema que resuelve:** worker A escribe un documento, worker B tiene ese mismo bloque cacheado desde antes en su memoria, sin coordinación, worker B seguiría devolviendo el valor viejo indefinidamente.

**Cómo lo resuelve:**
1. **`BlockManifest`** un archivo chico (`_manifest.json`) con `{blockId: version}`. Cada escritura exitosa incrementa la versión de ESE bloque. Los demás procesos hacen polling periódico (`manifestPollIntervalMs`) e invalidan solo los bloques que realmente cambiaron.
2. **`SharedFileLock`** un lock de lectores/escritor **por bloque** (no global), basado en archivos-marcador con heartbeat/TTL. Se adquiere antes de escribir un bloque en disco.

```python
db = MegaDB("pedidos", {
    "dir": "./",
    "multiProcess": True,           # activa la coordinacion
    "manifestPollIntervalMs": 3000, # default
    "lockTimeoutMs": 30000,         # default
    "lockTtlMs": 30000,             # default
})
```

**Costo si NO lo necesitas:** cero, con `multiProcess: False` (default) no se instancia ningún manifiesto ni lock. Solo actívalo si de verdad tienes más de un proceso tocando la misma carpeta `db/` al mismo tiempo. Para concurrencia entre **hilos** dentro de un mismo proceso, no hace falta `multiProcess`: eso ya lo cubre el lock interno (`QueuedLock`, basado en `threading.RLock`) de `MegaDBSafe`/`MegaDBFull`.

## TTL por documento
Documentos que expiran solos, pasado un tiempo. Se define por escritura, no por colección, ver [`set(path, value, opts)`](#set) para el parámetro `ttl`.

```python
db = MegaDB("sesiones", {"ttlSweepIntervalMs": 60000})  # default: usa flushInterval (30000ms)

db.set("s1", {"userId": "u1"}, {"ttl": 60000})  # expira en 60s
db.set("s2", {"userId": "u2"})                  # sin ttl, permanente

print(db.get("s1"))  # {'userId': 'u1'}
# ... pasan 60+ segundos ...
print(db.get("s1"))  # None — expiro
```

**Cómo se expira:**
- **Perezosa (lazy)**: si alguien pide una clave vencida vía `get()`, se borra ahí mismo y devuelve `None`.
- **Barrido activo**: cada `ttlSweepIntervalMs`, se revisan los bloques "tibios" en memoria y se borran los documentos vencidos que nadie volvió a leer.

> Mismo alcance que `schema`: el TTL aplica a documentos raíz completos. Si haces `set()` de una ruta anidada, no se le asigna TTL (y si el documento raíz ya tenía uno, un `set()` posterior sin `ttl` se lo quita).

## set
### `set(path, value, opts={})`
Crea o actualiza un valor en la ubicación indicada por `path`.

- **`path` (str):** ruta jerárquica donde almacenar el valor, soporta dot notation `.`
  Ejemplo: `"users.1.name"` → dentro de `users`, clave `1`, propiedad `name`.
- **`value` (any):** el valor que quieres guardar (str, número, bool, dict, lista, etc.).
- **`opts` (dict, opcional):**
  - `ttl` (número, ms): si se pasa, el documento raíz expira solo después de ese tiempo, ver [TTL por documento](#ttl-por-documento). Solo aplica cuando `path` es un documento raíz (sin dot notation).
- **`retorna`:** `True` si se guardó correctamente.

Si tienes un `schema` definido en las opciones del constructor, `set` lo valida antes de guardar.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})
db.set("users.1.email", "mega@gmail.com")
"""
{
  "users": {
    "1": {"name": "mega", "age": 13, "email": "mega@gmail.com"},
    "2": {"name": "ratsa", "age": 14}
  }
}
"""

db.set("counter", {"id": "1239858345", "count": 48})

# con TTL: este documento se borra solo despues de 60 segundos
db.set("sesiones.s1", {"userId": "u1"}, {"ttl": 60000})
```
---

## has
### `has(path)`
Verifica si existe una clave en la colección de la ruta `path`.

* **`path` (str):** ruta a la clave a verificar, soporta dot notation `.`
* **Retorna:** `True` si existe, `False` si no.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})

print(db.has("users"))              # True
print(db.has("users.1"))            # True
print(db.has("users.1.name"))       # True
print(db.has("users.2.age"))        # True
print(db.has("users.1.status"))     # False
print(db.has("users.1.name.data"))  # False
print(db.has("docs"))               # False
```
---

## get
### `get(path)`
Obtiene un documento almacenado por su clave.

* **`path` (str):** ruta al documento, soporta dot notation `.`
* **Retorna:** el valor almacenado o `None` si no existe.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})
db.set("docs", [])

user1 = db.get("users.1")
print(user1)  # {'name': 'mega', 'age': 13}
age = db.get("users.2.age")
print(age)    # 14
nodata = db.get("users.3")
print(nodata) # None
document = db.get("docs")
print(document)  # []
```
---

## delete
### `delete(path)`
Elimina un documento almacenado por su clave.

* **`path` (str):** ruta del documento a eliminar, soporta dot notation `.`
* **Retorna:** `True` si se eliminó, `False` si no existía.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})
db.delete("users.2.age")  # elimina el campo "age" del usuario 2
db.delete("users.1")      # elimina todo el objeto del usuario 1

print(db.get("users.1"))  # None
print(db.get("users"))    # {'2': {'name': 'ratsa'}}
```
---

## all
### `all()`
Obtiene todos los documentos de la colección.

* **Retorna:** un dict con todos los documentos actuales.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})

print(db.all())
"""
{
  "users": {
    "1": {"name": "mega", "age": 13},
    "2": {"name": "ratsa", "age": 14}
  }
}
"""
```

## stream
### `stream(path, query={})`
**Generador Python** cursor sobre `find()`. Para colecciones grandes: en vez de construir una lista completa en RAM (como `find()`/`all()`), recorre bloque por bloque y va entregando documentos con `yield` a medida que los encuentra, sin cargar la colección entera en memoria.

* **`path` (str):** igual que en `find`, ruta o `"*"`.
* **`query` (dict, opcional):** mismos operadores que `find`.
* **Retorna:** un generador de **valores** de documentos (no la clave, si necesitas la clave usa `entries()`).

```python
for doc in db.stream("usuarios", {"edad": {"$gt": 18}}):
    ...  # procesa doc de a uno; nunca tiene toda la coleccion en RAM a la vez
```
> Nota de memoria: si un bloque no estaba ya "tibio" en `blocksCache` antes de empezar el recorrido, se descarta de la caché al terminar de procesarlo. Si el bloque ya estaba cacheado de antes por otro uso, se respeta tal cual.

## entries
### `entries(path, query={})`
Igual que `stream()`, pero entrega `{"key": ..., "value": ...}` en vez de solo el valor. Hace falta cuando necesitas saber la clave del documento además de su contenido.

```python
for entry in db.entries("*", {}):
    print(entry["key"], entry["value"])
```

## keys
### `keys(path)`
Devuelve todas las claves de la colección.

* **`path` (str):** ruta de las claves, soporta dot notation `.` y wildcard `*` para las claves principales.
* **Retorna:** una lista con las claves encontradas.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})
db.set("docs", [])

print(db.keys("users"))        # ["1", "2"]
print(db.keys("users.1"))      # ["name", "age"]
print(db.keys("*"))            # wildcard => ["users", "docs"]
print(db.keys("notfound"))     # []
print(db.keys("users.2.data")) # []
```
---

## values
### `values(path)`
Devuelve todos los valores de la colección.

* **`path` (str):** ruta de los valores, soporta dot notation `.` y wildcard `*`.
* **Retorna:** una lista con los valores encontrados.

```python
db.set("users.1", {"name": "mega", "age": 13})
db.set("users.2", {"name": "ratsa", "age": 14})
db.set("docs", [])
db.set("status", False)

print(db.values("users"))         # [{'name': 'mega', 'age': 13}, {'name': 'ratsa', 'age': 14}]
print(db.values("users.1"))       # ['mega', 13]
print(db.values("users.1.name"))  # []
print(db.values("users.3"))       # []
print(db.values("*"))             # wildcard => [{"1": {...}, "2": {...}}, [], False]
```
---

## count
### `count(path, query)`
Devuelve el número total de documentos.

* **`path` (str):** ruta donde se hará el conteo; `'*'` escanea toda la colección, soporta dot notation `.`
* **`query` (dict):** objeto de filtro, cada clave del dict es un campo o ruta de campo.
* **Retorna:** el conteo de elementos que pasó el filtro.

Filtro simple (igualdad):
```python
{"edad": 30}  # equivale a edad == 30
```
Filtro avanzado con [operadores](#operadores-relacionales-y-textuales):
```python
{"edad": {"$gt": 30}}  # equivale a edad > 30
```
```python
db.set("users.1", {"name": "mega", "edad": 13})
db.set("users.2", {"name": "megast", "edad": 14})
db.set("users.3", {"name": "pedro", "edad": 15})
db.set("users.4", {"name": "ratsa", "edad": 16})

print(db.count("users", {"edad": {"$gt": 14}}))  # 2
print(db.count("users", {"edad": {"$lt": 16}}))  # 3
print(db.count("users", {"edad": {"$gt": 14}, "name": {"$prefix": "r"}}))  # 1
print(db.count("*", {}))              # wildcard, toda la coleccion
print(db.count("data.dato2", {}))     # dot notation
```
---

## find
### `find(path, query={})`
Busca y devuelve una lista con los documentos (o sub-documentos) que cumplen el filtro `query` dentro de la ruta indicada por `path`. Aprovecha índices secundarios (`secondaryIndexes`) cuando están configurados.

* **`path` (str, opcional):** ruta del documento donde se aplicará la búsqueda; `'*'` escanea toda la colección, soporta dot notation `.`
* **`query` (dict, opcional):** objeto de filtro.
* **Retorna:** una lista con los documentos que cumplieron el filtro.

*Soporta 2 formas de llamada:*
```python
modo_1 = db.find(path, query)
modo_2 = db.find(query)  # path se asume '*'
```

Filtro simple:
```python
{"edad": 30}  # equivale a edad == 30
```
Filtro avanzado con operadores:
```python
{"edad": {"$gt": 30}}  # equivale a edad > 30
```

* Ejemplos (con explicaciones)

```python
db.set('usuario1', {"nombre": "u1", "edad": 20})
db.set('usuario2', {"nombre": "u2", "edad": 19})
db.set('usuario3', {"nombre": "u3", "edad": 18})

print(db.find({"nombre": "u1"}))                    # [{'nombre': 'u1', 'edad': 20}]
print(db.find({"edad": {"$eq": 20}}))                # [{'nombre': 'u1', 'edad': 20}]
print(db.find({"edad": {"$gt": 19}}))                # [{'nombre': 'u1', 'edad': 20}]
print(db.find({"edad": {"$lt": 19}}))                # [{'nombre': 'u3', 'edad': 18}]
print(db.find({"nombre": {"$in": ["u1", "u3"]}}))
# [{'nombre': 'u1', 'edad': 20}, {'nombre': 'u3', 'edad': 18}]
print(db.find({"nombre": {"$nin": ["u1", "u3"]}}))
# [{'nombre': 'u2', 'edad': 19}]
print(db.find({"nombre": {"$prefix": "u"}}))
# los 3 documentos
print(db.find({"nombre": {"$regex": "^u", "$flags": "i"}}))
# los 3 documentos
```
* También con dot notation:
```python
db.set("usuarios.pablo", {
    "id": "u1", "nombre": "pablo pablon", "edad": 42, "rol": "admin",
    "direccion": {"ciudad": "xxxx", "distrito": "aaaaaa"}, "bio": "texto largo...",
})
db.set("usuarios.pedro", {
    "id": "u2", "nombre": "pedro pedron", "edad": 17, "rol": "moderador",
    "direccion": {"ciudad": "yyyyy", "distrito": "eeeeee"}, "bio": "texto largo...",
})
db.set("usuarios.juan", {
    "id": "u3", "nombre": "juan juanon", "edad": 25, "rol": "trusted",
    "direccion": {"ciudad": "zzzzz", "distrito": "iiiiii"}, "bio": "texto largo...",
})

print(db.find('usuarios', {'direccion.ciudad': 'yyyyy'}))          # documento de pedro
print(db.find('usuarios', {'nombre': {'$prefix': 'ju'}}))          # documento de juan
print(db.find('usuarios', {'edad': {'$between': [20, 40]}}))       # documento de juan
print(db.find('usuarios', {'rol': {'$in': ['admin', 'moderador', 'editor']}}))
# documentos de pedro y pablo

# Tambien puedes usar dot notation en el path:
db.find("dato1.dato2", {"propiedad1": {"$nin": ["valores", "a", "verificar"]}})
db.find("dato1.dato2", {"data4.data5": "valor"})
```
* En miles de datos, esto se agiliza si usas `secondaryIndexes`.
---

## aggregate
### `aggregate(path, pipeline)`
Ejecuta operaciones de agregación sobre los documentos en la ruta indicada por `path`. Permite un pipeline de etapas (`$match`, `$group`, `$sort`, `$limit`) para transformaciones y cálculos analíticos.

* **`path` (str):** ruta; `'*'` escanea toda la colección, soporta dot notation `.`
* **`pipeline` (list):** lista de etapas, cada elemento un dict con una sola clave (ej: `{"$match": {...}}`).
* **Retorna:** una lista con los resultados de la agregación.

* Etapas soportadas:
  * `$match` => Filtra documentos (igual que [find](#find)).
  * `$group` => Agrupa por un campo (`_id`) y permite acumuladores (`$sum`, `$avg`, `$min`, `$max`, `$push`).
    - `$` => Para usar un campo específico en un acumulador, colocá `$` antes del nombre: `"$rol"`.
    - `_id`: obligatorio para agrupar por campo, ej: `"$rol"`.
    - `$sum` => suma valores numéricos. Ej: `"totalEdad": {"$sum": "$edad"}`, `"total": {"$sum": 1}`.
    - `$avg` => promedio de un campo numérico. Ej: `"promedio": {"$avg": "$edad"}`.
    - `$min` / `$max` => valor mínimo/máximo por grupo.
  * `$sort` => Ordena resultados (`-1` descendente, `1` ascendente), soporta multi-campo.
  * `$limit` => Limita la cantidad de resultados finales.

#### Ejemplos, agregando primero estos datos:
```python
db.set("usuarios.pablo", {"id": "u1", "nombre": "pablo pablon", "edad": 42, "rol": "admin"})
db.set("usuarios.pedro", {"id": "u2", "nombre": "pedro pedron", "edad": 17, "rol": "moderador"})
db.set("usuarios.juan", {"id": "u3", "nombre": "juan juanon", "edad": 25, "rol": "trusted"})
db.set("usuarios.patricio", {"id": "u4", "nombre": "patricio patrico", "edad": 26, "rol": "trusted"})
```

##### Filtrar con $match y agrupar con $group
```python
data = db.aggregate("usuarios", [
    {"$match": {"id": {"$prefix": "u"}}},
    {"$group": {"_id": "$rol", "total": {"$sum": 1}}},
])
print(data)
# [{'_id': 'trusted', 'total': 2}, {'_id': 'admin', 'total': 1}, {'_id': 'moderador', 'total': 1}]
```
##### Acumuladores $sum, $avg, $min, $max
```python
data = db.aggregate("usuarios", [
    {"$group": {
        "_id": "$rol",
        "total": {"$sum": 1},
        "edadPromedio": {"$avg": "$edad"},
        "edadMinima": {"$min": "$edad"},
        "edadMaxima": {"$max": "$edad"},
    }},
])
```
##### Ordenar con $sort
```python
data = db.aggregate("usuarios", [
    {"$group": {"_id": "$rol", "total": {"$sum": 1}, "edadPromedio": {"$avg": "$edad"}}},
    {"$sort": {"total": -1, "edadPromedio": 1}},
])
```
##### Limitar con $limit
```python
data = db.aggregate("usuarios", [
    {"$group": {"_id": "$rol", "total": {"$sum": 1}}},
    {"$sort": {"total": -1}},
    {"$limit": 1},
])
```
Para toda la colección, usa `db.aggregate("*", [...])`.

## update
### `update(path, ops)`
Actualiza uno o varios valores dentro de un documento en la ruta indicada por `path`. `ops` define las operaciones a ejecutar; puedes combinar más de una.

* **`path` (str):** ruta del documento, soporta dot notation `.`
* **`ops` (dict):** una o más operaciones de actualización.
* **Retorna:** el documento actualizado.

* Operaciones soportadas:
  - `$set` => establece o crea una propiedad. Ej: `{"$set": {"nombre": "nuevo nombre"}}`.
  - `$inc` => incrementa/decrementa un número. Ej: `{"$inc": {"edad": 1}}`.
  - `$unset` => elimina una propiedad. Ej: `{"$unset": {"edad": ""}}`.
  - `$push` => agrega elementos a un array (lista de valores a agregar). Ej: `{"$push": {"frutas": ["manzana", "durazno"]}}`.
  - `$pull` => elimina elementos de un array si existen. Ej: `{"$pull": {"frutas": ["manzana"]}}`.

#### Ejemplos
```python
db.set("mario", {"nick": "mario30000", "edad": 13, "frutas": []})
db.set("pedro", {"nick": "pedro20000", "edad": 11, "frutas": []})
db.set("juan", {"nick": "juan10000", "edad": 15, "frutas": []})
```
#### Cambiar nick y sumar edad
```python
nuevos_datos = db.update("mario", {"$set": {"nick": "marioPRO30000"}, "$inc": {"edad": 1}})
print(nuevos_datos)  # {'nick': 'marioPRO30000', 'edad': 14, 'frutas': []}
```
#### Eliminar campo frutas y añadir verduras
```python
nuevos_datos = db.update("pedro", {"$unset": {"frutas": ""}, "$set": {"verduras": []}})
```
#### Agregar frutas y luego eliminar una
```python
db.update("juan", {"$push": {"frutas": ["manzana", "durazno"]}})
db.update("juan", {"$pull": {"frutas": ["durazno"]}})
```
#### Varias operaciones a la vez
```python
db.update("juan", {
    "$set": {"juegos.overwatch": "TAG#AAAA", "amigos": []},
    "$push": {"frutas": ["platano"]},
    "$inc": {"edad": 1},
})
# con dot notation => update("path1.path2.path3.etc")
```

## watch
### `watch(path, callback)`
Permite observar cambios en tiempo real sobre un documento o ruta específica.
Cada vez que ocurre una operación de escritura en esa ruta, se dispara el callback.

* **`path` (str):** ruta a observar, documento exacto, colección con wildcard (`"usuarios.*"` o `"usuarios"`), o `'*'` para toda la colección.
* **`callback` (función):** función que se ejecuta con 4 parámetros:
  * `path (str)` => ruta completa del valor afectado.
  * `new_val (any)` => valor nuevo.
  * `old_val (any)` => valor anterior.
  * `op (str)` => `"set"`, `"delete"` o `"update"`.

```python
db.set("mario", {"nick": "mario30000", "edad": 13, "frutas": []})
db.set("pedro", {"nick": "pedro20000", "edad": 11, "frutas": []})
db.set("juan", {"nick": "juan10000", "edad": 15, "frutas": []})
```
#### Observar un documento específico
```python
def on_mario_change(path, new_val, old_val, op):
    print(f"[WATCH] Ruta: {path}")
    print(f"Operación: {op}")
    print("Antes:", old_val)
    print("Ahora:", new_val)

db.watch("mario", on_mario_change)
db.set("mario", {"nick": "mario30000", "edad": 14, "frutas": []})
```
#### Observar toda la colección
```python
db.watch("*", lambda path, new_val, old_val, op: print(f"[WATCH] Cambio en {path} ({op})"))
db.set("juan", {"nick": "juan10000", "edad": 16, "frutas": []})
```
#### Detectar eliminación
```python
def on_pedro_change(path, new_val, old_val, op):
    if op == "delete":
        print(f"El usuario en {path} fue eliminado.", old_val)

db.watch("pedro", on_pedro_change)
db.delete("pedro")
```
#### detectar cambios en arrays
```python
def on_mario_update(path, new_val, old_val, op):
    if op == "update":
        print(f"[WATCH] Se actualizó la fruta de {path}")
        print("Antes:", old_val["frutas"])
        print("Ahora:", new_val["frutas"])

db.watch("mario", on_mario_update)
db.update("mario", {"$push": {"frutas": ["manzana"]}})

# Tambien puedes usar dot notation: db.watch("datos.usuario1", ...)
```

## stats
### `stats()`
Devuelve un dict con estadísticas internas de la base de datos, orientado a monitoreo/depuración.
* **Retorna:**
  * `keys` => cantidad total de claves registradas en el índice principal.
  * `dirtyBlocks` => número de bloques "sucios" (modificados en memoria, aún no persistidos).
  * `cachedBlocks` => número de bloques actualmente en caché.
  * `secondaryIndexes` => cantidad de índices secundarios definidos.
  * `keyCache` => `{"size": ..., "capacity": ...}`.
```python
# ejemplo db.stats()
{
    "keys": 3,
    "dirtyBlocks": 1,
    "cachedBlocks": 1,
    "secondaryIndexes": 2,
    "keyCache": {"size": 3, "capacity": 1000},
}
```

## flush
### `flush()`
Fuerza la escritura inmediata en disco de todos los datos en memoria (que aún no se persistieron por el `flushInterval` automático).

```python
db.set(...)
db.flush()
```
## close
### `close()`
Cierra la base de datos de manera ordenada: hace flush de todo lo pendiente y detiene los timers/hilos internos. Recomendado al final del ciclo de vida de tu proyecto.
```python
db.close()
```

## createBackup
### `createBackup()`
Crea un respaldo persistente del estado actual de la base de datos, con un nombre único (`counter` o `timestamp` según `backupMode`). Junto al archivo `.json.backup` se guarda también un `.sha256` con el checksum del contenido.

* **Retorna:** `True` si se creó, `False` si no.
```python
db.createBackup()
```

## restoreBackup
### `restoreBackup(name, opts={})`
Restaura un backup previamente creado, sobrescribiendo el estado actual de la base de datos.

**El checksum del backup se verifica ANTES de tocar la base de datos.** Si está corrupto, lanza un `MegaDBError` explícito y no aplica nada:
```python
from megadbx.classes.mega_db_error import MegaDBError
try:
    db.restoreBackup("backup_00000")
except MegaDBError as err:
    print(err)  # 'El backup "backup_00000" esta corrupto ...'
```

* **`name` (str):** nombre del backup a restaurar.
* **`opts` (dict, opcional):**
  - `requireChecksum` (bool, default `False`): si `True`, rechaza también backups viejos sin `.sha256`.
* **Retorna:** `True` si se restauró, `False` si no.
```python
db.restoreBackup("backup-2025-09-05T18-00-00")
```

## listBackups
### `listBackups()`
Lista los backups disponibles para esta colección.

* **Retorna:** una lista de `{"name": ..., "sizeBytes": ..., "createdAt": ...}`, ordenada del mas reciente al mas viejo.
```python
backups = db.listBackups()
# [{'name': 'backup_00003', 'sizeBytes': 4820, 'createdAt': 1735500000000.0}, ...]
```

## createSnapshot
### `createSnapshot()`
Genera un snapshot en memoria (bytes) del estado actual de la base de datos. Útil para enviar por red, guardar temporalmente o clonar estados.
* **Retorna:** `bytes` con el snapshot serializado.
```python
snapshot = db.createSnapshot()
```

## restoreSnapshot
### `restoreSnapshot(buffer)`
Restaura un snapshot previamente creado con `createSnapshot()`, sobrescribe completamente el estado actual.

* **`buffer` (bytes)**: snapshot previamente guardado.
* **Retorna:** `True` si se realizó, `False` si no.
```python
snap = db.createSnapshot()
# ... guardas miles de datos, luego decides volver atrás ...
estado = db.restoreSnapshot(snap)
print(estado)  # True
```
---
# 2. `MegaDBSafe`

## MegaDBSafe hereda de MegaDB, y añade capacidades pensadas para concurrencia, durabilidad y multi-proceso:
* **WAL (Write-Ahead Log)**
  * Todas las escrituras (`set`, `delete`, `update`) primero se registran en un log (`.wal`).
  * Si la app se cae, al reiniciar se hace replay de ese log para restaurar la consistencia.
* **IndexStore**
  * Persiste índices secundarios (`secondaryIndexes`) en un archivo separado y los recarga rápido al iniciar.
* **OpQueue (QueuedLock)**
  * Serializa operaciones (basado en `threading.RLock`) dentro de un mismo proceso.
  * Garantiza que múltiples llamados de escritura/guardado concurrentes (por ejemplo, desde varios hilos) no choquen.
* **Stats extendido.**
* **Pensado para proyectos medianos/grandes.**

## Conceptos clave más importantes
* **WAL (Write-Ahead Log)**: log secuencial en disco donde se escriben todas las operaciones antes de aplicarlas a los bloques. Garantiza durabilidad.
* **OpQueue (QueuedLock)**: serializa operaciones concurrentes dentro del mismo proceso; evita que dos escrituras se pisen.
* **IndexStore**: persiste y restaura los índices secundarios en un archivo separado.

## MegaDBSafe
```python
from megadbx import MegaDBSafe
db = MegaDBSafe(collection_name, options={})
```

## MegaDBSafe contiene todo lo que MegaDB ya tiene, metodos y opciones, más:

| Opción      | Default | Explicación                                          |
| ----------- | ------- | ----------------------------------------------------- |
| `replayWal` | `True`  | Si al iniciar encuentra un archivo `.wal`, lo reproduce. |

`replayWal`: Si al iniciar encuentra un archivo `.wal`, lo reproduce para aplicar operaciones pendientes y restaurar consistencia.
  - `True` → siempre reejecuta las operaciones pendientes.
  - `False` → ignora el WAL (solo recomendado para debugging).
  - El WAL no es acumulativo: se reinicia entre cada arranque.

Puedes seguir usando todas las opciones del constructor de [MegaDB](#megadb).

## ready
### `ready()`
En la versión síncrona, la inicialización ya termino cuando el constructor retorna, `ready()` existe por compatibilidad y simplemente retorna `True`. No hace falta llamarlo, pero puedes hacerlo si tu codigo viene de un patron que lo esperaba.
```python
db = MegaDBSafe("data", {"dir": "./", "secondaryIndexes": ["rol"], "replayWal": True})
db.ready()  # True, opcional

# resto de la logica de tu proyecto...
```
## set / has / get / delete / all / stream / entries / keys / values / count / find / aggregate / update / watch
Es lo mismo que `MegaDB.<metodo>`, ver la sección de [MegaDB](#megadb).

## stats
### `stats()`
Es lo mismo que `MegaDB.stats()` pero contiene más cosas:
* `wal` (dict): estado del Write-Ahead Log, `enabled`, `file`, `pendingOps`.
* `locks` (dict): `queueLength` operaciones esperando turno en el OpQueue.
* `indexStore` (dict): `loaded`, `file`.
```python
# ejemplo: db.stats()
{
    "keys": 120,
    "dirtyBlocks": 2,
    "cachedBlocks": 8,
    "secondaryIndexes": 2,
    "keyCache": {"size": 80, "capacity": 1000},
    "wal": {"enabled": True, "pendingOps": 3, "file": "./db/data/wal.log"},
    "locks": {"queueLength": 2},
    "indexStore": {"loaded": True, "file": "./db/data/indexes.json"},
}
```

## flush / close / createBackup / restoreBackup / listBackups / createSnapshot / restoreSnapshot
Es lo mismo que `MegaDB.<metodo>`, ver la sección de [MegaDB](#megadb).

---
# 3. `MegaDBFull`

## MegaDBFull hereda de MegaDBSafe, y añade funcionalidades avanzadas:
* **Índices avanzados**
  * CompositeIndexes => índices sobre combinaciones de campos.
  * TrieIndexes => índices de prefijos para $prefix.
  * SkipListIndexes => índices ordenados para $between/$gt/etc.
* **MVCC (Multi-Version Concurrency Control)**
  * Guarda múltiples versiones de un mismo documento.
  * Soporta queries por versión (`$version`) o por timestamp (`$since`).
  * Método nuevo: `history()`.
* **Append-Only Log (AOF)**
  * Todas las operaciones de escritura se registran en un archivo append-only (`aof.log`).
  * Opción de replay (`replayLog`).
* **Merkle Tree**
  * Verifica integridad (`verifyIntegrityFast`) y detecta corrupción de datos.
* **Stats más detallado.**
* **Consultas optimizadas (`find`)**: usa composite, trie y skiplist para reducir el escaneo.

## Conceptos clave más importantes
* `CompositeIndexes` => combinan varios campos en un índice, acelerando consultas multidimensionales.
* `TrieIndexes` => perfectos para búsquedas de prefijo con `$prefix`.
* `SkipListIndexes` => consultas por rangos numéricos muy rápido.
* `MVCC` => consistencia en concurrencia + consulta de versiones históricas.
* `Append-Only Log` => cada operación se escribe secuencialmente; útil para recuperación y auditoría.
* `Merkle Tree` => garantiza integridad de datos, detectando corrupción o cambios no autorizados.

## MegaDBFull
```python
from megadbx import MegaDBFull
db = MegaDBFull(collection_name, options={})
```

## MegaDBFull contiene todo lo que MegaDB y MegaDBSafe ya tienen, más:

| Opción                   | Default       | Explicación                                                                    |
| ------------------------- | -------------- | -------------------------------------------------------------------------------- |
| `compositeIndexes`        | `[]`           | Lista de definiciones de índices compuestos (campos múltiples).                  |
| `compositePaged`          | `False`        | Guarda el índice compuesto paginado en disco (por hash-shard) en vez de un solo archivo. |
| `compositeNumShards`      | `32`           | Cantidad de shards (archivos) por spec, en modo `compositePaged`.                |
| `compositeShardCacheCap`  | `16`           | Shards simultáneos en RAM (LRU) por spec.                                        |
| `trieIndexes`             | `[]`           | Campos indexados con trie, optimizados para `$prefix`.                            |
| `triePaged`               | `False`        | Guarda el índice trie paginado en disco (por prefijo).                            |
| `trieShardDepth`          | `2`            | Cuántos caracteres del string determinan el shard, en modo `triePaged`.           |
| `trieShardCacheCap`       | `16`           | Shards simultáneos en RAM (LRU) por campo.                                        |
| `skiplistIndexes`         | `[]`           | Campos indexados con skiplist, optimizados para `$between`, `$gt`, etc.           |
| `skiplistPaged`           | `False`        | Guarda el índice skiplist paginado en disco.                                       |
| `skiplistPageSize`        | `500`          | Entradas por página, en modo `skiplistPaged`.                                      |
| `skiplistPageCacheCap`    | `32`           | Páginas simultáneas en RAM (LRU) por campo.                                        |
| `mvcc`                    | `{}`           | Objeto con las opciones mvcc.                                                       |
| `mvcc.enabled`            | `False`        | Activa el control multiversión.                                                     |
| `mvcc.retainVersions`     | `3`            | Número de versiones que se retienen por clave.                                      |
| `appendOnly`              | `{}`           | Objeto con las opciones append-only (aof).                                          |
| `appendOnly.enabled`      | `False`        | Activa el log append-only.                                                          |
| `appendOnly.file`         | `aof.log`      | Nombre del archivo donde se guarda el log.                                          |
| `appendOnly.fsync`        | `False`        | Forzar escritura en disco inmediatamente.                                           |
| `appendOnly.replayLog`    | `False`        | Si `True`, al iniciar se hace replay del log para recuperar datos.                  |
| `merkle`                  | `{}`           | Objeto con las opciones del merkle.                                                 |
| `merkle.enabled`          | `False`        | Activa el árbol de Merkle para verificar integridad.                                |
| `merkle.file`             | `merkle.json`  | Archivo donde se guarda el árbol.                                                   |

## Índices paginados en disco (CompositeIndexManager, TrieIndexManager y SkipListIndexManager)

Por default, los tres índices viven **enteros en RAM** y se persisten como un solo archivo JSON. Con `*Paged: True`, cada uno reparte sus datos en varios archivos chicos en disco, cargando en RAM solo lo que una consulta puntual necesita:

- **`CompositeIndexManager`** (`compositePaged`) particionado por **hash** en `compositeNumShards` archivos fijos.
- **`TrieIndexManager`** (`triePaged`) particionado por **prefijo literal** (los primeros `trieShardDepth` caracteres).
- **`SkipListIndexManager`** (`skiplistPaged`) particionado en **páginas** de `skiplistPageSize` entradas ordenadas, con un directorio chico en RAM.

Los tres usan una caché LRU acotada para no volver a "cargar todo en RAM" por la puerta trasera.

```python
db = MegaDBFull("productos", {
    "compositeIndexes": [{"name": "cat_idx", "fields": ["categoria"]}],
    "compositePaged": True, "compositeNumShards": 32,

    "trieIndexes": ["nombre"],
    "triePaged": True, "trieShardDepth": 2,

    "skiplistIndexes": ["precio"],
    "skiplistPaged": True, "skiplistPageSize": 500,
})
```

**¿Cuándo usarlos?** Si tu colección tiene miles de documentos y tus índices ya no caben comodos en memoria. Para colecciones chicas/medianas, el modo default (todo en RAM) sigue siendo más simple y más rápido.

## Qué es un CompositeIndex?
Un índice compuesto combina varios campos de un documento en una sola clave interna, acelerando consultas que usan esos campos juntos.

```python
db = MegaDBFull("usuarios", {
    "compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}]
})
```
* **name** => identificador unico del índice.
* **fields** => lista de campos a combinar.

#### Internamente MegaDBFull compone esto asi:
* Para cada documento, toma los valores de los campos en orden.
* Los concatena con el separador `|^|` (ej: `{"data": "A", "pais": "USA"}` = `"A|^|USA"`).
* Si un campo falta, es `None`, o es un objeto/lista => no se indexa.
#### Persistencia
* Se guardan en **indexes_composite.json**.
#### Ejemplo
```python
db = MegaDBFull("datos", {"compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}]})

db.set("usuarios.u1", {"id": "u1", "data": "A", "pais": "USA"})
db.set("usuarios.u2", {"id": "u2", "data": "B", "pais": "USA"})
db.set("usuarios.u3", {"id": "u3", "data": "A", "pais": "MX"})

data = db.find("usuarios", {"data": "A", "pais": "USA"})
print(data)  # [{'id': 'u1', 'data': 'A', 'pais': 'USA'}]
```
* Acelera queries que combinan varios campos a la vez con igualdad exacta, evitando escaneos completos.
* Solo trabaja con los datos agregados despues de activarlo. Para sincronizar datos preexistentes, usa [`rebuildAllIndexes`](#rebuildAllIndexes).
* Soporta dot notation en `fields`.

## Qué es un TrieIndex?
Un Trie (árbol de prefijos) es una estructura optimizada para búsquedas por prefijo de texto.

```python
db = MegaDBFull("datos", {"trieIndexes": ["nick", "direccion.ciudad"]})
```
#### Inserción interna
```python
db.set("usuarios.mario", {"nick": "mario"})
db.set("usuarios.marco", {"nick": "marco"})
```
El trie comparte el camino `"m" -> "a" -> "r"` entre ambos, y luego se ramifica en `"i"->"o"` (mario) y `"c"->"o"` (marco).
#### Consulta por prefijo ($prefix)
```python
db.find("usuarios", {"nick": {"$prefix": "mar"}})
# [{'nick': 'mario'}, {'nick': 'marco'}]
```
#### Persistencia
* Se guardan en **trie_indexes.json**.

`trieIndexes` acelera búsquedas por prefijo `$prefix`, pasando de escanear toda la colección a recorrer solo la rama del prefijo. Solo trabaja con datos agregados después de activarlo. Soporta dot notation.

## Qué es un skiplistIndex?
Un skiplist es una estructura para búsquedas/inserciones/eliminaciones rápidas en listas ordenadas, rendimiento cercano a O(log n).

#### Para qué sirve en MegaDBFull?
Acelera consultas con `$gt`, `$lt`, `$gte`, `$lte`, `$between` sobre campos numéricos.

```python
db = MegaDBFull("datos", {"skiplistIndexes": ["edad", "score"]})

db.set("usuarios.pablo", {"nombre": "pablo", "edad": 42})
db.set("usuarios.pedro", {"nombre": "pedro", "edad": 17})
db.set("usuarios.juan",  {"nombre": "juan",  "edad": 25})
db.set("usuarios.maria", {"nombre": "maria", "edad": 30})

print(db.find("usuarios", {"edad": {"$gt": 20}}))
print(db.find("usuarios", {"edad": {"$between": [18, 30]}}))
```
#### Internamente
```python
state = {"edad": [{"v": 17, "k": "usuarios.pedro"}, {"v": 25, "k": "usuarios.juan"}, ...]}
```
Cada campo indexado mantiene una lista de `{v, k}` ordenada por `v` (inserción/eliminación por búsqueda binaria).
#### Persistencia
* Se guardan en **skip_indexes.json**.

`skiplistIndexes` solo trabaja con datos agregados después de activarlo. Soporta dot notation.
---
## Qué es MVCC? (Multi-Version Concurrency Control)
Mantiene múltiples versiones históricas de un documento, útil para auditoría, históricos de cambios o lecturas consistentes en paralelo.

- `mvcc` es un dict con las opciones:
  - `mvcc.enabled`: activa/desactiva MVCC (default: `False`).
  - `mvcc.retainVersions`: cuántas versiones mantener por documento (default: 3).
```python
db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 2}})
```
* MVCC solo trabaja con las rutas exactas que pasan por `MegaDBFull.set()`.
* Se usa en `get`, `has`, `all`, `keys`, `values`, `aggregate`, `count`, `history`.

## Qué es Append-Only (AOF)
Modo de persistencia donde cada cambio se agrega al final de un archivo de log, un "diario" de operaciones. A diferencia del WAL, aquí se almacena todo como historial, nunca se borra nada. Sirve incluso para exportar/replicar datos de una instancia a otra solo usando el `.log`.

```python
db = MegaDBFull("data", {
    "appendOnly": {
        "enabled": True,
        "file": "aof.log",
        "fsync": True,
        "replayLog": True,
    }
})
```

## Qué es Merkle?
Un árbol de Merkle es una estructura hash que permite verificar la integridad de los datos. Si un dato cambia, el hash de la raíz cambia si alguien manipula un archivo, el hash ya no coincide y la DB detecta la corrupción.

```python
db = MegaDBFull("datos", {"merkle": {"enabled": True, "file": "merkle.json"}})
```
---
Puedes seguir usando las opciones del constructor de [MegaDB](#megadb) y [MegaDBSafe](#megadbsafe).

## ready
### `ready()`
Igual que en [MegaDBSafe](#ready): en la versión síncrona, retorna `True`, la inicialización ya terminó dentro del constructor.
```python
db = MegaDBFull("data", {
    "dir": "./",
    "secondaryIndexes": ["rol"],
    "compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}],
    "replayWal": True,
    "merkle": {"enabled": True},
})
db.ready()  # True, opcional
```
## set
### `set(path, value)`
Igual que `MegaDB.set(path, value)`.

## get
### `get(path, opts)`
Igual que `MegaDB.get(path)`, con un nuevo parámetro opcional si MVCC está activado.

**`opts` (dict):** con MVCC habilitado, se reconocen:
  * `opts["$version"]` (int): solicita una versión específica.
  * `opts["$since"]` (timestamp ms): solicita el valor tal como existía en un momento dado.
```python
db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 3}})

db.set("usuarios.pedro", {"edad": 20})  # internamente v2
db.set("usuarios.pedro", {"edad": 21})  # internamente v1
db.set("usuarios.pedro", {"edad": 22})  # internamente v0

print(db.get("usuarios.pedro"))                          # {'edad': 22}
print(db.get("usuarios.pedro", {"$version": 1}))          # {'edad': 21}
import time
print(db.get("usuarios.pedro", {"$since": time.time()*1000 - 5000}))  # version vigente hace 5s
```

## has / all / stream / entries / delete / keys / values / count / find / aggregate
Igual que en `MegaDB`, con el mismo parámetro opcional `opts` (`$version`/`$since`) si MVCC está activado. Ver ejemplos análogos al de `get` arriba.

## update
### `update(path, ops)`
Igual que `MegaDB.update(path, ops)`.

## watch
### `watch(path, callback)`
Igual que `MegaDB.watch(path, callback)`.

## history
### `history(path, limit)`
Devuelve las últimas versiones (MVCC) de una clave exacta.

* **`path` (str):** ruta del documento (MVCC), debe haber sido agregada con [set](#set-2).
* **`limit` (int):** número de versiones a devolver.
* **Retorna:** una lista de `{"ts": ..., "value": ...}`.
* Lanza `RuntimeError` si MVCC no está activado.
```python
db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 3}})

db.set("usuarios", {"version": 2, "pedro": 14, "juan": 13})
db.set("usuarios", {"version": 1, "maria": 15, "dix": 14})
db.set("usuarios", {"version": 0, "maria": 15})

versiones = db.history("usuarios", 3)
print(versiones)
# [{'ts': ..., 'value': {'version': 0, 'maria': 15}}, {'ts': ..., 'value': {...v1}}, {'ts': ..., 'value': {...v2}}]
```

## stats
### `stats()`
Igual que `MegaDB.stats()`/`MegaDBSafe.stats()` pero con más información:
* `appendOnly`: `enabled`, `file`, `sizeBytes`.
* `mvcc`: `enabled`, `retainVersions`, `keysTracked`.
* `indexes`: `composite`, `trie`, `skiplist`.
* `merkle`: `enabled`, `root`.
```python
# ejemplo db.stats()
{
    "keys": 42, "dirtyBlocks": 3, "cachedBlocks": 5, "secondaryIndexes": 2,
    "keyCache": {"size": 12, "capacity": 100},
    "wal": {"file": "./db/wal.log", "pendingOps": 7},
    "appendOnly": {"enabled": True, "file": "./db/aof.log", "sizeBytes": 2048},
    "mvcc": {"enabled": True, "retainVersions": 3, "keysTracked": 10},
    "indexes": {"composite": {"001": 25}, "trie": 1, "skiplist": 2},
    "merkle": {"enabled": True, "root": "a9f3c8..."},
    "locks": {"queueLength": 0},
}
```

## flush / close / createBackup / restoreBackup / listBackups / createSnapshot / restoreSnapshot
Igual que en `MegaDB`/`MegaDBSafe`.

## verifyIntegrityFast
### `verifyIntegrityFast()`
Comprueba rápidamente si los archivos del db (`block_*.json`, `aof.log`) no han sido modificados desde la última reconstrucción del árbol Merkle, compara hashes de hojas ya almacenados contra los hashes actuales de disco, sin recalcular el árbol completo desde cero cada vez.

* **Retorna:** `{"ok": bool, "expected": ..., "computed": ...}`.
```python
ok = db.verifyIntegrityFast()
print(ok)
# {'ok': True, 'expected': 'a9f3c8...', 'computed': 'a9f3c8...'}
# o si algo se corrompio:
# {'ok': False, 'expected': 'a9f3c8...', 'computed': 'ff21bb...'}
```

## rebuildAllIndexes
### `rebuildAllIndexes(flush_every=500)`
Reconstruye **composite + trie + skiplist** (los que tengas configurados) en **un solo recorrido** de la colección, usando `entries()` internamente memoria acotada, sin cargar todos los documentos de una vez.

* **`flush_every`** (int, default `500`): cada cuántos documentos procesados se persisten a disco los shards/páginas modificados.
* **Retorna:** `{"processed": ..., "indexes": [...]}`.

```python
db = MegaDBFull("productos", {
    "dir": "./",
    "compositeIndexes": [{"name": "cat_idx", "fields": ["categoria"]}],
    "compositePaged": True,
    "trieIndexes": ["nombre"], "triePaged": True,
    "skiplistIndexes": ["precio"], "skiplistPaged": True,
})

# supongamos que la coleccion YA tenia 50,000 documentos antes de activar estos indices
result = db.rebuildAllIndexes(flush_every=500)
print(result)  # {'processed': 50000, 'indexes': ['CompositeIndexManager', 'TrieIndexManager', 'SkipListIndexManager']}
```
---

# MegaDBCloud

`MegaDBCloud` es un cliente que expone la **misma interfaz pública** que la clase
`MegaDB` (`get`, `set`, `delete`, `find`,
`count`, `update`, `aggregate`, `stats`), pero en vez de leer
y escribir archivos en disco local, se envia por HTTPS con el servidor
megadbx-cloud (https://megadbx.megaspades.xyz/api todo de manera segura y persistente).
Es como el "connection string" de Atlas, tu codigo de lectura/escritura no cambia, 
solo cambia de dónde vienen los datos.
```python
from megadbx import MegaDBCloud, MegaDBCloudError

db = MegaDBCloud("usuarios", {
    "apiKey": "mdbx_live_xxxxxxxxxxxxxxxxxxxxxxxx",
    "endpoint": "https://megadbx.megaspades.xyz/api",
})

try:
  db.set("u1", {"nombre": "Ana", "edad": 30})
  print(db.get("u1"))
except MegaDBCloudError as err:
    print(err.status, err)
```

## Como conseguir una API key

Las API keys las genera el servidor cloud, tienes que pedir acceso ingresando al servidor (https://discord.gg/rXngzDpAHf) 
y se te otorgará una cuenta para poder acceder al panel **(https://megadbx.megaspades.xyz/panel/account/)**, creas un proyecto, y la key se muestra **una sola vez**
al crearlo, copiala en ese momento, no se puede volver a ver (se puede
generar una nueva desde el panel si la pierdes).

## Constructor

```python
MegaDBCloud(collection_name, options={})
```

| Opción | Default | Explicación |
|---|---|---|
| `apiKey` | *(requerido)* | La API key de tu proyecto. |
| `endpoint` | *(requerido)* | URL base del servidor cloud (`https://megadbx.megaspades.xyz/api`). |
| `timeoutMs` | `10000` | Timeout por request (en ms). |
| `retries` | `2` | Reintentos automáticos ante errores transitorios (5xx, 429, timeouts de red). Nunca reintenta errores 4xx. |
| `retryDelayMs` | `300` | Base del backoff entre reintentos (en ms; crece linealmente con cada intento). |

## Metodos cloud

Todos son llamadas síncronas normales, igual que en `MegaDB`.

| Método | Uso | Equivale a |
|---|---|---|
| `get` | `db.get(path)` | `MegaDB.get` |
| `set` | `db.set(path, value, opts)` | `MegaDB.set` (mismo `opts["ttl"]`) |
| `has` | `db.has(path)` | `MegaDB.has` |
| `delete` | `db.delete(path)` | `MegaDB.delete` |
| `find` | `db.find(path, query)` | `MegaDB.find` (mismos operadores `$gt/$in/$regex/...`) |
| `count` | `db.count(path, query)` | `MegaDB.count` |
| `update` | `db.update(path, ops)` | `MegaDB.update` (`$set/$inc/$unset/$push/$pull`) |
| `aggregate` | `db.aggregate(path, pipeline)` | `MegaDB.aggregate` |
| `stats` | `db.stats()` | Stats del lado del servidor + uso/cuota de tu cuenta |

## Límites por cuenta

Estos límites se definen por defecto en una cuenta gratuita:

| Límite | Default | Alcance |
|---|---|---|
| Cuota de espacio | 100 MB | **Por cuenta**, compartida entre TODOS sus proyectos (no por proyecto individual). |
| Proyectos | 1 | Por cuenta. |
| Colecciones | 3 | Por proyecto (osea, por API key de ese proyecto), `MegaDBCloud("a", ...)`, `("b", ...)`, `("c", ...)` => TODO OK, una 4ta colección nueva es rechazada. |
| Peticiones por segundo | 30 | **Por API key individual**, no por cuenta ni por proyecto en conjunto. Ver la nota más abajo. |
| Tamaño máximo por documento | 3 MB | Por cuenta, aplica a cada documento de forma individual, es independiente de la cuota total de espacio. |

Estos 5 limites son ajustables, cualquier consulta se respondera en el servidor de **https://discord.gg/rXngzDpAHf**

### Como se cuenta el límite de peticiones por segundo

El contador es **por API key**, no por cuenta ni por proyecto en conjunto:

- Si usas la **misma** API key en varias colecciones de un mismo proyecto, todas esas peticiones se suman al mismo contador 
Con un límite de 30/s: 10 peticiones a la colección A + 10 a la colección B + 15 a la colección C en el mismo segundo = 35 peticiones totales con esa key, y las que excedan el límite reciben `429`.
- Si generas **varias** API keys para un mismo proyecto, cada una tiene su **propio** contador independiente, Tres keys distintas, usadas por separado, pueden procesar peticiones por separado.
- Cualquier API key de un proyecto sigue teniendo acceso a **todas** las colecciones de ese proyecto (hasta el límite de colecciones), sin importar cuántas keys existan. Repartir colecciones entre varias keys es una decisión de organización a tu criterio.

```python
# Ejemplo: repartir 3 colecciones entre 3 API keys distintas, para
# obtener un presupuesto de peticiones por segundo independiente en
# cada una, en lugar de compartir un solo contador de 30/s:
niveles = MegaDBCloud("niveles",  {"apiKey": KEY_A, "endpoint": endpoint})
xp = MegaDBCloud("xp",       {"apiKey": KEY_B, "endpoint": endpoint})
usuarios = MegaDBCloud("usuarios", {"apiKey": KEY_C, "endpoint": endpoint})
```

## Errores que puedes recibir

`MegaDBCloud` lanza `MegaDBCloudError` (importable desde `megadbx`), con
`.status` seteado al código HTTP:

| status | Cuándo |
|---|---|
| `401` | API key inválida o revocada. |
| `403` | La key es de solo lectura (`scope: "read_only"`) y llamaste a un método de escritura, o se alcanzó el límite de proyectos/colecciones. |
| `404` | El documento no existe (`get`/`has` lo manejan solos, devolviendo `None`/`False`, no hace falta capturarlo). |
| `413` | Se superó la cuota de espacio de la cuenta, el body del request es demasiado grande, o un documento individual supera el tamaño máximo por documento de tu cuenta. |
| `429` | Rate limit de peticiones por segundo excedido para esa API key en particular (se reintenta solo, ver `retries`). |
| `400` | Query/documento inválido, demasiado anidado, `$regex` no habilitado en el proyecto, arrays de `$in`/`$nin` demasiado grandes, nombre de colección/clave con caracteres no permitidos. |
| `500` | Error interno del servidor (nunca expone detalles internos por seguridad). |

```python
from megadbx import MegaDBCloud, MegaDBCloudError

try:
    db.set("u1", valor)
except MegaDBCloudError as err:
    print(err.status, err)

# IMPORTANTE!! SIEMPRE ENCAPSULAR LOS METODOS USADOS EN UN TRY/EXCEPT PARA OBTENER UN POSIBLE ERROR.
```

## Gestión de datos desde el panel de la cuenta

Además del cliente `MegaDBCloud`, el panel de tu cuenta (https://megadbx.megaspades.xyz/panel/account/) incluye herramientas de gestión por colección, separadas de los métodos de la clase:

- **Explorador de datos**: vista de solo lectura, paginada, con los mismos filtros que `find()`.
- **Eliminar una colección**: borra por completo los datos y los backups de una colección puntual (acción irreversible, requiere confirmación escrita).
- **Backups por colección**: desactivados por defecto, al activarlos quedan disponibles la creación manual y la automática cada 1 hora (intervalo fijo). Se pueden conservar entre 1 y 5 backups por colección (configurable), al superar el máximo se elimina el más antiguo. Al reducir el máximo con mas backups de los permitidos ya guardados, se conservan los mas recientes y se eliminan los sobrantes. Al desactivar los backups, se eliminan todos los existentes de esa colección. El espacio que ocupan los backups cuenta contra la cuota de tu cuenta, cada backup puede restaurarse de forma individual desde el panel.
---

# 4. `Transaction`

megadbx incluye un sistema de transacciones multi-base de datos que permite agrupar varias operaciones (set, delete, get, has) y aplicarlas de forma atómica, si algo falla, la transacción se revierte automáticamente.

#### Características principales
- Soporte para múltiples bases de datos en una misma transacción.
- Operaciones en memoria hasta que se confirme ([commit](#commit)).
- Rollback automático en caso de error, o manual con [rollback](#rollback).
- Compatible con WAL, AOF, QueuedLock, etc. (MegaDB, MegaDBSafe, MegaDBFull).

## Transaction
```python
from megadbx import Transaction
operaciones = Transaction(dbs={})
```

- **dbs** (dict): mapa `{nombre: instancia_del_db}` (MegaDB, MegaDBSafe o MegaDBFull).

```python
from megadbx import MegaDB, Transaction

db1 = MegaDB("users", {"dir": "./"})
db2 = MegaDB("games", {"dir": "./"})

tx = Transaction({"usuarios": db1, "juegos": db2})

# llamamos directamente con tx.usuarios. o tx.juegos.
```
#### Cada base de datos dentro de la transacción tiene su propio proxy con:
  * `tx.<db>.get(path)` => igual que `MegaDB.get`
  * `tx.<db>.set(path, value)` => igual que `MegaDB.set`
  * `tx.<db>.has(path)` => igual que `MegaDB.has`
  * `tx.<db>.delete(path)` => igual que `MegaDB.delete`

#### Diferencia con usar la DB normal
##### Sin transacción:
```python
db1.set('u123', {"name": "mega"})
db2.set('o456', {"id": "u123", "item": "pc"})
# si el segundo set falla, el primero ya quedó guardado: inconsistencia.
```
##### Con proxy dentro de una transacción:
```python
tx.usuarios.set('u123', {"name": "mega"})       # se guarda en memoria
tx.juegos.set('o456', {"id": "u123", "item": "pc"})  # también en memoria

try:
    status = tx.commit()  # recién aquí se aplican juntos
    if status["ok"]:
        print("Se aplicaron las transacciones a la db real!")
except Exception as error:
    print("no se hizo la transaccion, rollback automatico.", error)
```
* Los cambios no tocan la DB real hasta que confirmes con [commit()](#commit).
* Si algo falla antes del commit, puedes hacer [rollback()](#rollback) y nada cambia.

### Ventajas:
* Aísla los cambios: puedes leer dentro de la transacción y ver lo que modificaste, aunque todavía no esté en la DB real.
* Seguridad: garantiza que los cambios se apliquen de forma atómica (todos o ninguno).
* Compatibilidad: misma API (`get`/`set`/`delete`/`has`), pero bajo control transaccional.

## commit
### `commit()`
Aplica los cambios pendientes de memoria a la DB real. Es un **commit atómico real de 2 fases**:

1. **Fase 1 (prepare)** antes de tocar ninguna DB, se escribe a disco (fsync, atómico) el payload completo: todos los writes/deletes ya resueltos a su valor final, para todas las DBs involucradas.
2. **Fase 2 (apply)** se aplican los cambios a cada DB.
3. **Fase 3 (commit)** el registro de la fase 1 se borra; su ausencia ES la marca de "esta transacción ya quedó aplicada".

Si el proceso muere entre la fase 1 y la fase 3, el registro de la fase 1 queda en disco. Al reiniciar, [`Transaction.recoverPending()`](#transactionrecoverPending-estático) lo encuentra y reaplica el payload, es seguro porque los valores guardados ya son el **valor final resuelto** (no deltas).

* **Retorna:** un dict con `{"ok": True}` si se aplicó correctamente.

```python
from megadbx import MegaDB, Transaction

db1 = MegaDB("users", {"dir": "./"})
db2 = MegaDB("games", {"dir": "./"})
tx = Transaction({"usuarios": db1, "juegos": db2})

tx.usuarios.set('u123', {"name": "mega"})
tx.juegos.set('o456', {"id": "u123", "item": "pc"})

try:
    status = tx.commit()
    if status["ok"]:
        print("Se aplicaron las transacciones a la db real!")
except Exception as error:
    print("no se hizo la transaccion, rollback automatico.", error)
```

## rollback
### `rollback()`
Revierte manualmente todos los cambios pendientes, o restaura el estado anterior si ya intentaste un `commit()` y algo falló.
```python
tx = Transaction({"usuarios": db1, "juegos": db2})

tx.usuarios.set("u123", {"name": "mega", "balance": 500})
tx.juegos.set("o456", {"userId": "u123", "item": "pc", "price": 300})

print("db real:", db1.get("u123"))              # None
print("tx transaccion:", tx.usuarios.get("u123"))  # {'name': 'mega', 'balance': 500}

tx.rollback()
print("db real tras rollback:", db1.get("u123"))               # None
print("tx transaccion tras rollback:", tx.usuarios.get("u123")) # None
```

## Transaction.recoverPending (estático)
### `Transaction.recoverPending(dbs, opts={})`
Reaplica cualquier transacción que haya quedado a medias por un crash del proceso anterior. **Llamalo una vez al arrancar la app**, despues de abrir tus bases de datos y antes de empezar a procesar. Es seguro llamarlo siempre, incluso si no hay nada pendiente.

* **`dbs`** (dict): mapa `{nombre: instancia}`.
* **`opts["logDir"]`** (str, opcional): carpeta del log de transacciones. Por default, una carpeta hermana `_tx` junto a la primera DB del mapa.
* **Retorna:** una lista de `{"txId": ..., "recovered": ...}` por cada transacción pendiente encontrada.

```python
from megadbx import MegaDB, Transaction

usuarios = MegaDB("usuarios", {"dir": "./"})
pedidos = MegaDB("pedidos", {"dir": "./"})

# al arrancar la app:
resultados = Transaction.recoverPending({"usuarios": usuarios, "pedidos": pedidos})
print(resultados)  # [] si no habia nada pendiente, o [{'txId': '...', 'recovered': True}, ...]
```
---
# 5. `AdminPanel`

`AdminPanel` es un panel de administración web real: ver colecciones, buscar y paginar documentos, **crear/editar/borrar** con bloqueo optimista, gestionar índices y backups, revisar transacciones pendientes, y un registro de auditoría de cada acción (el panel requiere login obligatorio => configurable). Está construido sobre **Flask**.

---

## Qué se ve en la página?
- **Barra lateral**: lista de colecciones registradas (con su conteo de documentos y un mini "mapa de bloques"), más dos secciones de sistema: **Transacciones pendientes** y **Registro de auditoría**.
- **Vista de datos** (por colección): tabla paginada de documentos (clave + vista previa del valor), filtros avanzados, buscador que acepta la misma sintaxis de `query` que `find()` (ej: `{"edad": {"$gt": 18}}`), botón **+ Nuevo documento**, y por fila, **Editar**/**Borrar**.
- **Pestaña Info**: info general de la colección + botón para correr `verifyIntegrityFast()`.
- **Pestaña Metricas**: info general de todas mas metricas existentes en la base de datos, para monitoreo profundo.
- **Pestaña Índices**: botón para ejecutar `rebuildAllIndexes()`.
- **Pestaña Backups**: crear backup, listar backups existentes (con fecha y tamaño), restaurar uno (pide escribir `RESTAURAR` para confirmar).
- **Editar un documento**: abre un editor de JSON crudo. Si el documento cambió en el servidor desde que lo abriste, el guardado se rechaza y muestra el valor actual en vez de pisarlo silenciosamente (bloqueo optimista).
- **Borrar un documento**: pide escribir la clave exacta para confirmar.

Es una SPA en JavaScript plano (sin frameworks, sin CDNs externos).

---

## Seguridad, diseño y por qué

`AdminPanel` **no arranca sin credenciales**: si no le pasás `username`/`password`, tira un error en el constructor en vez de levantar un panel abierto por accidente. Es de un solo usuario (sin roles).

- **Autenticación**: usuario + contraseña (hasheada con `bcrypt`, nunca se persiste en texto plano). Sesión vía cookie `httpOnly` + `SameSite=Strict` (sesiones de Flask).
- **CSRF**: se emite un token al loguearse; toda escritura (`POST`/`PUT`/`DELETE`) lo exige en el header `X-CSRF-Token`.
- **Rate limiting de login**: 5 intentos fallidos cada 10 minutos por IP → `429`.
- **Bind a `127.0.0.1` por default** si necesitas exponerlo en la red, pon un reverse proxy con TLS delante (nginx/caddy).
- **CSP estricta** (`default-src 'self'`).
- **El panel nunca toca el filesystem directo**: todas sus operaciones pasan por la API pública de MegaDB.
- **Bloqueo optimista en ediciones**: cada lectura de un documento incluye un `etag` (hash del contenido). Al guardar, si el `etag` no coincide con el actual, se rechaza con `409`.
- **Auditoría**: cada `create`/`edit`/`delete`/`login`/`rebuild_indexes`/`createBackup`/`restoreBackup` queda registrado (usa `AppendOnlyLog` internamente).

---

## AdminPanel
```python
from megadbx import AdminPanel
panel = AdminPanel(opts={})
```

Si usas esta clase, es obligatorio instalar estas dependencias (quedaron opcionales para no forzarlas en quien nunca use el panel):
```bash
pip install megadbx[panel]
# equivalente a: pip install flask bcrypt
```

- **opts** (dict):

| Opción                    | Default                         | Explicación                                                            |
| -------------------------- | --------------------------------- | ------------------------------------------------------------------------ |
| `dbs`                       | *(requerido)*                    | Mapa `{nombreColeccion: instanciaDeMegaDB}` las colecciones que administra el panel. |
| `username`                  | *(requerido)*                    | Usuario para el login. Sin esto, el constructor tira error.               |
| `password`                  | *(requerido)*                    | Contraseña en texto plano (se hashea en memoria al llamar `start()`, nunca se persiste así). Mínimo 8 caracteres. |
| `port`                      | `4850`                            | Puerto donde escucha el panel.                                            |
| `host`                      | `127.0.0.1`                       | A qué interfaz de red se enlaza. No lo cambies a `0.0.0.0` sin un proxy TLS delante. |
| `sessionSecret`             | *(aleatorio por arranque)*        | Secret para firmar las cookies de sesión. Pasalo explícito si querés que las sesiones sobrevivan un reinicio. |
| `cookieSecure`              | `False`                           | `True` si el panel esta detras de un reverse proxy con HTTPS.       |
| `pageSize`                  | `50`                              | Documentos por página en la vista de datos.                               |
| `maxPageSize`               | `200`                             | Límite máximo de `limit` aceptado por query string.                       |
| `sessionMaxAgeMs`           | `28800000` ms (8h)                 | Duración de la sesión antes de requerir login de nuevo.                    |
| `txLogDir`                  | carpeta hermana `_tx`             | Dónde busca transacciones pendientes (ver [`Transaction.recoverPending`](#transactionrecoverPending-estático)). |
| `auditDir` / `auditFile`    | carpeta base / `panel_audit.log`  | Dónde se guarda el registro de auditoría.                                 |

```python
import os
from megadbx import MegaDB, AdminPanel

usuarios = MegaDB("usuarios", {"dir": "./"})
pedidos = MegaDB("pedidos", {"dir": "./"})

panel = AdminPanel({
    "dbs": {"usuarios": usuarios, "pedidos": pedidos},  # puedes registrar varias colecciones
    "username": "admin",
    "password": os.environ["PANEL_PASSWORD"], 
    "port": 4850,  # default
})

panel.start()
# [MegaDB Panel] abierto en http://127.0.0.1:4850
```

## start
### `start()`
Levanta el servidor Flask del panel (login, API, archivos estáticos del frontend) bloqueante, igual que `app.run()`. Es donde se hashea la contraseña y se registran todas las rutas.
```python
panel.start()
```
> Para producción, o si necesitas correr el panel junto a otro código en el mismo proceso, usa `panel.build_app()` en su lugar: devuelve la app de Flask sin arrancar un servidor, para servirla con `gunicorn`/`waitress`, o para testear con el `test_client()` de Flask.

## stop
### `stop()`
Detiene el servidor del panel (el servidor de desarrollo de Flask no siempre permite un shutdown limpio desde otro hilo, para eso, usar WSGI server de producción y usar los propios mecanismos de shutdown).
```python
panel.stop()
```

---

# Dato importante:
## Leer después de entender MegaDB, MegaDBSafe y MegaDBFull.
### Uso correcto de instancias de megadbx (MegaDB, MegaDBSafe, MegaDBFull)

- **Si creas una base de datos con `MegaDB(...)`, `MegaDBSafe(...)` o `MegaDBFull(...)` dentro de un mismo módulo y la usas solo ahí**, esta bien.
- **Si necesitas usar la misma base en varios módulos**, NO vuelvas a llamar al constructor en cada uno: eso crea **múltiples instancias independientes** que duplican carga, memoria, procesos internos, y pueden provocar **inconsistencias** o conflictos de escritura (WAL, AOF, etc.).
- Solución recomendable: **crear la instancia una vez** y **reutilizarla** (patrón singleton/módulo) o usar el modo [Multiproceso](#multiproceso).

### ¿Por qué es un problema instanciar varias veces?
Cuando haces `MegaDB("users", ...)`:
- Se crea una **instancia en memoria**.
- Esa instancia **carga** datos desde disco, prepara los bloques, sistemas internos, WAL/AOF, locks, colas internas, etc.
- Si en otro módulo volvés a hacer `MegaDB("users", ...)`, obtenés otra instancia que vuelve a cargar y mantener su propio estado en memoria.

Efectos negativos:
- **Doble I/O** y mayor uso de memoria.
- **Riesgo de inconsistencias**: dos instancias distintas pueden tener una vista distinta del estado en memoria.
- **Conflictos** en escrituras concurrentes al mismo archivo.
- **Comportamientos no deseados** en índices, locks o procesos en segundo plano.

### Qué hacer si no se usa el modo Multiproceso:

#### 1) Exportar una instancia única (simple y efectiva)
En Python, un módulo se ejecuta una sola vez e `import` reutiliza la misma instancia, así que este patron es directo:
```python
# archivo db.py
from megadbx import MegaDB

usuarios_db = MegaDB('users', {"dir": "./"})
mascotas_db = MegaDB('mascotas', {"dir": "./"})
```
Lo usas desde otro módulo:
```python
# comando1.py
from db import usuarios_db, mascotas_db

usuarios_db.set('u1', {"name": "Alice"})
mascotas_db.set(...)
```

#### 2) Factory / Singleton central (gestor que devuelve instancias)
Útil si vas a tener muchas colecciones o distintos tipos (MegaDB, MegaDBSafe, MegaDBFull):
```python
# manager.py
class DBManager:
    def __init__(self):
        self._instances = {}

    def get_db(self, type_class, name, options=None):
        options = options or {}
        key = f"{type_class.__name__}:{name}"
        if key in self._instances:
            return self._instances[key]

        db = type_class(name, options)  # creamos instancia (ya lista al retornar, es sincrona)
        self._instances[key] = db
        return db

manager = DBManager()  # exportamos singleton
```
Lo llamás desde otros módulos:
```python
# comando1.py
from manager import manager
from megadbx import MegaDB, MegaDBSafe, MegaDBFull

usuarios_db = manager.get_db(MegaDB, "users", {"dir": "./"})
mascotas_db = manager.get_db(MegaDBSafe, "mascotas", {"dir": "./"})
profesion_db = manager.get_db(MegaDBFull, "profesiones", {"dir": "./"})

print("Todas las instancias estan listas")

usuarios_db.set("mega", {"id": "00001", "coins": 10000})
print(usuarios_db.get("mega"))

mascotas_db.set("perro002", {"owner": "mega", "age": 3})
print(mascotas_db.get("perro002"))
```
##### Ventajas de este patrón singleton
* Una sola instancia por DB: si en otro módulo llamás `get_db` con los mismos parámetros, recibís la misma instancia ya inicializada.
* Un único proceso.
* No hay conflicto: puedes tener varias bases diferentes en el mismo proyecto.
* Soporte para todas las variantes: funciona igual con MegaDB, MegaDBSafe y MegaDBFull.
* Clave única por tipo + nombre.
* Recordatorio: megadbx actualmente soporta [multiprocesos, asi puedes usar la misma base de datos simultaneamente en diferentes nodos, procesos, etc](#multiproceso) 
