3131}
3232
3333# ' Thin transport wrapper around a Jupyter Comm: encodes/decodes `yr::Message`
34- # ' buffers so `CommWidget ` never touches the raw hera comm API.
34+ # ' buffers so `CommAttrWidget ` never touches the raw hera comm API.
3535# ' @noRd
3636CommProvider <- R6 :: R6Class(
3737 " CommProvider" ,
@@ -83,135 +83,167 @@ CommProvider <- R6::R6Class(
8383)
8484
8585
86- # ' Widget backed by a Jupyter Comm and a CRDT ydoc
86+ # ' Build a Comm-backed widget class with a chosen ydoc parent
8787# '
88- # ' Extends [Widget] by opening a Jupyter Comm and running the Y.js sync
89- # ' protocol over it, mirroring `ypywidgets.CommWidget`: on `SyncStep1` reply
90- # ' with a `SyncStep2` diff; on `SyncStep2` apply the peer's state and (once)
91- # ' start forwarding local changes as `Update`s; on `Update` apply the peer's
88+ # ' R6 fixes the parent class at definition time, so we expose a factory that
89+ # ' stamps out a Comm-backed class on top of either [YAttrWidget] or
90+ # ' [YRootWidget] (or any other [WidgetBase] subclass). The two common bases
91+ # ' are pre-built as [CommAttrWidget] and [CommRootWidget].
92+ # '
93+ # ' The generated class opens a Jupyter Comm and runs the Y.js sync protocol
94+ # ' over it, mirroring `ypywidgets.CommWidget`: on `SyncStep1` reply with a
95+ # ' `SyncStep2` diff; on `SyncStep2` apply the peer's state and (once) start
96+ # ' forwarding local changes as `Update`s; on `Update` apply the peer's
9297# ' incremental change.
9398# '
94- # ' @export
95- CommWidget <- R6 :: R6Class(
96- " CommWidget" ,
97- inherit = Widget ,
99+ # ' @param inherit Parent R6 class — typically [YAttrWidget] or [YRootWidget].
100+ # ' @param classname Name of the generated R6 class.
101+ # ' @return An [R6::R6Class] generator.
102+ # ' @noRd
103+ .make_comm_widget_class <- function (
104+ inherit = YAttrWidget ,
105+ classname = " CommAttrWidget"
106+ ) {
107+ R6 :: R6Class(
108+ classname ,
109+ inherit = inherit ,
98110
99- public = list (
100- # ' @description Open a Comm on the `"ywidget"` target and send `SyncStep1`.
101- # ' @param ydoc Optional existing `yr::Doc` to adopt.
102- # ' @param comm_metadata Overrides the default metadata sent on comm open.
103- initialize = function (ydoc = NULL , comm_metadata = NULL ) {
104- super $ initialize(ydoc )
105-
106- model_name <- self $ model_name()
107-
108- if (is.null(comm_metadata )) {
109- comm_metadata <- list (
110- ymodel_name = model_name ,
111- create_ydoc = is.null(ydoc )
112- )
113- }
111+ public = list (
112+ # ' @description Open a Comm on the `"ywidget"` target and send `SyncStep1`.
113+ # ' @param ydoc Optional existing `yr::Doc` to adopt.
114+ # ' @param comm_metadata Overrides the default metadata sent on comm open.
115+ initialize = function (ydoc = NULL , comm_metadata = NULL ) {
116+ super $ initialize(ydoc )
114117
115- # hera invokes the CommProvider callback with R6 bindings broken, so we
116- # capture `self` explicitly and re-enter via `widget$on_remote_message`.
117- widget <- self
118- private $ .comm_provider <- CommProvider $ new(
119- target_name = " ywidget" ,
120- description = model_name ,
121- metadata = comm_metadata ,
122- on_remote_message = function (msg ) widget $ on_remote_message(msg )
123- )
118+ if (is.null(comm_metadata $ ymodel_name )) {
119+ comm_metadata $ ymodel_name <- class(self )[[1L ]]
120+ }
121+ if (is.null(comm_metadata $ create_ydoc )) {
122+ comm_metadata $ create_ydoc <- is.null(ydoc )
123+ }
124124
125- state_vector <- self $ ydoc $ with_transaction(function (trans ) {
126- trans $ state_vector()
127- })
128- private $ .comm_provider $ send(
129- yr :: Message $ new(yr :: SyncMessage $ from_sync_step1(state_vector ))
130- )
131- },
125+ # hera invokes the CommProvider callback with R6 bindings broken, so we
126+ # capture `self` explicitly and re-enter via `widget$on_remote_message`.
127+ widget <- self
128+ private $ .comm_provider <- CommProvider $ new(
129+ target_name = " ywidget" ,
130+ description = comm_metadata $ ymodel_name ,
131+ metadata = comm_metadata ,
132+ on_remote_message = function (msg ) widget $ on_remote_message(msg )
133+ )
132134
133- # ' @description Dispatch an incoming `yr::Message` to its sync handler.
134- # ' Public so hera callbacks can re-enter via a captured `self`, where R6
135- # ' bindings are otherwise broken.
136- # ' @param msg A `yr::Message`.
137- on_remote_message = function (msg ) {
138- if (msg $ is_sync_message()) {
139- sync_msg <- msg $ inner()
140- if (isTRUE(sync_msg $ is_sync_step1())) {
141- private $ .on_sync_step1(sync_msg )
142- } else if (isTRUE(sync_msg $ is_sync_step2())) {
143- private $ .on_sync_step2(sync_msg )
144- } else if (isTRUE(sync_msg $ is_update())) {
145- private $ .on_update(sync_msg )
135+ state_vector <- self $ ydoc $ with_transaction(function (trans ) {
136+ trans $ state_vector()
137+ })
138+ private $ .comm_provider $ send(
139+ yr :: Message $ new(yr :: SyncMessage $ from_sync_step1(state_vector ))
140+ )
141+ },
142+
143+ # ' @description Dispatch an incoming `yr::Message` to its sync handler.
144+ # ' Public so hera callbacks can re-enter via a captured `self`, where R6
145+ # ' bindings are otherwise broken.
146+ # ' @param msg A `yr::Message`.
147+ on_remote_message = function (msg ) {
148+ if (msg $ is_sync_message()) {
149+ sync_msg <- msg $ inner()
150+ if (isTRUE(sync_msg $ is_sync_step1())) {
151+ private $ .on_sync_step1(sync_msg )
152+ } else if (isTRUE(sync_msg $ is_sync_step2())) {
153+ private $ .on_sync_step2(sync_msg )
154+ } else if (isTRUE(sync_msg $ is_update())) {
155+ private $ .on_update(sync_msg )
156+ }
157+ } else {
158+ # Only sync message are handled for now
159+ return (invisible ())
146160 }
147- } else {
148- # Only sync message are handled for now
149- return (invisible ())
150- }
151- },
161+ },
152162
153- # ' @description The underlying comm id (used to build the display payload).
154- comm_id = function () private $ .comm_provider $ comm_id()
155- ),
163+ # ' @description The underlying comm id (used to build the display payload).
164+ comm_id = function () private $ .comm_provider $ comm_id()
165+ ),
156166
157- private = list (
158- .comm_provider = NULL ,
159- .observer_registered = FALSE ,
160-
161- .apply_remote = function (update_bytes ) {
162- self $ ydoc $ with_transaction(
163- function (trans ) trans $ apply_update_v1(update_bytes ),
164- mutable = TRUE ,
165- origin = REMOTE_ORIGIN
166- )
167- },
167+ private = list (
168+ .comm_provider = NULL ,
169+ .observer_registered = FALSE ,
168170
169- .on_sync_step1 = function (sync_msg ) {
170- diff <- self $ ydoc $ with_transaction(function ( trans ) {
171- trans $ encode_diff_v1( sync_msg $ state_vector())
172- })
173- msg <- yr :: Message $ new( yr :: SyncMessage $ from_sync_step2( diff ))
174- private $ .comm_provider $ send( msg )
175- },
171+ .apply_remote = function (update_bytes ) {
172+ self $ ydoc $ with_transaction(
173+ function ( trans ) trans $ apply_update_v1( update_bytes ),
174+ mutable = TRUE ,
175+ origin = REMOTE_ORIGIN
176+ )
177+ },
176178
177- .on_sync_step2 = function (sync_msg ) {
178- private $ .apply_remote(sync_msg $ data())
179- if (private $ .observer_registered ) {
180- return ()
181- }
182- private $ .observer_registered <- TRUE
183- # Capture provider directly — extendr may break R6 bindings in callbacks.
184- provider <- private $ .comm_provider
185- self $ ydoc $ observe_transaction_cleanup(
186- function (trans , event ) {
187- origin <- trans $ origin()
188- if (is.null(origin ) || ! origin $ equal(LOCAL_ORIGIN )) {
189- return ()
190- }
191- diff <- trans $ encode_diff_v1(event $ before_state())
192- if (length(diff ) == 0L ) {
193- return ()
194- }
195- msg <- yr :: Message $ new(yr :: SyncMessage $ from_update(diff ))
196- provider $ send(msg )
197- },
198- key = 1L
199- )
200- },
179+ .on_sync_step1 = function (sync_msg ) {
180+ diff <- self $ ydoc $ with_transaction(function (trans ) {
181+ trans $ encode_diff_v1(sync_msg $ state_vector())
182+ })
183+ msg <- yr :: Message $ new(yr :: SyncMessage $ from_sync_step2(diff ))
184+ private $ .comm_provider $ send(msg )
185+ },
201186
202- .on_update = function (sync_msg ) {
203- private $ .apply_remote(sync_msg $ data())
204- }
187+ .on_sync_step2 = function (sync_msg ) {
188+ private $ .apply_remote(sync_msg $ data())
189+ if (private $ .observer_registered ) {
190+ return ()
191+ }
192+ private $ .observer_registered <- TRUE
193+ # Capture provider directly — extendr may break R6 bindings in callbacks.
194+ provider <- private $ .comm_provider
195+ self $ ydoc $ observe_transaction_cleanup(
196+ function (trans , event ) {
197+ origin <- trans $ origin()
198+ if (is.null(origin ) || ! origin $ equal(LOCAL_ORIGIN )) {
199+ return ()
200+ }
201+ diff <- trans $ encode_diff_v1(event $ before_state())
202+ if (length(diff ) == 0L ) {
203+ return ()
204+ }
205+ msg <- yr :: Message $ new(yr :: SyncMessage $ from_update(diff ))
206+ provider $ send(msg )
207+ },
208+ key = 1L
209+ )
210+ },
211+
212+ .on_update = function (sync_msg ) {
213+ private $ .apply_remote(sync_msg $ data())
214+ }
215+ )
205216 )
217+ }
218+
219+ # ' Widget backed by a Jupyter Comm and a CRDT ydoc, with map-attr storage
220+ # '
221+ # ' Opens a Jupyter Comm on the `"ywidget"` target and runs the Y.js sync
222+ # ' protocol on top of [YAttrWidget]'s map-attr storage. Constructor:
223+ # ' `CommAttrWidget$new(ydoc = NULL, comm_metadata = NULL)`.
224+ # ' @export
225+ CommAttrWidget <- R6 :: R6Class(
226+ " CommAttrWidget" ,
227+ inherit = .make_comm_widget_class(YAttrWidget , " .CommAttrWidgetBase" )
228+ )
229+
230+ # ' Widget backed by a Jupyter Comm and a CRDT ydoc, with root-CRDT storage
231+ # '
232+ # ' Like [CommAttrWidget] but on top of [YRootWidget]'s per-root storage.
233+ # ' Constructor: `CommRootWidget$new(ydoc = NULL, comm_metadata = NULL)`.
234+ # ' @export
235+ CommRootWidget <- R6 :: R6Class(
236+ " CommRootWidget" ,
237+ inherit = .make_comm_widget_class(YRootWidget , " .CommRootWidgetBase" )
206238)
207239
208240# ' @exportS3Method hera::mime_types
209- mime_types.CommWidget <- function (x ) {
241+ mime_types.CommAttrWidget <- function (x ) {
210242 c(" text/plain" , " application/vnd.jupyter.ywidget-view+json" )
211243}
212244
213245# ' @exportS3Method hera::mime_bundle
214- mime_bundle.CommWidget <- function (x , mimetypes = mime_types(x ), ... ) {
246+ mime_bundle.CommAttrWidget <- function (x , mimetypes = mime_types(x ), ... ) {
215247 list (
216248 data = list (
217249 " text/plain" = " " ,
@@ -226,14 +258,36 @@ mime_bundle.CommWidget <- function(x, mimetypes = mime_types(x), ...) {
226258 )
227259}
228260
229- # ' Generate a CommWidget subclass with named CRDT-backed attributes
261+ # ' @exportS3Method hera::mime_types
262+ mime_types.CommRootWidget <- mime_types.CommAttrWidget
263+
264+ # ' @exportS3Method hera::mime_bundle
265+ mime_bundle.CommRootWidget <- mime_bundle.CommAttrWidget
266+
267+ # ' Generate a Comm-backed widget subclass with named CRDT-backed attributes
230268# '
231- # ' Like [make_widget()], but the generated class inherits from [CommWidget],
269+ # ' Like [make_widget()], but the generated class inherits from a Comm-backed
270+ # ' base (default [CommAttrWidget]; pass [CommRootWidget] for root storage),
232271# ' so each instance opens a Jupyter Comm and syncs its ydoc over it.
233272# '
234273# ' @inheritParams make_widget
235- # ' @return An [R6::R6Class] generator producing [CommWidget] subclasses.
274+ # ' @return An [R6::R6Class] generator producing Comm-backed widget subclasses.
236275# ' @export
237- make_comm_widget <- function (classname , ... , inherit = CommWidget ) {
238- make_widget(classname , ... , inherit = inherit )
276+ make_comm_widget <- function (
277+ classname ,
278+ ... ,
279+ comm_metadata = NULL ,
280+ inherit = CommAttrWidget
281+ ) {
282+ md <- comm_metadata
283+ wrapper <- R6 :: R6Class(
284+ paste0(classname , " Base" ),
285+ inherit = inherit ,
286+ public = list (
287+ initialize = function (ydoc = NULL ) {
288+ super $ initialize(ydoc = ydoc , comm_metadata = md )
289+ }
290+ )
291+ )
292+ make_widget(classname , ... , inherit = wrapper )
239293}
0 commit comments