lba 2.1.0 → 3.0.0
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 +67 -55
- package/package.json +12 -9
- package/src/index.js +257 -0
- package/src/query-engine.js +43 -0
- package/src/types.ts +26 -0
- package/src/utils.js +38 -0
- package/index.d.ts +0 -44
- package/index.js +0 -561
package/README.md
CHANGED
|
@@ -1,77 +1,89 @@
|
|
|
1
|
-
# LBA (
|
|
1
|
+
# 🚀 LBA (Lightweight Binary Archive)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
It supports sharding, compression (zlib), and atomic updates.
|
|
3
|
+
LBA is an ultra-lightweight, high-performance, file-based key-value store for Node.js. It bridges the gap between the blazing speed of **Redis** and the querying flexibility of **MongoDB**, optimized specifically for modern multi-core environments.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/lba)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
npm install lba
|
|
10
|
-
```
|
|
8
|
+
## ✨ Key Features
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
- **⚡ Hybrid Architecture**: Combines simple Key-Value storage with powerful MongoDB-style NoSQL queries (`$gt`, `$in`, `$exists`, etc.).
|
|
11
|
+
- **🧩 Smart Sharding**: Automatically partitions data into multiple shards to eliminate I/O bottlenecks and improve concurrency.
|
|
12
|
+
- **⚙️ Auto-Vacuuming**: Background maintenance that automatically defragments storage files after deletions or updates.
|
|
13
|
+
- **🚀 Multi-Core Optimized**: Automatically detects your CPU core count to scale worker thread pools for maximum throughput.
|
|
14
|
+
- **📦 Built-in Compression**: Transparent `zlib` compression to save disk space without sacrificing usability.
|
|
15
|
+
- **🛡️ Atomic Integrity**: Uses CRC32 checksums and atomic write mechanisms to ensure data remains uncorrupted.
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
## 📦 Installation
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add lba
|
|
21
|
+
# or
|
|
22
|
+
npm install lba
|
|
23
|
+
```
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
## 🚀 Quick Start
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
**Basic Usage (Redis Style)**
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
```js
|
|
30
|
+
const { LBA } = require("lba");
|
|
31
|
+
|
|
32
|
+
// Initialize with auto-worker scaling
|
|
33
|
+
const db = new LBA("./storage", {
|
|
34
|
+
workerCount: "auto", // Automatically scales to your CPU (e.g., 15 workers for 20 cores)
|
|
35
|
+
shardCount: 32,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
// Set data
|
|
40
|
+
await db.set("user:123", {
|
|
41
|
+
name: "Gemini",
|
|
42
|
+
level: 99,
|
|
43
|
+
tags: ["ai", "developer"],
|
|
44
|
+
});
|
|
23
45
|
|
|
24
|
-
|
|
46
|
+
// Get data
|
|
47
|
+
const user = await db.get("user:123");
|
|
48
|
+
console.log(user);
|
|
49
|
+
}
|
|
50
|
+
main();
|
|
51
|
+
```
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
**NoSQL Querying (MongoDB Style)**
|
|
27
54
|
|
|
28
55
|
```js
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
// Search with operators ($gte, $in, $eq, etc.)
|
|
57
|
+
const proUsers = await db.find({
|
|
58
|
+
level: { $gte: 50 },
|
|
59
|
+
tags: { $in: ["ai"] },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Bulk updates based on criteria
|
|
63
|
+
await db.updateMany({ level: { $lt: 10 } }, { status: "newbie" });
|
|
64
|
+
```
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
await db.update("user:1001", (data) => {
|
|
41
|
-
data.age += 1;
|
|
42
|
-
return data;
|
|
43
|
-
});
|
|
66
|
+
## ⚙️ Configuration Options
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
| ---------------- | ------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
70
|
+
| shardCount | number | 32 | Number of shards to partition the data. |
|
|
71
|
+
| workerCount | number | 'auto','auto',Number of worker threads for parallel processing. |
|
|
72
|
+
| autoVacuum | boolean | true | Enables background storage optimization. |
|
|
73
|
+
| vacuumThreshold | number | 500 | Number of writes/deletes before triggering a vacuum. |
|
|
74
|
+
| syncOnWrite | boolean | true | Forces physical disk sync on every write (Safety vs Speed). |
|
|
75
|
+
| compressionLevel | number | 6 | zlib compression level (0-9). |
|
|
53
76
|
|
|
54
|
-
##
|
|
77
|
+
## 📊 Performance Benchmark
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
Tested on a **20-Core / 15-Worker** environment:
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
// Extremely fast: up to 1.7M items/sec processing
|
|
60
|
-
for await (const [key, value] of db.entries({ batchSize: 50 })) {
|
|
61
|
-
console.log(key, value);
|
|
62
|
-
}
|
|
63
|
-
```
|
|
81
|
+
**Read Latency:** ~0.002ms (via Indexing & LRU Caching)
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
**Write Throughput:** ~330+ ops/s (Sync Mode)
|
|
66
84
|
|
|
67
|
-
|
|
85
|
+
> Tip: Set syncOnWrite: false to achieve significantly higher write speeds using OS-level buffering.
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
await db.vacuum(); // Compacts files and removes deleted entries
|
|
71
|
-
```
|
|
87
|
+
## 📄 License
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
| :-------------- | :------------------: |
|
|
75
|
-
| **Batch Write** | 10,000+ ops/sec |
|
|
76
|
-
| **Batch Read** | 28,000+ ops/sec |
|
|
77
|
-
| **Streaming** | 1,700,000+ items/sec |
|
|
89
|
+
MIT License.
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lba",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"types": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Lightweight, high-performance, file-based key-value store with NoSQL query support.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/types",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "node test/test.js",
|
|
9
|
+
"bench": "node bench/bench.js"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
|
-
"database",
|
|
12
12
|
"key-value",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"nosql",
|
|
14
|
+
"database",
|
|
15
|
+
"sharding",
|
|
16
|
+
"nodejs",
|
|
17
|
+
"file-based"
|
|
15
18
|
],
|
|
16
|
-
"author": "
|
|
19
|
+
"author": "yeyok",
|
|
17
20
|
"license": "MIT"
|
|
18
21
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const {
|
|
5
|
+
deflateRawAsync,
|
|
6
|
+
calculateCRC32,
|
|
7
|
+
getShard,
|
|
8
|
+
safeInflate,
|
|
9
|
+
} = require("./utils");
|
|
10
|
+
const { matches } = require("./query-engine");
|
|
11
|
+
|
|
12
|
+
class LBA {
|
|
13
|
+
constructor(dbDir = "lba_storage", options = {}) {
|
|
14
|
+
this.dbDir = path.resolve(dbDir);
|
|
15
|
+
this.shardCount = options.shardCount || 32;
|
|
16
|
+
this.cacheLimit = options.cacheLimit || 10000;
|
|
17
|
+
this.syncOnWrite = options.syncOnWrite !== false;
|
|
18
|
+
this.compressionLevel = options.compressionLevel || 6;
|
|
19
|
+
this.maxDecompressedSize = options.maxDecompressedSize || 100 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
this.autoVacuum = options.autoVacuum !== false;
|
|
22
|
+
this.vacuumThreshold = options.vacuumThreshold || 500;
|
|
23
|
+
|
|
24
|
+
const cpuCores = os.cpus().length;
|
|
25
|
+
if (options.workerCount === "auto" || !options.workerCount) {
|
|
26
|
+
this.workerLimit = Math.max(1, Math.floor(cpuCores * 0.75));
|
|
27
|
+
} else {
|
|
28
|
+
this.workerLimit = options.workerCount;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.dirtyCounts = new Array(this.shardCount).fill(0);
|
|
32
|
+
this.isVacuuming = new Array(this.shardCount).fill(false);
|
|
33
|
+
this.indices = Array.from({ length: this.shardCount }, () => new Map());
|
|
34
|
+
this.cache = new Map();
|
|
35
|
+
this.queues = Array.from({ length: this.shardCount }, () =>
|
|
36
|
+
Promise.resolve(),
|
|
37
|
+
);
|
|
38
|
+
this.fileHandles = new Array(this.shardCount).fill(null);
|
|
39
|
+
this.isLoaded = new Array(this.shardCount).fill(false);
|
|
40
|
+
|
|
41
|
+
console.log(
|
|
42
|
+
`[LBA] 가동 시작 (CPU 코어: ${cpuCores}, 사용 워커: ${this.workerLimit})`,
|
|
43
|
+
);
|
|
44
|
+
this._ensureDbDir();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_ensureDbDir() {
|
|
48
|
+
if (!fs.existsSync(this.dbDir))
|
|
49
|
+
fs.mkdirSync(this.dbDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async _ensureShardLoaded(sIdx) {
|
|
53
|
+
if (this.isLoaded[sIdx]) return;
|
|
54
|
+
const fPath = path.join(this.dbDir, `shard_${sIdx}.lba`);
|
|
55
|
+
const handle = await fs.promises.open(fPath, "a+");
|
|
56
|
+
this.fileHandles[sIdx] = handle;
|
|
57
|
+
|
|
58
|
+
const { size } = await handle.stat();
|
|
59
|
+
let offset = 0;
|
|
60
|
+
const head = Buffer.allocUnsafe(11);
|
|
61
|
+
|
|
62
|
+
while (offset + 11 <= size) {
|
|
63
|
+
await handle.read(head, 0, 11, offset);
|
|
64
|
+
if (head[0] !== 0x4c || head[1] !== 0x42) {
|
|
65
|
+
offset++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const vLen = head.readUInt32BE(6);
|
|
69
|
+
const kLen = head[10];
|
|
70
|
+
const recordSize = 11 + kLen + vLen;
|
|
71
|
+
if (offset + recordSize > size) break;
|
|
72
|
+
|
|
73
|
+
const kBuf = Buffer.allocUnsafe(kLen);
|
|
74
|
+
await handle.read(kBuf, 0, kLen, offset + 11);
|
|
75
|
+
const key = kBuf.toString();
|
|
76
|
+
|
|
77
|
+
if (vLen > 0) {
|
|
78
|
+
this.indices[sIdx].set(key, {
|
|
79
|
+
offset: offset + 11 + kLen,
|
|
80
|
+
length: vLen,
|
|
81
|
+
crc: head.readUInt32BE(2),
|
|
82
|
+
kLen,
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
this.indices[sIdx].delete(key);
|
|
86
|
+
}
|
|
87
|
+
offset += recordSize;
|
|
88
|
+
}
|
|
89
|
+
this.isLoaded[sIdx] = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async get(key) {
|
|
93
|
+
const sIdx = getShard(key, this.shardCount);
|
|
94
|
+
return this._enqueue(sIdx, async () => {
|
|
95
|
+
const kStr = String(key);
|
|
96
|
+
if (this.cache.has(kStr)) return structuredClone(this.cache.get(kStr));
|
|
97
|
+
const meta = this.indices[sIdx].get(kStr);
|
|
98
|
+
if (!meta) return null;
|
|
99
|
+
|
|
100
|
+
const vBuf = Buffer.allocUnsafe(meta.length);
|
|
101
|
+
await this.fileHandles[sIdx].read(vBuf, 0, meta.length, meta.offset);
|
|
102
|
+
|
|
103
|
+
const decompressed = await safeInflate(vBuf);
|
|
104
|
+
const data = JSON.parse(decompressed.toString());
|
|
105
|
+
this._addToCache(kStr, data);
|
|
106
|
+
return data;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async set(key, value) {
|
|
111
|
+
const sIdx = getShard(key, this.shardCount);
|
|
112
|
+
return this._enqueue(sIdx, async () => {
|
|
113
|
+
const kStr = String(key);
|
|
114
|
+
const kBuf = Buffer.from(kStr);
|
|
115
|
+
let vBuf = null,
|
|
116
|
+
vLen = 0;
|
|
117
|
+
|
|
118
|
+
if (value !== null && value !== undefined) {
|
|
119
|
+
vBuf = await deflateRawAsync(JSON.stringify(value), {
|
|
120
|
+
level: this.compressionLevel,
|
|
121
|
+
});
|
|
122
|
+
vLen = vBuf.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const metaBuf = Buffer.allocUnsafe(5);
|
|
126
|
+
metaBuf.writeUInt32BE(vLen, 0);
|
|
127
|
+
metaBuf[4] = kBuf.length;
|
|
128
|
+
const checksum = calculateCRC32([metaBuf, kBuf, vBuf]);
|
|
129
|
+
|
|
130
|
+
const head = Buffer.allocUnsafe(11);
|
|
131
|
+
head[0] = 0x4c;
|
|
132
|
+
head[1] = 0x42;
|
|
133
|
+
head.writeUInt32BE(checksum, 2);
|
|
134
|
+
head.writeUInt32BE(vLen, 6);
|
|
135
|
+
head[10] = kBuf.length;
|
|
136
|
+
|
|
137
|
+
const { size: pos } = await this.fileHandles[sIdx].stat();
|
|
138
|
+
await this.fileHandles[sIdx].write(
|
|
139
|
+
vBuf ? Buffer.concat([head, kBuf, vBuf]) : Buffer.concat([head, kBuf]),
|
|
140
|
+
0,
|
|
141
|
+
11 + kBuf.length + vLen,
|
|
142
|
+
null,
|
|
143
|
+
);
|
|
144
|
+
if (this.syncOnWrite) await this.fileHandles[sIdx].datasync();
|
|
145
|
+
|
|
146
|
+
if (vLen > 0) {
|
|
147
|
+
this.indices[sIdx].set(kStr, {
|
|
148
|
+
offset: pos + 11 + kBuf.length,
|
|
149
|
+
length: vLen,
|
|
150
|
+
crc: checksum,
|
|
151
|
+
kLen: kBuf.length,
|
|
152
|
+
});
|
|
153
|
+
this._addToCache(kStr, value);
|
|
154
|
+
} else {
|
|
155
|
+
this.indices[sIdx].delete(kStr);
|
|
156
|
+
this.cache.delete(kStr);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.dirtyCounts[sIdx]++;
|
|
160
|
+
if (this.autoVacuum && this.dirtyCounts[sIdx] >= this.vacuumThreshold) {
|
|
161
|
+
this.vacuum(sIdx).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async delete(key) {
|
|
167
|
+
return this.set(key, null);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async vacuum(sIdx) {
|
|
171
|
+
if (this.isVacuuming[sIdx]) return;
|
|
172
|
+
this.isVacuuming[sIdx] = true;
|
|
173
|
+
try {
|
|
174
|
+
const fPath = path.join(this.dbDir, `shard_${sIdx}.lba`);
|
|
175
|
+
const tempPath = fPath + ".tmp";
|
|
176
|
+
const tempHandle = await fs.promises.open(tempPath, "w");
|
|
177
|
+
const newIndices = new Map();
|
|
178
|
+
let currentPos = 0;
|
|
179
|
+
|
|
180
|
+
for (const [key, meta] of this.indices[sIdx].entries()) {
|
|
181
|
+
const vBuf = Buffer.allocUnsafe(meta.length);
|
|
182
|
+
await this.fileHandles[sIdx].read(vBuf, 0, meta.length, meta.offset);
|
|
183
|
+
const kBuf = Buffer.from(key);
|
|
184
|
+
const metaBuf = Buffer.allocUnsafe(5);
|
|
185
|
+
metaBuf.writeUInt32BE(meta.length, 0);
|
|
186
|
+
metaBuf[4] = kBuf.length;
|
|
187
|
+
const checksum = calculateCRC32([metaBuf, kBuf, vBuf]);
|
|
188
|
+
const head = Buffer.allocUnsafe(11);
|
|
189
|
+
head[0] = 0x4c;
|
|
190
|
+
head[1] = 0x42;
|
|
191
|
+
head.writeUInt32BE(checksum, 2);
|
|
192
|
+
head.writeUInt32BE(meta.length, 6);
|
|
193
|
+
head[10] = kBuf.length;
|
|
194
|
+
|
|
195
|
+
const block = Buffer.concat([head, kBuf, vBuf]);
|
|
196
|
+
await tempHandle.write(block, 0, block.length, null);
|
|
197
|
+
newIndices.set(key, {
|
|
198
|
+
offset: currentPos + 11 + kBuf.length,
|
|
199
|
+
length: meta.length,
|
|
200
|
+
crc: checksum,
|
|
201
|
+
kLen: kBuf.length,
|
|
202
|
+
});
|
|
203
|
+
currentPos += block.length;
|
|
204
|
+
}
|
|
205
|
+
await tempHandle.close();
|
|
206
|
+
await this.fileHandles[sIdx].close();
|
|
207
|
+
await fs.promises.rename(tempPath, fPath);
|
|
208
|
+
this.fileHandles[sIdx] = await fs.promises.open(fPath, "a+");
|
|
209
|
+
this.indices[sIdx] = newIndices;
|
|
210
|
+
this.dirtyCounts[sIdx] = 0;
|
|
211
|
+
} finally {
|
|
212
|
+
this.isVacuuming[sIdx] = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async find(query = {}) {
|
|
217
|
+
const res = [];
|
|
218
|
+
for (let i = 0; i < this.shardCount; i++) {
|
|
219
|
+
await this._enqueue(i, async () => {
|
|
220
|
+
for (const key of this.indices[i].keys()) {
|
|
221
|
+
const val = await this.get(key);
|
|
222
|
+
if (matches(val, query)) res.push({ _key: key, ...val });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return res;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async updateMany(query, updateData) {
|
|
230
|
+
const targets = await this.find(query);
|
|
231
|
+
for (const item of targets) {
|
|
232
|
+
const { _key, ...oldVal } = item;
|
|
233
|
+
await this.set(_key, { ...oldVal, ...updateData });
|
|
234
|
+
}
|
|
235
|
+
return targets.length;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_addToCache(k, v) {
|
|
239
|
+
if (this.cache.has(k)) this.cache.delete(k);
|
|
240
|
+
else if (this.cache.size >= this.cacheLimit)
|
|
241
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
242
|
+
this.cache.set(k, v);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_enqueue(sIdx, task) {
|
|
246
|
+
return (this.queues[sIdx] = this.queues[sIdx]
|
|
247
|
+
.then(() => this._ensureShardLoaded(sIdx))
|
|
248
|
+
.then(task));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async close() {
|
|
252
|
+
await Promise.all(this.queues);
|
|
253
|
+
for (const h of this.fileHandles) if (h) await h.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = LBA;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function matches(data, query) {
|
|
2
|
+
if (!query || typeof query !== "object" || Object.keys(query).length === 0)
|
|
3
|
+
return true;
|
|
4
|
+
if (!data || typeof data !== "object") return false;
|
|
5
|
+
|
|
6
|
+
return Object.entries(query).every(([field, condition]) => {
|
|
7
|
+
const val = data[field];
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
condition !== null &&
|
|
11
|
+
typeof condition === "object" &&
|
|
12
|
+
!Array.isArray(condition)
|
|
13
|
+
) {
|
|
14
|
+
return Object.entries(condition).every(([op, target]) => {
|
|
15
|
+
switch (op) {
|
|
16
|
+
case "$eq":
|
|
17
|
+
return val === target;
|
|
18
|
+
case "$ne":
|
|
19
|
+
return val !== target;
|
|
20
|
+
case "$gt":
|
|
21
|
+
return val > target;
|
|
22
|
+
case "$gte":
|
|
23
|
+
return val >= target;
|
|
24
|
+
case "$lt":
|
|
25
|
+
return val < target;
|
|
26
|
+
case "$lte":
|
|
27
|
+
return val <= target;
|
|
28
|
+
case "$in":
|
|
29
|
+
return Array.isArray(target) && target.includes(val);
|
|
30
|
+
case "$nin":
|
|
31
|
+
return Array.isArray(target) && !target.includes(val);
|
|
32
|
+
case "$exists":
|
|
33
|
+
return (val !== undefined) === target;
|
|
34
|
+
default:
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return val === condition;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { matches };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QueryOperator<T = any> = {
|
|
2
|
+
$eq?: T; $ne?: T; $gt?: T; $gte?: T; $lt?: T; $lte?: T; $in?: T[]; $nin?: T[]; $exists?: boolean;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type Query<T = any> = { [K in keyof T]?: T[K] | QueryOperator<T[K]>; } & { [key: string]: any };
|
|
6
|
+
|
|
7
|
+
export interface LBAOptions {
|
|
8
|
+
shardCount?: number;
|
|
9
|
+
cacheLimit?: number;
|
|
10
|
+
syncOnWrite?: boolean;
|
|
11
|
+
compressionLevel?: number;
|
|
12
|
+
autoVacuum?: boolean;
|
|
13
|
+
vacuumThreshold?: number;
|
|
14
|
+
// 워커 스레드 설정: 'auto'면 CPU 코어의 75% 사용
|
|
15
|
+
workerCount?: number | 'auto';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare class LBA<T = any> {
|
|
19
|
+
constructor(dbDir?: string, options?: LBAOptions);
|
|
20
|
+
get(key: string | number): Promise<T | null>;
|
|
21
|
+
set(key: string | number, value: T | null): Promise<void>;
|
|
22
|
+
delete(key: string | number): Promise<void>;
|
|
23
|
+
find(query?: Query<T>): Promise<(T & { _key: string })[]>;
|
|
24
|
+
updateMany(query: Query<T>, updateData: Partial<T>): Promise<number>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const zlib = require("zlib");
|
|
2
|
+
const util = require("util");
|
|
3
|
+
const deflateRawAsync = util.promisify(zlib.deflateRaw);
|
|
4
|
+
|
|
5
|
+
const CRC32_TABLE = new Int32Array(256);
|
|
6
|
+
for (let i = 0; i < 256; i++) {
|
|
7
|
+
let c = i;
|
|
8
|
+
for (let j = 0; j < 8; j++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
9
|
+
CRC32_TABLE[i] = c;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function calculateCRC32(buffers) {
|
|
13
|
+
let crc = -1;
|
|
14
|
+
for (const buf of buffers) {
|
|
15
|
+
if (!buf) continue;
|
|
16
|
+
for (let i = 0; i < buf.length; i++) {
|
|
17
|
+
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ buf[i]) & 0xff];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return (crc ^ -1) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getShard(key, shardCount) {
|
|
24
|
+
let hash = 2166136261;
|
|
25
|
+
const str = String(key);
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
hash ^= str.charCodeAt(i);
|
|
28
|
+
hash = Math.imul(hash, 16777619);
|
|
29
|
+
}
|
|
30
|
+
return (hash >>> 0) % shardCount;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
deflateRawAsync,
|
|
35
|
+
calculateCRC32,
|
|
36
|
+
getShard,
|
|
37
|
+
safeInflate: require("util").promisify(zlib.inflateRaw),
|
|
38
|
+
};
|
package/index.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export interface LBAOptions {
|
|
2
|
-
shardCount?: number;
|
|
3
|
-
cacheLimit?: number;
|
|
4
|
-
fastMode?: boolean;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface LBAStats {
|
|
8
|
-
cacheHits: number;
|
|
9
|
-
cacheMisses: number;
|
|
10
|
-
reads: number;
|
|
11
|
-
writes: number;
|
|
12
|
-
cacheSize: number;
|
|
13
|
-
cacheLimit: number;
|
|
14
|
-
cacheHitRate: string;
|
|
15
|
-
shardCount: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default class LBA {
|
|
19
|
-
constructor(dbDir?: string, options?: LBAOptions);
|
|
20
|
-
|
|
21
|
-
get<T = any>(key: string): Promise<T | null>;
|
|
22
|
-
set(key: string, value: any): Promise<void>;
|
|
23
|
-
delete(key: string): Promise<void>;
|
|
24
|
-
update<T = any>(key: string, fn: (current: T | null) => T | Promise<T>): Promise<T>;
|
|
25
|
-
|
|
26
|
-
batchGet<T = any>(keys: string[]): Promise<Record<string, T>>;
|
|
27
|
-
batchSet(entries: Record<string, any>): Promise<void>;
|
|
28
|
-
|
|
29
|
-
forEach<T = any>(
|
|
30
|
-
callback: (key: string, value: T) => void | Promise<void>,
|
|
31
|
-
options?: { batchSize?: number }
|
|
32
|
-
): Promise<number>;
|
|
33
|
-
|
|
34
|
-
entries<T = any>(options?: { batchSize?: number }): AsyncGenerator<[string, T]>;
|
|
35
|
-
|
|
36
|
-
getAll<T = any>(options?: { maxSize?: number, batchSize?: number }): Promise<Record<string, T>>;
|
|
37
|
-
keys(): Promise<string[]>;
|
|
38
|
-
count(): Promise<number>;
|
|
39
|
-
|
|
40
|
-
getStats(): LBAStats;
|
|
41
|
-
resetStats(): void;
|
|
42
|
-
vacuum(): Promise<void>;
|
|
43
|
-
close(): Promise<void>;
|
|
44
|
-
}
|
package/index.js
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const zlib = require("zlib");
|
|
4
|
-
const util = require("util");
|
|
5
|
-
|
|
6
|
-
const inflateAsync = util.promisify(zlib.inflateRaw);
|
|
7
|
-
const deflateAsync = util.promisify(zlib.deflateRaw);
|
|
8
|
-
|
|
9
|
-
const MAGIC_BYTES = [0x4c, 0x42];
|
|
10
|
-
const HEADER_SIZE = 11;
|
|
11
|
-
const DEFAULT_SHARD_COUNT = 32;
|
|
12
|
-
const DEFAULT_CACHE_LIMIT = 10000;
|
|
13
|
-
const DEFAULT_COMPRESSION_LEVEL = 6;
|
|
14
|
-
const FAST_COMPRESSION_LEVEL = 1;
|
|
15
|
-
const FNV_OFFSET = 2166136261;
|
|
16
|
-
const FNV_PRIME = 16777619;
|
|
17
|
-
|
|
18
|
-
class LBA {
|
|
19
|
-
constructor(dbDir = "lba_storage", options = {}) {
|
|
20
|
-
this.dbDir = path.resolve(dbDir);
|
|
21
|
-
this.shardCount = options.shardCount || DEFAULT_SHARD_COUNT;
|
|
22
|
-
this.cacheLimit = options.cacheLimit || DEFAULT_CACHE_LIMIT;
|
|
23
|
-
this.compressionLevel = options.fastMode
|
|
24
|
-
? FAST_COMPRESSION_LEVEL
|
|
25
|
-
: DEFAULT_COMPRESSION_LEVEL;
|
|
26
|
-
|
|
27
|
-
this.indices = Array.from({ length: this.shardCount }, () => new Map());
|
|
28
|
-
this.cache = new Map();
|
|
29
|
-
|
|
30
|
-
this.queues = Array.from({ length: this.shardCount }, () =>
|
|
31
|
-
Promise.resolve(),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
this.fileHandles = new Array(this.shardCount).fill(null);
|
|
35
|
-
this.isLoaded = new Array(this.shardCount).fill(false);
|
|
36
|
-
|
|
37
|
-
this.stats = {
|
|
38
|
-
cacheHits: 0,
|
|
39
|
-
cacheMisses: 0,
|
|
40
|
-
reads: 0,
|
|
41
|
-
writes: 0,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
this.headerBuffer = Buffer.allocUnsafe(HEADER_SIZE);
|
|
45
|
-
|
|
46
|
-
this._ensureDbDir();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
_ensureDbDir() {
|
|
50
|
-
if (!fs.existsSync(this.dbDir)) {
|
|
51
|
-
fs.mkdirSync(this.dbDir, { recursive: true });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
_getShard(key) {
|
|
56
|
-
let hash = FNV_OFFSET;
|
|
57
|
-
const sKey = String(key);
|
|
58
|
-
const len = sKey.length;
|
|
59
|
-
|
|
60
|
-
for (let i = 0; i < len; i++) {
|
|
61
|
-
hash ^= sKey.charCodeAt(i);
|
|
62
|
-
hash = Math.imul(hash, FNV_PRIME);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return (hash >>> 0) % this.shardCount;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async _ensureShardLoaded(sIdx) {
|
|
69
|
-
if (this.isLoaded[sIdx]) return;
|
|
70
|
-
|
|
71
|
-
const fPath = path.join(this.dbDir, `shard_${sIdx}.lba`);
|
|
72
|
-
this.fileHandles[sIdx] = await fs.promises.open(fPath, "a+");
|
|
73
|
-
|
|
74
|
-
const stat = await this.fileHandles[sIdx].stat();
|
|
75
|
-
const size = stat.size;
|
|
76
|
-
|
|
77
|
-
if (size === 0) {
|
|
78
|
-
this.isLoaded[sIdx] = true;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let offset = 0;
|
|
83
|
-
|
|
84
|
-
while (offset + HEADER_SIZE <= size) {
|
|
85
|
-
const { bytesRead } = await this.fileHandles[sIdx].read(
|
|
86
|
-
this.headerBuffer,
|
|
87
|
-
0,
|
|
88
|
-
HEADER_SIZE,
|
|
89
|
-
offset,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (bytesRead < HEADER_SIZE) break;
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
this.headerBuffer[0] !== MAGIC_BYTES[0] ||
|
|
96
|
-
this.headerBuffer[1] !== MAGIC_BYTES[1]
|
|
97
|
-
) {
|
|
98
|
-
console.warn(
|
|
99
|
-
`Shard ${sIdx}: Data corruption detected (Offset: ${offset}). Subsequent data will be ignored for recovery.`,
|
|
100
|
-
);
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const vLen = this.headerBuffer.readUInt32BE(6);
|
|
105
|
-
const kLen = this.headerBuffer[10];
|
|
106
|
-
const recordTotalSize = HEADER_SIZE + kLen + vLen;
|
|
107
|
-
|
|
108
|
-
if (offset + recordTotalSize > size) {
|
|
109
|
-
console.warn(
|
|
110
|
-
`Shard ${sIdx}: Incomplete record detected (Offset: ${offset}). Discarding last data.`,
|
|
111
|
-
);
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const kBuf = Buffer.allocUnsafe(kLen);
|
|
116
|
-
await this.fileHandles[sIdx].read(kBuf, 0, kLen, offset + HEADER_SIZE);
|
|
117
|
-
const key = kBuf.toString();
|
|
118
|
-
|
|
119
|
-
if (vLen > 0) {
|
|
120
|
-
this.indices[sIdx].set(key, {
|
|
121
|
-
offset: offset + HEADER_SIZE + kLen,
|
|
122
|
-
length: vLen,
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
this.indices[sIdx].delete(key);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
offset += recordTotalSize;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
this.isLoaded[sIdx] = true;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async _readImpl(sIdx, key) {
|
|
135
|
-
const keyStr = String(key);
|
|
136
|
-
|
|
137
|
-
if (this.cache.has(keyStr)) {
|
|
138
|
-
this.stats.cacheHits++;
|
|
139
|
-
const val = this.cache.get(keyStr);
|
|
140
|
-
this.cache.delete(keyStr);
|
|
141
|
-
this.cache.set(keyStr, val);
|
|
142
|
-
return val;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.stats.cacheMisses++;
|
|
146
|
-
this.stats.reads++;
|
|
147
|
-
|
|
148
|
-
const meta = this.indices[sIdx].get(keyStr);
|
|
149
|
-
if (!meta) return null;
|
|
150
|
-
|
|
151
|
-
const buf = Buffer.allocUnsafe(meta.length);
|
|
152
|
-
await this.fileHandles[sIdx].read(buf, 0, meta.length, meta.offset);
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const decompressed = await inflateAsync(buf);
|
|
156
|
-
const data = JSON.parse(decompressed.toString());
|
|
157
|
-
|
|
158
|
-
this._addToCache(keyStr, data);
|
|
159
|
-
|
|
160
|
-
return data;
|
|
161
|
-
} catch (err) {
|
|
162
|
-
console.error(`Read error for key ${key}:`, err);
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_addToCache(key, value) {
|
|
168
|
-
if (this.cache.has(key)) {
|
|
169
|
-
this.cache.delete(key);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
while (this.cache.size >= this.cacheLimit) {
|
|
173
|
-
const firstKey = this.cache.keys().next().value;
|
|
174
|
-
this.cache.delete(firstKey);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this.cache.set(key, value);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async _writeImpl(sIdx, key, value) {
|
|
181
|
-
this.stats.writes++;
|
|
182
|
-
|
|
183
|
-
const kStr = String(key);
|
|
184
|
-
const kBuf = Buffer.from(kStr);
|
|
185
|
-
|
|
186
|
-
let vBuf;
|
|
187
|
-
let vLen = 0;
|
|
188
|
-
|
|
189
|
-
if (value !== null && value !== undefined) {
|
|
190
|
-
const jsonStr = JSON.stringify(value);
|
|
191
|
-
vBuf = await deflateAsync(jsonStr, { level: this.compressionLevel });
|
|
192
|
-
vLen = vBuf.length;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const head = Buffer.allocUnsafe(HEADER_SIZE);
|
|
196
|
-
head[0] = MAGIC_BYTES[0];
|
|
197
|
-
head[1] = MAGIC_BYTES[1];
|
|
198
|
-
head.writeUInt32BE(0, 2);
|
|
199
|
-
head.writeUInt32BE(vLen, 6);
|
|
200
|
-
head[10] = kBuf.length;
|
|
201
|
-
|
|
202
|
-
const parts = [head, kBuf];
|
|
203
|
-
if (vLen > 0) parts.push(vBuf);
|
|
204
|
-
const block = Buffer.concat(parts);
|
|
205
|
-
|
|
206
|
-
const stat = await this.fileHandles[sIdx].stat();
|
|
207
|
-
const pos = stat.size;
|
|
208
|
-
|
|
209
|
-
await this.fileHandles[sIdx].write(block, 0, block.length, pos);
|
|
210
|
-
|
|
211
|
-
if (vLen > 0) {
|
|
212
|
-
this.indices[sIdx].set(kStr, {
|
|
213
|
-
offset: pos + HEADER_SIZE + kBuf.length,
|
|
214
|
-
length: vLen,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
this._addToCache(kStr, value);
|
|
218
|
-
} else {
|
|
219
|
-
this.indices[sIdx].delete(kStr);
|
|
220
|
-
this.cache.delete(kStr);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
_enqueue(sIdx, task) {
|
|
225
|
-
const next = this.queues[sIdx]
|
|
226
|
-
.then(() => this._ensureShardLoaded(sIdx))
|
|
227
|
-
.then(task)
|
|
228
|
-
.catch((err) => {
|
|
229
|
-
console.error(`LBA Error (Shard ${sIdx}):`, err);
|
|
230
|
-
throw err;
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
this.queues[sIdx] = next;
|
|
234
|
-
return next;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async get(key) {
|
|
238
|
-
const sIdx = this._getShard(key);
|
|
239
|
-
return this._enqueue(sIdx, () => this._readImpl(sIdx, key));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async set(key, value) {
|
|
243
|
-
const sIdx = this._getShard(key);
|
|
244
|
-
return this._enqueue(sIdx, () => this._writeImpl(sIdx, key, value));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async delete(key) {
|
|
248
|
-
return this.set(key, null);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async update(key, fn) {
|
|
252
|
-
const sIdx = this._getShard(key);
|
|
253
|
-
return this._enqueue(sIdx, async () => {
|
|
254
|
-
const current = await this._readImpl(sIdx, key);
|
|
255
|
-
const next = await fn(current);
|
|
256
|
-
if (next !== undefined) {
|
|
257
|
-
await this._writeImpl(sIdx, key, next);
|
|
258
|
-
}
|
|
259
|
-
return next;
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async forEach(callback, options = {}) {
|
|
264
|
-
const batchSize = options.batchSize || 100;
|
|
265
|
-
let processed = 0;
|
|
266
|
-
let batch = [];
|
|
267
|
-
|
|
268
|
-
for (let i = 0; i < this.shardCount; i++) {
|
|
269
|
-
await this._enqueue(i, async () => {
|
|
270
|
-
for (const key of this.indices[i].keys()) {
|
|
271
|
-
try {
|
|
272
|
-
const value = await this._readImpl(i, key);
|
|
273
|
-
if (value !== null) {
|
|
274
|
-
batch.push({ key, value });
|
|
275
|
-
|
|
276
|
-
if (batch.length >= batchSize) {
|
|
277
|
-
for (const item of batch) {
|
|
278
|
-
await callback(item.key, item.value);
|
|
279
|
-
processed++;
|
|
280
|
-
}
|
|
281
|
-
batch = [];
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} catch (err) {
|
|
285
|
-
console.error(`Error reading key ${key} from shard ${i}:`, err);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
for (const item of batch) {
|
|
292
|
-
await callback(item.key, item.value);
|
|
293
|
-
processed++;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return processed;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async *entries(options = {}) {
|
|
300
|
-
const batchSize = options.batchSize || 50;
|
|
301
|
-
|
|
302
|
-
for (let i = 0; i < this.shardCount; i++) {
|
|
303
|
-
const entries = await this._enqueue(i, async () => {
|
|
304
|
-
const batch = [];
|
|
305
|
-
|
|
306
|
-
for (const key of this.indices[i].keys()) {
|
|
307
|
-
try {
|
|
308
|
-
const value = await this._readImpl(i, key);
|
|
309
|
-
if (value !== null) {
|
|
310
|
-
batch.push([key, value]);
|
|
311
|
-
|
|
312
|
-
if (batch.length >= batchSize) {
|
|
313
|
-
const result = [...batch];
|
|
314
|
-
batch.length = 0;
|
|
315
|
-
return result;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
} catch (err) {
|
|
319
|
-
console.error(`Error reading key ${key} from shard ${i}:`, err);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return batch;
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
for (const entry of entries) {
|
|
327
|
-
yield entry;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async getAll(options = {}) {
|
|
333
|
-
const maxSize = options.maxSize || Infinity;
|
|
334
|
-
const results = {};
|
|
335
|
-
let count = 0;
|
|
336
|
-
|
|
337
|
-
await this.forEach(
|
|
338
|
-
(key, value) => {
|
|
339
|
-
if (count >= maxSize) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
results[key] = value;
|
|
343
|
-
count++;
|
|
344
|
-
},
|
|
345
|
-
{ batchSize: options.batchSize || 100 },
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
return results;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async keys() {
|
|
352
|
-
const allKeys = [];
|
|
353
|
-
|
|
354
|
-
for (let i = 0; i < this.shardCount; i++) {
|
|
355
|
-
await this._enqueue(i, async () => {
|
|
356
|
-
allKeys.push(...this.indices[i].keys());
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return allKeys;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async count() {
|
|
364
|
-
let total = 0;
|
|
365
|
-
|
|
366
|
-
const counts = await Promise.all(
|
|
367
|
-
Array.from({ length: this.shardCount }, (_, i) =>
|
|
368
|
-
this._enqueue(i, async () => this.indices[i].size),
|
|
369
|
-
),
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
return counts.reduce((sum, count) => sum + count, 0);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async batchGet(keys) {
|
|
376
|
-
const results = {};
|
|
377
|
-
const keysByShard = new Map();
|
|
378
|
-
|
|
379
|
-
for (const key of keys) {
|
|
380
|
-
const sIdx = this._getShard(key);
|
|
381
|
-
if (!keysByShard.has(sIdx)) {
|
|
382
|
-
keysByShard.set(sIdx, []);
|
|
383
|
-
}
|
|
384
|
-
keysByShard.get(sIdx).push(key);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const promises = [];
|
|
388
|
-
for (const [sIdx, shardKeys] of keysByShard) {
|
|
389
|
-
promises.push(
|
|
390
|
-
this._enqueue(sIdx, async () => {
|
|
391
|
-
const shardResults = {};
|
|
392
|
-
for (const key of shardKeys) {
|
|
393
|
-
try {
|
|
394
|
-
const value = await this._readImpl(sIdx, key);
|
|
395
|
-
if (value !== null) {
|
|
396
|
-
shardResults[key] = value;
|
|
397
|
-
}
|
|
398
|
-
} catch (err) {
|
|
399
|
-
console.error(`Error in batchGet for key ${key}:`, err);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return shardResults;
|
|
403
|
-
}),
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const shardResults = await Promise.all(promises);
|
|
408
|
-
|
|
409
|
-
for (const shardResult of shardResults) {
|
|
410
|
-
Object.assign(results, shardResult);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return results;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async batchSet(entries) {
|
|
417
|
-
const entriesByShard = new Map();
|
|
418
|
-
|
|
419
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
420
|
-
const sIdx = this._getShard(key);
|
|
421
|
-
if (!entriesByShard.has(sIdx)) {
|
|
422
|
-
entriesByShard.set(sIdx, []);
|
|
423
|
-
}
|
|
424
|
-
entriesByShard.get(sIdx).push([key, value]);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const promises = [];
|
|
428
|
-
for (const [sIdx, shardEntries] of entriesByShard) {
|
|
429
|
-
promises.push(
|
|
430
|
-
this._enqueue(sIdx, async () => {
|
|
431
|
-
for (const [key, value] of shardEntries) {
|
|
432
|
-
await this._writeImpl(sIdx, key, value);
|
|
433
|
-
}
|
|
434
|
-
}),
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
await Promise.all(promises);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
getStats() {
|
|
442
|
-
const hitRate =
|
|
443
|
-
this.stats.cacheHits + this.stats.cacheMisses > 0
|
|
444
|
-
? (
|
|
445
|
-
(this.stats.cacheHits /
|
|
446
|
-
(this.stats.cacheHits + this.stats.cacheMisses)) *
|
|
447
|
-
100
|
|
448
|
-
).toFixed(2)
|
|
449
|
-
: 0;
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
...this.stats,
|
|
453
|
-
cacheSize: this.cache.size,
|
|
454
|
-
cacheLimit: this.cacheLimit,
|
|
455
|
-
cacheHitRate: `${hitRate}%`,
|
|
456
|
-
shardCount: this.shardCount,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
resetStats() {
|
|
461
|
-
this.stats = {
|
|
462
|
-
cacheHits: 0,
|
|
463
|
-
cacheMisses: 0,
|
|
464
|
-
reads: 0,
|
|
465
|
-
writes: 0,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async vacuum() {
|
|
470
|
-
const tasks = [];
|
|
471
|
-
|
|
472
|
-
for (let i = 0; i < this.shardCount; i++) {
|
|
473
|
-
tasks.push(
|
|
474
|
-
this._enqueue(i, async () => {
|
|
475
|
-
if (!this.isLoaded[i] || this.indices[i].size === 0) return;
|
|
476
|
-
|
|
477
|
-
const tmpPath = path.join(this.dbDir, `vacuum_${i}.tmp`);
|
|
478
|
-
const oldPath = path.join(this.dbDir, `shard_${i}.lba`);
|
|
479
|
-
|
|
480
|
-
let tmpHandle = null;
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
tmpHandle = await fs.promises.open(tmpPath, "w");
|
|
484
|
-
let newPos = 0;
|
|
485
|
-
|
|
486
|
-
for (const [key, meta] of this.indices[i]) {
|
|
487
|
-
const vBuf = Buffer.allocUnsafe(meta.length);
|
|
488
|
-
await this.fileHandles[i].read(vBuf, 0, meta.length, meta.offset);
|
|
489
|
-
|
|
490
|
-
const kBuf = Buffer.from(key);
|
|
491
|
-
|
|
492
|
-
const head = Buffer.allocUnsafe(HEADER_SIZE);
|
|
493
|
-
head[0] = MAGIC_BYTES[0];
|
|
494
|
-
head[1] = MAGIC_BYTES[1];
|
|
495
|
-
head.writeUInt32BE(0, 2);
|
|
496
|
-
head.writeUInt32BE(vBuf.length, 6);
|
|
497
|
-
head[10] = kBuf.length;
|
|
498
|
-
|
|
499
|
-
const block = Buffer.concat([head, kBuf, vBuf]);
|
|
500
|
-
|
|
501
|
-
await tmpHandle.write(block);
|
|
502
|
-
|
|
503
|
-
meta.offset = newPos + HEADER_SIZE + kBuf.length;
|
|
504
|
-
newPos += block.length;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
await tmpHandle.close();
|
|
508
|
-
tmpHandle = null;
|
|
509
|
-
|
|
510
|
-
await this.fileHandles[i].close();
|
|
511
|
-
|
|
512
|
-
await fs.promises.rename(tmpPath, oldPath);
|
|
513
|
-
|
|
514
|
-
this.fileHandles[i] = await fs.promises.open(oldPath, "a+");
|
|
515
|
-
} catch (err) {
|
|
516
|
-
console.error(`Vacuum error for shard ${i}:`, err);
|
|
517
|
-
|
|
518
|
-
if (tmpHandle) {
|
|
519
|
-
try {
|
|
520
|
-
await tmpHandle.close();
|
|
521
|
-
} catch (e) {}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (fs.existsSync(tmpPath)) {
|
|
525
|
-
try {
|
|
526
|
-
await fs.promises.unlink(tmpPath);
|
|
527
|
-
} catch (e) {}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
throw err;
|
|
531
|
-
}
|
|
532
|
-
}),
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
await Promise.all(tasks);
|
|
537
|
-
this.cache.clear();
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
async close() {
|
|
541
|
-
await Promise.all(this.queues);
|
|
542
|
-
|
|
543
|
-
const closePromises = this.fileHandles.map(async (handle) => {
|
|
544
|
-
if (handle) {
|
|
545
|
-
try {
|
|
546
|
-
await handle.close();
|
|
547
|
-
} catch (err) {
|
|
548
|
-
console.error("Error closing file handle:", err);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
await Promise.all(closePromises);
|
|
554
|
-
|
|
555
|
-
this.fileHandles.fill(null);
|
|
556
|
-
this.isLoaded.fill(false);
|
|
557
|
-
this.cache.clear();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
module.exports = LBA;
|