Skip to content

Live Y::Map handles: read and edit shared maps from Ruby - #33

Open
jpcamara wants to merge 6 commits into
mainfrom
feat/live-maps
Open

Live Y::Map handles: read and edit shared maps from Ruby#33
jpcamara wants to merge 6 commits into
mainfrom
feat/live-maps

Conversation

@jpcamara

@jpcamara jpcamara commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Exposes the first yrs shared type for building and editing state from Ruby, not just opaque CRDT sync. Doc#get_map(name) returns a live Y::Map handle: reads reflect the current document and writes mutate the CRDT, so changes sync to every peer.

map = doc.get_map("state")
map["title"] = "Dashboard"
map["tags"]  = %w[a b c]
map["user"]  = { "name" => "Ada", "role" => "eng" }   # nested Y.Map

map["title"]            # => "Dashboard"
map.to_h                # => { "title" => "Dashboard", ... }

user = map.get_map("user")   # live handle to the nested map
user["name"] = "Grace"       # doc now has state.user.name == "Grace"

map.delete("title")     # => "Dashboard"

Full surface: []/get, []=/set, get_map, delete, clear, keys, size/length, key?/has_key?, to_h, each. Values round-trip primitives, arrays, and nested hashes (a nested hash becomes a real nested Y.Map). Symbol keys stringify. This complements the read-only read_text/read_xml/read_map snapshots.

Thread safety mirrors Y::Doc:

  • Every operation opens its own transaction inside nogvl (GVL released) and holds no lock across the GVL boundary.
  • Ruby values are read and built only with the GVL held; the closure works purely on Send data via an InValue intermediate, so no Ruby calls happen off-GVL.
  • A handle is addressed by root name plus a path of keys and re-resolves on every operation, so it never caches a raw yrs branch pointer that could dangle when the tree is mutated. A nested handle keeps working as sibling keys change.
  • Y::Map is asserted Send + Sync at compile time alongside Doc.

Tests: test/map_test.rb (29 tests) covers round-trips, assignment-returns-value, nested live maps, deep nesting, get_map on non-map/missing, live-handle-survives-reassignment, sync propagation to a peer, and an 8-thread concurrent read/write stress test. Rust unit tests cover the pure Send-side helpers (resolve, to_in, invalue_to_any). Full suite green: 201 Ruby runs, 38 Rust tests, clippy/rustfmt/rubocop clean. No version bump; the entry sits in the Unreleased section.

@jpcamara jpcamara changed the title Live Y::Map handles: read and edit shared maps from Ruby Live Y::Map handles: read and edit shared maps from Ruby (0.4.0) Jul 1, 2026
@jpcamara

jpcamara commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main (now includes the 0.2.3 delete-fix and 0.3.0 gap-free-sync releases) and bumped to 0.4.00.3.0 was taken by gap-free-sync. CHANGELOG slots [0.4.0] above [0.3.0]. Both feature sets coexist: full local suite green (121 Ruby, 28 Rust, clippy + rustfmt + rubocop clean).

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@jpcamara
jpcamara force-pushed the feat/live-maps branch 2 times, most recently from 96d2709 to a81b296 Compare August 4, 2026 05:21
jpcamara and others added 2 commits August 5, 2026 14:58
Expose the first actual yrs shared type for building/editing state from
Ruby. `Doc#get_map(name)` returns a live `Y::Map` handle: reads reflect the
current document, writes mutate the CRDT (so changes sync to every peer).

Supports `map[key]`, `map[key] = value`, `get`/`set`, `delete`, `clear`,
`keys`, `size`/`length`, `key?`/`has_key?`, `to_h`, and `each`. Values
round-trip primitives, arrays, and nested hashes; a nested hash becomes a
real nested `Y.Map`, and `map.get_map(key)` returns a live handle to it so
mutating the child mutates the document. Symbol keys stringify.

Thread safety mirrors `Y::Doc` exactly: every operation opens its own
transaction inside `nogvl` (GVL released) and holds no lock across the GVL
boundary. Ruby values are read/built only with the GVL held, before/after
the closure, which works purely on `Send` data (an `InValue` intermediate).

A handle is addressed by root name plus a path of keys and re-resolves per
operation, so it never caches a raw yrs branch pointer that could dangle when
the tree is mutated (possibly on another thread) -- a nested handle keeps
working even as sibling keys change around it. `Y::Map` is asserted
`Send + Sync` at compile time alongside `Doc`.

Covered by test/map_test.rb (round-trips, nested live maps, sync propagation,
concurrent access) and Rust unit tests for the pure Send-side helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TeaovH2jyHARHJSyQ8Afo
@jpcamara jpcamara changed the title Live Y::Map handles: read and edit shared maps from Ruby (0.4.0) Live Y::Map handles: read and edit shared maps from Ruby Aug 5, 2026
The README examples now execute in the test suite; a literal ... in the
each block is not valid Ruby.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant