@k3000/store 1.9.1 → 1.9.2
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 +33 -0
- package/architect.mjs +35 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -367,3 +367,36 @@ const recent = storage.user.indexByCreatedAt({ after: new Date('2024-01-01') })
|
|
|
367
367
|
**备份与迁移补充**
|
|
368
368
|
|
|
369
369
|
- 每次 `upgrade` 会将旧版本目录完整复制为备份;迁移仅需复制整个存储目录到目标项目,并确保使用兼容版本的库与正确密码即可。
|
|
370
|
+
|
|
371
|
+
# 缺点与不足
|
|
372
|
+
|
|
373
|
+
这个项目实现了一个轻量级的、无依赖的、基于文件的结构化存储系统(类似简易数据库),虽然设计精巧且自给自足,但在生产环境中使用存在显著的风险,**“不建议用作主数据库”**。
|
|
374
|
+
|
|
375
|
+
以下是存在的主要缺点与不足:
|
|
376
|
+
|
|
377
|
+
**1. 严重的安全与稳定性隐患 (Critical)**
|
|
378
|
+
|
|
379
|
+
非异步 I/O 阻塞事件循环: architect.mjs 中大量使用了 readSync, writeSync, openSync 等同步文件操作。Node.js 是单线程的,这意味着每次读写数据库都会完全卡死整个应用程序的事件循环,直到文件操作完成。在高并发或大文件读写时,会导致服务器无响应。
|
|
380
|
+
缺乏 Crash Safety (崩溃安全): 没有发现 Write-Ahead Log (WAL) 或事务日志机制。依赖内存缓存 (needSave 标志) 和每 3 秒的定时器 (Storage) 落盘。
|
|
381
|
+
数据丢失风险: 如果进程在 3 秒间隔内崩溃或断电,这期间的所有数据修改将永久丢失。
|
|
382
|
+
文件损坏风险: 如果在写入数据的瞬间崩溃,文件可能只写了一半,导致数据库彻底损坏且无法恢复。
|
|
383
|
+
并发控制脆弱: 仅依赖文件头部的“打开计数器”来防止并发,并建议“防止同目录并发写入”。这无法有效处理多进程环境,容易导致竞态条件(Race Condition)和数据错乱。
|
|
384
|
+
|
|
385
|
+
**2. 性能与扩展性瓶颈**
|
|
386
|
+
|
|
387
|
+
内存占用高: 索引(Indexes)似乎是全量加载到内存中的 (#index Set)。对于大数据集,这会消耗大量 RAM。
|
|
388
|
+
写入性能随数据量下降: 索引维护使用了 splice 插入 (set2 方法中),这是一个 O(N) 操作。随着数据量增加,插入新数据并维护排序索引的速度会越来越慢。
|
|
389
|
+
缓存限制: 硬编码了 MaxSize = 99999 的缓存上限,超过后直接落盘。这种策略比较生硬。
|
|
390
|
+
|
|
391
|
+
**3. 功能局限性**
|
|
392
|
+
|
|
393
|
+
查询能力有限: 虽然支持 filter, find, indexBy...,但本质上很多操作是在遍历内存或文件。不支持复杂的 SQL 语义(如复杂的 JOIN, GROUP BY, 子查询等)。flat 系列方法仅提供了非常基础的关联能力。
|
|
394
|
+
缺乏生态工具: 作为一个自定义的二进制格式,没有现成的 GUI 客户端、备份工具、数据迁移工具(除了自带的 upgrade)、可视化监控等。调试数据非常困难,只能写代码查。
|
|
395
|
+
|
|
396
|
+
**4. 安全性细节**
|
|
397
|
+
|
|
398
|
+
密码学实践不标准:
|
|
399
|
+
使用 md5(key) 作为后续密钥派生的基础。MD5 如今被认为是不够安全的(抗碰撞性弱),虽然后续用了 scrypt,但起手式用 MD5 会降低整体熵值。
|
|
400
|
+
IV 来源于 MD5 的中间字节,这种非随机生成的 IV 在某些加密场景下可能是不安全的。
|
|
401
|
+
测试代码中暗示用户密码可能只存了 MD5 (String('存MD5指')),这是过时的做法,现代应用应使用 bcrypt/argon2。
|
|
402
|
+
总结建议
|
package/architect.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {createCipheriv, createDecipheriv, createHash, scryptSync} from "node:crypto"
|
|
3
|
-
import {fileURLToPath} from 'node:url'
|
|
4
|
-
import {dirname} from 'node:path'
|
|
1
|
+
import { promises as fs, existsSync } from "node:fs"
|
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, scryptSync } from "node:crypto"
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
5
|
/**
|
|
6
6
|
* 结构化存储运行时核心模块
|
|
7
7
|
* - 定义数据读写协议(定长 Buffer 字段、外置文件字段)
|
|
@@ -36,7 +36,7 @@ Array.prototype.eachFlat = function (array, predicate, map) {
|
|
|
36
36
|
|
|
37
37
|
if (result.length === 0) return item
|
|
38
38
|
|
|
39
|
-
return result.map(entry => map({...entry, ...item}, entry))
|
|
39
|
+
return result.map(entry => map({ ...entry, ...item }, entry))
|
|
40
40
|
|
|
41
41
|
}).flat()
|
|
42
42
|
|
|
@@ -48,7 +48,7 @@ Array.prototype.eachFlat = function (array, predicate, map) {
|
|
|
48
48
|
|
|
49
49
|
if (result.length === 0) return item
|
|
50
50
|
|
|
51
|
-
return result.map(entry => ({...entry, ...item}))
|
|
51
|
+
return result.map(entry => ({ ...entry, ...item }))
|
|
52
52
|
|
|
53
53
|
}).flat()
|
|
54
54
|
}
|
|
@@ -82,8 +82,8 @@ Array.prototype.filterFlat = function (array, predicate, map) {
|
|
|
82
82
|
} else {
|
|
83
83
|
|
|
84
84
|
return this.map(item => array
|
|
85
|
-
.filter(entry => predicate({...entry, ...item}, entry))
|
|
86
|
-
.map(entry => ({...entry, ...item}))
|
|
85
|
+
.filter(entry => predicate({ ...entry, ...item }, entry))
|
|
86
|
+
.map(entry => ({ ...entry, ...item }))
|
|
87
87
|
).flat()
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -223,42 +223,43 @@ export class Entity {
|
|
|
223
223
|
return this.#position
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
get(name) {
|
|
226
|
+
async get(name) {
|
|
227
227
|
|
|
228
|
-
return this.#storage.getValue(this.#position + this.#positionSet[name], this.#lengthSet[name])
|
|
228
|
+
return await this.#storage.getValue(this.#position + this.#positionSet[name], this.#lengthSet[name])
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
set(name, buffer) {
|
|
231
|
+
async set(name, buffer) {
|
|
232
232
|
|
|
233
|
-
this.#storage.setValue(this.#position + this.#positionSet[name], this.#lengthSet[name], buffer)
|
|
233
|
+
await this.#storage.setValue(this.#position + this.#positionSet[name], this.#lengthSet[name], buffer)
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
#indexPosition(index, buffer, length, s, e) {
|
|
236
|
+
async #indexPosition(index, buffer, length, s, e) {
|
|
237
237
|
|
|
238
238
|
if (s + 1 === e) return e
|
|
239
239
|
|
|
240
240
|
const m = Math.round((s + e) / 2)
|
|
241
241
|
|
|
242
|
-
const
|
|
242
|
+
const val = await this.#storage.getValue(index[m], length)
|
|
243
|
+
const result = buffer.compare(val)
|
|
243
244
|
|
|
244
|
-
if (result < 0) return this.#indexPosition(index, buffer, length, s, m)
|
|
245
|
-
if (result > 0) return this.#indexPosition(index, buffer, length, m, e)
|
|
245
|
+
if (result < 0) return await this.#indexPosition(index, buffer, length, s, m)
|
|
246
|
+
if (result > 0) return await this.#indexPosition(index, buffer, length, m, e)
|
|
246
247
|
|
|
247
248
|
return m
|
|
248
249
|
}
|
|
249
250
|
|
|
250
|
-
set2(name, buffer) {
|
|
251
|
+
async set2(name, buffer) {
|
|
251
252
|
|
|
252
253
|
// 维护索引列:写入值并按二分/边界插入保持 index[name] 有序
|
|
253
254
|
const position = this.#position + this.#positionSet[name]
|
|
254
255
|
const index = this.#indexSet[name]
|
|
255
256
|
const length = this.#lengthSet[name]
|
|
256
|
-
const buf = this.#storage.getValue(position, length)
|
|
257
|
+
const buf = await this.#storage.getValue(position, length)
|
|
257
258
|
const changed = !buf.equals(buffer)
|
|
258
259
|
|
|
259
260
|
if (changed) {
|
|
260
261
|
|
|
261
|
-
this.#storage.setValue(position, length, buffer)
|
|
262
|
+
await this.#storage.setValue(position, length, buffer)
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
let i = index.indexOf(position)
|
|
@@ -272,7 +273,8 @@ export class Entity {
|
|
|
272
273
|
index.splice(i, 1)
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
|
|
276
|
+
const firstVal = await this.#storage.getValue(index[0], length)
|
|
277
|
+
let resutl = buffer.compare(firstVal)
|
|
276
278
|
|
|
277
279
|
if (index.length === 0) {
|
|
278
280
|
|
|
@@ -288,7 +290,8 @@ export class Entity {
|
|
|
288
290
|
|
|
289
291
|
} else {
|
|
290
292
|
|
|
291
|
-
|
|
293
|
+
const lastVal = await this.#storage.getValue(index[e], length)
|
|
294
|
+
resutl = buffer.compare(lastVal)
|
|
292
295
|
|
|
293
296
|
if (resutl >= 0) {
|
|
294
297
|
|
|
@@ -296,7 +299,7 @@ export class Entity {
|
|
|
296
299
|
|
|
297
300
|
} else {
|
|
298
301
|
|
|
299
|
-
i = this.#indexPosition(index, buffer, length, s, e)
|
|
302
|
+
i = await this.#indexPosition(index, buffer, length, s, e)
|
|
300
303
|
}
|
|
301
304
|
}
|
|
302
305
|
|
|
@@ -304,22 +307,22 @@ export class Entity {
|
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
|
|
307
|
-
|
|
310
|
+
async read(name) {
|
|
308
311
|
|
|
309
|
-
// 读取外置 buffer/text
|
|
310
|
-
return
|
|
312
|
+
// 读取外置 buffer/text 文件内容
|
|
313
|
+
return await fs.readFile(this.#storage.bufferDir + (this.#position + this.#positionSet[name]))
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
buffer(value, name) {
|
|
316
|
+
async buffer(value, name) {
|
|
314
317
|
|
|
315
318
|
const buffer = Buffer.alloc(TypeLen.buffer)
|
|
316
319
|
|
|
317
320
|
if (value instanceof Buffer) {
|
|
318
321
|
|
|
319
322
|
if (!existsSync(this.#storage.bufferDir)) {
|
|
320
|
-
|
|
323
|
+
await fs.mkdir(this.#storage.bufferDir, { recursive: true })
|
|
321
324
|
}
|
|
322
|
-
|
|
325
|
+
await fs.writeFile(this.#storage.bufferDir + (this.#position + this.#positionSet[name]), value)
|
|
323
326
|
|
|
324
327
|
buffer[0] = 1
|
|
325
328
|
}
|
|
@@ -377,16 +380,16 @@ export class Entity {
|
|
|
377
380
|
return buffer
|
|
378
381
|
}
|
|
379
382
|
|
|
380
|
-
textToBuffer(value, name) {
|
|
383
|
+
async textToBuffer(value, name) {
|
|
381
384
|
|
|
382
385
|
const buffer = Buffer.alloc(TypeLen.text)
|
|
383
386
|
|
|
384
387
|
if (value) {
|
|
385
388
|
|
|
386
389
|
if (!existsSync(this.#storage.bufferDir)) {
|
|
387
|
-
|
|
390
|
+
await fs.mkdir(this.#storage.bufferDir, { recursive: true })
|
|
388
391
|
}
|
|
389
|
-
|
|
392
|
+
await fs.writeFile(this.#storage.bufferDir + (this.#position + this.#positionSet[name]),
|
|
390
393
|
typeof value === "string" ? value : String(value))
|
|
391
394
|
|
|
392
395
|
buffer[0] = 1
|
|
@@ -874,7 +877,7 @@ export class Entities extends Array {
|
|
|
874
877
|
return result
|
|
875
878
|
}
|
|
876
879
|
|
|
877
|
-
findByTime(name, {after = 0, before = MaxTime}) {
|
|
880
|
+
findByTime(name, { after = 0, before = MaxTime }) {
|
|
878
881
|
|
|
879
882
|
const index = this.#index[name]
|
|
880
883
|
const position = this.#positionSet[name]
|