npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@k3000/store

v1.9.3

Published

storage

Downloads

606

Readme

结构化存储工具

*1、低版本升级到1.3.0请执行upgrade和submit更新存储文件。
*2、1.5.0版本请注意page的用法。
*3、1.6.1修改String中文存储的BUG建议更新,更新请注意第1、2条和page的用法。
*4、1.7.0修复中文路径问题。
*5、1.8.0添加存储目录的备份与迁移的描述(无功能调整)。
*6、1.9.0让Trae builder优化代码添加了注释。
*7、反馈与讨论:[email protected]

初始化和更新升级

import upgrade from 'dmb3'

const {appendSet, submit} = upgrade('test', {version: 1, password: '', newPwd: ''})

appendSet(/* */)

submit()

文件结构为:假设存储名为:test

test ┳ 1.2 ┳ buffer ┳
     ┃     ┃        ┇ // buffer格式或者文本格式的存储目录
     ┃     ┃        ┗
     ┃     ┣ data          // 数据存储文件
     ┃     ┣ index         // 数据索引文件
     ┃     ┗ index.mjs     // 数据访问文件,可以通过该文件访问指定版本
     ┣ ...
     ┣ 3   ┳ ...
     ┃     ┗ index.mjs     // 数据访问文件,可以通过该文件访问指定版本
     ┣ 4 ...
     ┗ index.mjs    // 默认入口文件,指向最新版本

数据类型

类型:Id, Uint, Int, Bool, Time, Float, String, Buffer, Text

| 类型 | 说明 | 默认长度(字节) | 默认值 | 备注 | | :--- | :--- | :--- | :--- | :--- | | Id | 自增主键 | 4 | 自动自增 | 默认为索引 | | Uint | 无符号整数 | 4 | 0 | 可选 1/2/3/4 字节 | | Int | 有符号整数 | 4 | 0 | 可选 1/2/3/4 字节 | | Float | 双精度浮点 | 8 | 0 | - | | Bool | 布尔值 | 1 | false | 实际存储为 Uint(1) | | Time | 时间 | 6 | 0 | 可设 Time.now / Time.update | | String | 定长字符串 | 12 | '' | 超出长度会被截断 | | Bigint | 64位整数 | 8 | 0n | - | | Buffer | 二进制数据 | 1 | - | 存储在外置文件,主文件存标志位 | | Text | 长文本 | 1 | - | 存储在外置文件,主文件存标志位 |

import upgrade, {
    Bigint,
    bigintSerialize,
    BigUint,
    Buffer,
    Float,
    Id,
    Int,
    String,
    Text,
    Time,
    Uint
} from 'dmb3'
const {appendSet, submit} = upgrade('test')
appendSet('user', {
    uid: String,
    pwd: String('存MD5指').length(32),
    loginTime: Time().value(Time.update)
}, '用户表')
submit().then(() => console.log('完成'))

缺省写法与完整写法

// Time.value(Time.update) 和 Time().value(Time.update) 两种写法一样。
/* 例如:
appendSet('test', {
    id: Id,
})
等价
appendSet('test', {
    id: Id(),
})
*/

// Id 用法
Id('备注') // 会补全默认值
Id()
    .index(false) // 是否索引,此类型默认为 true
    .value('2') // 默认值,此类型缺省为 1
    .step(2) // 缺省自增值,此类型缺省为 1
    .remark('备注') // 可缺省
    
// Uint 用法
Uint(4)
    .index(false) // 是否索引,此类型默认为 false
    .value(2) // 默认值,此类型没有缺省值
    .step(2) // 缺省自增值,此类型没有缺省值
    .remark('备注') // 可缺省
    
// Int 用法
Int()
    .index(false) // 是否索引,此类型默认为 false
    .value(2) // 默认值,此类型没有缺省值
    .step(2) // 缺省自增值,此类型没有缺省值
    .length(4) // 长度,此类型默认为 4
    .remark('备注') // 可缺省
    
// Bool 用法
Bool()
    .index(false) // 是否索引,此类型默认为 false
    .value(false) // 默认值,此类型缺省为 false
    .remark('备注') // 可缺省
    
// Time 用法
Time()
    .index(false) // 是否索引,此类型默认为 false
    .value(Time.update) // 默认值,此类型没有缺省值,可取值:Time.update、Time.now
    .remark('备注') // 可缺省
    
// Float 用法
Float()
    .index(false) // 是否索引,此类型默认为 false
    .value(0.1) // 默认值,此类型没有缺省值
    .step(0.1) // 缺省自增值,此类型没有缺省值
    .remark('备注') // 可缺省
    
// String 用法
String()
    .index(false) // 是否索引,此类型默认为 false
    .value('123456') // 默认值,此类型没有缺省值
    .length(8) // 长度,此类型默认为 12
    .remark('备注') // 可缺省
    
// Buffer 用法
Buffer().remark('备注') // 可缺省
    
// Text 用法
Text().remark('备注') // 可缺省

存储结构更新写法

// 新增集合
appendSet('user', {
    uid: String,
    pwd: String('存MD5指').length(32)
}, '用户表')

// 更新集合备注
updateSetRemark('log', '日志表')

// 更新列
updateCol('user', {
    loginTime: Time().value(Time.update).remark('登录时间')
})

// 更新索引
updateIndex('test', {
    uid: true,
    pwd: false,
})

// 更名操作
renameSet('test', 'test2')
renameCol('user', {
    uid: 'usn',
    test: 'test2',
})

// 删除操作
deleteSet('test2')
deleteCol('user', 'test2')

const {submit} = upgrade('test', {
        version: 7,
        password: '123123',
        newPwd: '111111' // 新密码
    })

submit()

打开使用已有存储文件

使用

import storage from './test/index.mjs'  // 使用最新版本
import storage from './test/7/index.mjs'// 使用指定版本
import storage, {remark, getStruct} from './test/7/index.mjs' // 获取存储结构和集合备注

查:

storage.admin.find(admin => admin.usn.includes('test')).role

if (storage.admin.indexByUid('admin')[0].pwd === '123456');

// 带索引的时间
storage.admin.indexByLoginTime(-Infinity, '2022/01/01')

// 简单分页查询
storage.admin.page(admin => predicate(admin), index, size)

// 结果缓存的分页查询,缓存10秒
const params = {key: 'name', index: 1, size: 10}
storage.admin.page(admin => predicate(admin), 'index', 'size', params)
// 联合查询 eachFlat类似LEFT JOIN,filterFlat类似INNER JOIN
storage.user.eachFlat(storage.userRole, (a, b) => a.userId === b.userId)
            .filterFlat(storage.role, 'roleId')

增:

storage.admin.push(
    {
        usn: 'admin',
        pwd: '123456'
    },
    {
        usn: 'test',
        pwd: '123456'
    }
)

改:

storage.admin.find(admin => admin.usn === 'admin').role = 'root'

删:

storage.admin.remove(...storage.admin.filter(admin => admin.usn.startsWith('test')))

更多使用方法参考 test.mjs

保存

没3秒会自动保存,也可以手工操作

import {close} from './test/index.mjs'
close() // 关闭并保存

备份与迁移

当使用upgrade升级的时候会按照版本号备份历史版本。

也能直接将整个存储目录保存,复制妥存。

同样道理,迁移就是将存储目录拷贝到相应项目目录,使用相应版本的库运行即可。


概述

  • 本工具提供一种轻量的、可版本化的本地结构化存储方案,面向 Node.js 项目;以“集合/列”定义模式,生成访问层代码,数据落盘到二进制文件并配合加密索引文件。
  • 适合嵌入式、桌面端或需要无需外部数据库的应用,支持索引查询、范围查询、分页与联合查询,且提供平滑的结构升级与密码迁移能力。

核心概念

  • Storage:底层存储运行时,负责数据读写、缓存、加解密、结构与记录的持久化以及并发打开检查。
  • Entity:单条记录对象,字段访问器封装了二进制读写与索引维护;支持 Buffer/Text 外置文件字段。
  • Entities:集合的代理,提供 push/pop/unshift/splice/remove/page 等数组语义与 indexBy<Field> 查询。
  • 集合/列定义:通过类型 DSL(Id/Uint/Int/BigUint/Bigint/Time/Float/String/Buffer/Text)声明列类型、索引、默认值、长度、步进。

目录结构详解

  • index:二进制索引文件,包含两段加密 JSON:结构(struct)与记录(record)。头部额外包含一个 4 字节整数作为“打开计数”,用于并发打开检测。
  • data:二进制数据文件,按集合总长度定长布局存储;每条记录按列 position/length 定位读取。
  • buffer/:外置文件目录,存放 Buffer/Text 字段实际内容;字段本体存储一个字节作为标志位,值为 1 表示存在外置内容。
  • <version>/index.mjs:访问层代码,按结构自动生成集合实体类与集合代理;顶层 index.mjs 始终重导出最新版本。

加密与密码

  • 结构与记录的加解密使用 AES-192-CBC,密钥由 md5(password)+salt 派生,IV 来自 MD5 的中间字节;密码来源于 Storage(import.meta.url) 的查询参数或 upgrade 传参。
  • 密码迁移:通过 upgrade('name', { newPwd, password }) 在提交时重新加密结构与记录;若密码不匹配或存储文件损坏会抛出错误。

版本与升级流程

  • upgrade(name, options) 会:
    • 计算最新版本 v 并将其目录完整复制到新版本目录 version
    • 打开旧/新 Storage(支持密码切换),准备生成路径(index.mjs/type.ts);
    • 返回结构变更 API;submit() 会写入加密结构与记录、关闭句柄、生成访问代码,并在集合结构有更新时把旧版数据迁移到新版集合。

索引与查询

  • 为列开启索引可使用 updateIndex('set', { col: true }) 或在 DSL 中 String().index(true) 等;索引是按字段二进制值排序的记录位置列表。
  • 字段写入使用两种模式:索引列通过 set2 维护有序索引(二分定位插入),普通列使用 set 直接写入。
  • 查询方法:
    • 值查询:indexBy<Field>(value) 返回与二进制值相等的所有记录;
    • 范围查询:indexBy<Field>(value1, value2) 或针对 TimeindexByTime({ after, before })
    • 普通数组查询:filter/find/some/findIndex 等对集合进行内存遍历。

分页与缓存

  • page(predicate, index, size, params) 支持两种模式:
    • 简单分页:page(fn, 1, 10) 返回第 1 页、大小 10 的结果(按记录顺序);
    • 缓存分页:page(fn, 'index', 'size', params)params 作为缓存键,命中时不重复计算;内部每轮(3 秒)标记过期,下一轮未访问的缓存将被移除。

大字段(Buffer/Text)

  • Buffer/Text 字段内容存储在 buffer/ 目录,以记录起始位置+列偏移作为文件名;字段本体只存放 1 字节标记位。
  • 删除或替换记录时,会自动清理关联的外置文件;读取时通过 readFileSync 返回二进制或文本内容。

性能与注意事项

  • 自动保存与清理:每 3 秒写回需要保存的值、刷新记录并清理过期缓存;
  • 缓存上限:Value/Entity/Cache 的 Map 最多各 99999 条;超过上限的写入会直接落盘而不进入缓存;
  • 并发打开:打开计数变化会抛错提示“已在其他地方打开”,防止同目录并发写入;
  • 路径解析:Storage(import.meta.url) 在 file URL 情况下使用 process.cwd() 作为目录基础,请在正确工作目录下运行或传显式路径;
  • 时间编码:Time 字段以定长十六进制 Buffer 表示,使用 d2b/b2d 辅助范围比较;最大时间常量为 MaxTime

API 参考(结构变更)

const gen = upgrade(name, { version, password, newPwd, store })
gen.appendSet(name, set, remark)       // 新增集合
gen.updateCol(name, set)               // 更新集合列
gen.renameSet(name, newName)           // 集合更名
gen.renameCol(name, { old: new })      // 列更名
gen.deleteSet(name)                    // 删除集合
gen.deleteCol(name, colName)           // 删除列
gen.updateSetRemark(name, remark)      // 更新集合备注
gen.updateIndex(name, { col: true })   // 索引开关
gen.submit()                           // 提交生成与数据迁移

最小示例

import upgrade, { Id, String, Time, Uint } from '@k3000/store/generator.mjs'

const { appendSet, submit } = upgrade('myStore')

appendSet('user', {
  id: Id,
  uid: String('账号').index(true),
  pwd: String('密码').length(64),
  createdAt: Time('创建时间').index(true).value(Time.now),
  status: Uint(1).remark('状态位')
}, '用户表')

await submit()

// 使用
import storage from './myStore/index.mjs'
storage.user.push({ uid: 'alice', pwd: '***', status: 1 })
const recent = storage.user.indexByCreatedAt({ after: new Date('2024-01-01') })

常见问题(FAQ)

  • Error: ENOENT: no such file or directory, open '.../data':当前工作目录与版本目录不匹配或目录未初始化。请先执行 upgrade 生成版本,并在该目录下运行。
  • 密码不匹配或者存储文件不正确:传入密码与存储目录加密信息不一致,或索引文件损坏。请确认密码或重新初始化。
  • 索引查询无结果:检查是否为该列开启索引(定义或 updateIndex),以及值/范围是否与列类型与长度匹配。
  • page 返回空:检查 size 是否为正数,predicate 是否正确,或缓存参数键名是否与 'index'/'size' 一致。

备份与迁移补充

  • 每次 upgrade 会将旧版本目录完整复制为备份;迁移仅需复制整个存储目录到目标项目,并确保使用兼容版本的库与正确密码即可。

缺点与不足

这个项目实现了一个轻量级的、无依赖的、基于文件的结构化存储系统(类似简易数据库),虽然设计精巧且自给自足,但在生产环境中使用存在显著的风险,“不建议用作主数据库”

以下是存在的主要缺点与不足:

1. 严重的安全与稳定性隐患 (Critical)

非异步 I/O 阻塞事件循环: architect.mjs 中大量使用了 readSync, writeSync, openSync 等同步文件操作。Node.js 是单线程的,这意味着每次读写数据库都会完全卡死整个应用程序的事件循环,直到文件操作完成。在高并发或大文件读写时,会导致服务器无响应。 缺乏 Crash Safety (崩溃安全): 没有发现 Write-Ahead Log (WAL) 或事务日志机制。依赖内存缓存 (needSave 标志) 和每 3 秒的定时器 (Storage) 落盘。 数据丢失风险: 如果进程在 3 秒间隔内崩溃或断电,这期间的所有数据修改将永久丢失。 文件损坏风险: 如果在写入数据的瞬间崩溃,文件可能只写了一半,导致数据库彻底损坏且无法恢复。 并发控制脆弱: 仅依赖文件头部的“打开计数器”来防止并发,并建议“防止同目录并发写入”。这无法有效处理多进程环境,容易导致竞态条件(Race Condition)和数据错乱。

2. 性能与扩展性瓶颈

内存占用高: 索引(Indexes)似乎是全量加载到内存中的 (#index Set)。对于大数据集,这会消耗大量 RAM。 写入性能随数据量下降: 索引维护使用了 splice 插入 (set2 方法中),这是一个 O(N) 操作。随着数据量增加,插入新数据并维护排序索引的速度会越来越慢。 缓存限制: 硬编码了 MaxSize = 99999 的缓存上限,超过后直接落盘。这种策略比较生硬。

3. 功能局限性

查询能力有限: 虽然支持 filter, find, indexBy...,但本质上很多操作是在遍历内存或文件。不支持复杂的 SQL 语义(如复杂的 JOIN, GROUP BY, 子查询等)。flat 系列方法仅提供了非常基础的关联能力。 缺乏生态工具: 作为一个自定义的二进制格式,没有现成的 GUI 客户端、备份工具、数据迁移工具(除了自带的 upgrade)、可视化监控等。调试数据非常困难,只能写代码查。

4. 安全性细节

密码学实践不标准: 使用 md5(key) 作为后续密钥派生的基础。MD5 如今被认为是不够安全的(抗碰撞性弱),虽然后续用了 scrypt,但起手式用 MD5 会降低整体熵值。 IV 来源于 MD5 的中间字节,这种非随机生成的 IV 在某些加密场景下可能是不安全的。 测试代码中暗示用户密码可能只存了 MD5 (String('存MD5指')),这是过时的做法,现代应用应使用 bcrypt/argon2。 总结建议