diff --git a/src/y_array.rs b/src/y_array.rs index fc03f25..0436f63 100644 --- a/src/y_array.rs +++ b/src/y_array.rs @@ -11,7 +11,7 @@ use pyo3::exceptions::{PyIndexError, PyTypeError}; use pyo3::prelude::*; use pyo3::types::{PyList, PySlice, PySliceIndices}; use yrs::types::array::{ArrayEvent, ArrayIter}; -use yrs::{Array, Subscription, Transaction}; +use yrs::{Array, SubscriptionId, Transaction}; /// A collection used to store data in an indexed sequence structure. This type is internally /// implemented as a double linked list, which may squash values inserted directly one after another @@ -185,24 +185,38 @@ impl YArray { /// Subscribes to all operations happening over this instance of `YArray`. All changes are /// batched and eventually triggered during transaction commit phase. - /// Returns an `YObserver` which, when free'd, will unsubscribe current callback. - pub fn observe(&mut self, f: PyObject) -> PyResult { + /// Returns a `SubscriptionId` which can be used to cancel the callback with `unobserve`. + pub fn observe(&mut self, f: PyObject) -> PyResult { match &mut self.0 { - SharedType::Integrated(v) => Ok(v - .observe(move |txn, e| { + SharedType::Integrated(array) => { + let subscription = array.observe(move |txn, e| { Python::with_gil(|py| { let event = YArrayEvent::new(e, txn); if let Err(err) = f.call1(py, (event,)) { err.restore(py) } }) - }) - .into()), + }); + Ok(subscription.into()) + } SharedType::Prelim(_) => Err(PyTypeError::new_err( "Cannot observe a preliminary type. Must be added to a YDoc first", )), } } + + /// Cancels the callback of an observer using the Subscription ID returned from the `observe` method. + pub fn unobserve(&mut self, subscription_id: SubscriptionId) -> PyResult<()> { + match &mut self.0 { + SharedType::Integrated(v) => { + v.unobserve(subscription_id); + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err( + "Cannot call unobserve on a preliminary type. Must be added to a YDoc first", + )), + } + } } impl YArray { @@ -407,12 +421,3 @@ impl YArrayEvent { } } } - -#[pyclass(unsendable)] -pub struct YArrayObserver(Subscription); - -impl From> for YArrayObserver { - fn from(o: Subscription) -> Self { - YArrayObserver(o) - } -} diff --git a/src/y_map.rs b/src/y_map.rs index e7566df..b8b1537 100644 --- a/src/y_map.rs +++ b/src/y_map.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use std::mem::ManuallyDrop; use std::ops::DerefMut; use yrs::types::map::{MapEvent, MapIter}; -use yrs::{Map, Subscription, Transaction}; +use yrs::{Map, SubscriptionId, Transaction}; use crate::shared_types::SharedType; use crate::type_conversions::{PyValueWrapper, ToPython}; @@ -172,7 +172,7 @@ impl YMap { YMapKeyIterator(self.items()) } - pub fn observe(&mut self, f: PyObject) -> PyResult { + pub fn observe(&mut self, f: PyObject) -> PyResult { match &mut self.0 { SharedType::Integrated(v) => Ok(v .observe(move |txn, e| { @@ -189,6 +189,18 @@ impl YMap { )), } } + /// Cancels the observer callback associated with the `subscripton_id`. + pub fn unobserve(&mut self, subscription_id: SubscriptionId) -> PyResult<()> { + match &mut self.0 { + SharedType::Integrated(map) => { + map.unobserve(subscription_id); + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err( + "Cannot unobserve a preliminary type. Must be added to a YDoc first", + )), + } + } } pub enum InnerYMapIterator { @@ -316,12 +328,3 @@ impl YMapEvent { } } } - -#[pyclass(unsendable)] -pub struct YMapObserver(Subscription); - -impl From> for YMapObserver { - fn from(o: Subscription) -> Self { - YMapObserver(o) - } -} diff --git a/src/y_text.rs b/src/y_text.rs index 806b1be..1e0f93e 100644 --- a/src/y_text.rs +++ b/src/y_text.rs @@ -3,7 +3,7 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PyList; use yrs::types::text::TextEvent; -use yrs::{Subscription, Text, Transaction}; +use yrs::{SubscriptionId, Text, Transaction}; use crate::shared_types::SharedType; use crate::type_conversions::ToPython; @@ -112,7 +112,7 @@ impl YText { } } - pub fn observe(&mut self, f: PyObject) -> PyResult { + pub fn observe(&mut self, f: PyObject) -> PyResult { match &mut self.0 { SharedType::Integrated(v) => Ok(v .observe(move |txn, e| { @@ -129,6 +129,18 @@ impl YText { )), } } + /// Cancels the observer callback associated with the `subscripton_id`. + pub fn unobserve(&mut self, subscription_id: SubscriptionId) -> PyResult<()> { + match &mut self.0 { + SharedType::Integrated(text) => { + text.unobserve(subscription_id); + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err( + "Cannot unobserve a preliminary type. Must be added to a YDoc first", + )), + } + } } /// Event generated by `YYText.observe` method. Emitted during transaction commit phase. @@ -207,12 +219,3 @@ impl YTextEvent { } } } - -#[pyclass(unsendable)] -pub struct YTextObserver(Subscription); - -impl From> for YTextObserver { - fn from(o: Subscription) -> Self { - YTextObserver(o) - } -} diff --git a/src/y_xml.rs b/src/y_xml.rs index 45015e3..f09f342 100644 --- a/src/y_xml.rs +++ b/src/y_xml.rs @@ -4,7 +4,7 @@ use std::mem::ManuallyDrop; use std::ops::Deref; use yrs::types::xml::{Attributes, TreeWalker, XmlEvent, XmlTextEvent}; use yrs::types::{EntryChange, Path, PathSegment}; -use yrs::Subscription; +use yrs::SubscriptionId; use yrs::Transaction; use yrs::Xml; use yrs::XmlElement; @@ -164,8 +164,8 @@ impl YXmlElement { /// Subscribes to all operations happening over this instance of `YXmlElement`. All changes are /// batched and eventually triggered during transaction commit phase. - /// Returns an `YXmlObserver` which, when free'd, will unsubscribe current callback. - pub fn observe(&mut self, f: PyObject) -> YXmlObserver { + /// Returns an `SubscriptionId` which, can be used to unsubscribe the observer. + pub fn observe(&mut self, f: PyObject) -> SubscriptionId { self.0 .observe(move |txn, e| { Python::with_gil(|py| { @@ -177,6 +177,10 @@ impl YXmlElement { }) .into() } + /// Cancels the observer callback associated with the `subscripton_id`. + pub fn unobserve(&mut self, subscription_id: SubscriptionId) { + self.0.unobserve(subscription_id); + } } /// A shared data type used for collaborative text editing, that can be used in a context of @@ -295,8 +299,8 @@ impl YXmlText { /// Subscribes to all operations happening over this instance of `YXmlText`. All changes are /// batched and eventually triggered during transaction commit phase. - /// Returns an `YXmlObserver` which, when free'd, will unsubscribe current callback. - pub fn observe(&mut self, f: PyObject) -> YXmlTextObserver { + /// Returns an `SubscriptionId` which, which can be used to unsubscribe the callback function. + pub fn observe(&mut self, f: PyObject) -> SubscriptionId { self.0 .observe(move |txn, e| { Python::with_gil(|py| { @@ -308,23 +312,10 @@ impl YXmlText { }) .into() } -} - -#[pyclass(unsendable)] -pub struct YXmlObserver(Subscription); - -impl From> for YXmlObserver { - fn from(o: Subscription) -> Self { - YXmlObserver(o) - } -} - -#[pyclass(unsendable)] -pub struct YXmlTextObserver(Subscription); -impl From> for YXmlTextObserver { - fn from(o: Subscription) -> Self { - YXmlTextObserver(o) + /// Cancels the observer callback associated with the `subscripton_id`. + pub fn unobserve(&mut self, subscription_id: SubscriptionId) { + self.0.unobserve(subscription_id); } } diff --git a/tests/test_y_array.py b/tests/test_y_array.py index 08dbbd9..6f73212 100644 --- a/tests/test_y_array.py +++ b/tests/test_y_array.py @@ -160,7 +160,7 @@ def callback(e): target = e.target delta = e.delta - observer = x.observe(callback) + subscription_id = x.observe(callback) # insert initial data to an empty YArray with d1.begin_transaction() as txn: @@ -189,8 +189,9 @@ def callback(e): target = None delta = None - # free the observer and make sure that callback is no longer called - del observer + # Cancel the observer and make sure that callback is no longer called + x.unobserve(subscription_id) + with d1.begin_transaction() as txn: x.insert(txn, 1, [6]) diff --git a/tests/test_y_map.py b/tests/test_y_map.py index 8b2b33e..9c1bd03 100644 --- a/tests/test_y_map.py +++ b/tests/test_y_map.py @@ -90,7 +90,7 @@ def callback(e): target = e.target entries = e.keys - observer = x.observe(callback) + subscription_id = x.observe(callback) # insert initial data to an empty YMap with d1.begin_transaction() as txn: @@ -121,7 +121,7 @@ def callback(e): entries = None # free the observer and make sure that callback is no longer called - del observer + x.unobserve(subscription_id) with d1.begin_transaction() as txn: x.set(txn, "key1", [6]) assert target == None diff --git a/tests/test_y_text.py b/tests/test_y_text.py index 8a008ec..8251a78 100644 --- a/tests/test_y_text.py +++ b/tests/test_y_text.py @@ -75,7 +75,7 @@ def callback(e): x = d1.get_text("test") - observer = x.observe(callback) + subscription_id = x.observe(callback) # insert initial data to an empty YText with d1.begin_transaction() as txn: @@ -105,7 +105,7 @@ def callback(e): delta = None # free the observer and make sure that callback is no longer called - del observer + x.unobserve(subscription_id) with d1.begin_transaction() as txn: x.insert(txn, 1, "fgh") assert target == None diff --git a/tests/test_y_xml.py b/tests/test_y_xml.py index d8410a1..8d743e7 100644 --- a/tests/test_y_xml.py +++ b/tests/test_y_xml.py @@ -106,7 +106,7 @@ def callback(e): attributes = e.keys delta = e.delta - observer = x.observe(callback) + subscription_id = x.observe(callback) # set initial attributes with d1.begin_transaction() as txn: @@ -168,7 +168,7 @@ def callback(e): delta = None # free the observer and make sure that callback is no longer called - del observer + x.unobserve(subscription_id) with d1.begin_transaction() as txn: x.insert(txn, 1, "fgh") assert target == None @@ -192,7 +192,7 @@ def callback(e): attributes = e.keys nodes = e.delta - observer = x.observe(callback) + subscription_id = x.observe(callback) # insert initial attributes with d1.begin_transaction() as txn: @@ -260,7 +260,7 @@ def callback(e): nodes = None # free the observer and make sure that callback is no longer called - del observer + x.unobserve(subscription_id) with d1.begin_transaction() as txn: x.insert_xml_element(txn, 0, "head") assert target == None diff --git a/y_py.pyi b/y_py.pyi index 5bfce36..e57c9f3 100644 --- a/y_py.pyi +++ b/y_py.pyi @@ -12,6 +12,8 @@ from typing import ( Dict, ) +SubscriptionId = int + class YDoc: """ A Ypy document type. Documents are most important units of collaborative resources management. @@ -364,16 +366,6 @@ class YText: Returns: The length of an underlying string stored in this `YText` instance, understood as a number of UTF-8 encoded bytes. """ - def __iter__(self) -> Iterator[str]: - """ - Returns: - Iterator over the characters in the `YText` string. - """ - def __contains__(self, pattern: str) -> bool: - """ - Returns: - Whether the string specified in pattern exists in `YText`. - """ def to_json(self) -> str: """ Returns: @@ -392,7 +384,7 @@ class YText: Deletes a specified range of of characters, starting at a given `index`. Both `index` and `length` are counted in terms of a number of UTF-8 character bytes. """ - def observe(self, f: Callable[[YTextEvent]]) -> YTextObserver: + def observe(self, f: Callable[[YTextEvent]]) -> SubscriptionId: """ Assigns a callback function to listen to YText updates. @@ -401,8 +393,13 @@ class YText: Returns: A reference to the callback subscription. """ + def unobserve(self, subscription_id: SubscriptionId): + """ + Cancels the observer callback associated with the `subscripton_id`. -YTextObserver = Any + Args: + subscription_id: reference to a subscription provided by the `observe` method. + """ class YTextEvent: target: YText @@ -490,14 +487,21 @@ class YArray: for item in array: print(item) """ - def observe(self, f: Callable[[YArrayEvent]]) -> YArrayObserver: + def observe(self, f: Callable[[YArrayEvent]]) -> SubscriptionId: """ Assigns a callback function to listen to YArray updates. Args: f: Callback function that runs when the array object receives an update. Returns: - A reference to the callback subscription. + An identifier associated with the callback subscription. + """ + def unobserve(self, subscription_id: SubscriptionId): + """ + Cancels the observer callback associated with the `subscripton_id`. + + Args: + subscription_id: reference to a subscription provided by the `observe` method. """ YArrayObserver = Any @@ -589,7 +593,7 @@ class YMap: for (key, value) in map.items()): print(key, value) """ - def observe(self, f: Callable[[YMapEvent]]) -> YMapObserver: + def observe(self, f: Callable[[YMapEvent]]) -> SubscriptionId: """ Assigns a callback function to listen to YMap updates. @@ -598,8 +602,13 @@ class YMap: Returns: A reference to the callback subscription. Delete this observer in order to erase the associated callback function. """ + def unobserve(self, subscription_id: SubscriptionId): + """ + Cancels the observer callback associated with the `subscripton_id`. -YMapObserver = Any + Args: + subscription_id: reference to a subscription provided by the `observe` method. + """ class YMapEvent: target: YMap @@ -617,9 +626,7 @@ class YMapEventKeyChange(TypedDict): newValue: Optional[Any] YXmlAttributes = Iterator[Tuple[str, str]] -YXmlObserver = Any -YXmlTextObserver = Any Xml = Union[YXmlElement, YXmlText] YXmlTreeWalker = Iterator[Xml] EntryChange = Dict[Literal["action", "newValue", "oldValue"], Any] @@ -719,11 +726,22 @@ class YXmlElement: Returns an iterator that enables a deep traversal of this XML node - starting from first child over this XML node successors using depth-first strategy. """ - def observe(self, f: Callable[[YXmlElementEvent]]) -> YXmlObserver: + def observe(self, f: Callable[[YXmlElementEvent]]) -> SubscriptionId: """ Subscribes to all operations happening over this instance of `YXmlElement`. All changes are batched and eventually triggered during transaction commit phase. - Returns an `YXmlObserver` which, when free'd, will unsubscribe current callback. + + Args: + f: A callback function that receives update events. + Returns: + A `SubscriptionId` that can be used to cancel the observer callback. + """ + def unobserve(self, subscription_id: SubscriptionId): + """ + Cancels the observer callback associated with the `subscripton_id`. + + Args: + subscription_id: reference to a subscription provided by the `observe` method. """ class YXmlText: @@ -779,11 +797,21 @@ class YXmlText: An iterator that enables to traverse over all attributes of this XML node in unspecified order. """ - def observe(self, f: Callable[[YXmlTextEvent]]) -> YXmlTextObserver: + def observe(self, f: Callable[[YXmlTextEvent]]) -> SubscriptionId: """ Subscribes to all operations happening over this instance of `YXmlText`. All changes are batched and eventually triggered during transaction commit phase. - Returns an `YXmlObserver` which, when free'd, will unsubscribe current callback. + Args: + f: A callback function that receives update events. + Returns: + A `SubscriptionId` that can be used to cancel the observer callback. + """ + def unobserve(self, subscription_id: SubscriptionId): + """ + Cancels the observer callback associated with the `subscripton_id`. + + Args: + subscription_id: reference to a subscription provided by the `observe` method. """ class YXmlTextEvent: