Skip to content

v0.41.0-beta.5

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Sep 17:07
· 107 commits to develop since this release
fa8fa59
  • Local full-text search in atomic_lib (lib/src/search/): a KV inverted
    index on the existing redb / sled / BTreeMap (OPFS) store. Indexes title,
    description, and Loro document body on every commit; queries AND tokens,
    rank with BM25, and match 1-edit prefix-fuzzy (avacado finds avocado).
    Exact property:"value" filters reuse the PropValSub index (empty q +
    isA is the file picker); there is no query language and no key escaping.
    Db / AtomicNode / WASM ClientDb.search expose it. Hosted /search
    is the same engine — Tantivy is removed.
    The browser uses this index (MiniSearch is gone) and merges with /search
    when online. Scale benches:
    cargo bench -p atomic_lib --bench search_bench --features db-redb.
    See planning/local-search.md.

  • Live collaboration is one binary frame on both transports. Edits in
    progress, cursors and drive presence cross the WebSocket as
    EPHEMERAL (0x40), the frame the Iroh link already used, in both
    directions; the text frames LORO_SYNC_UPDATE, LORO_EPHEMERAL_UPDATE
    and PRESENCE_UPDATE (base64 in JSON) are removed, the four subscribe /
    unsubscribe text frames stay. The server ignores the agent a client puts
    in the frame and stamps the connection's authenticated identity on every
    copy it fans out. Between nodes the payload is now the raw bytes rather
    than their base64 text, so cursors from a pre-2026-09-04 peer do not
    decode on this build (and vice versa); nothing persistent is affected.
    WsClient::send_loro_sync_update / send_loro_ephemeral_update /
    send_presence_update send the binary frame. Capability ephemeral.

  • Drive sync is one binary frame on both transports. The browser's
    hash-first probe and its reduced reconcile now arrive as SYNC (0x30)
    with "probe": true or "subjects": [...] in the JSON tail
    (encode_sync_probe, encode_sync_filtered), handled by the shared sync
    engine; a stale probe is answered with the new SYNC_RESEND (0x38).
    The text SYNC_VV request, its text SYNC_RESEND <drive> answer and the
    server handler's private copy of the reconcile are removed. Capability
    sync-probe. WsClient reports WsMessage::SyncResend.

  • One WebSocket subscription frame. SUB <subject> registers a
    drive-wide subscription when the subject is a drive and a per-resource one
    otherwise; the per-resource text frame SUBSCRIBE <subject> (which also
    registered a spurious drive fan-out entry that delivered URL subjects'
    commits twice) and the filter subscription SUBSCRIBE_QUERY (sent by
    nothing outside the Rust integration tests) are removed, along with the
    commit monitor's third subscription map, the MembershipNotification
    fan-out and WsClient::subscribe_query. WsClient::subscribe_resource
    sends SUB. The lib's watched-query index (QueryFilter::watch,
    DbEvent::QueryMembershipChanged) is unchanged; the Flutter event stream
    still consumes it.

  • atomic_lib::runtime::AtomicNode is cut down to what binds to it. The
    WASM ClientDb is the only adapter on the node and uses from_db, db,
    query and apply_commit; those, plus the agent accessors and
    IngestPolicy::{Hub, Peer, LocalCache}, are what remains. open /
    NodeConfig / NodeStorage, get, mutate / ResourceEdit, subscribe
    and sync_with_peer are removed (each was one line over Db, with no
    consumer three days after landing). The seam stays and grows again from
    what the next binding actually calls (planning/atomic-lib-runtime.md).

  • Sync stack cleanup. No wire changes. WsMessage lost its Commit and
    Resource variants and the client no longer parses the pre-v2 COMMIT ,
    RESOURCE , AUTHENTICATED and ERROR text frames (nothing has sent
    them since the binary protocol); IngestPolicy::Replica went with them.
    CommitIngestOpts::hub / ::peer are the two validation presets that were
    copied in three places; ws_apply::apply_destroy_checked (a byte-identical
    twin of apply_destroy) is merged into it; the server's three copies of
    "encode an UPDATE/DESTROY for a change" are one encode_change_frame.
    Iroh: the dead sync_drive_with_peer wrapper and the private _forced
    indirection are gone, the live-peer registry is a LazyLock<Mutex<HashMap>>
    holding the pinned QUIC connection alongside the stream (a peer registered
    before the registry was initialised used to be dropped silently), and the
    sync-event debounce map is pruned instead of growing for the life of the
    process. Test-only helpers engine::drive_items / drive_sync_hash and
    OwnerPolicy::{owner_agent, hosted_drive_subjects} are removed.

  • Sync protocol follow-ups (see docs/src/websockets.md, "Changed in
    2026-09"):

    • Challenge-bound AUTH over WebSocket: the server's first frame is
      CHALLENGE (0x42) with a per-connection nonce; a client signs
      requestedSubject = "{origin}#{nonce}" and the proof is good on that
      socket only, so a captured frame can no longer be replayed elsewhere
      inside the five-minute window. Nonce-less proofs are still accepted
      (engine::AuthChallenge::Issued); AuthChallenge::Required is the
      strict mode, not yet wired to an option. Capability auth-nonce.
    • The WebSocket handler reads a client HELLO (capability client-hello)
      and answers COMMIT with a slim COMMIT_OK of [request_id] [commit_id]
      for a client that listed commit-ok-slim (the browser and the Rust
      WsClient both do). protocol::decode_commit_ok reads both forms.
    • Subscriptions follow the connection's identity: when an AUTH changes
      it, the commit monitor re-runs check_read for every subject, drive and
      filter subscription the connection holds and drops the unreadable ones
      (RebindAgent, capability rebind-on-auth). Until now a SUB accepted
      as the owner kept delivering after the socket re-authenticated as a
      stranger.
    • WsMessage::Error is a struct (request_id, code, message);
      WsClient::post_commit settles only on its own request_id and returns
      the commit id, so several commits can be in flight on one connection.
      Unknown text frames surface as WsMessage::Unrecognized instead of an
      error. WsClient sends a HELLO on connect and answers the server's
      challenge automatically (WsClient::auth_subject).
    • New error code INVALID_SIGNATURE (9) for a COMMIT whose signature
      does not verify; it went out as UNKNOWN before.
    • Iroh: LIVE_CONNECTIONS is keyed by peer and pruned when the peer is
      removed. It was an append-only list that pinned every QUIC connection
      ever dialed for the life of the process.
    • ws_apply::apply_commit_json (the replica applier that skips rights
      checks, audit finding F6) is gone: its only route was the pre-v2 COMMIT
      text frame, which no server sends. The server's rights-checked
      handlers::commit::apply_commit_json is the only function of that name.
    • New integration tests: ws_errors (ERROR format, request_id echo and
      codes), the nonce, the identity re-bind, and the slim ack.
  • Sync protocol hardening (WebSocket and Iroh; wire reference rewritten in
    docs/src/websockets.md, see its "Changed in 2026-09" section):

    • AUTH proofs expire: a signed authentication older than five minutes
      (atomic_lib::authentication::AUTH_MAX_AGE_MS) is refused, on WebSocket,
      Iroh and the HTTP auth headers alike. Until now only future-dated
      timestamps were rejected, so a captured proof never expired.
    • Over WebSocket, AUTH.requestedSubject must name the server: either the
      origin the socket was opened on or the configured server URL
      (AuthBinding::Origins, the same tolerance the HTTP auth headers have); a
      proof signed for another server, or for the agent's own subject, no
      longer opens a session. A refused AUTH answers
      with the new error code AUTH_FAILED (8). The Rust WsClient now signs
      the server origin (it signed the agent subject before). The Iroh
      initiator only accepts an auth-back signed for its own node id.
    • The hash-first SYNC_VV probe and the RBSR_FP / RBSR_ITEMS frames
      are gated by check_read per subject, like the full SYNC_VV exchange.
      Previously any socket could enumerate every subject and version vector of
      any drive it could name.
    • On the Iroh live link, destroys travel as the signed destroy COMMIT
      (DbEvent::Destroyed now carries commit_json); a naked DESTROY frame
      from a peer is ignored instead of applied on the connection's drive-level
      write verdict.
    • Capability advertisement: AUTH_OK carries a JSON array of capability
      names (atomic_lib::sync::protocol::CAPABILITIES), HELLO carries the
      same list after the device name. Both are optional trailing bytes.
    • UNSUB (0x21) now actually cancels a drive subscription (it previously
      edited a set nothing read). KEEPALIVE (0x41) is echoed over WebSocket so
      browsers can detect a dead socket.
    • Shared golden wire vectors (lib/src/sync/protocol_vectors.json, mirrored
      at browser/lib/src/protocol_vectors.json) pin the Rust and TypeScript
      codecs byte-for-byte.
    • Integration tests bind the test server to 127.0.0.1 and fall back to an
      OS-assigned port when portpicker finds none (hosts without IPv6).
  • Tauri Android: ship arm64-v8a only. The sideloadable universal APK was ~369 MB because it bundled four copies of libatomic_server_tauri.so (armeabi-v7a / x86 / x86_64 as well). Phones and tablets we install on are arm64; override with cargo tauri android build --target … for an Intel emulator.