@k3000/store 1.7.0 → 1.9.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 +123 -1
- package/architect.mjs +54 -2
- package/generator.mjs +67 -55
- package/package.json +1 -1
- package/test/1/buffer/683 +1 -0
- package/test/1/buffer/684 +1 -0
- package/test/1/data +0 -0
- package/test/1/index +0 -0
- package/test/2/buffer/683 +1 -0
- package/test/2/buffer/684 +1 -0
- package/test/2/data +0 -0
- package/test/2/index +0 -0
- package/test/2/index.mjs +25 -25
- package/test/2/type.ts +33 -33
- package/test/3/buffer/683 +1 -0
- package/test/3/buffer/684 +1 -0
- package/test/3/data +0 -0
- package/test/3/index +0 -0
- package/test/3/index.mjs +26 -26
- package/test/3/type.ts +33 -33
- package/test/4/buffer/683 +1 -0
- package/test/4/buffer/684 +1 -0
- package/test/4/data +0 -0
- package/test/4/index +0 -0
- package/test/4/index.mjs +26 -26
- package/test/4/type.ts +33 -33
- package/test/5/buffer/683 +1 -0
- package/test/5/buffer/684 +1 -0
- package/test/5/data +0 -0
- package/test/5/index +0 -0
- package/test/5/index.mjs +26 -26
- package/test/5/type.ts +33 -33
- package/test/6/buffer/683 +1 -0
- package/test/6/buffer/684 +1 -0
- package/test/6/data +0 -0
- package/test/6/index +0 -0
- package/test/6/index.mjs +11 -11
- package/test/6/type.ts +23 -23
- package/test/7/buffer/683 +1 -0
- package/test/7/buffer/684 +1 -0
- package/test/7/data +0 -0
- package/test/7/index +0 -0
- package/test/7/index.mjs +12 -12
- package/test/7/type.ts +23 -23
- package/test/8/buffer/683 +1 -0
- package/test/8/buffer/684 +1 -0
- package/test/8/data +0 -0
- package/test/8/index +0 -0
- package/test/8/index.mjs +12 -12
- package/test/8/type.ts +23 -23
- package/test/9/buffer/683 +1 -0
- package/test/9/buffer/684 +1 -0
- package/test/9/data +0 -0
- package/test/9/index +0 -0
- package/test/9/index.mjs +390 -0
- package/test/9/type.ts +174 -0
- package/test/index.mjs +3 -3
- package/test.mjs +98 -10
package/README.md
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*2、1.5.0版本请注意page的用法。
|
|
6
6
|
*3、1.6.1修改String中文存储的BUG建议更新,更新请注意第1、2条和page的用法。
|
|
7
7
|
*4、1.7.0修复中文路径问题。
|
|
8
|
+
*5、1.8.0添加存储目录的备份与迁移的描述(无功能调整)。
|
|
9
|
+
*6、1.9.0让Trae builder优化代码添加了注释。
|
|
10
|
+
*7、反馈与讨论:19815488@qq.com
|
|
11
|
+
|
|
8
12
|
```
|
|
9
13
|
|
|
10
14
|
**初始化和更新升级**
|
|
@@ -227,9 +231,127 @@ storage.admin.remove(...storage.admin.filter(admin => admin.usn.startsWith('test
|
|
|
227
231
|
```
|
|
228
232
|
更多使用方法参考 `test.mjs`
|
|
229
233
|
|
|
230
|
-
|
|
234
|
+
**保存**
|
|
231
235
|
|
|
236
|
+
没3秒会自动保存,也可以手工操作
|
|
232
237
|
```
|
|
233
238
|
import {close} from './test/index.mjs'
|
|
234
239
|
close() // 关闭并保存
|
|
235
240
|
```
|
|
241
|
+
**备份与迁移**
|
|
242
|
+
|
|
243
|
+
当使用`upgrade`升级的时候会按照版本号备份历史版本。
|
|
244
|
+
|
|
245
|
+
也能直接将整个存储目录保存,复制妥存。
|
|
246
|
+
|
|
247
|
+
同样道理,迁移就是将存储目录拷贝到相应项目目录,使用相应版本的库运行即可。
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
**概述**
|
|
252
|
+
|
|
253
|
+
- 本工具提供一种轻量的、可版本化的本地结构化存储方案,面向 Node.js 项目;以“集合/列”定义模式,生成访问层代码,数据落盘到二进制文件并配合加密索引文件。
|
|
254
|
+
- 适合嵌入式、桌面端或需要无需外部数据库的应用,支持索引查询、范围查询、分页与联合查询,且提供平滑的结构升级与密码迁移能力。
|
|
255
|
+
|
|
256
|
+
**核心概念**
|
|
257
|
+
|
|
258
|
+
- `Storage`:底层存储运行时,负责数据读写、缓存、加解密、结构与记录的持久化以及并发打开检查。
|
|
259
|
+
- `Entity`:单条记录对象,字段访问器封装了二进制读写与索引维护;支持 Buffer/Text 外置文件字段。
|
|
260
|
+
- `Entities`:集合的代理,提供 `push/pop/unshift/splice/remove/page` 等数组语义与 `indexBy<Field>` 查询。
|
|
261
|
+
- 集合/列定义:通过类型 DSL(`Id/Uint/Int/BigUint/Bigint/Time/Float/String/Buffer/Text`)声明列类型、索引、默认值、长度、步进。
|
|
262
|
+
|
|
263
|
+
**目录结构详解**
|
|
264
|
+
|
|
265
|
+
- `index`:二进制索引文件,包含两段加密 JSON:结构(struct)与记录(record)。头部额外包含一个 4 字节整数作为“打开计数”,用于并发打开检测。
|
|
266
|
+
- `data`:二进制数据文件,按集合总长度定长布局存储;每条记录按列 `position/length` 定位读取。
|
|
267
|
+
- `buffer/`:外置文件目录,存放 `Buffer/Text` 字段实际内容;字段本体存储一个字节作为标志位,值为 1 表示存在外置内容。
|
|
268
|
+
- `<version>/index.mjs`:访问层代码,按结构自动生成集合实体类与集合代理;顶层 `index.mjs` 始终重导出最新版本。
|
|
269
|
+
|
|
270
|
+
**加密与密码**
|
|
271
|
+
|
|
272
|
+
- 结构与记录的加解密使用 `AES-192-CBC`,密钥由 `md5(password)+salt` 派生,IV 来自 MD5 的中间字节;密码来源于 `Storage(import.meta.url)` 的查询参数或 `upgrade` 传参。
|
|
273
|
+
- 密码迁移:通过 `upgrade('name', { newPwd, password })` 在提交时重新加密结构与记录;若密码不匹配或存储文件损坏会抛出错误。
|
|
274
|
+
|
|
275
|
+
**版本与升级流程**
|
|
276
|
+
|
|
277
|
+
- `upgrade(name, options)` 会:
|
|
278
|
+
- 计算最新版本 `v` 并将其目录完整复制到新版本目录 `version`;
|
|
279
|
+
- 打开旧/新 `Storage`(支持密码切换),准备生成路径(`index.mjs/type.ts`);
|
|
280
|
+
- 返回结构变更 API;`submit()` 会写入加密结构与记录、关闭句柄、生成访问代码,并在集合结构有更新时把旧版数据迁移到新版集合。
|
|
281
|
+
|
|
282
|
+
**索引与查询**
|
|
283
|
+
|
|
284
|
+
- 为列开启索引可使用 `updateIndex('set', { col: true })` 或在 DSL 中 `String().index(true)` 等;索引是按字段二进制值排序的记录位置列表。
|
|
285
|
+
- 字段写入使用两种模式:索引列通过 `set2` 维护有序索引(二分定位插入),普通列使用 `set` 直接写入。
|
|
286
|
+
- 查询方法:
|
|
287
|
+
- 值查询:`indexBy<Field>(value)` 返回与二进制值相等的所有记录;
|
|
288
|
+
- 范围查询:`indexBy<Field>(value1, value2)` 或针对 `Time` 用 `indexByTime({ after, before })`;
|
|
289
|
+
- 普通数组查询:`filter/find/some/findIndex` 等对集合进行内存遍历。
|
|
290
|
+
|
|
291
|
+
**分页与缓存**
|
|
292
|
+
|
|
293
|
+
- `page(predicate, index, size, params)` 支持两种模式:
|
|
294
|
+
- 简单分页:`page(fn, 1, 10)` 返回第 1 页、大小 10 的结果(按记录顺序);
|
|
295
|
+
- 缓存分页:`page(fn, 'index', 'size', params)` 将 `params` 作为缓存键,命中时不重复计算;内部每轮(3 秒)标记过期,下一轮未访问的缓存将被移除。
|
|
296
|
+
|
|
297
|
+
**大字段(Buffer/Text)**
|
|
298
|
+
|
|
299
|
+
- `Buffer`/`Text` 字段内容存储在 `buffer/` 目录,以记录起始位置+列偏移作为文件名;字段本体只存放 1 字节标记位。
|
|
300
|
+
- 删除或替换记录时,会自动清理关联的外置文件;读取时通过 `readFileSync` 返回二进制或文本内容。
|
|
301
|
+
|
|
302
|
+
**性能与注意事项**
|
|
303
|
+
|
|
304
|
+
- 自动保存与清理:每 3 秒写回需要保存的值、刷新记录并清理过期缓存;
|
|
305
|
+
- 缓存上限:Value/Entity/Cache 的 Map 最多各 99999 条;超过上限的写入会直接落盘而不进入缓存;
|
|
306
|
+
- 并发打开:打开计数变化会抛错提示“已在其他地方打开”,防止同目录并发写入;
|
|
307
|
+
- 路径解析:`Storage(import.meta.url)` 在 file URL 情况下使用 `process.cwd()` 作为目录基础,请在正确工作目录下运行或传显式路径;
|
|
308
|
+
- 时间编码:`Time` 字段以定长十六进制 Buffer 表示,使用 `d2b/b2d` 辅助范围比较;最大时间常量为 `MaxTime`。
|
|
309
|
+
|
|
310
|
+
**API 参考(结构变更)**
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
const gen = upgrade(name, { version, password, newPwd, store })
|
|
314
|
+
gen.appendSet(name, set, remark) // 新增集合
|
|
315
|
+
gen.updateCol(name, set) // 更新集合列
|
|
316
|
+
gen.renameSet(name, newName) // 集合更名
|
|
317
|
+
gen.renameCol(name, { old: new }) // 列更名
|
|
318
|
+
gen.deleteSet(name) // 删除集合
|
|
319
|
+
gen.deleteCol(name, colName) // 删除列
|
|
320
|
+
gen.updateSetRemark(name, remark) // 更新集合备注
|
|
321
|
+
gen.updateIndex(name, { col: true }) // 索引开关
|
|
322
|
+
gen.submit() // 提交生成与数据迁移
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**最小示例**
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
import upgrade, { Id, String, Time, Uint } from '@k3000/store/generator.mjs'
|
|
329
|
+
|
|
330
|
+
const { appendSet, submit } = upgrade('myStore')
|
|
331
|
+
|
|
332
|
+
appendSet('user', {
|
|
333
|
+
id: Id,
|
|
334
|
+
uid: String('账号').index(true),
|
|
335
|
+
pwd: String('密码').length(64),
|
|
336
|
+
createdAt: Time('创建时间').index(true).value(Time.now),
|
|
337
|
+
status: Uint(1).remark('状态位')
|
|
338
|
+
}, '用户表')
|
|
339
|
+
|
|
340
|
+
await submit()
|
|
341
|
+
|
|
342
|
+
// 使用
|
|
343
|
+
import storage from './myStore/index.mjs'
|
|
344
|
+
storage.user.push({ uid: 'alice', pwd: '***', status: 1 })
|
|
345
|
+
const recent = storage.user.indexByCreatedAt({ after: new Date('2024-01-01') })
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**常见问题(FAQ)**
|
|
349
|
+
|
|
350
|
+
- `Error: ENOENT: no such file or directory, open '.../data'`:当前工作目录与版本目录不匹配或目录未初始化。请先执行 `upgrade` 生成版本,并在该目录下运行。
|
|
351
|
+
- `密码不匹配或者存储文件不正确`:传入密码与存储目录加密信息不一致,或索引文件损坏。请确认密码或重新初始化。
|
|
352
|
+
- `索引查询无结果`:检查是否为该列开启索引(定义或 `updateIndex`),以及值/范围是否与列类型与长度匹配。
|
|
353
|
+
- `page 返回空`:检查 `size` 是否为正数,`predicate` 是否正确,或缓存参数键名是否与 `'index'/'size'` 一致。
|
|
354
|
+
|
|
355
|
+
**备份与迁移补充**
|
|
356
|
+
|
|
357
|
+
- 每次 `upgrade` 会将旧版本目录完整复制为备份;迁移仅需复制整个存储目录到目标项目,并确保使用兼容版本的库与正确密码即可。
|
package/architect.mjs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {openSync, closeSync, readFileSync, writeFileSync, readSync, writeSync, unlinkSync} from "node:fs"
|
|
1
|
+
import {openSync, closeSync, readFileSync, writeFileSync, readSync, writeSync, unlinkSync, mkdirSync, existsSync} from "node:fs"
|
|
2
2
|
import {createCipheriv, createDecipheriv, createHash, scryptSync} from "node:crypto"
|
|
3
|
+
import {fileURLToPath} from 'node:url'
|
|
4
|
+
import {dirname} from 'node:path'
|
|
5
|
+
/**
|
|
6
|
+
* 结构化存储运行时核心模块
|
|
7
|
+
* - 定义数据读写协议(定长 Buffer 字段、外置文件字段)
|
|
8
|
+
* - 提供实体(Entity)与集合(Entities)抽象,支持索引查询、范围查询与分页
|
|
9
|
+
* - 管理结构/记录的持久化与加解密、变更缓存与周期性保存
|
|
10
|
+
* - 为生成器输出的访问层(index.mjs)提供底层能力
|
|
11
|
+
*/
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
*
|
|
@@ -10,6 +19,8 @@ import {createCipheriv, createDecipheriv, createHash, scryptSync} from "node:cry
|
|
|
10
19
|
*/
|
|
11
20
|
Array.prototype.eachFlat = function (array, predicate, map) {
|
|
12
21
|
|
|
22
|
+
// eachFlat:类似 LEFT JOIN,将本数组与目标数组按谓词进行展开合并
|
|
23
|
+
|
|
13
24
|
if (typeof predicate === "string") {
|
|
14
25
|
|
|
15
26
|
const key = predicate
|
|
@@ -52,6 +63,8 @@ Array.prototype.eachFlat = function (array, predicate, map) {
|
|
|
52
63
|
*/
|
|
53
64
|
Array.prototype.filterFlat = function (array, predicate, map) {
|
|
54
65
|
|
|
66
|
+
// filterFlat:类似 INNER JOIN,仅保留满足谓词的组合项
|
|
67
|
+
|
|
55
68
|
if (typeof predicate === "string") {
|
|
56
69
|
|
|
57
70
|
const key = predicate
|
|
@@ -75,16 +88,19 @@ Array.prototype.filterFlat = function (array, predicate, map) {
|
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
// 缓存容量上限(value/entity/cache 三类 Map 的最大条目数)
|
|
78
92
|
const MaxSize = 99999
|
|
79
93
|
/**
|
|
80
94
|
* 时间筛选的最大值
|
|
81
95
|
* @type {number}
|
|
82
96
|
*/
|
|
97
|
+
// 时间筛选允许的最大毫秒值(用于范围查询边界)
|
|
83
98
|
export const MaxTime = 99999999999999
|
|
84
99
|
/**
|
|
85
100
|
* 用于获取存储的位置
|
|
86
101
|
* @type {symbol}
|
|
87
102
|
*/
|
|
103
|
+
// 实体在数据文件中的起始位置标识,用于删除/回收与定位
|
|
88
104
|
export const position = Symbol('position')
|
|
89
105
|
|
|
90
106
|
export const TypeLen = {
|
|
@@ -233,6 +249,7 @@ export class Entity {
|
|
|
233
249
|
|
|
234
250
|
set2(name, buffer) {
|
|
235
251
|
|
|
252
|
+
// 维护索引列:写入值并按二分/边界插入保持 index[name] 有序
|
|
236
253
|
const position = this.#position + this.#positionSet[name]
|
|
237
254
|
const index = this.#indexSet[name]
|
|
238
255
|
const length = this.#lengthSet[name]
|
|
@@ -289,6 +306,7 @@ export class Entity {
|
|
|
289
306
|
|
|
290
307
|
readFileSync(name) {
|
|
291
308
|
|
|
309
|
+
// 读取外置 buffer/text 文件内容(字段本体只存标志位与位置)
|
|
292
310
|
return readFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]))
|
|
293
311
|
}
|
|
294
312
|
|
|
@@ -298,6 +316,9 @@ export class Entity {
|
|
|
298
316
|
|
|
299
317
|
if (value instanceof Buffer) {
|
|
300
318
|
|
|
319
|
+
if (!existsSync(this.#storage.bufferDir)) {
|
|
320
|
+
mkdirSync(this.#storage.bufferDir)
|
|
321
|
+
}
|
|
301
322
|
writeFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]), value)
|
|
302
323
|
|
|
303
324
|
buffer[0] = 1
|
|
@@ -362,6 +383,9 @@ export class Entity {
|
|
|
362
383
|
|
|
363
384
|
if (value) {
|
|
364
385
|
|
|
386
|
+
if (!existsSync(this.#storage.bufferDir)) {
|
|
387
|
+
mkdirSync(this.#storage.bufferDir)
|
|
388
|
+
}
|
|
365
389
|
writeFileSync(this.#storage.bufferDir + (this.#position + this.#positionSet[name]),
|
|
366
390
|
typeof value === "string" ? value : String(value))
|
|
367
391
|
|
|
@@ -444,6 +468,7 @@ export class Entities extends Array {
|
|
|
444
468
|
|
|
445
469
|
if (index >= record.length || index < 0) return null
|
|
446
470
|
|
|
471
|
+
// 索引访问返回实体代理(含字段 getter/setter)
|
|
447
472
|
return storage.getEntity(record[p], classify, this.#index, this.#lengthSet, this.#positionSet)
|
|
448
473
|
}
|
|
449
474
|
|
|
@@ -453,6 +478,7 @@ export class Entities extends Array {
|
|
|
453
478
|
|
|
454
479
|
if (typeof result !== "function") return result
|
|
455
480
|
|
|
481
|
+
// 将原生数组方法委托到实体对象数组
|
|
456
482
|
return (...arg) => result.call(record.map(position => storage.getEntity(position, classify,
|
|
457
483
|
this.#index, this.#lengthSet, this.#positionSet)), ...arg)
|
|
458
484
|
}
|
|
@@ -496,6 +522,7 @@ export class Entities extends Array {
|
|
|
496
522
|
|
|
497
523
|
push(...items) {
|
|
498
524
|
|
|
525
|
+
// 追加:复用空闲位或推进指针,返回实体对象(已应用默认值创建)
|
|
499
526
|
if (this.#storage.needSave === false) {
|
|
500
527
|
|
|
501
528
|
this.#storage.needSave = true
|
|
@@ -524,6 +551,7 @@ export class Entities extends Array {
|
|
|
524
551
|
|
|
525
552
|
pop() {
|
|
526
553
|
|
|
554
|
+
// 弹出最后一条:回收索引与外置文件,位置进入空闲池
|
|
527
555
|
if (this.#storage.needSave === false) {
|
|
528
556
|
|
|
529
557
|
this.#storage.needSave = true
|
|
@@ -540,6 +568,7 @@ export class Entities extends Array {
|
|
|
540
568
|
|
|
541
569
|
unshift(...items) {
|
|
542
570
|
|
|
571
|
+
// 头部插入:同 push 但写入到记录头部
|
|
543
572
|
if (this.#storage.needSave === false) {
|
|
544
573
|
|
|
545
574
|
this.#storage.needSave = true
|
|
@@ -568,6 +597,7 @@ export class Entities extends Array {
|
|
|
568
597
|
|
|
569
598
|
shift() {
|
|
570
599
|
|
|
600
|
+
// 移除首条:与 pop 相似
|
|
571
601
|
if (this.#storage.needSave === false) {
|
|
572
602
|
|
|
573
603
|
this.#storage.needSave = true
|
|
@@ -583,6 +613,7 @@ export class Entities extends Array {
|
|
|
583
613
|
|
|
584
614
|
splice(start, deleteCount, ...items) {
|
|
585
615
|
|
|
616
|
+
// 区间替换:批量分配位置并回收被删除位置
|
|
586
617
|
if (this.#storage.needSave === false) {
|
|
587
618
|
|
|
588
619
|
this.#storage.needSave = true
|
|
@@ -642,6 +673,7 @@ export class Entities extends Array {
|
|
|
642
673
|
*/
|
|
643
674
|
page(predicate, page, size, params) {
|
|
644
675
|
|
|
676
|
+
// 分页:支持带缓存键的分页与简单分页两种模式
|
|
645
677
|
if (typeof page === "string" && typeof size === "string" && typeof params === "object") {
|
|
646
678
|
|
|
647
679
|
return this.#page(predicate, params[page], params[size], this.#name + ' ' +
|
|
@@ -681,6 +713,7 @@ export class Entities extends Array {
|
|
|
681
713
|
|
|
682
714
|
remove(...items) {
|
|
683
715
|
|
|
716
|
+
// 从集合中移除实体(按位置比对),触发回收与空闲入池
|
|
684
717
|
let index
|
|
685
718
|
|
|
686
719
|
if (Array.isArray(items[0])) {
|
|
@@ -964,7 +997,17 @@ export class Storage {
|
|
|
964
997
|
|
|
965
998
|
constructor(url, password, newPwd) {
|
|
966
999
|
|
|
967
|
-
let dir
|
|
1000
|
+
let dir
|
|
1001
|
+
|
|
1002
|
+
if (url.startsWith('file:///')) {
|
|
1003
|
+
|
|
1004
|
+
const filePath = fileURLToPath(url)
|
|
1005
|
+
dir = dirname(filePath)
|
|
1006
|
+
|
|
1007
|
+
} else {
|
|
1008
|
+
|
|
1009
|
+
dir = url
|
|
1010
|
+
}
|
|
968
1011
|
|
|
969
1012
|
dir = dir.replace(/\\/g, '/')
|
|
970
1013
|
|
|
@@ -1022,6 +1065,7 @@ export class Storage {
|
|
|
1022
1065
|
|
|
1023
1066
|
#save() {
|
|
1024
1067
|
|
|
1068
|
+
// 将需要保存的缓存值写回数据文件;如记录有结构改动,刷新记录块
|
|
1025
1069
|
for (const [position, item] of this.#values.entries()) {
|
|
1026
1070
|
|
|
1027
1071
|
if (item.needsSave) {
|
|
@@ -1047,6 +1091,7 @@ export class Storage {
|
|
|
1047
1091
|
|
|
1048
1092
|
#check(...arg) {
|
|
1049
1093
|
|
|
1094
|
+
// 简单的每轮过期机制:第二次巡检仍为 expired=true 的条目会被移除
|
|
1050
1095
|
for (const map of arg) {
|
|
1051
1096
|
|
|
1052
1097
|
for (const [key, value] of map) {
|
|
@@ -1065,6 +1110,7 @@ export class Storage {
|
|
|
1065
1110
|
|
|
1066
1111
|
#writeNumber(number, position) {
|
|
1067
1112
|
|
|
1113
|
+
// 向索引文件写入 4 字节无符号整数(版本/长度/指针等)
|
|
1068
1114
|
const buffer = Buffer.alloc(4)
|
|
1069
1115
|
|
|
1070
1116
|
buffer.writeUint32BE(number, 0)
|
|
@@ -1089,6 +1135,7 @@ export class Storage {
|
|
|
1089
1135
|
extra: {},
|
|
1090
1136
|
}) {
|
|
1091
1137
|
|
|
1138
|
+
// 更新结构并写入索引文件(加密 JSON)
|
|
1092
1139
|
this.#struct = struct
|
|
1093
1140
|
|
|
1094
1141
|
this.#update(struct)
|
|
@@ -1102,6 +1149,7 @@ export class Storage {
|
|
|
1102
1149
|
pointer: 0,
|
|
1103
1150
|
}, length = 4 + this.#getLength()) {
|
|
1104
1151
|
|
|
1152
|
+
// 更新记录并写入索引文件(结构长度之后的区域)
|
|
1105
1153
|
this.#record = record
|
|
1106
1154
|
|
|
1107
1155
|
this.#update(record, length)
|
|
@@ -1228,6 +1276,7 @@ export class Storage {
|
|
|
1228
1276
|
*/
|
|
1229
1277
|
getValue(position, length) {
|
|
1230
1278
|
|
|
1279
|
+
// 读取数据文件指定位置的定长 Buffer,配合 LRU 式过期标记缓存
|
|
1231
1280
|
if (this.#values.has(position)) {
|
|
1232
1281
|
|
|
1233
1282
|
const item = this.#values.get(position)
|
|
@@ -1267,6 +1316,7 @@ export class Storage {
|
|
|
1267
1316
|
|
|
1268
1317
|
setValue(position, length, value) {
|
|
1269
1318
|
|
|
1319
|
+
// 设置缓存并标记是否需要保存(或直接写盘,当缓存已满)
|
|
1270
1320
|
if (this.#values.has(position)) {
|
|
1271
1321
|
|
|
1272
1322
|
const item = this.#values.get(position)
|
|
@@ -1326,6 +1376,7 @@ export class Storage {
|
|
|
1326
1376
|
|
|
1327
1377
|
getEntity(position, classify, ...arg) {
|
|
1328
1378
|
|
|
1379
|
+
// 获取实体代理对象,带过期标记以配合周期性清理
|
|
1329
1380
|
if (this.#entities.has(position)) {
|
|
1330
1381
|
|
|
1331
1382
|
const item = this.#entities.get(position)
|
|
@@ -1387,6 +1438,7 @@ export class Storage {
|
|
|
1387
1438
|
|
|
1388
1439
|
updateValue(value, name, key) {
|
|
1389
1440
|
|
|
1441
|
+
// 维护有步进的列(如自增 Id):当未提供值时按步进递增并返回
|
|
1390
1442
|
if (value === undefined) return this.#record.value[name][key] += this.#struct.base[name][key].step
|
|
1391
1443
|
|
|
1392
1444
|
if (Number.isNaN(Number.parseFloat(value))) {
|
package/generator.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import {copyFileSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync} from "node:fs";
|
|
2
|
-
import {buffer, string, Storage, TypeLen, readInt24BE, readUint24BE} from './architect.mjs'
|
|
2
|
+
import {buffer, string, Storage, TypeLen, readInt24BE, readUint24BE, b2d} from './architect.mjs'
|
|
3
3
|
import {resolve} from 'node:path'
|
|
4
|
+
/**
|
|
5
|
+
* 代码生成与结构升级模块
|
|
6
|
+
* - 提供类型 DSL(Id/Uint/Int/...)定义集合结构、索引、默认值、长度与步进
|
|
7
|
+
* - 生成运行时访问代码(index.mjs)与类型声明(type.ts)
|
|
8
|
+
* - 管理版本目录复制、密码迁移与数据迁移
|
|
9
|
+
*/
|
|
4
10
|
|
|
5
11
|
const modulePath = import.meta.url.endsWith('?test=true') ? '../../architect.mjs' : '@k3000/store/architect.mjs'
|
|
6
12
|
const modulePath2 = import.meta.url.endsWith('?test=true') ? '../../generator.mjs' : '@k3000/store'
|
|
@@ -49,6 +55,7 @@ const TimeAttr = {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
const SCOPE = {}
|
|
58
|
+
// SCOPE:内部哨兵对象,限制链式配置仅由类型构造器内部调用生效
|
|
52
59
|
|
|
53
60
|
export const bigintSerialize = (_, value) => typeof value === "bigint" ? value.toString() : value
|
|
54
61
|
|
|
@@ -495,6 +502,7 @@ export const Text = remark => new class Text extends Type {
|
|
|
495
502
|
|
|
496
503
|
export const struct = (storage, remark, name) => {
|
|
497
504
|
|
|
505
|
+
// 将底层结构反射为可读的元信息(类型、备注、索引、默认值、长度等)
|
|
498
506
|
const struct = {}, base = storage.struct.base
|
|
499
507
|
|
|
500
508
|
for (const [key, value] of Object.entries(storage.struct.extra)) {
|
|
@@ -545,6 +553,7 @@ export const struct = (storage, remark, name) => {
|
|
|
545
553
|
|
|
546
554
|
const updateColPos = (base, extra) => {
|
|
547
555
|
|
|
556
|
+
// 重新计算列起始位置与集合总长度(按顺序累加定长字段)
|
|
548
557
|
let position = 0
|
|
549
558
|
|
|
550
559
|
for (const value of Object.values(base)) {
|
|
@@ -559,6 +568,7 @@ const updateColPos = (base, extra) => {
|
|
|
559
568
|
|
|
560
569
|
function appendSet(name, set, remark = '') {
|
|
561
570
|
|
|
571
|
+
// 新增集合:初始化结构/记录占位,并根据列定义填充索引/步进默认值
|
|
562
572
|
if (!Reflect.has(this.curr.struct.base, name)) {
|
|
563
573
|
|
|
564
574
|
console.log('append set:', name)
|
|
@@ -619,6 +629,7 @@ function appendSet(name, set, remark = '') {
|
|
|
619
629
|
|
|
620
630
|
function updateCol(name, set) {
|
|
621
631
|
|
|
632
|
+
// 更新集合列:覆盖或新增列定义,维护索引与步进默认值
|
|
622
633
|
if (Reflect.has(this.curr.struct.base, name)) {
|
|
623
634
|
|
|
624
635
|
console.log('current set:', name)
|
|
@@ -674,6 +685,7 @@ function updateCol(name, set) {
|
|
|
674
685
|
|
|
675
686
|
function renameSet(name, newName) {
|
|
676
687
|
|
|
688
|
+
// 集合更名:同步结构/备注/记录各处键名
|
|
677
689
|
if (typeof newName === "string"
|
|
678
690
|
&& Reflect.has(this.curr.struct.base, name)
|
|
679
691
|
&& !Reflect.has(this.curr.struct.base, newName)) {
|
|
@@ -708,6 +720,7 @@ function renameSet(name, newName) {
|
|
|
708
720
|
|
|
709
721
|
function renameCol(name, set) {
|
|
710
722
|
|
|
723
|
+
// 列更名:同步结构/备注/记录各处键名与对应索引容器
|
|
711
724
|
if (Reflect.has(this.curr.struct.base, name)) {
|
|
712
725
|
|
|
713
726
|
for (let [key, newKey] of Object.entries(set)) {
|
|
@@ -748,6 +761,7 @@ function renameCol(name, set) {
|
|
|
748
761
|
|
|
749
762
|
function deleteSet(name) {
|
|
750
763
|
|
|
764
|
+
// 删除集合:移除结构与记录的全部条目
|
|
751
765
|
if (Reflect.has(this.curr.struct.base, name)) {
|
|
752
766
|
|
|
753
767
|
console.log(`delete set: ${name}`)
|
|
@@ -763,6 +777,7 @@ function deleteSet(name) {
|
|
|
763
777
|
|
|
764
778
|
function deleteCol(name, col) {
|
|
765
779
|
|
|
780
|
+
// 删除列:移除结构与记录条目并重算位置
|
|
766
781
|
if (Reflect.has(this.curr.struct.base, name)
|
|
767
782
|
&& Reflect.has(this.curr.struct.base[name], col)) {
|
|
768
783
|
|
|
@@ -783,6 +798,7 @@ function deleteCol(name, col) {
|
|
|
783
798
|
|
|
784
799
|
function updateSetRemark(name, remark) {
|
|
785
800
|
|
|
801
|
+
// 更新集合备注
|
|
786
802
|
if (Reflect.has(this.curr.struct.base, name)) {
|
|
787
803
|
|
|
788
804
|
console.log(`update ${name} remark:`, remark)
|
|
@@ -793,93 +809,74 @@ function updateSetRemark(name, remark) {
|
|
|
793
809
|
|
|
794
810
|
const updateIndex2 = function (store, record, struct) {
|
|
795
811
|
|
|
796
|
-
|
|
812
|
+
const positions = record.map(position => position + struct.position)
|
|
813
|
+
const length = struct.length
|
|
797
814
|
|
|
798
815
|
switch (struct.type) {
|
|
799
816
|
|
|
800
817
|
case typeMap.id:
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
- store.getValue(b, struct.length).readUInt32BE())
|
|
818
|
+
return positions.map(p => [p, store.getValue(p, length).readUInt32BE(0)])
|
|
819
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
804
820
|
|
|
805
821
|
case typeMap.uint:
|
|
806
|
-
|
|
807
|
-
switch (struct.length) {
|
|
808
|
-
|
|
822
|
+
switch (length) {
|
|
809
823
|
case 1:
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
- store.getValue(b, struct.length).readUInt8())
|
|
813
|
-
|
|
824
|
+
return positions.map(p => [p, store.getValue(p, length).readUInt8(0)])
|
|
825
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
814
826
|
case 2:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
- store.getValue(b, struct.length).readUInt16BE())
|
|
818
|
-
|
|
827
|
+
return positions.map(p => [p, store.getValue(p, length).readUInt16BE(0)])
|
|
828
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
819
829
|
case 3:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
- readUint24BE(store.getValue(b, struct.length)))
|
|
823
|
-
|
|
830
|
+
return positions.map(p => [p, readUint24BE(store.getValue(p, length))])
|
|
831
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
824
832
|
case 4:
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
- store.getValue(b, struct.length).readUInt32BE())
|
|
833
|
+
return positions.map(p => [p, store.getValue(p, length).readUInt32BE(0)])
|
|
834
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
828
835
|
}
|
|
836
|
+
break
|
|
829
837
|
|
|
830
838
|
case typeMap.int:
|
|
831
|
-
|
|
832
|
-
switch (struct.length) {
|
|
833
|
-
|
|
839
|
+
switch (length) {
|
|
834
840
|
case 1:
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
- store.getValue(b, struct.length).readInt8())
|
|
838
|
-
|
|
841
|
+
return positions.map(p => [p, store.getValue(p, length).readInt8(0)])
|
|
842
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
839
843
|
case 2:
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
- store.getValue(b, struct.length).readInt16BE())
|
|
843
|
-
|
|
844
|
+
return positions.map(p => [p, store.getValue(p, length).readInt16BE(0)])
|
|
845
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
844
846
|
case 3:
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
- readInt24BE(store.getValue(b, struct.length)))
|
|
848
|
-
|
|
847
|
+
return positions.map(p => [p, readInt24BE(store.getValue(p, length))])
|
|
848
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
849
849
|
case 4:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
- store.getValue(b, struct.length).readInt32BE())
|
|
850
|
+
return positions.map(p => [p, store.getValue(p, length).readInt32BE(0)])
|
|
851
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
853
852
|
}
|
|
853
|
+
break
|
|
854
854
|
|
|
855
855
|
case typeMap.bigUint:
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
- store.getValue(b, struct.length).readBigUInt64BE())
|
|
856
|
+
return positions.map(p => [p, store.getValue(p, length).readBigUInt64BE(0)])
|
|
857
|
+
.sort((a, b) => (a[1] - b[1])).map(([p]) => p)
|
|
859
858
|
|
|
860
859
|
case typeMap.bigint:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
- store.getValue(b, struct.length).readBigInt64BE())
|
|
860
|
+
return positions.map(p => [p, store.getValue(p, length).readBigInt64BE(0)])
|
|
861
|
+
.sort((a, b) => (a[1] - b[1])).map(([p]) => p)
|
|
864
862
|
|
|
865
863
|
case typeMap.time:
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
- b2d(store.getValue(b, struct.length)))
|
|
864
|
+
return positions.map(p => [p, b2d(store.getValue(p, length))])
|
|
865
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
869
866
|
|
|
870
867
|
case typeMap.float:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
- store.getValue(b, struct.length).readDoubleBE())
|
|
868
|
+
return positions.map(p => [p, store.getValue(p, length).readDoubleBE(0)])
|
|
869
|
+
.sort((a, b) => a[1] - b[1]).map(([p]) => p)
|
|
874
870
|
|
|
875
871
|
case typeMap.string:
|
|
876
|
-
|
|
877
|
-
|
|
872
|
+
return positions.map(p => [p, store.getValue(p, length)])
|
|
873
|
+
.sort((a, b) => a[1].compare(b[1])).map(([p]) => p)
|
|
878
874
|
}
|
|
879
875
|
}
|
|
880
876
|
|
|
881
877
|
const updateIndex = function (name, set) {
|
|
882
878
|
|
|
879
|
+
// 开启/关闭指定列索引:重建或清理索引映射
|
|
883
880
|
const struct = this.curr.struct.base[name]
|
|
884
881
|
const index = this.curr.record.index[name]
|
|
885
882
|
|
|
@@ -918,6 +915,11 @@ const commentRemark = (remark = '') => remark.replace(/\n/g, '\n\t * ')
|
|
|
918
915
|
|
|
919
916
|
function submit() {
|
|
920
917
|
|
|
918
|
+
// 提交:
|
|
919
|
+
// 1) 清理更新集合的运行时记录
|
|
920
|
+
// 2) 写入结构/记录(加密)并关闭旧/新存储句柄
|
|
921
|
+
// 3) 生成类型声明与访问层代码
|
|
922
|
+
// 4) 将旧版本数据迁移到新版本(当集合结构有更新)
|
|
921
923
|
for (const name of this.updateSet.values()) {
|
|
922
924
|
|
|
923
925
|
this.curr.record.idle[name] = []
|
|
@@ -986,6 +988,7 @@ export default storage
|
|
|
986
988
|
|
|
987
989
|
const initDir = (name, dir) => {
|
|
988
990
|
|
|
991
|
+
// 初始化版本目录:创建 index.mjs/index/data/buffer 与 Storage 实例
|
|
989
992
|
const existsDir = existsSync(name) && statSync(name).isDirectory()
|
|
990
993
|
|
|
991
994
|
if (!existsDir) {
|
|
@@ -1042,6 +1045,7 @@ export default new class extends Store {
|
|
|
1042
1045
|
|
|
1043
1046
|
const copyDir = (src, dest) => {
|
|
1044
1047
|
|
|
1048
|
+
// 复制目录(用于版本演进的目录克隆)
|
|
1045
1049
|
mkdirSync(dest)
|
|
1046
1050
|
|
|
1047
1051
|
for (const name of readdirSync(src)) {
|
|
@@ -1125,6 +1129,7 @@ const gen = {
|
|
|
1125
1129
|
|
|
1126
1130
|
const clipStatic = (name, [key, value]) => {
|
|
1127
1131
|
|
|
1132
|
+
// 生成实体创建阶段的默认值赋值代码(含步进值与特殊时间默认)
|
|
1128
1133
|
if (!Reflect.has(value, 'value')) return ''
|
|
1129
1134
|
|
|
1130
1135
|
let valueStr = ''
|
|
@@ -1166,6 +1171,7 @@ const clipStatic = (name, [key, value]) => {
|
|
|
1166
1171
|
|
|
1167
1172
|
const clipIndex = (name, key, k, v) => {
|
|
1168
1173
|
|
|
1174
|
+
// 生成字段 setter 写入语句:区分索引列与普通列的写入方式
|
|
1169
1175
|
let main = '0'
|
|
1170
1176
|
|
|
1171
1177
|
if (v.type === typeMap.int) {
|
|
@@ -1215,6 +1221,7 @@ const sort2 = ([a], [b]) => a.localeCompare(b)
|
|
|
1215
1221
|
|
|
1216
1222
|
const clipEntity = ([key, value]) => {
|
|
1217
1223
|
|
|
1224
|
+
// 生成实体类:包含静态 create 默认值、字段属性访问器与自动更新时间
|
|
1218
1225
|
const update = Object.entries(value).find(([_, v]) => v.type === typeMap.time && v.value === TimeAttr.update)
|
|
1219
1226
|
|
|
1220
1227
|
const name = key[0].toUpperCase() + key.substring(1)
|
|
@@ -1253,6 +1260,7 @@ ${update ? `
|
|
|
1253
1260
|
|
|
1254
1261
|
const clipEntities = ([key, value]) => {
|
|
1255
1262
|
|
|
1263
|
+
// 生成集合代理类:屏蔽内部方法、提供索引查询方法
|
|
1256
1264
|
const name = key[0].toUpperCase() + key.substring(1)
|
|
1257
1265
|
|
|
1258
1266
|
return `
|
|
@@ -1438,6 +1446,10 @@ export default function upgrade(name, {
|
|
|
1438
1446
|
store,
|
|
1439
1447
|
} = {}) {
|
|
1440
1448
|
|
|
1449
|
+
// 升级流程:
|
|
1450
|
+
// - 计算最新版本 v,克隆 v 到新版本目录
|
|
1451
|
+
// - 打开旧/新 Storage(支持密码切换),准备代码生成路径
|
|
1452
|
+
// - 返回结构变更操作器,最终由 submit() 完成落盘与代码生成
|
|
1441
1453
|
const dir = name[name.length - 1] === '/' ? name : name + '/'
|
|
1442
1454
|
|
|
1443
1455
|
if (!existsSync(dir)) {
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abc
|