@effected/store

中文文档
0.25.0发布于 2026/11/10MITTypeScriptES Module构建来源已验证近 30 天下载 8321

持久本地状态 Effect:一个被计划化,迁移的 SQLite 商店和一个带有标记无效的 TTL 缓存, 驱逐, 以及缓存事件流 .

中文使用文档
文档目录19 节
  1. 为什么 @ effect/ store
  2. 安装
  3. 快开始
  4. 层三重奏
  5. 一个真正的服务 一个内幕商店
  6. 驱动选项
  7. 移徙
  8. 采用因实际移徙而迁移的数据库
  9. 几个版本共享一个数据库:反映迁移分类账
  10. 打开一个数据库的几个进程
  11. 连接设置: WAL、 繁忙超时、 外国密钥和 onConnect
  12. 多个数据库
  13. 缓存
  14. 贬低到失职
  15. 一通电话读取
  16. Uint8 来自Utf8 的箭头
  17. 错误
  18. 特征
  19. 许可证

@ 受影响/ 商店@ info

npm License: MIT Node.js %3E%3D24.11.0 TypeScript 7.0

持久局部效应状态:两个服务 超过一个原始. Store 是一个被计划化,迁移的 SqlClient ——与用户定义的迁移分类账建立管理下的数据库连接,支持 up, (中文(简体) ), down,回滚和状态预测. Cache 是一个 key → Uint8Array 带有 TTL 的缓存、 标记、 散装无效化、 驱离政策和 a PubSub 生命周期事件。 都通过节点的内置运行在SQLite上 node:sqlite,所以没有原生编译步骤,两者都以标记错误来显示它们的故障,这些错误带有底部 SqlError 结构上.

预1.0.0。 。 。 . 此软件包是 @effected/* 在稳定基础上建造的工具包 效应v4(effect ~4.1.0) 并仍在 0.x 发展。 稳定效果 制作一个工具箱 1.0.0 可能,不是自动的。 为了保持你的 effect 财务报告和审定财务报表 @effect/* 该工具包在线上构建和测试、安装 @effected/pnpm-plugin-effected。 。 。 .

稳定:不稳定. 此软件包的 API 表面尚未考虑 完成并可能改变 0.x 释放 定一个确切的版本——甚至 标记软件包 稳定 在此之前 1.0.0 可以引入突破性更改 和精确的针 变成一个类型检查错误 而不是一个 跑步时间惊喜。 完全政策: 释放策略。 。 。 .

为什么 @ effect/ store

一个商店和一个缓存 看起来像一样的东西 上面有一面旗帜, 对待它们的方式是 缓存 如何最终持有 数据 没有人可以承担损失。 被删除的缓存条目是正确的行为; 丢失的状态行是错误 。 因此,他们是在这里的两个服务,有不同的合同——只有 Cache 有TTL,标签和驱逐,只有 Store 有你的移民分类账。 他们确实分享了下面的分类账引擎,按表名键,所以 Store (单位:千美元) Cache 可以不相撞地在同一数据库文件中生活.

这个包裹拒绝的另外一件事是 叛逃洗钱 投放的迁移是一个程序员错误,而不是数据库的失败,它保持一个缺陷,而不是作为一个 StoreError 你可能会想重试。 仅输入 SqlError 变成一个域错误, 它被带全而不是平整成一个 reason 绳子。 层构造在等待迁移时运行,并将故障放入层的打出错误通道上——否 orDie 将一个破碎的计划隐藏在工作服务后面.

安装

npm install @effected/store effect
pnpm add @effected/store effect

需要节点.js~~24.11.0.

全体 @effected/* 软件包仅为无害环境管理:只公布出口地图 import 条件,所以 require() 包括解析 CJS 模式的工具 —— 与节点失败 ERR_PACKAGE_PATH_NOT_EXPORTED 而不是装入不存在的 CJS 建筑。 从ES模块导入.

effect v4是唯一的对等依赖. SQLite 驱动程序(@effect/sql-sqlite-node)是这个软件包的经常依赖性,所以你不会自己安装它——它骑着节点的内置 node:sqlite,没有本地建筑,也没有自己的过渡性同伴。 单次运行的依赖性 才是Repo的整合级软件包 @effected/store 继承驱动程序.

快开始

声明您的迁移情况, 将地层绑入一个 Const, 并使用 store.client 用于询问:

import { Store, type StoreMigration } from "@effected/store";
import { Effect } from "effect";

const migrations: ReadonlyArray<StoreMigration> = [
  {
    id: 1,
    name: "create-notes",
    up: (sql) => sql`CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)`,
    down: (sql) => sql`DROP TABLE notes`,
  },
];

// The filename reaches SQLite as given, so its parent directory must already
// exist. Use `@effected/xdg` to resolve (and create) a real application data dir.
const StoreLive = Store.layerSqlite({ filename: "state.db", migrations });

const program = Effect.gen(function* () {
  const store = yield* Store;
  const sql = store.client;
  yield* sql`INSERT INTO notes (body) VALUES (${"first"})`;
  return yield* sql<{ id: number; body: string }>`SELECT id, body FROM notes`;
});

Effect.runPromise(program.pipe(Effect.provide(StoreLive))).then(console.log);
// [ { id: 1, body: "first" } ]

层静态参数化 工厂而不是层。 通话 Store.layerSqlite(...) 两次构建出两层,而"地层"记忆是通过参考——将结果绑定在同上,或者数据库被打开两次.

父目录 filename 必须存在。 缺少目录的图层已键入 StoreError (单位:千美元)operation: "setup"以物配主 @effected/xdg 是知道目录所属位置的软件包.

层三重奏

两种服务都暴露出相同的三个静态,而分裂是接合:

静态它提供的东西所需资源
layer(options)服务比抽象 SqlClient - 任何效果 SQL 驱动程序满足它SqlClient
layerSqlite(options & { filename })服务加 SQLite 驱动程序无
layerTest(options)layerSqlite 时间 :memory:{\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}那套套套房用什么来着 {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}无

一个真正的服务 一个内幕商店

当您自己的服务建立在 Store,测试 实际执行 结束 Store.layerTest'"是内相的 SQLite 而不是手写双相. 公约:一项执行效果 Store页:1 layer(filename) 用于生产及 layerMemory() 工厂 测试, 两者都是从它。 每一个建筑 Store.layerTest 打开新空的 :memory: 数据库中应用了您的迁移,所以测试从不看到别人的行. layerMemory() 是一个函数而不是一个算盘,这样两个消费者组成一个图层,就可以得到两个数据库,而不是通过逐一引用来共享一个数据库.

import { Store, type StoreMigration } from "@effected/store";
import { Context, Effect, Layer } from "effect";

const migrations: ReadonlyArray<StoreMigration> = [
  {
    id: 1,
    name: "create-snapshots",
    up: (sql) => sql`CREATE TABLE snapshots (name TEXT PRIMARY KEY, body TEXT NOT NULL)`,
    down: (sql) => sql`DROP TABLE snapshots`,
  },
];

class Snapshots extends Context.Service<Snapshots, {
  readonly save: (name: string, body: string) => Effect.Effect<void>;
}>()("app/Snapshots") {
  // The one implementation, blind to where the database lives.
  static readonly make = Effect.gen(function* () {
    const sql = (yield* Store).client;
    return {
      save: (name: string, body: string) =>
        sql`INSERT OR REPLACE INTO snapshots (name, body) VALUES (${name}, ${body})`.pipe(Effect.asVoid, Effect.orDie),
    };
  });

  static layer(filename: string) {
    return Layer.effect(Snapshots, Snapshots.make).pipe(Layer.provide(Store.layerSqlite({ filename, migrations })));
  }

  static layerMemory() {
    return Layer.effect(Snapshots, Snapshots.make).pipe(Layer.provide(Store.layerTest({ migrations })));
  }
}

对 layerMemory() 与 @effected/memfs 服务也读取或书写在磁盘上 整个管道的运行非常顺利 没有额外的帮助者可以学习: Store.layerTest 已经是内幕工厂了 生产代码仍然有约束力 Snapshots.layer(path) 就像每层静态一样.

SQL核心生活在 effect 本身,在 effect/sql 没有 @effect/sql v4 线上的软件包,所以 SqlClient 从 effect/sql/SqlClient。 。 。 .

驱动选项

layerSqlite 通过其他驱动程序配置 client,所以调试 SQLite 客户端 不再意味着向抽象 layer 手电线驱动程序:

const StoreLive = Store.layerSqlite({
  filename: "state.db",
  migrations,
  client: { disableWAL: true, prepareCacheSize: 50 },
  checkpointOnClose: true,
});
  • client 穿过一切 SqliteClient.layer 接受除外 filename (由这层拥有)和两个名称转换选项—— transformResultNames编号 :transformQueryNames 将改写移徙分类账本身查询的结果名称,并默地报告所有待处理的移徙。 如果您需要更改姓名, 请将您的客户端连接到抽象中 layer。 。 。 .
  • checkpointOnClose: true 登记a PRAGMA wal_checkpoint(TRUNCATE) 与仍然开放的连接相悖的定稿器,在驱动程序关闭之前——每个耐用SQLite的消费者都是手写. 最佳效果:一个失败的检查站从来不会把干净的关闭变成失败的关闭. SQLite的特异性,所以它只活在平地层上; layerTest (单位:千美元):memory:)没有WAL,从未设置检查站.

Cache.layerSqlite 选择相同的两个选项.

移徙

移民是您拥有的列表:正整数 id页:1 name 记录在分类账中,a up,可选 down双向返回 Effect<unknown, SqlError>,所以一个贴有标签的SQL模板可以追溯到现在——引擎丢弃了值,a CREATE TABLE 已经描述自己 不需要 Effect.asVoid 把它包起来 分层构造确保分类表,并应用所有待处理事项,所以新造的 Store 已经迁移. migrate 重新运行等待迁移, rollback(toId) 解开一切 id > toId 最新一届(rollback(0) 解开一切,和 status 将完整列表与每次迁移的 appliedAt数字 :

import { Store } from "@effected/store";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const store = yield* Store;
  yield* store.rollback(0);
  return yield* store.status;
});
// Every migration is listed, each with `appliedAt` absent — they are all pending again.

重复代号、非正整数代号和非正整数代号 toId 是线条错误,而不是数据条件:它们死在地层构造中而不是被输入失败。 一个迁移 掷出 也保持缺陷, 周围的交易卷回.

移徙进展记录在册 Debug,实际上/sql's Migrator 形状, 这样一个程序移动会失去什么: 一 Running migration 按应用迁移情况分列的记录(附加说明) migration_id, (中文(简体) ), migration_name)和一个 Migrations complete 记录(附加说明) latest_migration_id, (中文(简体) ), latest_migration_name) (中文(简体) ). 采用复制行后添加一个 Adopted migrator ledger 记录migrator_table, (中文(简体) ), adopted_count, (中文(简体) ), latest_migration_id, (中文(简体) ), latest_migration_name) (中文(简体) ). 与 mirrorMigratorLedger,用于导入行中的旧程序添加一个 Imported migrator ledger rows 记录migrator_table, (中文(简体) ), imported_count),在运行日志中,另一个进程已经应用了迁移 Imported migration (单位:千美元)migration_id, (中文(简体) ), migration_name改为 Running migration。 。 。 . Cache 通过同一引擎来修改自己的计划,从而释放出相同的记录. 将最小日志级别提高到 Debug 看到他们.

采用因实际移徙而迁移的数据库

Store 保持自己的分类账, _store_migrations。以前通过效果/sql迁移的数据库 Migrator (单位:千美元)SqliteMigrator.layer 记录其历史 effect_sql_migrations 而不是,所以一个 Store 在那座乐园的上面,有一本空的簿子,每一次迁移,都要重新运行 CREATE TABLE 然后失败。 选择通过这个历史:

const StoreLive = Store.layerSqlite({
  filename: "data.db",
  migrations, // { id: 1, name: "initial", up: … }, { id: 2, name: "test_artifacts", up: … }, …
  adoptMigratorLedger: true, // or { table: "my_migrations" } for a non-default Migrator table
});

收养是 由第一层大楼决定的一发在确保分类账之后,在等待迁移之前,在一个被记入的交易中,将每行外国字复制成 _store_migrations 若为 _store_migrations 并存在外国表格,在每一个案件中都记录了决定是在 _store_meta 表格,在同一交易中。 超过收养的移民照常适用。 以后的每个建筑都会看到标记并跳过收养,所以选择可以安全地离开,包括之后 rollback(0):标记能活过回滚,所以重开的重新应用从头到尾的每次迁移,而不是重新采用回滚的不伤历史. 外表已读,从来不作. 一个失败的收养记录没有记录,包括标记, 并在下一个建筑上重新测试。 移徒者从未碰过的数据库完全按照它没有选择的方式,加上标记。 收 养 荣誉回滚历史A级 Store 在没有选项的情况下跑 然后回滚 留下一块墓碑 每一个外行都根据它来判断 由于这一回滚的历史是陈旧的:它没有被采纳,它的迁移又在继续。 自通过以来,修改或写入一行。 对一个外国表格的一行, 回滚没有音轨被拒绝, 键入 。 因此,在这样退后再开始选择是安全的.

匹配是准确的, 任何分歧都会与 StoreError (单位:千美元)operation: "adopt"在记录或应用之前:

  • 每一排外行都需要同样的迁移 id 财务报告和审定财务报表 name。 。 。 . 效果/ sql 的加载器在录制前会剥去数字密钥前缀 : fromRecord是因为 "0001_initial" 作为 id 存储 1名称 "initial"。所以匹配 StoreMigration 实值 { id: 1, name: "initial" } -没有 "0001_initial"。 。 。 .
  • 在外国分类账最高的ID上或以下的每次迁移都必须有外行. 效果/ sql 只运行于它的最新 IDs 之上, 因此它从未记录的更低的 ID 从未被应用过; 现在运行它会偏离数据库实际的历史 。 改为在高水分上方重新编号.

每个收养行 created_at 变成它的 appliedAt. 无区值——SQLite的 current_timestamp 文本( 文本 (2026-10-03 12:00:00)或(或) 2026-10-03T12:00:00 − 读取为世界协调时;不能读取为日期的值以相同的键入的图层失效 StoreError选项继续存在 StoreOptions,所以它的工作通过 layer, (中文(简体) ), layerSqlite 财务报告和审定财务报表 @effected/app是因为 AppStore 但收养是 仅输入 SQLite: 在任何其他方言上, 选项会以图层失败 StoreError (单位:千美元)operation: "adopt") (中文(简体) ).

几个版本共享一个数据库:反映迁移分类账

收养涵盖一个新程序打开的旧数据库。 反之需要自己的选择: 年龄 仍然通过效果/ sql 迁移的程序 Migrator 打开新程序创建的数据库 effect_sql_migrations 空地再运行它的首次迁移. 虽然多个版本共享一个文件,但保持两个分类账步入:

const StoreLive = Store.layerSqlite({
  filename: "registry.db",
  migrations, // { id: 1, name: "initial", up: … } — the prefix-stripped names
  adoptMigratorLedger: true, // old history in, once
  mirrorMigratorLedger: true, // new history out, on every apply and rollback
});

镜子是 匹配行的双向:一个更古老的程序可能在此程序打开之间向前迁移共享文件,它所应用的东西必须被导入,永远不要重运行.

  • 每层建筑 创建 effect_sql_migrations 以有效/ sql 本身的 SQLite DDL 如果它缺席。 这个 进口 每个外行 _store_migrations 缺少匹配 id 和名称的迁移, 然后复制出每个 _store_migrations 排出国外的表格, 所以两个分类账结束相等.
  • 每个应用程序 首先检查移民内部的外国表格。 登记在册的旧程序是进口的, 而不是重新运行, 所以同时开放的新旧程序也是安全的。 否则会插入 (migration_id, name) 与迁移时相同的交易. 每回滚回 删除同一交易中的行.
  • 进口遵循收养规则 拒绝, 键入, 一个外来行, 其编号没有迁移, 名称不匹配, 无法读取 created_at,或者在输入的高水分标记下已知的迁移,但分类账中都没有记录.
  • 倒转会留下历史,镜子打开或关闭. 每个 rollback 墓碑上的未伤者身份 _store_meta记录每个外国分类账的行... ...随着回滚离开它。 因此,一个没有镜像的存储器可以翻回一个数据库中另一个打开的镜像,而不邀请幽灵. 在下一个倒影的建筑上,一个外国一行没有改变,因为回滚已经停滞了,它的迁移再次展开. 在回滚导入后写出一行旧程序 。 一张从未被快照过的表格被拒绝,输入,而不是被猜测. 没有选项的 Store 绝不删除外国行; 该分类账属于旧程序 。 效果/sql's Migrator 在最高记录上运行每个 id,所以之后 rollback(n) 旧程序重新应用了未受伤害的东西,就像一个 Store 重新开放会的.
  • 收养,两个选项默认到同一个表格,即预定的对接:收养先行,再将旧历史复制一次;镜像继续写作.
  • 名称 以 StoreMigration.name,前缀缩放的形式效果/ sql 记录。 效果/ sql 本身只比较 ids .

SQLite 仅: 在任何其他方言上, 选项会以图层失败 StoreError (单位:千美元)operation: "setup") (中文(简体) ).

打开一个数据库的几个进程

一次打开同一文件的进程——平行的 CLI 钩子,一个服务器旁边的 CLI —— 每一个程序都运行待处理的迁移检查,没有一个程序运行 up 两次 每次迁移都会在自己的交易中进行,交易在运行前重新核对分类账 up。在 SQLite 上,驱动程序开始 BEGIN IMMEDIATE,在检查前取出写入锁,所以丢失的过程等待(最多) client.busyTimeout),然后跳过已应用的胜者迁移. 这个担保取决于驾驶员的交易 使用一个写锁,它持有 layerSqlite,用于 Store.layer 只有一个司机的生意也一样.

一个全新的文件的首次打开也覆盖了. 从默认日记中切换新文件到 WAL 需要一锁, SQLite 一次拒绝, 而无需等待 busy_timeout。驱动程序重复切换到 client.busyTimeout laps,所以几个进程在同一时刻创建出相同的文件,全部打开. 在截止日期时仍然拒绝的开关会以打出键盘失败图层 StoreError (单位:千美元)operation: "setup"从来没有缺陷.

不要在重试中包裹整个程序 SQLITE_BUSY同一代码也意味着写作机超速 busyTimeout,而且一个已经工作过的程序不能再次运行.

连接设置: WAL、 繁忙超时、 外国密钥和 onConnect

SqliteClient 每个层块打开一个序列化连接并配置它本身: journal_mode = WAL 除非 client.disableWAL 已经设定,以及 busy_timeout 从 client.busyTimeout (默认为5秒). 两者都是每个连接设置,所以设置通过 client ——永远不要在每次数据库运行一次而不是每次连接的迁移中:

const StoreLive = Store.layerSqlite({ filename: "data.db", migrations, client: { busyTimeout: "10 seconds" } });

外国密钥已经执行 . node:sqlite 打开每个连接 foreign_keys = 1司机留着 所以 REFERENCES … ON DELETE RESTRICT 限制保持不变,没有任何设置.

用于每次连接的设置 client 无法携带, 通过 onConnect它一度与新打开的连接相悖,在分类账得到保证之前,在通过之前和任何迁移之前,以及在任何交易之外,而对于SQLite来说,这种交易很重要。 每个建筑一个连接意味着每个建筑一次连接就是每个建筑一次连接. 故障层为 StoreError (单位:千美元)operation: "setup") (中文(简体) ). Cache.layerSqlite 使用相同的选项, 失败为 CacheError (单位:千美元)operation: "setup":

const StoreLive = Store.layerSqlite({
  filename: "data.db",
  migrations,
  onConnect: (sql) => sql`PRAGMA synchronous = NORMAL`,
});

多个数据库

Store 财务报告和审定财务报表 Cache 是单服务标签。 具有第二个数据库的应用程序在相同的形状上声明自己的密钥并用它构建 layerSqliteAs数字 :

import { Cache, Store } from "@effected/store";
import type { CacheShape, StoreShape } from "@effected/store";
import { Context } from "effect";

class RegistryStore extends Context.Service<RegistryStore, StoreShape>()("myapp/RegistryStore") {}
class TarballCache extends Context.Service<TarballCache, CacheShape>()("myapp/TarballCache") {}

// Every one bound once, to a const.
const RegistryStoreLive = Store.layerSqliteAs(RegistryStore, { filename: "/data/registry.db", migrations });
const TarballCacheLive = Cache.layerSqliteAs(TarballCache, { filename: "/cache/tarballs.db" });

暗号写在赤裸裸的 SqlClient – 一个已存在的寄存层–通过键入商店的客户端 Store.sqlClient(tag)单独提供给这些层 Layer.provide从一秒钟起 SqlClient 合并后,它将蒙蔽:

const RegistrySql = Store.sqlClient(RegistryStore).pipe(Layer.provide(RegistryStoreLive));
const RegistryRepo = RegistryRepoLive.pipe(Layer.provide(RegistrySql)); // RegistryRepoLive needs SqlClient

产出是唯一的关键—— Store 或者说 Cache 内部所建的系统从不暴露——因此,一个密钥层在主层旁边作曲,没有阴影,每个文件都有自己的分类账. 密钥的服务类型必须是 StoreShape 编号 : CacheShape:不相容的形状是编译错误,添加成份(StoreShape & { … }),作为不能转让给 never 因为层层无法提供它们。 检查无法通过方法语法参数bivariance来查看:一个成员重新声明是一种方法,其参数更宽仍会编译.

缓存

Cache 在字符串密钥下存储字节, 并有可选的 TTL 和批量无效的一组标签 :

import { Cache } from "@effected/store";
import { Duration, Effect } from "effect";

const CacheLive = Cache.layerSqlite({ filename: "cache.db", maxEntries: 1000 });

const program = Effect.gen(function* () {
  const cache = yield* Cache;

  yield* cache.set({
    key: "npm:effect",
    value: new TextEncoder().encode(`{"name":"effect"}`),
    contentType: "application/json",
    tags: ["npm", "registry"],
    ttl: Duration.minutes(10),
  });

  const hit = yield* cache.get("npm:effect");
  // Option.some(CacheEntry) while the entry is live; Option.none() once the TTL has passed

  return yield* cache.invalidateByTag("npm");
  // { count: 1, keys: [ "npm:effect" ] }
});

Effect.runPromise(program.pipe(Effect.provide(CacheLive))).then(console.log);

变化者值得了解:

  • 过期是懒. get 财务报告和审定财务报表 has 删除已过期行; prune 整批扫地. 钟被读取 DateTime.now这样 TestClock 在测试中确定过期驱动器——提供 TestClock.layer() 外部 页:1 Effect.provide 提供缓存, 从来没有在它下面, 或者测试身体没有 TestClock 在这方面, TestClock.adjust 死亡是一个缺陷, 和任何你试图到期 永远过期.
  • 驱逐是最近最少的..写入不是LRU读。 与 maxEntries 设置, a set 将同一交易中最古老的写作条目驱逐,直到约束保存,并出版 Evicted 活动.
  • onRemoved 在删除交易中运行. invalidate, (中文(简体) ), invalidateByTag, (中文(简体) ), invalidateAll 财务报告和审定财务报表 prune 每一个都选择在删除承诺之前运行的回调: 输入失败将删除卷回并压制事件, 您的错误类型在签名中存活为 CacheError | E。这就是如何保存缓存条目和它所显示的文件.
  • 钥匙和标记是数据,从来不是SQL. 所有东西都通过标有标签的模板到达 SQLite SqlClient,标记匹配的逃逸 %, (中文(简体) ), _ 财务报告和审定财务报表 \ 在它插入之前, 包含回击的标签 符合它自己的条目 .

贬低到失职

无法构建的缓存会失败其层, 而失败属于整个程序 . Cache.degrading 覆盖任意 Cache 层, 构造失败产生一个工作、 空缓存 : 读错、 写入被丢弃、 清除报告没有被删除、 任何操作都无法失败, 原因在警告时记录一次 .

import { Cache } from "@effected/store";
import { Effect } from "effect";

const CacheLive = Cache.degrading(Cache.layerSqlite({ filename: "cache.db" }));

const program = Effect.gen(function* () {
  const cache = yield* Cache;
  return cache.degraded;
  // false for a live cache; true when construction failed and this is the fallback
});

它之所以选择加入,是因为相反的姿态是合法的——消费者希望缓存问题是致命的,或者希望更窄的per-operation形式,通过不叫它来保持它。 有两个细节使得它值得拥有而不是手写. SQLite 驱动程序报告施工失败——a filename 其父目录不存在的普通案例——作为 缺陷 而不是打出失败,所以只有失败的捕获错过了这个案例的存在。 而中断被故意重新提出 因为它的中断纤维完好无损, 因为一个调用关闭 不是一个破碎的缓存. degraded 是一个平地,而不是一个 CacheEvent 因为退化是在建筑中决定的,在任何订阅者存在之前,事件中心不会重放.

一通电话读取

上面的回路——得到,解码,取出一个错,编码,设定——是缓存的全部原因,所以是每个消费者的单调而不是25行:

import { Cache } from "@effected/store";
import { Effect, Schema } from "effect";

const Members = Schema.Struct({ login: Schema.String });

const program = Effect.gen(function* () {
  const members = yield* Cache.through("team:platform", Schema.fromJsonString(Members), {
    ttl: "1 hour",
    tags: ["team"],
  })(fetchMembersFromApi); // ← only runs on a miss

  return members;
});

schema 编码为 string; 字节的最后一步是 Uint8ArrayFromUtf8,所以编码决定 生活在一个审计地点 而不是每个消费者一个。 使用 Cache.throughVerbose 当呼叫者需要知道该值来自何处时——它返回 { value, hit },这是您想要的打印 (被夹住) 在输出行旁边。 这个 CacheEvent PubSub是进行遥测的正确通道,是读取器已经知道的事实错误的通道.

两项政策使得这个包 而不是你的:

  • 无法解码的值是错的,而不是失败. 这些字节是由您自己的程序的更古老的构建所写的:用户没有引起它,无法在不知道缓存存在的情况下修复它,这里的所有东西按照定义都可以被重新定义. 失败会把他们困在他们看不见的缓存后。 出道时所入所入所出所出所出所出所出所出所出所出所出所入所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所出所入所出所出所出所出所出所出所出所出所出所出所出所出所出所出.
  • CacheError 浮出水面,而不是被吞没. 缓存是添加剂,你很可能想 推动一个破碎的,但这是你的呼吁 Effect.catchTag("CacheError", …),因为无法读取的数据库是一个真实和可报告的条件。 此软件包默认不会隐藏它 .

Uint8 来自Utf8 的箭头

缓存值是字节,而制作这些字节的诚实方式是一出出谋划. 核心船舶 Uint8ArrayFromBase64, (中文(简体) ), Uint8ArrayFromBase64Url 财务报告和审定财务报表 Uint8ArrayFromHex 也没有给UTF -8的 所以 Schema.fromJsonString(schema) 让你 string 并停止一英寸短. Uint8ArrayFromUtf8 这是一英寸:

const Payload = Schema.fromJsonString(Settings).pipe(Schema.encodeTo(Uint8ArrayFromUtf8));

编码 失败 在 UTF-8 上,而不是替换替换字符,所以一个腐败的值仍然可以区别于一个恰好包含的有效值 U+FFFD如果核心船是等同的,就更喜欢那个.

每一次行动都会向 cache.events,一个无约束 PubSub<CacheEvent> —— 说 Hit, (中文(简体) ), Miss, (中文(简体) ), Set, (中文(简体) ), Expired, (中文(简体) ), Evicted, (中文(简体) ), Invalidated, (中文(简体) ), InvalidatedByTag, (中文(简体) ), InvalidatedAll 财务报告和审定财务报表 Pruned。它是故意不绑定的:一个缓慢的订阅者决不能对缓存写入进行回压.

错误

标记手段恢复
StoreError内容 message 读取 Store <operation> failed: <reason>,在原因信息中折叠——对于一个 SQL 失败,数据库本身的文本(UNIQUE constraint failed: t.id),从没有约束值. 存储操作本身的 SQL 失败——分类账簿记账,或关于迁移的查询——或(operation: "adopt"移民分类账与您的移民名单不一致。 卡莉丝 operation 结构 cause。 。 。 .通常致命;报告手术和原因. 一个 adopt 失败导致名称不匹配的迁移 .
StoreMigrationError用户提供的迁移失败, 输入失败 SqlError. 承运人 direction, (中文(简体) ), id, (中文(简体) ), name 结构 cause。 。 。 .报告哪些迁移和方向;分类账保持不变.
CacheError缓存操作的 SQL 失败 。 卡莉丝 operation,可选 key 结构 cause。 。 。 .缓存是缓存——倒回源头通常是正确的.

缺陷不是错的 扔回移民 扔回移民 onRemoved页:1 maxEntries 这不是正整数 : 所有这些都是程序员的错误, 并留在他们所属的缺陷通道上 .

特征

  • Store - 迁移 SqlClient 与 migrate, (中文(简体) ), rollback, (中文(简体) ), status 和生的 client 给自己有意识的询问.
  • Cache - TTL、标记、散装无效,a maxEntries 驱逐政策和a CacheEvent 流出,结束 key → Uint8Array。 。 。 .
  • Cache.degrading ——一个可选入的地层梳理器,将施工失败转化为工作,空缓存 degraded 字段在服务中将两者分开.
  • layer 编号 : layerSqlite 编号 : layerTest 既包括司机不可知性,也包括电池,也有相同的选择.
  • layerSqliteAs 在两者中 —— 在您定义的服务密钥下的第二个数据库, 永远不要阴影主键 .
  • adoptMigratorLedger ——将实时数据库移出效果/sql's Migrator 而不重操旧业.
  • mirrorMigratorLedger ——将效果/sql的分类账分出,同时更古老的版本仍然打开同一个数据库.
  • onConnect 每条连接钩,在分类账前运行,任何交易都外出.
  • Store.sqlClient - 一个按键店的客户端作为赤裸裸的 SqlClient。 。 。 .
  • StoreError, (中文(简体) ), StoreMigrationError, (中文(简体) ), CacheError - 带有底部的标记错误 SqlError 结构,从来没有 reason 绳子.
  • CacheEntry, (中文(简体) ), CacheEntryMeta, (中文(简体) ), CacheRemovalResult, (中文(简体) ), StoreMigrationStatus 归还的记录; entries 列出元数据而不装入 BLOB.
  • 命名的跨度在每一种公共错误方法上(Store.migrate, (中文(简体) ), Cache.get在驾驶员自己的陈述上筑巢.

许可证

麻省理工学院

版本与发布记录

@effected/store 共发布 33 个版本,首次发布于 2026/07/17;过去 12 个月发布了 33 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.25.0latest2026/11/102 天前
0.24.12026/11/103 天前
0.24.02026/11/067 天前
0.23.02026/11/0310 天前
0.22.02026/11/0112 天前
0.21.02026/10/2915 天前

关于 @effected/store 的常见问题

@effected/store 是什么?

持久本地状态 Effect:一个被计划化,迁移的 SQLite 商店和一个带有标记无效的 TTL 缓存, 驱逐, 以及缓存事件流 .

如何安装 @effected/store?

在项目目录运行 npm install @effected/store,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(~4.1.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effected/store 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effected/store 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effected/store 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@effected/store 可以商用吗?

@effected/store 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effected/store 的下载量有多少?

根据 npm 官方下载统计,@effected/store 在 2026/10/13 – 2026/11/11 这 30 天内被下载 8,321 次,日均约 277 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effected/store 还在维护吗?

最新版本 0.25.0 发布于 2026/11/10(2 天前);至今共发布 33 个版本,其中过去 12 个月发布了 33 个;首次发布于 2026/07/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。