Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG-actioncable.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.4] - 2026-07-01

Fixes from a full source review.

### Fixed

- **A lost-ack retry now re-broadcasts.** If the original attempt recorded the
update and then crashed (or the pub/sub broadcast failed) before
distributing, the retry was previously settled as `:applied` without
re-broadcasting — live subscribers stayed stale until their next full resync,
and nothing else could reach them. The retry now re-broadcasts before acking;
idempotent CRDT apply makes the duplicate free for every receiver.
- **A missing document key now fails closed.** Under a transport that doesn't
keep the channel instance alive across actions (AnyCable), an app that forgot
to pass `key` to `sync_receive` silently recorded updates under a nil key,
broadcast them to a stream no one subscribes to, and still acked them. The
frame now raises `Y::Error` instead.

### Changed

- Raised the `yrby` floor to `>= 0.3.1`, whose `update_ready?` is exact
(trial-integration, not just per-client clocks). With an older core, a
cross-client-origin gap passed the ready check and the `update_advances?`
probe then acked-and-dropped real content.

## [0.2.3] - 2026-07-01

### Changed
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,45 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.3.1] - 2026-07-01

Fixes from a full source review.

### Fixed

- **`Doc#update_ready?` is now exact.** It previously checked only the
per-client clock lower bound, but yrs's real integration gate also requires
every block referenced by an item's origin / right-origin / parent — which
routinely belong to *other* clients — and post-Skip blocks in a merged update
sit above the lower bound. An update could pass the clock check yet park as
pending; downstream, `update_advances?` then misread the parked update as an
already-applied retry (pending doesn't move a state vector) and the sync
channel **acked and dropped real content**. `update_ready?` now
trial-integrates on a throwaway probe seeded with the doc's integrated state
(the clock check remains as a cheap pre-filter), so a cross-client-origin gap
is correctly rejected for a resync. `update_advances?` also gained defense in
depth: an update that would park reports as advancing, never as a duplicate.
- **`Doc#read_text` could deadlock the process.** It opened a second read
transaction while still holding the first (a chained temporary); yrs's lock is
write-preferring, so a concurrent writer between the two acquisitions
deadlocked reader-vs-writer inside the GVL-released (uninterruptible) region.
Now uses a single transaction.
- **TOCTOU in gap-free encoding.** The pending check and the encode ran in
separate transactions, so a concurrent gappy `apply_update` between them could
make `handle_sync_message`/`compacted_state_update` serve pending structs
anyway. Both now happen under one transaction.
- `read_xml`: Lexical soft line breaks and tabs now come through as `\n`/`\t`
instead of vanishing (`"foo⏎bar"` no longer extracts as `"foobar"`).

### Changed

- `update_advances?` skips its full-document probe when the update carries
blocks beyond the doc's state vector (a novel update trivially advances) —
the common case no longer pays O(doc) per frame.
- The gem no longer packages the `yrby-decoder` gem's files (they ship in that
gem; the duplicate copy could shadow a newer standalone release), and now
ships `Cargo.lock` so source builds compile the exact crate graph CI tested.

## [0.3.0] - 2026-07-01

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion examples/actioncable-demo/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
get "docs/:id/forms", to: "documents#forms", as: :document_forms
get "docs/:id/content", to: "documents#content", as: :document_content
get "docs/:id/audit", to: "documents#audit", as: :document_audit
post "docs/:id/audit/control", to: "documents#audit_control", as: :document_audit_control
# DEMO/TEST ONLY — never mount in production. One anonymous POST can wipe a
# document's durable history (reset=1) or inject a per-write sleep (delay_ms)
# that starves the worker pool. The e2e suites depend on it, so it's gated by
# environment rather than removed.
unless Rails.env.production?
post "docs/:id/audit/control", to: "documents#audit_control", as: :document_audit_control
end

root to: redirect("/docs/demo")
end
Binary file added ext/yrby/src/fixtures/lexical_linebreak.bin
Binary file not shown.
8 changes: 5 additions & 3 deletions ext/yrby/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,11 @@ impl RbDoc {
fn read_text(&self, name: String) -> Option<String> {
let doc = &self.0;
nogvl(move || {
doc.transact()
.get_text(name.as_str())
.map(|t| t.get_string(&doc.transact()))
// Exactly ONE transaction per call. Opening a second while the
// first is still held deadlocks against a waiting writer — and
// inside nogvl that hang can't be interrupted.
let txn = doc.transact();
txn.get_text(name.as_str()).map(|t| t.get_string(&txn))
})
}

Expand Down
Loading
Loading