编年史
编年史(英語:listle)是一所以仅以附加记录为主的嵌入式商店,有复制于写作的分行和时间出行. 被命名的状态作为该日志的更改而保留,因此其中任何一种状态都可以像以前一样被读取,每个分支都会改变自己的状态. 大型数据或共享数据在一个内容地址的blob商店中。 核心是Rust图书馆,Node.js通过它使用 @animalabs/chronicle。 。 。 .
编年史是康乃馨的存储层: 代理框架, (中文(简体) ), 上下文管理器 财务报告和审定财务报表 连接主机 保存他们的记录,并在其中声明.
安装
npm install @animalabs/chronicle
该套件包括Linux(x64和arm64,glibc),macOS(x64和arm64)和Windows(x64)的预建本土模块. 在另一个平台上,从源头构建模块(参见 建设和贡献) (中文(简体) ).
先看一眼,再看一眼
在您安装了软件包的新目录中, 请保存为 example.mjs 运行 node example.mjs。 。 。 .
import { JsStore } from '@animalabs/chronicle';
// A new store: registerState and createBranch throw for a state or branch
// that already exists, so run this once, or delete ./my-store first.
const store = JsStore.openOrCreate({ path: './my-store' });
// Records: an append-only log. IDs are decimal strings.
const asked = store.appendJson('message', { from: 'user', text: 'Hello' });
const answered = store.appendJsonWithLinks('message', { from: 'agent', text: 'Hi!' }, { causedBy: [asked.id] });
store.getEffects(asked.id); // [answered.id]
// States: named values kept as changes in the log.
store.registerState({ id: 'messages', strategy: 'append_log' });
store.appendToStateJson('messages', { text: 'Hello' });
const before = store.currentSequence();
store.appendToStateJson('messages', { text: 'Hi!' });
store.getStateJson('messages'); // [{ text: 'Hello' }, { text: 'Hi!' }]
store.getStateJsonAt('messages', before); // [{ text: 'Hello' }]
// Branches: copy-on-write. A store starts with one branch, `main`.
store.createBranch('experiment');
store.switchBranch('experiment');
store.appendToStateJson('messages', { text: 'Only on experiment' });
store.switchBranch('main');
store.getStateJson('messages'); // still the two items
// Blobs: bytes stored under their SHA-256 hash.
const hash = store.storeBlob(Buffer.from('large data'), 'text/plain');
store.getBlob(hash); // the same bytes
store.close();
每一种方法都是同步的: 它运行在调用线条上 pollSubscriptionTimeout 屏蔽它直到超时。 包裹 index.d.ts从 src/napi.rs 列出所有方法及其选项.
店里有这么多东西
记录
每个记录都有一个ID,一个序列号,一个类型,一个有效载荷,一个时间戳,以及两个与其他记录的链接列表: causedBy 为它的原因 linkedTo 对于任何其他关系. append 以有效载荷为字节并 appendJson 作为JSON,和 WithLinks 每个链接的窗体. getEffects(id) 记录 causedBy 记录的名字,和 getLinksTo(id) (b) 其 linkedTo 已经.
getRecord 用身份证读取任何记录 getRecords(ids) 在一个呼叫中,按给出的顺序读取几个 null 对于一个没有记录的ID. getRecordIdsByType 列出一个类型在所有分支中的记录ID,按照上升的ID顺序,所以最后一个是最新的;ID是十进制字符串,所以把它们作为数字比较. 记录一个被撕裂了的尾巴滴出在开放, 和他们的ID再次发布到新的记录, 可能是另一种类型。 所以在坠机前的最后一张ID可以在之后命名不同的记录,然后取出一个伯爵就可以跳过新的记录. query 读取当前分支所附加的记录,最多按序列范围和类型分列 limit 他们中有10 000人.
国家
一个状态是一个被命名的值,其每个变化都是日志中的记录. 这就是让你读它 因为它在早期序列(getStateAt, (中文(简体) ), getStateJsonAt),并让每个分支分别修改. 以三种战略之一登记一个国家:
-
snapshot: 每个更改都存储整个值 (setState, (中文(简体) ),setStateJson) (中文(简体) ). -
append_log: 您附加到的列表( s)appendToState, (中文(简体) ),appendToStateJson),按索引编辑(editStateItem)和按范围调整(redactStateItems) (中文(简体) ). 更改被存储为操作 。 一跑一跑的附子会得到周期性的三角洲快照,所以一读不会回放整个历史,随着列表的增长,完整的快照会更加相去甚远.deltaSnapshotEvery(默认100个)和fullSnapshotEvery(10) 设定基准间隔.getStateLen, (中文(简体) ),getStateSlice, (中文(简体) ),getStateTail财务报告和审定财务报表getStateItemJson读取列表的一部分,以及getStateItemsJson(id, ordinals)在一个呼叫中逐项读取.appendManyToStateJson(id, items)以一个顺序将多个项目作为单项记录,然后以firstOrdinal,它的第一个项目是: 清单的长度就在批次之前,因为getStateLen一旦批次数到项目(项目)i处于firstOrdinal + i;在一个其值不是JSON阵列的列表中,或者持有一个不属于JSON的物品,批量取出所储存的计数,被记录下来,然后状态未能实现,就像一个appendToState,所以项目不能 读在那些指数。 记录错误的0.1.0到0.4.x的计数,在开通后,在每家分店的第一批清单中重新列出compactState。阅读器将每个项目视为单独附件,但历史读取或按顺序切出的一个分支保留或降下批次整体,因为它有一个序列,批次算作向快照间隔的一个操作。 坠机会保持或失去一个批量的完整, 从来没有一部分。 商店的第一批更改格式:见MANIFEST下级 磁盘。 。 。 .appendToStateJsonWithIdentity(id, item, idField, sequenceField)附加一个 JSON 对象,并附有它写在这两个键上的记录的ID和顺序,因此,该物品在日志中带有它自己的位置.appendManyToStateJsonWithIdentity(id, items, idField, sequenceField)是其分批的形式: 分批的每件物品都带有分批记录的ID和序列, 因此在分批物品的序列中切出一个分支保存整个批次, 在投放前一个分支保存一个序列. 它抛出和写什么idField财务报告和审定财务报表sequenceField名称为相同的密钥; 单式为这样的一对并留下该密钥的序列, 高于 id. -
tree: 从路径到取名blobs的条目的地图(treeSet, (中文(简体) ),treeRemove, (中文(简体) ),treeGet, (中文(简体) ),treeList, (中文(简体) ),treeDiff等),默认情况下每50个操作有一个三角洲快照.
店里有登记 所以又开了一家店, registerState 扔出“ 状态已经存在 ” , 用于以前注册的状态 ; listStates() 列出他们.
registerStateFieldIndex 保留列表项目中的一个字段的索引,用于范围查询和平等查询(queryStateIndexRange, (中文(简体) ), queryStateIndexEq) (中文(简体) ). 它属于它注册的分行: 更改另一个分行的状态会掉下它, 而查询返回 null 直到它再次注册.
分支
createBranch(name, from) 在首端启动分支 from,或当 from 被排除在外. createBranchAt(name, from, sequence) 以较早的顺序开始 from一个新的分支开始于其母国的当时。 记录和状态变化到当前分支, 该分支 switchBranch 选择.
块
storeBlob 将字节保存在他们的SHA-256散列下,所以同一内容存储一次,以及 getBlob 读回来. getBlob 检查文件的校验和散列, 并扔出一个不全的文件; 当商店不再持有存储中的blob 时, 同样的内容会再次替换这样的文件 。 超过65,535字节的内容类型被拒绝. 一个开放的商店保留它最近写的或读取的blobs作为内存(blobCacheSize 其中,1000个默认)和从那里得到的答案,所以从店外对一个blob文件造成的损坏,只显示在blob离开缓存或商店重新开放时.
订阅
subscribe(config) 返回一个ID。 装扮的 filter 指定记录类型和接下来的状态,并打开您想要的每一种事件 includeRecords, (中文(简体) ), includeStateChanges 财务报告和审定财务报表 includeBranchEvents,除非设定,否则关闭。 还需要一个 branch现场活动可以从任何分支到达。 事件只涉及一个 已经赶上的订阅 catchUpSubscription 就是让事情发生 与一个 fromSequence 在配置中,它首先按当时的状态发送每个后续状态(或现在的状态,如果当时不存在),并重放当前分支自此所附加的匹配记录. 一个大于配置的状态 maxSnapshotBytes 剪切( 默认为 10 MiB) 以适应: 列表只发送其最新项目, 其它任何值发送 null。该活动 data一个JSON弦,然后 truncated 设定和列表, from_index 财务报告和审定财务报表 total_length 说哪部分到了 变化 subscribe 财务报告和审定财务报表 catchUpSubscription 只有在重播时才能到达 之后,每个 pollSubscription 返回下一个事件,或 null 当没有人等待的时候 pollSubscriptionTimeout 等待一个超时。 用户允许 bufferSize 事件(默认情况下为1,000起) 堆积起来的无保值被删除.
磁盘
商店是一个目录。 日志是 records.log,州链的头在 state.bin字段索引 state-indexes.bin,在 branches.bin下方的块 blobs/。 。 。 . MANIFEST 命名格式版本: 1, 每次发行打开它, 直到它移动到 2 , 就在商店的第一个批次附件之前(appendManyToStateJson 或者说 appendManyToStateJsonWithIdentity,或 AppendMany 从Rust)所写的. 从那时起,它就保持2, 即使这批失败, 并释放那些 先前的批次附件 拒绝打开商店 MANIFEST (对于一个链接的,持有它的目标的)是同步的(在Windows上,这个进程可能不打开该目录,或者在一个不同步目录的系统上,一个功率损失可以撤销移动到版本2并保留批次;见"在更改日志中断开";如果移动本身失败,调用和批次没有被记录下来,如果移动失败后只有目录同步,它也会抛出 MANIFEST 已经在第2版了. create 拒绝已经持有商店的目录, 因此它从来不写商店的 MANIFEST 返回到 1: 打开可能存在的商店 openOrCreate 或者说 open。 。 。 . LOCK 在商店开放时持有. LOCK 必须是正则文件, 或缺失: 打开并创建拒绝链接或其他任何内容 。 不要删除或替换 LOCK 当商店打开时: 下一个打开会锁起新文件, 并打开商店 .
-
一次一个过程. 店开着,又开着
open它与“ Store 被另一个进程锁定” 失败。 与LOCK一个正则文件,一个由于任何其他原因而失败的锁会因 I/O 错误而失败. -
杜尔比特. 日志被冲入每100个记录的磁盘中,因此机器崩溃可能损失到最后100个.
sync()财务报告和审定财务报表close()冲去保存state.bin财务报告和审定财务报表branches.bin,每个字写入一个新的文件,然后替换旧文件,所以在sync()保存之前或之后保存每个文件。 注册状态,改变策略,并在返回前创建、删除或切换分支.storeBlob将 blob 写入新文件blobs/,将其冲出并重命名为返回前的位置,所以一个崩溃会留下文件整体,或者像呼叫前一样(缺席,或者它正在替换被损坏的复制件). 当一个进程被杀死或提前退出时close()(坠机,Ctrl-C等信号,或process.exit()),下个开口截取了部分被写入的上个记录,将每个分支的头抬到日志上,并重放状态自上个保存后更改了日志所持有,但保存的分支上保存的则不持有.recovery()报告它发现了什么。 打开也会删除新文件( O).chronicle-blob-*内blobs/在重名前被切断的blob商店。 一旦日志的同步失败,sync()丢出错误开始Durability lost:直到店铺重新开业, 上面的电话也一样,在返回之前。 打电话close()离开之前,和sync()更改后,您不能输. -
看着里面. 存储为二进制.
tools/有读取的 Python 脚本records.log{\fn黑体\fs20\shad2\2ah82\3ah20\4ah33\fscx95\3ch592001\be1}直接地,没有人会拿走..LOCK在经营写作机前关闭商店.trim-records-log.py <store>/records.log报告一条被撕裂的尾巴 用它修剪--apply过道后.scan-delta-poison.py检查append_log毒药三角洲的快照,rebuild-state-index.py重写state.bin从日志,和chain-report.py报告命名状态的更新链messages。这三人读取状态变化为 JSON, 0: 3: 0 之前的编码, 并用一个自此写出的错误止步, 这是 MessagePack .
server/是快取的中间软件, Vue 应用程序在其中ui/浏览商店 Npm 包里也没有.
从Rust取出
图书馆是 chronicle 装入箱子 src/它不在箱子上, io,在哪里 chronicle 这是另一个项目,所以依赖它从Git:
[dependencies]
chronicle = { git = "https://github.com/anima-research/chronicle" }
serde_json = "1"
use chronicle::{RecordInput, Store, StoreConfig};
use serde_json::json;
fn main() -> chronicle::Result<()> {
let store = Store::open_or_create(StoreConfig {
path: "./my-store".into(),
..Default::default()
})?;
let record = store.append(RecordInput::json("message", &json!({ "text": "Hello" }))?)?;
println!("record {} at sequence {}", record.id.0, record.sequence.0);
Ok(())
}
examples/store_probe.rs量度状态读取在真实商店的副本上 也可以直接使用箱子 节点绑定是 src/napi.rs,与 napi-bindings 特性.
建设和贡献
捐款额 涵盖建筑和测试cargo test,本地模块的构造,和 npm test),拉动请求应包含什么,更改日志片段在 changelog.d/并释放.
文档
- 文献/日志:分枝原木的代数规格,和嵌入原木的原木.
- 文档/rfc-状态-原产地- 关于记录记录的RFC草案,记录产生了一个记录的每个项目
append_log状态. - 变动情况:每次发行都改变了什么.
许可证
麻省理工学院,作为 package.json 财务报告和审定财务报表 Cargo.toml 声明.