MoonCRDTKit

    A MoonBit CRDT toolkit for offline-first state merge and collaborative data structures.

    crdt
    offline
    sync
    merge
    collaboration
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    3 months ago
    Downloads
    31

    No dependencies

    #MoonCRDTKit

    MoonCRDTKit 是一个面向 MoonBit 的离线协同 CRDT 状态合并基础库。

    #安装与最小接入

    在你的 MoonBit 项目根目录安装已发布包:

    moon add ddd-1234d/MoonCRDTKit

    最小离线任务同步示例:

    import { "ddd-1234d/MoonCRDTKit" @crdt }

    let left = @crdt.ReplicaState::new(1)
    left.add_task("draft")
    let right = @crdt.ReplicaState::new(2)
    right.add_task("review")
    let converged = left.merge(right)
    println(converged.tasks.contains("draft"))
    println(converged.tasks.contains("review"))

    ReplicaState 为每次本地操作分配副本内单调序号;ORSet::add_from 使用 (replica, sequence) 作为 Dot 身份,避免不同副本相同序号的并发添加碰撞。旧的 ORSet::add 仅保留给单写者兼容场景。

    项目聚焦弱网与离线场景下的多端状态收敛,提供向量时钟、LWW 寄存器、G-Counter、PN-Counter、OR-Set、变更日志和同步摘要等能力,适合协作文档、离线表单、边缘设备状态同步、多人编辑器和教学算法示例。

    #创新点

    • 离线优先:副本可以先在本地修改状态,恢复连接后再合并。
    • 无中心合并:不依赖服务端仲裁,多个副本按同一规则收敛。
    • 因果可解释:向量时钟和变更日志记录状态来源,便于同步调试。
    • 组合式 CRDT:寄存器、计数器、集合和摘要可以组合成应用状态。
    • 增量同步积木:提供 DeltaBatch 与 SyncPlan,便于上层协议判断是否需要交换状态。

    #与已有 MoonBit CRDT 工作的关系

    MoonCRDTKit 不是对 mizchi/crdt_db 或 mizchi/converge 的重复实现。mizchi/converge 更接近 EG-Walker inspired Local-First DB Sync Engine,关注数据库同步、事件图、operation log、WASM/JS SDK 和后端同步部署。MoonCRDTKit 的边界更小:只做状态型 CRDT 原语和轻量同步辅助,不提供数据库 CRUD、网络传输、WASM Component、Cloudflare/Deno 后端或 BFT 签名层。

    MoonBit 官方文章《Implementing CRDT Algorithms with MoonBit and Building Real-time Collaborative Applications》主要介绍 OT、RGA、EG-Walker、Lomo 等协同文本算法路线。MoonCRDTKit 不实现文本 CRDT,也不做 RGA/EG-Walker/Lomo 富文本编辑器;它面向离线表单、任务集合、计数器、边缘设备状态和教学示例这类非文本状态同步场景。

    因此,本项目定位是 MoonBit 生态中的 CRDT 基础积木库:上层项目可以在它之上自行选择存储、网络协议和应用模型。

    #当前能力

    • VectorClock:记录副本计数,支持 tick、merge、dominates、concurrent 判断。
    • LwwRegister:最后写入胜出寄存器,支持时间戳和副本号确定性裁决。
    • GCounter:只增计数器,按副本取最大值合并。
    • PNCounter:正负计数器,支持离线增减后收敛。
    • ORSet:观察删除集合;多副本调用 add_from(element, replica, sequence),以 (replica, sequence) 区分并发添加,支持离线添加、观察后删除和合并。
    • ChangeLog:记录副本变更事件并按事件 id 去重。
    • DeltaBatch:根据对端向量时钟提取缺失事件,支持幂等应用。
    • SyncSummary:输出副本同步摘要,便于 CLI、调试面板和状态心跳使用。
    • SyncPlan:比较两个摘要中的完整 CRDT 状态,而非只比较数量;可识别“元素数相同但内容不同”的副本,判断是否需要 push / pull / 双向交换。
    • ReplicaState:将因果时钟、PN-Counter、OR-Set 与 ChangeLog 组合为可直接用于离线任务和计数场景的副本状态。

    #快速示例

    let left = @MoonCRDTKit.VectorClock::new()
    let right = @MoonCRDTKit.VectorClock::new()
    left.tick(1)
    right.tick(2)
    let merged = left.merge(right)

    let counter = @MoonCRDTKit.PNCounter::new()
    counter.increment(1, amount=8)
    counter.decrement(2, amount=3)
    let log = @MoonCRDTKit.ChangeLog::new()
    ignore(log.append(@MoonCRDTKit.ChangeEvent::new(1, 1, "set", "title")))
    let delta = log.delta_after(left, origin=1)

    println(merged.concurrent_with(left))
    println(counter.value())
    println(delta.to_json())

    运行演示:

    moon run cmd/main

    运行测试:

    moon test

    #设计原则

    1. 核心库保持后端中立,不依赖网络、数据库、浏览器或平台 API。
    2. 所有合并函数都保持确定性,便于在多端同步中复现结果。
    3. 用简单 MoonBit 结构体表达 CRDT 状态,优先保证教学性和可测试性。
    4. 不和数据库同步引擎、协同文本编辑器绑定,避免污染基础库抽象。

    #仓库

    更完整的竞品/参考关系说明见 docs/RELATED_WORK.md。

    ChangeEvent

    pub(all) struct ChangeEvent {
    replica : Int
    sequence : Int
    kind : String
    target : String
    } derive(Eq,
    Debug
    )

    ChangeEvent::id

    fn ChangeEvent::id(self : ChangeEvent) -> Int

    ChangeEvent::new

    fn ChangeEvent::new(replica : Int, sequence : Int, kind : String, target : String) -> ChangeEvent

    ChangeEvent::same_identity

    fn ChangeEvent::same_identity(self : ChangeEvent, other : ChangeEvent) -> Bool

    ChangeLog

    pub(all) struct ChangeLog {
    events : Array[ChangeEvent]
    } derive(
    Debug
    )

    ChangeLog::append

    fn ChangeLog::append(self : ChangeLog, event : ChangeEvent) -> Bool

    ChangeLog::apply_delta

    fn ChangeLog::apply_delta(self : ChangeLog, delta : DeltaBatch) -> ChangeLog

    ChangeLog::delta_after

    fn ChangeLog::delta_after(self : ChangeLog, known : VectorClock, origin? : Int) -> DeltaBatch

    ChangeLog::length

    fn ChangeLog::length(self : ChangeLog) -> Int

    ChangeLog::merge

    fn ChangeLog::merge(self : ChangeLog, other : ChangeLog) -> ChangeLog

    ChangeLog::new

    fn ChangeLog::new() -> ChangeLog

    ClockEntry

    pub(all) struct ClockEntry {
    replica : Int
    counter : Int
    } derive(Eq,
    Debug
    )

    ClockEntry::new

    fn ClockEntry::new(replica : Int, counter : Int) -> ClockEntry

    CounterEntry

    pub(all) struct CounterEntry {
    replica : Int
    value : Int
    } derive(Eq,
    Debug
    )

    CounterEntry::new

    fn CounterEntry::new(replica : Int, value : Int) -> CounterEntry

    DeltaBatch

    pub(all) struct DeltaBatch {
    origin : Int
    events : Array[ChangeEvent]
    } derive(
    Debug
    )

    DeltaBatch::is_empty

    fn DeltaBatch::is_empty(self : DeltaBatch) -> Bool

    DeltaBatch::length

    fn DeltaBatch::length(self : DeltaBatch) -> Int

    DeltaBatch::new

    fn DeltaBatch::new(origin : Int) -> DeltaBatch

    DeltaBatch::to_json

    fn DeltaBatch::to_json(self : DeltaBatch) -> String

    GCounter

    pub(all) struct GCounter {
    entries : Array[CounterEntry]
    } derive(
    Debug
    )

    GCounter::get

    fn GCounter::get(self : GCounter, replica : Int) -> Int

    GCounter::increment

    fn GCounter::increment(self : GCounter, replica : Int, amount? : Int) -> Unit

    GCounter::merge

    fn GCounter::merge(self : GCounter, other : GCounter) -> GCounter

    GCounter::new

    fn GCounter::new() -> GCounter

    GCounter::set

    fn GCounter::set(self : GCounter, replica : Int, value : Int) -> Unit

    GCounter::value

    fn GCounter::value(self : GCounter) -> Int

    LwwRegister

    pub(all) struct LwwRegister {
    value : String
    timestamp : Int
    replica : Int
    } derive(Eq,
    Debug
    )

    LwwRegister::assign

    fn LwwRegister::assign(self : LwwRegister, value : String, timestamp : Int, replica : Int) -> LwwRegister

    LwwRegister::merge

    fn LwwRegister::merge(self : LwwRegister, other : LwwRegister) -> LwwRegister

    LwwRegister::new

    fn LwwRegister::new(value : String, timestamp : Int, replica : Int) -> LwwRegister

    LwwRegister::to_json

    fn LwwRegister::to_json(self : LwwRegister) -> String

    ORSet

    pub(all) struct ORSet {
    entries : Array[ORSetEntry]
    } derive(
    Debug
    )

    ORSet::add

    fn ORSet::add(self : ORSet, element : String, dot : Int) -> Unit

    Compatibility helper for single-writer sets. Multi-replica callers should use add_from.

    ORSet::add_from

    fn ORSet::add_from(self : ORSet, element : String, replica : Int, dot : Int) -> Unit

    Adds with an explicit replica-scoped sequence number. The pair (replica, dot) is the identity of an observed add.

    ORSet::contains

    fn ORSet::contains(self : ORSet, element : String) -> Bool

    ORSet::merge

    fn ORSet::merge(self : ORSet, other : ORSet) -> ORSet

    ORSet::new

    fn ORSet::new() -> ORSet

    ORSet::remove

    fn ORSet::remove(self : ORSet, element : String) -> Unit

    ORSet::size

    fn ORSet::size(self : ORSet) -> Int

    ORSetEntry

    pub(all) struct ORSetEntry {
    element : String
    replica : Int
    dot : Int
    removed : Bool
    } derive(Eq,
    Debug
    )

    ORSetEntry::new

    fn ORSetEntry::new(element : String, replica : Int, dot : Int, removed? : Bool) -> ORSetEntry

    PNCounter

    pub(all) struct PNCounter {
    positive : GCounter
    negative : GCounter
    } derive(
    Debug
    )

    PNCounter::decrement

    fn PNCounter::decrement(self : PNCounter, replica : Int, amount? : Int) -> Unit

    PNCounter::increment

    fn PNCounter::increment(self : PNCounter, replica : Int, amount? : Int) -> Unit

    PNCounter::merge

    fn PNCounter::merge(self : PNCounter, other : PNCounter) -> PNCounter

    PNCounter::new

    fn PNCounter::new() -> PNCounter

    PNCounter::value

    fn PNCounter::value(self : PNCounter) -> Int

    ReplicaState

    pub(all) struct ReplicaState {
    replica : Int
    clock : VectorClock
    counter : PNCounter
    tasks : ORSet
    log : ChangeLog
    } derive(
    Debug
    )

    A complete, mergeable replica state for offline task and counter workloads.

    ReplicaState::add_task

    fn ReplicaState::add_task(self : ReplicaState, task : String) -> Unit

    Adds a task using a replica-scoped OR-Set dot, avoiding cross-replica collisions.

    ReplicaState::decrement

    fn ReplicaState::decrement(self : ReplicaState, amount? : Int) -> Unit

    Records a counter decrement that can later be merged with an offline peer.

    ReplicaState::increment

    fn ReplicaState::increment(self : ReplicaState, amount? : Int) -> Unit

    Records a counter increment that can later be merged with an offline peer.

    ReplicaState::merge

    fn ReplicaState::merge(self : ReplicaState, other : ReplicaState) -> ReplicaState

    Merges independently mutated replica state and operation history.

    ReplicaState::new

    fn ReplicaState::new(replica : Int) -> ReplicaState

    Creates an offline-first state machine whose mutations carry causal metadata.

    ReplicaState::remove_task

    fn ReplicaState::remove_task(self : ReplicaState, task : String) -> Unit

    Removes all adds for a task that this replica has observed.

    ReplicaState::summary

    fn ReplicaState::summary(self : ReplicaState) -> SyncSummary

    Produces an exact-state sync summary for peer exchange planning.

    SyncPlan

    pub(all) struct SyncPlan {
    source : Int
    target : Int
    needs_push : Bool
    needs_pull : Bool
    source_events : Int
    target_events : Int
    } derive(Eq,
    Debug
    )

    SyncPlan::should_exchange

    fn SyncPlan::should_exchange(self : SyncPlan) -> Bool

    SyncPlan::to_json

    fn SyncPlan::to_json(self : SyncPlan) -> String

    SyncSummary

    pub(all) struct SyncSummary {
    replica : Int
    clock : VectorClock
    counter : PNCounter
    set : ORSet
    log : ChangeLog
    clock_entries : Int
    counter_value : Int
    set_size : Int
    log_events : Int
    } derive(
    Debug
    )

    SyncSummary::new

    fn SyncSummary::new(replica : Int, clock : VectorClock, counter : PNCounter, set : ORSet, log : ChangeLog) -> SyncSummary

    SyncSummary::plan_with

    fn SyncSummary::plan_with(self : SyncSummary, remote : SyncSummary) -> SyncPlan

    SyncSummary::to_json

    fn SyncSummary::to_json(self : SyncSummary) -> String

    VectorClock

    pub(all) struct VectorClock {
    entries : Array[ClockEntry]
    } derive(
    Debug
    )

    VectorClock::concurrent_with

    fn VectorClock::concurrent_with(self : VectorClock, other : VectorClock) -> Bool

    VectorClock::dominates

    fn VectorClock::dominates(self : VectorClock, other : VectorClock) -> Bool

    VectorClock::get

    fn VectorClock::get(self : VectorClock, replica : Int) -> Int

    VectorClock::merge

    fn VectorClock::merge(self : VectorClock, other : VectorClock) -> VectorClock

    VectorClock::new

    VectorClock::set

    fn VectorClock::set(self : VectorClock, replica : Int, counter : Int) -> Unit

    VectorClock::tick

    fn VectorClock::tick(self : VectorClock, replica : Int) -> Unit

    Source Files