Repository navigation
Releases: jpcamara/yrby
Release list
yrby-client 0.7.0
This release updates yrby-client only. The gems are unchanged.
Added
- yrby-client:
<yrby-document>has a read-onlycurrentproperty that
returns theyrby:synceddetail of the bound session, or undefined when
nothing is bound. A binding that loads after the event fired can bind from
it. - yrby-client:
<yrby-document>has aretry()method that acquires the
document again after its session blocked or was discarded, without changing
whether the page is live. It works from a lease abort handler or right after
session.discard(), so a binding can replace a broken document with a fresh
session. It replaces callingactivate(), which is internal to the Turbo
adapter. - yrby-client:
YrbyDocumentElement.consumeraccepts a function that returns
a consumer or a promise of one. The element calls it when it first needs a
consumer and reuses the result. A function that throws or rejects is called
again on the next attempt, and assigning a different value replaces the
result.
yrby 0.8.1
Added
unknown_typesonY::LexicalandY::ProseMirror, and so onY::Lexxy
andY::Tiptap. It lists the node types in a document that neither the
built-in schema nor a rule handles, which are the onesto_htmlcan't fully
render. Render a real document from your editor in a test and assert the list
is empty.- The renderer crates export
escape_textandescape_attrfor callers that
build their own markup (lexical-yjs-html0.1.4,prosemirror-yjs-html
0.1.5).
Changed
- The demo app and the render-parity test run on Lexxy 1.0.
Y::Lexxy
output matches Lexxy 1.0's ownvalue, including image alt text edited
after the document syncs.
Fixed
- The Lexical renderers keep the text of an unknown inline wrapper, such as a
mark with no rule. The text renders without the wrapper. Before, the whole
node rendered as nothing, so its text was missing from the HTML. Y::Lexxy's list-item rule escapes the stored__checkedand__value
attributes. Collaborators write those attributes, and a crafted value could
break out of the HTML attribute. Normal values (booleans and integers) render
the same as before.- The crate READMEs' callback examples escape the values they insert, and both
READMEs describe how unknown node types render.
yrby 0.8.0
Added
Doc#read_array(name)returns aY.Arrayroot as a JSON string, for
documents whose content lives in an array.
This release ships alongside yrby-rails 0.7.0 and yrby-client 0.6.0.
yrby-rails 0.7.0
Added
-
Y::DocumentChannel.authorize_document { |record, name| ... }runs the
application's permission check when a client subscribes, on top of the
signed grant. The block runs in channel context. If it denies access, the
channel rejects that subscription without opening a stream or serving
state, and other subscriptions on the connection keep working. Without a
block, a valid signed grant is enough.The check runs once per subscription, so a permission you revoke
mid-session takes effect the next time that client subscribes. -
record.collaborative_document(name)returns a bound
Y::Collaborative::Attribute, which both application code and the shipped
channel use to read and write the document.y_docbuilds a native
Y::Doc, andload_state,append, andkeyall use the same storage
class.keyandload_statedon't create a row, but the firstappend
does. Use.document_rowfor the built-in row operations. -
Y::Document.key_for(record, name)returns the conventional key without
creating a document row. -
collaborative_sgid(name, expires_in:)and
collaborative_document_tag(record, name, expires_in:, refresh:)let you
set the grant lifetime, which used to be GlobalID's default with no way to
shorten it.refresh:is a URL the element fetches when a subscription is
rejected. That action re-runs the app's authorization and renders
{ grant: ... }, and the client resubscribes the same session with the new
grant. The client fetches the URL once per rejection and does not poll. -
Y::DocumentChannelships in the gem, so apps don't need to write a
channel. Clients subscribe with the signed grant the page rendered
({ grant:, name: }). The channel finds the record from the grant, loads
the document, and stores changes inY::Document. It rejects grants that
are missing, tampered with, for the wrong attribute, or for a destroyed
record. -
collaborative_document_tag(record, name, **options)is available in
Action View through the engine. It renders the mount element with the
signed grant, the attribute name, and the channel name as data attributes.
Anyone holding the grant can open the document, the same as with a
turbo_stream_fromstream name, so only render the tag on pages where the
user is allowed to edit the record. -
Channels get
Y::Documentstorage by default. Declareon_loadand
on_changeif you want a different store. Outside a yrby-rails app there
is still no default, and subscribing raises until both hooks are declared. -
has_collaborative_document :name, encrypted: truedeclares which storage
class backs an attribute, andY::DocumentChanneluses that class. Every
load and append for an encrypted attribute goes through
Y::EncryptedDocument, so the bytes are ciphertext at rest and the plain
classes can't read them. The model decides whether an attribute is
encrypted, and a page or client can't change that. Undeclared attributes use
plainY::Document. -
Y::Collaborativeadds signed tokens for record-backed documents, and the
engine includes it into ActiveRecord::Base. A page creates a token with
record.collaborative_sgid(:body), and the channel finds the record with
Y::Collaborative.locate(params[:grant], :body). The token is a signed
GlobalID scoped to one attribute, so a token for one field can't open
another. This is the standard way to implementauthorized?for
record-backed documents. lexxy-realtime already uses this flow, and now it
ships in yrby-rails.
Changed
-
Breaking: channels reject every subscription until they define
authorized?(key).sync_subscribednow calls it before opening a stream
or serving state, and the default returnsfalse. Add the method to every
channel that includesY::ActionCable:private def authorized?(key) current_user&.can_edit?(key) end
Return
truefor public documents. If the default is what rejected a
subscription, the log message says so. -
yrby:installnow creates only the storage migration, since the gem ships
Y::DocumentChannel. Pass--channelto also generate an application
channel for custom authorization or room-keyed documents. That channel
rejects every subscription until you implementauthorized?.
Fixed
-
Document elements now attach editors to sessions that exist independently
of any editor, so pending edits survive navigation under their original
grant and rejected deliveries can still be recovered. Turbo previews are
inert, and cached HTML contains no CRDT bytes. -
When an attribute morphs, the element releases the old editor binding and
acquires the new document. It doesn't move pending edits or authorization
over to the new document. -
Default storage now supplies the loader and recorder together, and
declaring only one custom hook raises before subscribing or acknowledging
an update. Previously, reads and writes silently went to different stores. -
Signed grants work even when the host app doesn't load Active Job.
-
The
yrby:tablesmigration template capsy_documents.stateat
1.gigabyte - 1instead of4.gigabytes - 1. Postgres raises
ArgumentErrorfor binary limits above 1 GB - 1, sodb:migrateon a
fresh Postgres app failed. MySQL maps both values tolongblob, and
SQLite ignores limits, so nothing changes there. Apps that already
migrated are unaffected.
yrby-client 0.6.0
Changed
-
yrby-client document elements now use shared document sessions scoped to
the consumer. A session tracks pending delivery separately from the editors
attached to it, and each editor binding gets an abort signal for cleanup. A
clean remount after a delay reloads from the server.docandprovider
are unavailable until the session is acquired and while it is retargeting.
Turbo no longer serializes CRDT state into cached HTML. Turbolinks 5 works
the same way, with its before-cache, render, load, and preview signals
handled alongside Turbo's. -
Managed sessions expose their delivery status and hold rejected work until
you callretry()ordiscard(). Each session uses its own subscription
nonce, so acknowledgment sequences from different sessions don't mix.
Provider status events now include apendingflag. -
The element accepts a
refreshattribute. If a subscription is rejected
andrefreshis set, the session fetches that URL once, expects
{ "grant": ... }back, and resubscribes with the new grant, keeping the
same document and pending edits. A failed fetch or a second rejection
blocks the session as before. The session doesn't renew grants on a timer. -
YProtocolSessionandReliableSyncrenamed their transport hooks to read
as commands.onConnect(),onDisconnect(), andack()/onAck()are now
resume(),pause(), andacknowledge(id), andReliableSync's
onTick()is nowretransmit().onStatusChangekeeps theonprefix
because it's the only one that registers a listener.ReliableSync#pending
returns an independent snapshot with its own copy of the update bytes, and
enqueueing copies the buffer you pass in. -
A grant refresh request now times out after 15 seconds and blocks the
session. Previously the session stayed offline indefinitely. If a consumer
throws while resubscribing with a renewed grant, that now blocks the
session too, where it used to be an unhandled rejection. -
When a status listener throws, the error goes to
onError, and the other
listeners and the cable callback that fired it still run. The provider also
reports failures in awareness events and unsubscribe. A throwing callback
can't interrupt presence removal or leave the awareness timer running. If
the error handler itself throws, the error goes to the console.
Fixed
- ActionCable providers now ignore callbacks from subscriptions that have
been replaced, so a late disconnect or rejection can't stop the new
subscription from delivering edits. Synchronous consumer callbacks wait
until subscription creation returns, and managed sessions use distinct
subscription identifiers so old ACKs stay separate.
v0.7.1
Fixed
Y::ProseMirrorandY::Tiptapkeep marks shared by adjacent text
runs open across them, the way ProseMirror's own serializer does:
bold then bold-italic renders as<strong>a<em>b</em></strong>, not
as two sibling<strong>wraps. A mark whose attributes differ
between runs never merges. Found by the new cross-renderer
verification, which checks the renderers against a live Tiptap
editor,@tiptap/html's staticgenerateHTML,tiptap-php,
Lexical's$generateHtmlFromNodes, and Payload CMS's
convertLexicalToHTMLon a shared corpus.
yrby 0.7.0
Added
Y::Decoderships in the core gem and loads withrequire "y". It was
scaffolded as a separateyrby-decodergem, but it is 66 lines of pure
Ruby overDoc#read_text/read_xml, requires the native core either
way, and the separate gem was never published, so the split left the
module in no gem at all. Theyrby-decodername is retired unused.
Changed
Doc#handle_sync_messageanswers a SyncStep1 with the doc's full state,
pending included, matching Y.js'sencodeStateAsUpdate. It previously
served integrated-only state. A peer parks a served pending struct the
same way the doc did and heals it when the missing dependency arrives
from its sender's ack-driven retransmit.compacted_state_updatestill
excludes pending, so compaction cannot freeze a gap into a snapshot.
Fixed
Doc#update_advances?no longer misreads a gappy merged update carrying
novel content as a no-op. A crafted frame can hide an internal gap behind
a Skip block while its post-gap blocks still integrate (yrs plants a Skip
hole in the store). That moves neither the doc's public state vector nor
pending, so the probe comparison reported genuinely novel content as
"doesn't advance";update_ready?accepted the frame and the doc applied
it, but it was never recorded to the durable log or broadcast. Any
insertion past the update's own (Skip-capped)state_vector()now
conservatively reports as advancing. Not reachable through standard Yjs
providers (a client's own updates and diffs are gap-free), so this closes
a hostile-input hole, not a real-world regression.
yrby-rails 0.6.1
Fixed
Y::Document.load_stateserves lossless state (encode_state_as_update).
It previously served gap-free state, so a client joining while a gap was
open never received the parked edit and could not heal it until its next
handshake. The quarantined row was always preserved; now the pending
struct rides along in served state and a mid-gap joiner heals the moment
the missing dependency arrives.
yrby-rails 0.6.0
Changed
-
Causal gaps are now accepted. A causally-incomplete update, one whose
causally-prior update the store hasn't seen, is recorded and acked like any
other (ack-on-durable) instead of being rejected with a resync request,
and served onward like any other state (a peer parks a pending struct
exactly as the server does). The gap heals through the ack loop: the
missing dependency is an update its own sender still holds unacked and
keeps retransmitting, and join or reconnect handshakes let any client
that holds it supply it. The write path no longer rebuilds the document
per update:
it appends, relays, and acks, so a lost-ack retry records again (replay
converges; CRDT apply is idempotent).This tightens the store contract:
on_loadmust preserve pending
(encode_state_as_updateor a replayed raw append log), compaction must
never fold a pending update into a gap-free snapshot and drop the raw
row (the bundledY::Documentquarantines pending rows and folds only
clean ones), andon_changemust tolerate duplicate deltas. An acked
update that leaves durable storage before it integrates is a silent
data loss.
Changed (Y::Document)
- Compaction folds past an open gap. A batch holding a causal gap still
compacts everything integrable: the fold captures every struct that
integrates, rows independent of the gap included, and only the gap
tail survives as quarantined raw rows. A healed gap folds out at the
next compaction. Rows are judged per row against the folded state, so
a row that causally builds on the gap quarantines with it and an
acked update never leaves the table before its content is durably in
state.
Added
on_gapchannel hook: fires with the document key at join/serve time
whenever the loaded document still holds a causal gap, for metrics on
unhealed gaps (which no longer surface as resync traffic). An open gap is
also logged atinfo.
yrby-rails v0.5.0
Added
Y::EncryptedDocument/Y::EncryptedDocumentUpdate: document storage
encrypted with Active Record encryption (stateand update payloads),
on the same tables; the class you access through decides the
cryptography, the wayActionText::EncryptedRichTextdoes. Point a
channel'son_load/on_change(or a record association) at
Y::EncryptedDocumentand configure the app's encryption keys. Keep
one access path per document: rows written encrypted read back as
ciphertext through the plain classes. Ciphertext is larger than the
plaintext, so the effective payload cap is roughly three quarters of
the column limit.