@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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/architect.mjs +35 -32
  3. 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 {openSync, closeSync, readFileSync, writeFileSync, readSync, writeSync, unlinkSync, mkdirSync, 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'
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 result = buffer.compare(this.#storage.getValue(index[m], length))
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
- let resutl = buffer.compare(this.#storage.getValue(index[0], length))
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
- resutl = buffer.compare(this.#storage.getValue(index[e], length))
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
- readFileSync(name) {
310
+ async read(name) {
308
311
 
309
- // 读取外置 buffer/text 文件内容(字段本体只存标志位与位置)
310
- return readFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]))
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
- mkdirSync(this.#storage.bufferDir)
323
+ await fs.mkdir(this.#storage.bufferDir, { recursive: true })
321
324
  }
322
- writeFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]), value)
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
- mkdirSync(this.#storage.bufferDir)
390
+ await fs.mkdir(this.#storage.bufferDir, { recursive: true })
388
391
  }
389
- writeFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]),
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]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k3000/store",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "storage",
5
5
  "main": "generator.mjs",
6
6
  "scripts": {