diff --git a/examples/drawing/client.py b/examples/drawing/client.py index 6e47208..9db6dbe 100644 --- a/examples/drawing/client.py +++ b/examples/drawing/client.py @@ -25,10 +25,7 @@ def async_loop(): def send_updates(self, txn_event: Y.AfterTransactionEvent): update = txn_event.get_update() - # Sometimes transactions don't write, which means updates are empty. - # We only care about updates with meaningful mutations. - if update != b'\x00\x00': - self.send_q.put_nowait(update) + self.send_q.put_nowait(update) def apply_updates(self, doc: Y.YDoc): while not self.recv_q.empty(): diff --git a/src/y_doc.rs b/src/y_doc.rs index 87631fe..db5bf72 100644 --- a/src/y_doc.rs +++ b/src/y_doc.rs @@ -168,12 +168,14 @@ impl YDoc { pub fn observe_after_transaction(&mut self, callback: PyObject) -> SubscriptionId { self.0 .observe_transaction_cleanup(move |txn, event| { - Python::with_gil(|py| { - let event = AfterTransactionEvent::new(event, txn); - if let Err(err) = callback.call1(py, (event,)) { - err.restore(py) - } - }) + if event.before_state != event.after_state { + Python::with_gil(|py| { + let event = AfterTransactionEvent::new(event, txn); + if let Err(err) = callback.call1(py, (event,)) { + err.restore(py) + } + }) + } }) .into() } diff --git a/tests/test_y_doc.py b/tests/test_y_doc.py index 4127508..83e9b34 100644 --- a/tests/test_y_doc.py +++ b/tests/test_y_doc.py @@ -113,7 +113,6 @@ def test_get_update(): """ d = Y.YDoc() m = d.get_map("foo") - r = d.get_map("foo") update: bytes = None def get_update(event: AfterTransactionEvent) -> None: @@ -126,3 +125,35 @@ def get_update(event: AfterTransactionEvent) -> None: m.set(txn, "hi", "there") assert type(update) == bytes + + +def test_empty_updates_observe_after_transaction(): + """ + Transactions should be observable only when they are used to update the CRDT state. + If a transaction does not differ between its before and after state, it should be ignored. + """ + doc = Y.YDoc() + items = doc.get_array("items") + + def handle_update(e: Y.AfterTransactionEvent): + update = e.get_update() + # Ensures observe_after_transaction is only called on contentful updates. + assert update != b'\x00\x00' + + doc.observe_after_transaction(handle_update) + # Updating the document state should result in a non empty update. + with doc.begin_transaction() as txn: + items.append(txn, "This triggers an AfterTransactionEvent") + + # The following methods create an empty transaction in order to perform their operation. + # Check that none of these yield an empty update. + Y.encode_state_vector(doc) + update = Y.encode_state_as_update(doc) + Y.apply_update(doc, update) + doc.get_array("array") + doc.get_map("map") + doc.get_text("text") + doc.get_xml_element("xml_el") + doc.get_xml_text("xml_text") + +