@syllkom/hyper-db 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,528 @@
1
+ ## 1. HyperDB
2
+
3
+ **HyperDB** es una base de datos binaria, fragmentada (sharded) y orientada a documentos para Node.js. Su diseño se centra en el alto rendimiento, la ausencia de dependencias externas (Zero-dependency) y una experiencia de desarrollo fluida mediante el uso de `Proxy` de JavaScript.
4
+
5
+ ### ¿Para qué sirve?
6
+ Sirve para almacenar estructuras de datos complejas y profundas sin cargar todo el conjunto de datos en memoria RAM. HyperDB divide automáticamente los objetos anidados en archivos binarios separados, cargándolos bajo demanda (Lazy Loading) y gestionando la memoria mediante un sistema LRU (Least Recently Used).
7
+
8
+ ### ¿A quién está dirigido?
9
+ * Desarrolladores de **Node.js** que requieren almacenamiento persistente local rápido.
10
+ * Proyectos que necesitan atomicidad en la escritura sin la complejidad de bases de datos SQL o NoSQL pesadas (como MongoDB).
11
+ * Sistemas embebidos o aplicaciones de escritorio (Electron) donde minimizar las dependencias es crítico.
12
+
13
+ ---
14
+
15
+ ## 2. Características Principales
16
+
17
+ * **Fragmentación Automática (Sharding):** Al guardar un objeto (`{}`) dentro de la base de datos, HyperDB lo detecta y automáticamente lo separa en su propio archivo binario (`.bin`), referenciándolo en el índice padre. Esto permite manejar bases de datos de gran tamaño sin saturar la memoria.
18
+ * **API Transparente (Proxy):** La interacción con la base de datos es idéntica a manipular un objeto JavaScript nativo (`db.data.usuario = "x"`). No requiere métodos `get()` o `set()` explícitos para el uso básico.
19
+ * **Serialización V8:** Utiliza el motor de serialización nativo de V8 (`v8.serialize/deserialize`), lo que lo hace significativamente más rápido y compacto que JSON.
20
+ * **Escritura Atómica:** Garantiza la integridad de los datos escribiendo primero en archivos temporales (`.tmp`) y renombrándolos al finalizar.
21
+ * **Gestión de Memoria (LRU Cache):** Incluye un gestor de memoria interno que descarga archivos poco utilizados cuando se supera un límite configurado (por defecto 20MB).
22
+ * **Debouncing de Escritura:** Agrupa múltiples operaciones de escritura en un corto periodo de tiempo para reducir el I/O en disco.
23
+ * **Zero-dependency:** No utiliza librerías de terceros, solo módulos nativos de Node.js (`fs`, `path`, `crypto`, `v8`).
24
+
25
+ ---
26
+
27
+ ## 3. Limitaciones Conocidas
28
+
29
+ * **Entorno:** Exclusivo para **Node.js (>= 18.0.0)** debido al uso de `fs/promises` y sintaxis moderna.
30
+ * **Tipos de Datos:** La fragmentación automática (creación de nuevos archivos) solo ocurre con objetos planos (`Plain Objects`). Instancias de clases personalizadas se serializan pero no generan nuevos fragmentos automáticamente a menos que se configure explícitamente.
31
+ * **Consultas Avanzadas:** No posee un motor de consultas integrado (como SQL `WHERE` o Mongo `find`). Las búsquedas requieren recorrer el objeto o implementar índices manuales.
32
+ * **Sincronía Aparente:** Aunque la API parece síncrona (asignación de variables), las escrituras en disco ocurren de forma asíncrona y diferida (debounced). Si el proceso de Node.js se mata abruptamente (SIGKILL) antes del *flush*, podrían perderse los últimos cambios en memoria (aunque el método `flush()` mitiga esto).
33
+
34
+ ---
35
+
36
+ ## 4. Instalación y Configuración
37
+
38
+ ### Requisitos Previos
39
+ * Node.js v18.0.0 o superior.
40
+
41
+ ### Instalación
42
+ Dado que es un paquete local o git, se instala vía:
43
+
44
+ ```bash
45
+ npm install git+https://github.com/Syllkom/HyperDB.git
46
+ # O si tienes los archivos locales:
47
+ npm install ./ruta-a-hyper-db
48
+ ```
49
+
50
+ ### Inicialización Básica
51
+ Para iniciar la base de datos, se debe instanciar la clase `HyperDB`.
52
+
53
+ ```javascript
54
+ import { HyperDB } from 'hyper-db';
55
+
56
+ const db = new HyperDB({
57
+ folder: './mi_base_de_datos', // Carpeta donde se guardarán los archivos
58
+ memory: 50, // Límite de caché en MB
59
+ depth: 2, // Profundidad de carpetas para el sharding
60
+ index: {
61
+ threshold: 10, // Operaciones antes de forzar guardado del índice
62
+ debounce: 5000 // Tiempo de espera para guardar índice (ms)
63
+ },
64
+ nodes: {
65
+ threshold: 5, // Operaciones antes de forzar guardado de nodos
66
+ debounce: 3000 // Tiempo de espera para guardar nodos (ms)
67
+ }
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 5. Arquitectura y Mapas
74
+
75
+ ### Estructura de Archivos Generada
76
+ HyperDB organiza los datos en una estructura jerárquica para evitar saturar un solo directorio.
77
+
78
+ ```text
79
+ ./mi_base_de_datos/
80
+ ├── index.bin # Índice maestro (Entry point)
81
+ ├── root.bin # Datos de la raíz
82
+ └── data/ # Almacenamiento fragmentado
83
+ ├── A1/ # Carpetas generadas por ID (según profundidad)
84
+ │ └── B2C3.bin # Archivo binario con datos de un sub-objeto
85
+ └── ...
86
+ ```
87
+
88
+ ### Flujo de Datos (Arquitectura)
89
+
90
+ ```mermaid
91
+ graph TD
92
+ User[Usuario / App] -->|Operación get/set| Proxy[HyperDB Proxy]
93
+ Proxy -->|Intercepta| Cluster[Cluster Manager]
94
+
95
+ subgraph "Lógica de Cluster"
96
+ Cluster -->|Es primitivo?| IndexData[Memoria Local]
97
+ Cluster -->|Es Objeto nuevo?| Shard[Shard Logic]
98
+ end
99
+
100
+ Shard -->|Genera ID| ID_Gen[Crypto ID]
101
+ Shard -->|Serializa| V8[V8 Serializer]
102
+
103
+ IndexData -->|Debounce Timer| FileMan[File Manager]
104
+ Shard -->|Write| Disk[Disk I/O]
105
+
106
+ FileMan -->|Persiste| Disk
107
+ Disk -->|Cache| Memory[Memory LRU]
108
+ Disk -->|File System| FS[fs / fs.promises]
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 6. Ejemplos de Uso
114
+
115
+ ### Caso 1: Escritura y Lectura Básica
116
+ Este ejemplo muestra cómo guardar datos simples y cómo HyperDB los persiste.
117
+
118
+ ```javascript
119
+ import { HyperDB } from 'hyper-db';
120
+
121
+ const db = new HyperDB({ folder: './db' });
122
+
123
+ // 1. Asignación directa (SET)
124
+ // Esto escribe en memoria inmediatamente y programa la escritura en disco.
125
+ db.data.nombre = "HyperDB";
126
+ db.data.version = 1.0;
127
+
128
+ // 2. Lectura (GET)
129
+ console.log(db.data.nombre); // Salida: "HyperDB"
130
+ ```
131
+
132
+ ### Caso 2: Fragmentación Automática (Sharding)
133
+ Al asignar un objeto, HyperDB crea un archivo separado. Esto es útil para listas de usuarios o datos masivos.
134
+
135
+ ```javascript
136
+ // Al asignar un objeto, se crea un nuevo archivo binario en ./db/data/...
137
+ // El índice principal solo guardará una referencia: { "configuracion": { "$file": "xx/xxxx.bin" } }
138
+ db.data.configuracion = {
139
+ tema: "oscuro",
140
+ notificaciones: true,
141
+ limites: {
142
+ diario: 100
143
+ }
144
+ };
145
+
146
+ // Acceso transparente (Lazy Loading)
147
+ // Al acceder a .configuracion, HyperDB carga el archivo correspondiente automáticamente.
148
+ console.log(db.data.configuracion.tema);
149
+ ```
150
+
151
+ ### Caso 3: Persistencia y Cierre
152
+ Cómo asegurar que los datos se guarden antes de cerrar la aplicación.
153
+
154
+ ```javascript
155
+ async function cerrarApp() {
156
+ console.log("Guardando datos...");
157
+
158
+ // Fuerza la escritura de todos los procesos pendientes en el Pipe
159
+ await db.flush();
160
+
161
+ console.log("Datos guardados. Saliendo.");
162
+ process.exit(0);
163
+ }
164
+ ```
165
+
166
+ ### Caso 4: Gestión de Ramas (Flow & Proxy)
167
+ Uso avanzado de la lógica de "Flow" para interceptar o manejar rutas específicas.
168
+
169
+ ```javascript
170
+ // Obtener estadísticas de uso de memoria
171
+ console.log(db.memory());
172
+ // Salida: { used: "1.20 MB", limit: "20.00 MB", items: 5 }
173
+
174
+ // Eliminar una propiedad (y su archivo asociado si era un shard)
175
+ delete db.data.configuracion;
176
+ // Esto elimina la referencia y, tras el proceso de 'prune' o limpieza, el archivo físico.
177
+ ```
178
+
179
+ ## 7. Referencia de API Interna
180
+
181
+ Aunque la interacción principal es vía Proxy, estas clases componen el sistema:
182
+
183
+ | Clase | Descripción |
184
+ | :--- | :--- |
185
+ | **HyperDB** | Fachada principal. Configura inyecciones de dependencias y expone `this.data`. |
186
+ | **Disk** | Maneja I/O. Posee métodos `read`, `write`, `remove`. Usa colas (`Pipe`) para evitar colisiones de escritura. |
187
+ | **Memory** | Caché LRU. Evita leer del disco si el objeto ya está cargado. |
188
+ | **Shard** | Lógica de fragmentación. Decide cuándo y dónde crear nuevos archivos binarios (`forge`) y limpia referencias (`purge`). |
189
+ | **Cluster** | Coordina el índice y los datos. Intercepta las escrituras para decidir si van al archivo actual o a un nuevo shard. |
190
+ | **Flow** | Utilidad para manejar estructuras de árbol y referencias a Proxies activos. |
191
+
192
+ ---
193
+
194
+ ## 8. Configuración Detallada y Parámetros
195
+
196
+ El constructor de `HyperDB` acepta un objeto de opciones complejo que permite afinar el comportamiento de la base de datos, desde la ubicación de los archivos hasta la agresividad del sistema de caché y escritura.
197
+
198
+ ### Estructura del Objeto `options`
199
+
200
+ ```javascript
201
+ const options = {
202
+ folder: './data', // Ruta base del almacenamiento
203
+ memory: 20, // Límite de memoria en MB
204
+ depth: 2, // Profundidad de subcarpetas para sharding
205
+
206
+ // Configuración del Índice Principal (index.bin)
207
+ index: {
208
+ threshold: 10, // Operaciones antes de escritura forzada
209
+ debounce: 5000 // Tiempo de espera (ms) para escritura diferida
210
+ },
211
+
212
+ // Configuración de Nodos/Fragmentos (archivos .bin individuales)
213
+ nodes: {
214
+ threshold: 5, // Operaciones antes de escritura forzada
215
+ debounce: 3000 // Tiempo de espera (ms) para escritura diferida
216
+ },
217
+
218
+ // Inyección de Dependencias (Avanzado)
219
+ $class: {
220
+ Disk: null, // Clase o instancia personalizada para I/O
221
+ Index: null, // Clase o instancia para gestión del índice
222
+ Flow: null, // Clase o instancia para flujo de navegación
223
+ Cluster: null, // Clase o instancia para gestión de clusters
224
+ Shard: null, // Clase o instancia para lógica de sharding
225
+ Memory: null // Clase o instancia para gestión de RAM
226
+ }
227
+ };
228
+ ```
229
+
230
+ ### Explicación de Parámetros
231
+
232
+ 1. **`folder`**: Define dónde se crearán los archivos. Si no existe, la clase `Disk` intentará crearla recursivamente.
233
+ 2. **`memory`**: Define el "techo suave" (soft limit) del caché LRU en Megabytes. Si los objetos cargados superan este tamaño, los menos usados se eliminan de la RAM.
234
+ 3. **`depth`**: Controla cómo se generan los IDs de los shards.
235
+ * `depth: 0`: Todos los archivos se guardan planos en la carpeta `data`.
236
+ * `depth: 2`: Se crea una estructura de carpetas basada en los primeros 2 caracteres del hash (ej. `data/A1/A1B2...bin`). Esto evita límites del sistema de archivos en directorios con miles de archivos.
237
+ 4. **`threshold` (Umbral)**: Número de operaciones de escritura (`set` / `delete`) que ocurren en memoria antes de forzar una escritura física en disco.
238
+ 5. **`debounce` (Retardo)**: Milisegundos que el sistema espera tras una escritura en memoria antes de guardar en disco si no se alcanza el umbral. *Reinicia el temporizador con cada nueva operación.*
239
+
240
+ ---
241
+
242
+ ## 9. Mecánica Interna de E/S (Disk & Memory)
243
+
244
+ HyperDB implementa su propio sistema de gestión de archivos y memoria para garantizar rendimiento y consistencia.
245
+
246
+ ### Clase `Disk` (library/Disk.js)
247
+
248
+ Es el motor de persistencia. Sus responsabilidades clave son:
249
+
250
+ * **Atomicidad (`write` / `writeSync`):**
251
+ Nunca sobrescribe un archivo directamente.
252
+ 1. Serializa los datos con `v8`.
253
+ 2. Escribe en un archivo temporal (`filename.bin.tmp`).
254
+ 3. Fuerza el volcado al disco físico (`fsync`).
255
+ 4. Renombra el temporal al nombre final. Esto previene corrupción de datos si el proceso falla a mitad de escritura.
256
+
257
+ * **Cola de Promesas (`Pipe`):**
258
+ Para evitar condiciones de carrera en operaciones asíncronas sobre el mismo archivo, `Disk` utiliza un `Map` llamado `Pipe`.
259
+ * Si se solicitan 3 escrituras seguidas al mismo archivo, se encadenan en una promesa secuencial (`.then().then()`).
260
+
261
+ * **Mantenimiento (`prune`):**
262
+ Escanea recursivamente los directorios de datos. Si encuentra carpetas vacías (remanentes de shards eliminados), las borra para mantener el sistema de archivos limpio.
263
+
264
+ ### Clase `Memory` (library/Memory.js)
265
+
266
+ Implementa un caché **LRU (Least Recently Used)** personalizado.
267
+
268
+ * **Cálculo de Tamaño:** Usa `v8.serialize(data).length` para estimar el peso real en bytes de los objetos.
269
+ * **Pinned Keys:** Ciertas claves críticas (como `index.bin` y `root.bin`) pueden marcarse como "pinned" para que nunca sean desalojadas del caché, garantizando que la estructura base siempre esté disponible.
270
+ * **Eviction Policy:** Cuando `currentSize + dataSize > limit`, elimina el elemento más antiguo (`cache.keys().next().value`) hasta tener espacio.
271
+
272
+ ---
273
+
274
+ ## 10. Lógica de Fragmentación y Clúster
275
+
276
+ El corazón de la capacidad de escalar de HyperDB reside en la interacción entre `Cluster.js` y `Shard.js`.
277
+
278
+ ### El proceso de `forge` (Forjado)
279
+ Cuando se asigna un objeto a una propiedad:
280
+
281
+ 1. **Detección:** `Cluster` detecta que el valor es un objeto plano (`Plain Object`).
282
+ 2. **Delegación:** Llama a `Shard.forge(index, value)`.
283
+ 3. **Recursividad:** `Shard` recorre el objeto. Si encuentra sub-objetos anidados que también son *shardables*, los separa recursivamente.
284
+ 4. **Generación de ID:** `Shard` genera un ID aleatorio hexadecimal (ej. `A1B2C3D4`) y calcula su ruta basada en la `depth`.
285
+ 5. **Persistencia:** Escribe el contenido del objeto en el archivo `.bin` generado.
286
+ 6. **Referencia:** Devuelve un objeto "puntero": `{ $file: "A1/A1B2C3D4.bin" }`.
287
+ 7. **Actualización del Padre:** El objeto padre guarda solo el puntero, no los datos completos.
288
+
289
+ ### Diagrama de Flujo de Escritura (Cluster)
290
+
291
+ ```mermaid
292
+ graph TD
293
+ Input["Set Key: Value"] --> Check{"Es Objeto?"}
294
+ Check -- No --> SaveDirect[Guardar valor en Index actual]
295
+ Check -- Sí --> ShardForge[Llamar a Shard.forge]
296
+
297
+ ShardForge --> GenID[Generar ID Único]
298
+ ShardForge --> WriteFile[Escribir Value en ID.bin]
299
+ ShardForge --> ReturnRef["Retornar { $file: ID.bin }"]
300
+
301
+ ReturnRef --> SaveDirect
302
+ SaveDirect --> CheckThres{"Contador >= Threshold?"}
303
+
304
+ CheckThres -- Sí --> DiskWrite[Escribir Index en Disco ahora]
305
+ CheckThres -- No --> SetTimer[Iniciar Timer Debounce]
306
+ SetTimer -->|Timeout| DiskWrite
307
+ ```
308
+
309
+ ---
310
+
311
+ ## 11. Sistema de Flow y Proxies
312
+
313
+ HyperDB utiliza `Proxy` de ES6 para interceptar todas las operaciones. La clase `Flow` y el mecanismo de `DB.js` gestionan la "navegación" por la base de datos.
314
+
315
+ * **Navegación Lazy:** Cuando accedes a `db.data.usuario.perfil`, el sistema no carga `perfil` hasta que lo tocas.
316
+ * **Apertura Dinámica:** Si el valor recuperado es un puntero `{ $file: "..." }`, el método `Proxy` en `DB.js` detecta esto, lee el archivo desde `Disk` (o `Memory`), crea un nuevo `Cluster` para ese archivo y devuelve un nuevo `Proxy` envolviendo esos datos.
317
+ * **Flow Tree:** La clase `Flow` mantiene un árbol paralelo de referencias. Esto permite inyectar lógica personalizada o metadatos en rutas específicas del árbol de datos sin contaminar los datos almacenados.
318
+
319
+ ---
320
+
321
+ ## 12. Inyección de Dependencias y Extensibilidad
322
+
323
+ HyperDB permite reemplazar casi cualquiera de sus componentes internos pasando clases o instancias en el constructor. Esto es útil para testing (mocks) o para cambiar el comportamiento (ej. guardar en S3 en lugar de disco local).
324
+
325
+ ### Ejemplo: Reemplazar el almacenamiento (Mocking)
326
+
327
+ Si quieres usar HyperDB puramente en memoria (sin escribir a disco) para pruebas unitarias:
328
+
329
+ ```javascript
330
+ import { HyperDB, Disk } from 'hyper-db';
331
+
332
+ // Creamos un Disk personalizado o configurado solo en memoria
333
+ // Nota: La clase Disk original ya soporta esto si no se le da folder,
334
+ // pero aquí forzamos el comportamiento mediante inyección.
335
+
336
+ class InMemoryDisk extends Disk {
337
+ write(filename, data) {
338
+ // Sobrescribir para no tocar 'fs'
339
+ this.memory.set(filename, data);
340
+ return Promise.resolve(true);
341
+ }
342
+ // ... implementar resto de métodos necesarios
343
+ }
344
+
345
+ const db = new HyperDB({
346
+ $class: {
347
+ Disk: new InMemoryDisk()
348
+ }
349
+ });
350
+ ```
351
+
352
+ ### Inyección de Funciones Compartidas
353
+ El código en `DB.js` verifica `DB.shared`. Esto permite añadir métodos globales disponibles en cualquier nivel del proxy.
354
+
355
+ ```javascript
356
+ // (Internamente en una versión modificada de HyperDB)
357
+ this.shared = {
358
+ hola: function() { console.log("Hola desde", this.index.$file); }
359
+ }
360
+
361
+ // Uso:
362
+ // db.data.users.hola() -> Imprime el archivo donde viven los usuarios
363
+ ```
364
+
365
+ ---
366
+
367
+ ## 13. Mantenimiento y Manejo de Errores
368
+
369
+ ### Manejo de Errores (`onError`)
370
+ La clase `Disk` acepta un callback `onError`. Por defecto, imprime a `console.error`.
371
+
372
+ ```javascript
373
+ const db = new HyperDB({
374
+ $class: {
375
+ Disk: new Disk({
376
+ onError: (err) => {
377
+ // Enviar a sistema de logs (Sentry, Datadog, etc.)
378
+ alertAdmin("Error crítico de IO en DB", err);
379
+ }
380
+ })
381
+ }
382
+ });
383
+ ```
384
+
385
+ ### Limpieza de Archivos (`Prune`)
386
+ Con el tiempo, al borrar objetos, pueden quedar carpetas vacías en la estructura de shards. El método `prune` las elimina.
387
+
388
+ ```javascript
389
+ // Se recomienda ejecutar esto en tareas cron o al inicio/cierre
390
+ await db.disk.prune();
391
+ ```
392
+
393
+ ### Recuperación de Fallos
394
+ Si la aplicación se cierra inesperadamente (`SIGKILL`), los archivos `.tmp` pueden quedar en la carpeta de datos.
395
+ * **Al inicio:** HyperDB no limpia automáticamente los `.tmp`.
396
+ * **Integridad:** Dado que la operación de renombrado (`fs.rename`) es atómica en sistemas POSIX, el archivo `.bin` original estará intacto o será la nueva versión completa. No hay estados intermedios de archivo corrupto.
397
+
398
+ ---
399
+
400
+ ## Resumen de Archivos Fuente
401
+
402
+ * **`package.json`**: Metadatos y scripts (sin tests definidos).
403
+ * **`Disk.js`**: Capa física. `fs`, `v8`, `Pipe`, `Memory`.
404
+ * **`Memory.js`**: Capa lógica de caché. LRU, cálculo de tamaño.
405
+ * **`Shard.js`**: Lógica de particionado. Generación de IDs, purga recursiva.
406
+ * **`Flow.js`**: Utilidad de estructura de árbol para metadatos de navegación.
407
+ * **`DB.js`**: Entry point. Gestión de Proxies e Inyección de Dependencias.
408
+ * **`Cluster.js`**: Gestión de nodos. `Index`, `File` (debounce/threshold), `Cluster` (lógica get/set).
409
+ * **`index.js`**: Exportador de módulos.
410
+
411
+ Aquí tienes la tercera y última parte de la documentación técnica de **HyperDB**. Esta sección cubre **Seguridad**, **Optimización de Rendimiento**, **Solución de Problemas** y una **Referencia Rápida de API**.
412
+
413
+ ---
414
+
415
+ ## 14. Consideraciones de Seguridad
416
+
417
+ Dado que HyperDB opera directamente sobre el sistema de archivos local y utiliza serialización binaria, existen vectores de seguridad específicos que deben considerarse.
418
+
419
+ ### 1. Deserialización de Datos (`v8.deserialize`)
420
+ HyperDB utiliza el motor nativo de V8 para serializar/deserializar datos.
421
+ * **Riesgo:** La deserialización de datos no confiables puede llevar a la ejecución de código arbitrario o comportamientos inestables si el archivo binario ha sido manipulado externamente.
422
+ * **Mitigación:** Asegúrese de que la carpeta de datos (`./data` por defecto) tenga permisos de escritura **exclusivos** para el usuario del sistema que ejecuta el proceso Node.js. Nunca apunte HyperDB a una carpeta donde usuarios externos puedan subir archivos.
423
+
424
+ ### 2. Permisos de Archivos (File System)
425
+ HyperDB necesita permisos de lectura y escritura (`fs.read`, `fs.write`, `fs.mkdir`, `fs.rm`).
426
+ * **Requisito:** El proceso Node.js debe tener control total sobre el directorio raíz definido en `options.folder`.
427
+ * **Error Común:** Ejecutar la aplicación como `root` y luego como `user` puede causar errores de `EACCES` si los archivos fueron creados por `root`.
428
+
429
+ ### 3. Inyección de Dependencias
430
+ La característica de inyección (`$class`) permite reemplazar componentes internos.
431
+ * **Precaución:** Si su aplicación permite configuración externa que se pasa directamente al constructor de `HyperDB`, un atacante podría inyectar clases maliciosas. Valide siempre el objeto `options` antes de pasarlo al constructor.
432
+
433
+ ---
434
+
435
+ ## 15. Guía de Rendimiento y Buenas Prácticas
436
+
437
+ Para obtener el máximo rendimiento (High-Performance) prometido por HyperDB, siga estas directrices:
438
+
439
+ ### Estrategia de Sharding (Fragmentación)
440
+ El sharding automático es potente, pero tiene un costo de I/O (crear archivo, abrir handle, escribir).
441
+
442
+ * **Cuándo usarlo:** Para objetos grandes o listas que crecen indefinidamente (ej. `db.data.usuarios`, `db.data.logs`).
443
+ * **Cuándo evitarlo:** Para objetos pequeños o de configuración (ej. `{ x: 1, y: 2 }`). Si un objeto tiene pocas propiedades primitivas, es mejor dejarlo dentro del archivo padre (`Index`) en lugar de aislarlo en un nuevo archivo.
444
+ * **Consejo:** HyperDB decide hacer shard si asignas un `Plain Object`. Si asignas primitivos (`string`, `number`), se quedan en el archivo actual.
445
+
446
+ ### Ajuste de Memoria (`Memory Limit`)
447
+ * **Escenario:** Servidor con mucha RAM.
448
+ * **Acción:** Aumente `options.memory`. Por defecto es 20MB. Subirlo a 100MB o 500MB reducirá drásticamente las lecturas a disco, ya que más shards permanecerán "calientes" en RAM.
449
+ * **Escenario:** Entorno Serverless (AWS Lambda) o contenedores pequeños.
450
+ * **Acción:** Mantenga el límite bajo (10-20MB) y reduzca `options.index.debounce` a valores cercanos a 0 o 100ms para asegurar que los datos se escriban antes de que la función se congele.
451
+
452
+ ### Uso de `flush()`
453
+ Las operaciones de escritura son asíncronas y diferidas ("debounced").
454
+ * **Mejor Práctica:** Siempre llame a `await db.flush()` antes de finalizar el proceso o en puntos críticos de la lógica de negocio donde la persistencia inmediata es obligatoria.
455
+
456
+ ---
457
+
458
+ ## 16. Solución de Problemas Frecuentes
459
+
460
+ ### Problema: "Los cambios no se guardan al reiniciar"
461
+ * **Causa:** La aplicación se detuvo forzosamente (Crash o `SIGKILL`) antes de que el temporizador de *debounce* (por defecto 3-5 segundos) se disparara.
462
+ * **Solución:** Reduzca los tiempos de `debounce` en la configuración o asegúrese de capturar eventos de cierre (`process.on('SIGINT', ...)`) y llamar a `db.flush()`.
463
+
464
+ ### Problema: "Error: Invalid options"
465
+ * **Causa:** Se pasó `null`, `undefined` o un tipo no objeto al constructor.
466
+ * **Verificación:** Asegúrese de instanciar con `{}` como mínimo: `new HyperDB({})`.
467
+
468
+ ### Problema: Consumo excesivo de inodos en disco
469
+ * **Causa:** `depth: 0` con miles de objetos crea miles de archivos en una sola carpeta.
470
+ * **Solución:** Use `depth: 2` o superior en la configuración. Esto distribuye los archivos en subcarpetas (ej. `A1/B2/...`), lo cual es más amigable para sistemas de archivos como EXT4 o NTFS.
471
+
472
+ ### Problema: "Object exceeds cache limit"
473
+ * **Síntoma:** Advertencia en consola `[HyperDB:Memory] Object '...' exceeds cache limit`.
474
+ * **Causa:** Está intentando guardar un solo objeto (un solo shard) que es más grande que el límite total de memoria asignado a la DB.
475
+ * **Solución:** Aumente `options.memory` o divida ese objeto gigante en sub-objetos más pequeños para que HyperDB pueda fragmentarlo.
476
+
477
+ ---
478
+
479
+ ## 17. Referencia Rápida de API Pública
480
+
481
+ Resumen de los métodos y propiedades expuestos para el desarrollador.
482
+
483
+ ### Constructor
484
+ ```javascript
485
+ const db = new HyperDB(options);
486
+ ```
487
+ Ver sección de configuración para detalles de `options`.
488
+
489
+ ### Propiedades
490
+ * **`db.data`**: (Proxy) El punto de entrada principal para leer y escribir datos. Se comporta como un objeto JS estándar.
491
+ * **`db.disk`**: (Instancia de `Disk`) Acceso directo a operaciones de archivo (bajo nivel).
492
+ * **`db.index`**: (Instancia de `Index`) Acceso al gestor del archivo índice raíz.
493
+
494
+ ### Métodos
495
+ * **`db.open(...path)`**:
496
+ * *Descripción:* Abre manualmente una ruta específica y devuelve un Proxy para ese nodo.
497
+ * *Uso:* `const userConfig = db.open('users', 'id_123', 'config');`
498
+ * **`db.flush()`**:
499
+ * *Descripción:* Fuerza la escritura de todas las operaciones pendientes en la cola (`Pipe`) y espera a que terminen.
500
+ * *Retorno:* `Promise<true>`
501
+ * **`db.memory()`**:
502
+ * *Descripción:* Devuelve estadísticas del uso de memoria actual.
503
+ * *Retorno:* `{ used: string, limit: string, items: number }`
504
+ * **`delete db.data.propiedad`**:
505
+ * *Descripción:* Elimina la clave y, eventualmente, purga el archivo asociado del disco.
506
+
507
+ ### Métodos Internos (Accesibles vía `db.disk`)
508
+ * **`db.disk.prune()`**:
509
+ * *Async.* Escanea y elimina carpetas vacías en el directorio de datos.
510
+
511
+ ---
512
+
513
+ ## 18. Comparativa Técnica
514
+
515
+ Para entender dónde encaja HyperDB:
516
+
517
+ | Característica | HyperDB | JSON (fs.writeFile) | SQLite | MongoDB |
518
+ | :--- | :--- | :--- | :--- | :--- |
519
+ | **Formato** | Binario (V8) Fragmentado | Texto Plano (Monolítico) | Binario (Relacional) | Binario (BSON) |
520
+ | **Carga en RAM** | **Lazy (Solo lo necesario)** | Todo el archivo | Paginada | Paginada |
521
+ | **Escritura** | Atómica y Parcial | Reescribe todo el archivo | Transaccional (SQL) | Documento |
522
+ | **Dependencias** | **Cero (Nativo)** | Cero | `sqlite3` (binding C++) | Driver + Servidor |
523
+ | **Consultas** | Traversal (JS nativo) | Array methods | SQL | Query Language |
524
+ | **Uso Ideal** | Config, Estado local, Grafos de objetos | Config simple | Datos tabulares | Big Data / Cloud |
525
+
526
+ **Conclusión:** HyperDB es ideal cuando JSON se queda corto por rendimiento/memoria, pero SQLite es excesivo o demasiado rígido (schemas) para la estructura de datos dinámica que maneja la aplicación.
527
+
528
+ ---
package/index.js ADDED
@@ -0,0 +1,17 @@
1
+ import { HyperDB } from './library/DB.js';
2
+ import { Shard } from './library/Shard.js';
3
+ import { File, Index, Cluster } from './library/Cluster.js';
4
+ import { Flow } from './library/Flow.js';
5
+ import { Memory } from './library/Memory.js';
6
+ import { Disk } from './library/Disk.js';
7
+
8
+ export {
9
+ HyperDB,
10
+ Shard,
11
+ File,
12
+ Index,
13
+ Cluster,
14
+ Flow,
15
+ Memory,
16
+ Disk
17
+ }
@@ -0,0 +1,161 @@
1
+ import { Shard } from "./Shard.js";
2
+
3
+ export class File {
4
+ #count = 0;
5
+ #timer = null;
6
+ constructor(disk, file, options = {}) {
7
+ this.file = file;
8
+ this.disk = disk;
9
+
10
+ this._limit = options.limit ?? 10;
11
+ this._delay = options.delay ?? 5000;
12
+ }
13
+
14
+ get data() {
15
+ if (this.disk.existsSync(this.file)) {
16
+ return this.disk.readSync(this.file);
17
+ } else {
18
+ this.disk.writeSync(this.file, {});
19
+ return this.disk.readSync(this.file)
20
+ }
21
+ }
22
+
23
+ save(force = false) {
24
+ if (force || ++this.#count >= this._limit) {
25
+ clearTimeout(this.#timer);
26
+ this.#timer = null;
27
+ this.#count = 0;
28
+ return this.disk.write(this.file, this.data);
29
+ }
30
+
31
+ clearTimeout(this.#timer);
32
+ this.#timer = setTimeout(() => this.save(true), this._delay);
33
+ }
34
+ }
35
+
36
+ export class Index {
37
+ constructor(disk, options = {}) {
38
+ this.disk = disk;
39
+
40
+ if (options?.$class?.File) {
41
+ const a0 = options.$class.File;
42
+ if (a0.constructor.name === 'Array') {
43
+ this.file = new File(disk, 'index.bin', ...a0);
44
+ } else if (a0.constructor.name === 'Object') {
45
+ this.file = new File(disk, 'index.bin', a0);
46
+ } else this.file = a0;
47
+ }
48
+
49
+ if (!this.file) {
50
+ this.file = new File(disk, 'index.bin', {
51
+ limit: options?.file?.limit || 10,
52
+ delay: options.file?.delay || 5000
53
+ });
54
+ }
55
+
56
+ this.data = this.file.data;
57
+ if (!this.data.$file) {
58
+ this.data.$file = 'root.bin';
59
+ this.save();
60
+ }
61
+ }
62
+
63
+ get(...args) {
64
+ return args.reduce((acc, k) => acc?.[k], this.data) ?? false;
65
+ }
66
+
67
+ save() {
68
+ return this.file.save();
69
+ }
70
+ }
71
+
72
+ export class Cluster {
73
+ constructor(disk, indexMap, options = {}) {
74
+ this.disk = disk;
75
+ this.indexMap = indexMap;
76
+
77
+ if (options.$class?.Shard) {
78
+ const a0 = options.$class.Shard;
79
+ if (a0.constructor.name === 'Array') {
80
+ this.shard = new Shard(disk, ...a0);
81
+ } else if (a0.constructor.name === 'Number') {
82
+ this.shard = new Shard(disk, a0);
83
+ } else this.shard = a0;
84
+ }
85
+
86
+ if (options?.$class?.File) {
87
+ const a0 = options.$class.File;
88
+ if (a0.constructor.name === 'Array') {
89
+ this.file = new File(disk, this.indexMap.$file, ...a0);
90
+ } else if (a0.constructor.name === 'Object') {
91
+ this.file = new File(disk, this.indexMap.$file, a0);
92
+ } else this.file = a0;
93
+ }
94
+
95
+ if (!this.shard) {
96
+ this.shard = new Shard(disk, options?.shard?.depth || 2);
97
+ }
98
+
99
+ if (!this.file) {
100
+ this.file = new File(disk, this.indexMap.$file, {
101
+ limit: options?.file?.limit || 5,
102
+ delay: options.file?.delay || 3000
103
+ });
104
+ }
105
+ }
106
+
107
+ get data() {
108
+ return this.file.data;
109
+ }
110
+
111
+ get(key) {
112
+ const value = this.data[key];
113
+ if (typeof value === 'string' && value.endsWith('.bin')) {
114
+ if (!this.indexMap[key]) this.indexMap[key] = { $file: value };
115
+ return { $file: value }
116
+ }
117
+ return value;
118
+ }
119
+
120
+ set(key, value) {
121
+ if (this.indexMap[key]) {
122
+ this.shard.purge(this.indexMap[key]);
123
+ delete this.indexMap[key];
124
+ }
125
+
126
+ let isObject = false;
127
+ if (value && typeof value === 'object') {
128
+ const proto = Object.getPrototypeOf(value);
129
+ isObject = (proto === Object.prototype || proto === null);
130
+ }
131
+
132
+ if (isObject) {
133
+ let tmpIndex = {}
134
+ const file = this.shard.forge(tmpIndex, value);
135
+
136
+ if (file) {
137
+ this.indexMap[key] = tmpIndex;
138
+ this.data[key] = file;
139
+ } else {
140
+ return false;
141
+ }
142
+ } else {
143
+ this.data[key] = value;
144
+ }
145
+
146
+ this.file.save();
147
+ }
148
+
149
+ delete(key) {
150
+ if (this.indexMap[key]) {
151
+ this.shard.purge(this.indexMap[key]);
152
+ delete this.indexMap[key];
153
+ }
154
+ delete this.data[key];
155
+ this.file.save();
156
+ }
157
+
158
+ keys() {
159
+ return Object.keys(this.data);
160
+ }
161
+ }
package/library/DB.js ADDED
@@ -0,0 +1,222 @@
1
+ import { Disk } from "./Disk.js";
2
+ import { Index, Cluster } from "./Cluster.js";
3
+ import { Flow } from "./Flow.js";
4
+
5
+ /* Uso:
6
+ new HyperDB({
7
+ depth: 2,
8
+ folder: './storage/database',
9
+ memory: 20,
10
+ index: { threshold: 10, debounce: 5000 },
11
+ nodes: { threshold: 5, debounce: 3000 }
12
+ })
13
+ */
14
+
15
+ export class HyperDB {
16
+ #options = null;
17
+ constructor(options = {}) {
18
+ if (options?.constructor?.name !== 'Object') {
19
+ throw new Error('Invalid options');
20
+ }
21
+
22
+ this.#options = options;
23
+
24
+ // Inyección de dependencias: Disk
25
+ if (options.$class?.Disk) {
26
+ const a0 = options.$class.Disk;
27
+ if (a0.constructor.name == 'Array') {
28
+ this.disk = new Disk(...a0);
29
+ } else if (a0.constructor.name == 'Object') {
30
+ this.disk = new Disk(a0);
31
+ } else this.disk = a0
32
+ }
33
+
34
+ if (!this.disk) {
35
+ this.disk = new Disk({
36
+ memory: { limit: options.memory || 20 },
37
+ folder: options.folder || './data',
38
+ });
39
+ }
40
+
41
+ // Inyección de dependencias: Index
42
+ if (options.$class?.Index) {
43
+ const a0 = options.$class.Index;
44
+ if (a0.constructor.name == 'Array') {
45
+ this.index = new Index(this.disk, ...a0);
46
+ } else if (a0.constructor.name == 'Object') {
47
+ this.index = new Index(this.disk, a0);
48
+ } else this.index = a0
49
+ }
50
+
51
+ if (!this.index) {
52
+ this.index = new Index(this.disk, {
53
+ file: {
54
+ limit: options.index?.threshold || 10,
55
+ delay: options.index?.debounce || 5000
56
+ }
57
+ })
58
+ }
59
+
60
+ // Inyección de dependencias: Flow
61
+ if (options.$class?.Flow) {
62
+ const a0 = options.$class.Flow;
63
+ if (a0.constructor.name == 'Array') {
64
+ this.flow = new Flow(...a0);
65
+ } else this.flow = a0
66
+ }
67
+
68
+ if (!this.flow) {
69
+ this.flow = new Flow();
70
+ }
71
+
72
+ /////////////////////////////
73
+
74
+ this.proxies = new WeakMap();
75
+ this.flows = new WeakMap();
76
+
77
+ this.data = this.Proxy(this.index.data);
78
+ this.shared = {}
79
+ }
80
+
81
+ memory() {
82
+ return this.disk.memory.stats()
83
+ }
84
+
85
+ flush() {
86
+ return this.disk.flush()
87
+ }
88
+
89
+ open(...path) {
90
+ const o = this.index.get(...path)
91
+ const router = this.flow.get(...path)
92
+ if (o && o.$file) return this.Proxy(o, router)
93
+ return false
94
+ }
95
+
96
+ Proxy(index, flow) {
97
+ const DB = this
98
+ if (!index) index = this.index.data;
99
+ if (index.$file == 'root.bin') flow = this.flow.tree
100
+
101
+ if (flow) this.flows.set(index, flow);
102
+ if (this.proxies.has(index)) return this.proxies.get(index);
103
+
104
+ let root = null;
105
+
106
+ // Inyección de dependencias: Cluster
107
+ if (this.#options?.$class?.Cluster) {
108
+ const a0 = this.#options.$class.Cluster;
109
+ if (a0.constructor.name === 'Array') {
110
+ root = new Cluster(this.disk, index, ...a0);
111
+ } else if (a0.constructor.name === 'Object') {
112
+ root = new Cluster(this.disk, index, a0);
113
+ } else root = a0;
114
+ }
115
+
116
+ if (!root) root = new Cluster(this.disk, index, {
117
+ shard: { depth: this.#options?.depth || 2 },
118
+ file: {
119
+ limit: this.#options?.nodes?.threshold || 5,
120
+ delay: this.#options?.nodes?.debounce || 3000
121
+ }
122
+ });
123
+
124
+ ////////////////////////////
125
+
126
+ const open = (args, index, flow) => {
127
+ const Open = (object) => () => args.reduce((acc, k) => acc?.[k], object) ?? false;
128
+ const $index = Open(index)();
129
+ const $flow = Open(flow)();
130
+ if ($index && $index.$file) return this.Proxy($index, $flow)
131
+ }
132
+
133
+ const guard = (method) => (...args) => {
134
+ if (flow?.$proxy && flow?.$proxy?.[method]) {
135
+ let control = { end: false, value: null, error: null };
136
+ const receiver = (method === 'delete') ? null : args[args.length - 1];
137
+
138
+ flow.$proxy[method].apply({
139
+ resolve: (val) => { control.end = true; control.value = val },
140
+ reject: (err) => { control.end = true; control.error = err },
141
+ open: (...args) => open(args, index, flow),
142
+ data: receiver, index: index, flow: flow,
143
+ }, args);
144
+
145
+ return control
146
+ }
147
+ }
148
+
149
+ const proxy = new Proxy({}, {
150
+ get(target, key, receiver) {
151
+ if (typeof key === 'symbol') return Reflect.get(target, key);
152
+
153
+ const flow = DB.flows.get(index);
154
+
155
+ // Flow logic
156
+ if (flow?.$call && flow?.$call?.[key]) {
157
+ const fun = flow.$call[key];
158
+ if (typeof fun === 'function') {
159
+ return (...args) => fun.apply({
160
+ data: receiver, index: index, flow: flow,
161
+ open: (...args) => open(args, index, flow),
162
+ DB: DB
163
+ }, args);
164
+ }
165
+ } else if (DB.shared[key]) {
166
+ const fun = DB.shared[key];
167
+ if (typeof fun === 'function') {
168
+ return (...args) => fun.apply({
169
+ data: receiver, index: index, flow: flow,
170
+ open: (...args) => open(args, index, flow),
171
+ DB: DB
172
+ }, args);
173
+ }
174
+ }
175
+
176
+ const r = guard('get')(target, key, receiver);
177
+ if (r?.end && r?.error) throw r.error;
178
+ if (r?.end) return r.value;
179
+
180
+ // Index logic
181
+ const rootGet = root.get(key);
182
+
183
+ if (rootGet?.constructor?.name === 'Object' && rootGet.$file) {
184
+ return DB.Proxy(index[key], flow?.[key]);
185
+ } else {
186
+ return rootGet;
187
+ }
188
+ },
189
+ set(target, key, value, receiver) {
190
+ const r = guard('set')(target, key, value, receiver);
191
+ if (r?.end && r?.error) throw r.error;
192
+ if (r?.end) return r.value;
193
+
194
+ root.set(key, value);
195
+ DB.index.save();
196
+ return true;
197
+ },
198
+ deleteProperty(target, key) {
199
+ const r = guard('delete')(target, key);
200
+ if (r?.end && r?.error) throw r.error;
201
+ if (r?.end) return r.value;
202
+
203
+ root.delete(key);
204
+ DB.index.save();
205
+ return true;
206
+ },
207
+ ownKeys(target) {
208
+ return root.keys();
209
+ },
210
+ getOwnPropertyDescriptor(_, key) {
211
+ return {
212
+ enumerable: true,
213
+ configurable: true,
214
+ value: root.get(key)
215
+ };
216
+ }
217
+ })
218
+
219
+ this.proxies.set(index, proxy);
220
+ return proxy;
221
+ }
222
+ }
@@ -0,0 +1,178 @@
1
+ import fs from 'fs';
2
+ import fsp from 'fs/promises';
3
+ import path from 'path';
4
+ import v8 from 'v8';
5
+
6
+ import { Memory } from './Memory.js';
7
+
8
+ export class Disk {
9
+ constructor(options = {}) {
10
+ if (options.constructor.name !== 'Object')
11
+ throw new Error('Invalid options');
12
+
13
+ if (options?.$class?.Memory) {
14
+ const a0 = options.$class.Memory;
15
+ if (a0.constructor.name == 'Array') {
16
+ this.memory = new Memory(...a0);
17
+ } else this.memory = a0
18
+ }
19
+
20
+ if (!options.folder) options.folder = './data';
21
+
22
+ if (!this.memory) this.memory = new Memory(
23
+ options.memory?.limit || 20,
24
+ ['index.bin', 'root.bin']);
25
+
26
+ this.onError = options.onError || ((err) =>
27
+ console.error(`[HyperDB_IO_Error]:`, err));
28
+
29
+ this.Pipe = new Map();
30
+ this.basePath = path.resolve(options.folder);
31
+ this.dataPath = path.join(this.basePath, 'data');
32
+ this.files = ['index.bin', 'root.bin']
33
+
34
+ if (!fs.existsSync(this.basePath)) fs.mkdirSync(this.basePath, { recursive: true });
35
+ if (!fs.existsSync(this.dataPath)) fs.mkdirSync(this.dataPath, { recursive: true });
36
+ }
37
+
38
+ #path(filename) {
39
+ if (!this.files.includes(filename))
40
+ return path.join(this.dataPath, filename);
41
+ return path.join(this.basePath, filename);
42
+ }
43
+
44
+ ////////////////////// Sync Methods
45
+
46
+ readSync(filename) {
47
+ const cached = this.memory.get(filename);
48
+ if (cached) return cached;
49
+
50
+ const filePath = this.#path(filename);
51
+ try {
52
+ const buffer = fs.readFileSync(filePath);
53
+ const data = v8.deserialize(buffer);
54
+ this.memory.set(filename, data);
55
+ return data;
56
+ } catch (e) {
57
+ this.onError(e);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ writeSync(filename, data = {}) {
63
+ const filePath = this.#path(filename);
64
+ try {
65
+ const dir = path.dirname(filePath);
66
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
67
+
68
+ const tempPath = filePath + '.tmp';
69
+ const buffer = v8.serialize(data);
70
+ const fd = fs.openSync(tempPath, 'w');
71
+ fs.writeSync(fd, buffer);
72
+ fs.fsyncSync(fd);
73
+ fs.closeSync(fd);
74
+ fs.renameSync(tempPath, filePath);
75
+ this.memory.set(filename, data);
76
+ return true;
77
+ } catch (e) {
78
+ this.onError(e);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ removeSync(filename) {
84
+ const filePath = this.#path(filename);
85
+ try {
86
+ if (fs.existsSync(filePath)) fs.rmSync(filePath, { recursive: true, force: true });
87
+ this.memory.delete(filename);
88
+ return true;
89
+ } catch (e) {
90
+ this.onError(e);
91
+ return false;
92
+ }
93
+ }
94
+
95
+ existsSync(filename) {
96
+ if (this.memory.has(filename)) return true;
97
+ return fs.existsSync(this.#path(filename));
98
+ }
99
+
100
+ ////////////////////// Async Methods
101
+
102
+ async #pipe(filename, action) {
103
+ const next = (this.Pipe.get(filename) || Promise.resolve())
104
+ .then(() => action().catch((e) => this.onError(e))).finally(() =>
105
+ (this.Pipe.get(filename) === next) ? this.Pipe.delete(filename) : false);
106
+ return this.Pipe.set(filename, next).get(filename);
107
+ }
108
+
109
+ async read(filename) {
110
+ const cached = this.memory.get(filename);
111
+ if (cached) return cached;
112
+
113
+ return this.#pipe(filename, async () => {
114
+ const filePath = this.#path(filename);
115
+ try {
116
+ const buffer = await fsp.readFile(filePath);
117
+ const data = v8.deserialize(buffer);
118
+ this.memory.set(filename, data);
119
+ return data;
120
+ } catch (e) {
121
+ return null;
122
+ }
123
+ });
124
+ }
125
+
126
+ async write(filename, data = {}) {
127
+ this.memory.set(filename, data);
128
+ return this.#pipe(filename, async () => {
129
+ const filePath = this.#path(filename);
130
+ const dir = path.dirname(filePath);
131
+ await fsp.mkdir(dir, { recursive: true });
132
+ const tempPath = filePath + '.tmp';
133
+ const buffer = v8.serialize(data);
134
+ const handle = await fsp.open(tempPath, 'w');
135
+ await handle.write(buffer);
136
+ await handle.sync();
137
+ await handle.close();
138
+ await fsp.rename(tempPath, filePath);
139
+ return true;
140
+ });
141
+ }
142
+
143
+ async remove(filename) {
144
+ this.memory.delete(filename);
145
+ return this.#pipe(filename, async () => {
146
+ const filePath = this.#path(filename);
147
+ await fsp.rm(filePath, { recursive: true, force: true });
148
+ return true;
149
+ });
150
+ }
151
+
152
+ async exists(filename) {
153
+ if (this.memory.has(filename)) return true;
154
+ try {
155
+ await fsp.access(this.#path(filename));
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ async flush() {
163
+ await Promise.all(this.Pipe.values());
164
+ return true;
165
+ }
166
+
167
+ async prune() {
168
+ return this.#pipe('__maint__', async () => {
169
+ const scan = async (d) => {
170
+ const items = await fsp.readdir(d, { withFileTypes: true });
171
+ for (const i of items) if (i.isDirectory()) await scan(path.join(d, i.name));
172
+ if (d !== this.dataPath && d !== this.basePath && !(await fsp.readdir(d)).length)
173
+ await fsp.rmdir(d).catch(() => null);
174
+ };
175
+ if (fs.existsSync(this.dataPath)) await scan(this.dataPath);
176
+ });
177
+ }
178
+ }
@@ -0,0 +1,34 @@
1
+ export class Flow {
2
+ constructor() {
3
+ this.tree = {};
4
+ }
5
+
6
+ forge(...args) {
7
+ const val = args.pop();
8
+ return args.reduceRight((acc, key) => ({ [key]: acc }), val);
9
+ }
10
+
11
+ get(...args) {
12
+ return args.reduce((acc, k) => acc?.[k], this.tree) ?? false;
13
+ }
14
+
15
+ set(...args) {
16
+ const val = args.pop();
17
+ const target = args.reduce((acc, k) => acc[k] ??= {}, this.tree);
18
+ const merge = (t, s) => Object.keys(s).forEach(k =>
19
+ (s[k] instanceof Object && k in t) ? merge(t[k], s[k]) : t[k] = s[k]);
20
+ merge(target, val);
21
+ }
22
+
23
+ delete(...args) {
24
+ const del = (obj, [k, ...rest]) => {
25
+ if (!obj?.[k]) return false;
26
+ const success = rest.length ? del(obj[k], rest)
27
+ : (delete obj[k].$proxy, delete obj[k].$call, true);
28
+ if (success && !Object.keys(obj[k]).length)
29
+ delete obj[k];
30
+ return success;
31
+ };
32
+ return del(this.tree, args);
33
+ }
34
+ }
@@ -0,0 +1,78 @@
1
+ import v8 from 'v8';
2
+
3
+ export class Memory {
4
+ constructor(limitMB = 20, pinnedKeys = []) {
5
+ this.limit = limitMB * 1024 * 1024;
6
+ this.pinnedKeys = new Set(pinnedKeys);
7
+ this.pinned = new Map();
8
+ this.cache = new Map();
9
+ this.currentSize = 0;
10
+ }
11
+
12
+ #size(data) {
13
+ try {
14
+ return v8.serialize(data).length;
15
+ } catch (e) {
16
+ return 0;
17
+ }
18
+ }
19
+
20
+ set(key, data) {
21
+ if (this.pinnedKeys.has(key)) {
22
+ this.pinned.set(key, data);
23
+ return true;
24
+ }
25
+
26
+ if (this.cache.has(key)) this.delete(key);
27
+
28
+ const dataSize = this.#size(data);
29
+ if (dataSize > this.limit) {
30
+ console.warn(`[HyperDB:Memory] Object '${key}' exceeds cache limit (${(this.limit / 1024 / 1024).toFixed(2)} MB)`);
31
+ return false;
32
+ }
33
+
34
+ while (this.currentSize + dataSize > this.limit && this.cache.size > 0) {
35
+ const oldestKey = this.cache.keys().next().value;
36
+ this.delete(oldestKey);
37
+ }
38
+
39
+ this.cache.set(key, { data, size: dataSize });
40
+ this.currentSize += dataSize;
41
+ return true;
42
+ }
43
+
44
+ get(key) {
45
+ if (this.pinned.has(key)) return this.pinned.get(key);
46
+
47
+ const item = this.cache.get(key);
48
+ if (!item) return null;
49
+
50
+ // Refresh LRU
51
+ const data = item.data;
52
+ this.cache.delete(key);
53
+ this.cache.set(key, item);
54
+ return data;
55
+ }
56
+
57
+ delete(key) {
58
+ if (this.pinned.has(key)) return this.pinned.delete(key);
59
+
60
+ const item = this.cache.get(key);
61
+ if (!item) return false
62
+ this.currentSize -= item.size;
63
+ this.cache.delete(key);
64
+ return true;
65
+ }
66
+
67
+ stats() {
68
+ return {
69
+ used: (this.currentSize / 1024 / 1024).toFixed(2) + " MB",
70
+ limit: (this.limit / 1024 / 1024).toFixed(2) + " MB",
71
+ items: this.cache.size
72
+ };
73
+ }
74
+
75
+ has(key) {
76
+ return this.pinned.has(key) || this.cache.has(key);
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ import crypto from 'crypto';
2
+
3
+ export const genId = (depth) => {
4
+ const id = crypto.randomBytes(4).toString('hex').toUpperCase();
5
+ if (!depth || depth <= 0) return `${id}.bin`
6
+ const folder = id.substring(0, depth);
7
+ return `${folder}/${id}.bin`;
8
+ };
9
+
10
+ function isShardable(val) {
11
+ if (!val || typeof val !== 'object') return false;
12
+ const proto = Object.getPrototypeOf(val);
13
+ return proto === Object.prototype || proto === null;
14
+ }
15
+
16
+ export class Shard {
17
+ constructor(disk, depth) {
18
+ this.disk = disk;
19
+ this.depth = depth;
20
+ }
21
+
22
+ purge(index) {
23
+ if (index.$file) this.disk.remove(index.$file);
24
+ for (const key in index) {
25
+ if (key === '$file') continue;
26
+ if (index[key] && typeof index[key] === 'object')
27
+ this.purge(index[key]);
28
+ }
29
+ }
30
+
31
+ forge(index, value, file) {
32
+ try {
33
+ const Id = file || genId(this.depth);
34
+ value = structuredClone(value);
35
+ index.$file = Id;
36
+
37
+ for (const key in value) {
38
+ if (!isShardable(value[key])) continue;
39
+
40
+ index[key] = {};
41
+ value[key] = this.forge(index[key], value[key]);
42
+ }
43
+
44
+ this.disk.write(Id, value);
45
+ return Id;
46
+ } catch (e) {
47
+ this.disk.onError(e.message);
48
+ return null
49
+ }
50
+ }
51
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@syllkom/hyper-db",
3
+ "version": "1.0.1",
4
+ "description": "High-performance sharded binary database for Node.js. Zero-dependency, atomic I/O, and transparent Proxy state management.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "index.js",
9
+ "library/**/*"
10
+ ],
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Syllkom/HyperDB.git"
17
+ },
18
+ "keywords": [
19
+ "hyper-db",
20
+ "database",
21
+ "sharding",
22
+ "nosql",
23
+ "binary-storage",
24
+ "v8-serialization",
25
+ "high-performance",
26
+ "proxy",
27
+ "atomic-write",
28
+ "embedded",
29
+ "zero-dependency"
30
+ ],
31
+ "author": "Syllkom",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/Syllkom/HyperDB/issues"
35
+ },
36
+ "homepage": "https://github.com/Syllkom/HyperDB#readme",
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ }
40
+ }