#node #text-node #broadcast #db #distributed #connected #pages

assemblage_db

用于连接和重叠页面的分布式文档/图数据库

1个不稳定版本

0.1.0 2021年8月18日

#2278数据库接口


assemblage_view 中使用

AGPL-3.0

220KB
3.5K SLoC

用于连接页面的分布式文档/图数据库

AssemblageDB 是一个事务性的高级数据库,用于连接的页面、笔记、文本和其他媒体网络。将其视为一个 个人网络,但易于编辑,比网络有更多的连接和更好的导航。它在高级意义上的定义是一个类似于HTML但简单得多,并且具有图形化双向链接而不是树形单向跳转链接的文档模型。数据模型是

  • 面向文档的:支持嵌套文档,没有固定模式
  • 基于图:文档可以有多个父级,形成一个有向的、可能为循环的图

特性

  • 版本控制:旧值在合并前仍然可访问
  • 事务性:通过 MVCC 隔离快照
  • 存储无关:支持本地(文件)和wasm(IndexedDB)目标
  • 索引:维护一个自动索引以进行相似度/重叠搜索
  • 分布式:节点可以作为远程广播进行发布/订阅

数据模型

AssemblageDB 中的节点可以是原子的(例如一行文本)或嵌套的,可以是包含多个子项的 列表 或仅包含单个子项的 样式 节点。 List 节点有一个 布局,它控制子项之间的相对布局,而 Styled 节点有零个或多个 块样式行样式,它们控制其(可能嵌套的)子项的显示方式。布局和样式的示例

  • Layout::Chain:将子项布局为连续的行内跨度链。对于两个文本子项 "foo" 和 "bar",链将被显示为 "foobar"。
  • Layout::Page:将子项布局为页面上的块,垂直分隔新行。对于两个文本子项 "foo" 和 "bar",页面将被显示为两行,第一行包含 "foo",第二行包含 "bar"。
  • SpanStyle::Italic:一个行内样式,将子项 "foo" 显示为 "foo"
  • BlockStyle::Heading:一种块样式,将子元素“foo”显示在其自己的块中,字体大小更大。

节点始终是spanblock。默认情况下,文本节点被视为span,如果使用如SpanStyle::ItalicSpanStyle::Boldspan样式进行样式化,则保持为span。然而,一组样式中的单个块样式(如BlockStyle::Heading)始终是“传染性”的,并将同时使用SpanStyle::ItalicBlockStyle::Heading样式的文本节点“foo”转换为块。同样,布局控制列表是否以spanblock显示:Layout::Chain将列表转换为span,而Layout::Page将列表转换为一系列块。

强制警告

AssemblageDB仍处于实验阶段。不言而喻,它不是一个经过实战考验的生产就绪数据库,并且可能随时会吃掉您的所有数据。如果您需要持久化生产数据,请使用真正的数据库。

示例

let storage = FileStorage::open("db1").await?;
let db = Db::open(storage).await?;

// Nodes support layouts and styles, for example as a page of blocks...
let page1_id = tx!(|db| {
    db.add(Node::list(
        Layout::Page,
        vec![
            Node::styled(BlockStyle::Heading, Node::text("A Heading!")),
            Node::text("This is the first paragraph."),
            Node::text("Unsurprisingly this is the second one..."),
        ],
    ))
    .await?
});

// ...or as inline spans that are chained together:
let page2_id = tx!(|db| {
    db.add(Node::list(
        Layout::Page,
        vec![Node::list(
            Layout::Chain,
            vec![
                Node::text("And this is the "),
                Node::styled(SpanStyle::Italic, Node::text("last")),
                Node::text(" paragraph..."),
            ],
        )],
    ))
    .await?
});

// Documents can form a graph, with nodes keeping track of all parents:
tx!(|db| {
    db.add(Node::list(Layout::Page, vec![page1_id, page1_id])).await?;
    assert_eq!(db.parents(page1_id).await?.len(), 2);
    assert_eq!(db.parents(page2_id).await?.len(), 0);
});

// All text is indexed, the DB supports "overlap" similarity search:
tx!(|db| {
    let paragraph1_id = db.get(page1_id).await?.unwrap().children()[1].id()?;
    let paragraph3_id = db.get(page2_id).await?.unwrap().children()[0].id()?;
    let overlaps_of_p1 = db.overlaps(paragraph1_id).await?;
    assert_eq!(overlaps_of_p1.len(), 1);
    assert_eq!(overlaps_of_p1[0].id, paragraph3_id);
    assert!(overlaps_of_p1[0].score() > 0.5);
});

// Nodes (with all their descendants) can be published globally...
let broadcast = tx!(|db| { db.publish_broadcast(page1_id).await? });

// ...and the broadcast can then be fetched in another remote DB:
let other_storage = FileStorage::open("db2").await?;
let db2 = Db::open(other_storage).await?;
tx!(|db2| {
    let paragraph_id = db2
        .add(Node::text("This is the first paragraph, right?"))
        .await?;
    db2.add(Node::list(Layout::Page, vec![paragraph_id]))
        .await?;
    // The DB is empty except for this paragraph, so no overlaps:
    assert_eq!(db2.overlaps(paragraph_id).await?.len(), 0);
    // But when the broadcast paragraph is fetched, there is an overlap:
    db2.fetch_broadcast(&broadcast.broadcast_id).await?;
    assert_eq!(db2.overlaps(paragraph_id).await?.len(), 1);
});

依赖项

~3–17MB
~220K SLoC