@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 +528 -0
- package/index.js +17 -0
- package/library/Cluster.js +161 -0
- package/library/DB.js +222 -0
- package/library/Disk.js +178 -0
- package/library/Flow.js +34 -0
- package/library/Memory.js +78 -0
- package/library/Shard.js +51 -0
- package/package.json +40 -0
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
|
+
}
|
package/library/Disk.js
ADDED
|
@@ -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
|
+
}
|
package/library/Flow.js
ADDED
|
@@ -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
|
+
}
|
package/library/Shard.js
ADDED
|
@@ -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
|
+
}
|