Skip to content

Commit dfa8b16

Browse files
committed
feat: Add with_read with_write for safe transaction (#3)
* Add Active transation holder * Add with_read with_write to widgets
1 parent 8719ed3 commit dfa8b16

9 files changed

Lines changed: 201 additions & 40 deletions

File tree

R/widget.R

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ REMOTE_ORIGIN <- NULL
1515
if (inherits(value, "Prelim")) value else yr::Prelim$any(value)
1616
}
1717

18+
#' Holds the currently-active `yr::Transaction`, or NULL. Shared by a
19+
#' WidgetBase and its storages so storage operations can join an ongoing
20+
#' transaction instead of opening a nested one.
21+
#'
22+
#' We need this for syntaxix sugar because the getter/setter on widget work
23+
#' without function calls (as Python property) so there is no alternative place
24+
#' to pass a transaction argument.
25+
#'
26+
#' @noRd
27+
TransactionState <- R6::R6Class(
28+
"TransactionState",
29+
public = list(
30+
transaction = NULL
31+
)
32+
)
33+
1834
#' Shared base for `YAttrWidget` and `YRootWidget`
1935
#'
2036
#' Owns the `yr::Doc` and the per-attribute storage registry, and provides
@@ -33,6 +49,7 @@ WidgetBase <- R6::R6Class(
3349
initialize = function(ydoc = NULL) {
3450
self$ydoc <- if (is.null(ydoc)) yr::Doc$new() else ydoc
3551
private$.storages <- list()
52+
private$.active_transaction <- TransactionState$new()
3653
},
3754

3855
#' @description Subscribe callbacks to attribute changes by name. Each
@@ -56,11 +73,73 @@ WidgetBase <- R6::R6Class(
5673
#' @param ... Subclass-specific arguments (e.g. `default` or `prelim`).
5774
register_storage = function(name, ...) {
5875
stop("register_storage() must be implemented by a subclass.")
76+
},
77+
78+
#' @description Run `fn(trans)` inside a read-only transaction, exposing
79+
#' `trans` as the active transaction so storage reads join it.
80+
#' @param fn Function called with the transaction.
81+
with_read = function(fn) {
82+
private$with_active_transaction(
83+
fn,
84+
mutable = FALSE,
85+
origin = LOCAL_ORIGIN
86+
)
87+
},
88+
89+
#' @description Run `fn(trans)` inside a writable transaction tagged with
90+
#' `LOCAL_ORIGIN`, exposing `trans` as the active transaction so storage
91+
#' writes join it.
92+
#' @param fn Function called with the transaction.
93+
with_write = function(fn) {
94+
private$with_active_transaction(fn, mutable = TRUE, origin = LOCAL_ORIGIN)
5995
}
6096
),
6197

6298
private = list(
63-
.storages = NULL
99+
.storages = NULL,
100+
.active_transaction = NULL,
101+
102+
# Run `fn(trans)` inside a `with_transaction`, exposing `trans` as the
103+
# active transaction to storages for the duration of the call.
104+
with_active_transaction = function(fn, ...) {
105+
state <- private$.active_transaction
106+
self$ydoc$with_transaction(
107+
function(trans) {
108+
state$transaction <- trans
109+
on.exit(state$transaction <- NULL)
110+
fn(trans)
111+
},
112+
...
113+
)
114+
}
115+
)
116+
)
117+
118+
#' Storage mixin that joins an ongoing transaction when one is active.
119+
#' Holds a TransactionState shared with the owning widget and exposes a
120+
#' private with_transaction() that runs fn(trans) on the active transaction
121+
#' when set, and otherwise opens a fresh one on ydoc.
122+
#' @noRd
123+
YActiveTransactionStorage <- R6::R6Class(
124+
"YActiveTransactionStorage",
125+
private = list(
126+
ydoc = NULL,
127+
active_transaction = NULL,
128+
129+
with_transaction = function(fn, mutable = FALSE, origin = NULL) {
130+
active <- private$active_transaction$transaction
131+
if (!is.null(active)) {
132+
return(fn(active))
133+
}
134+
private$ydoc$with_transaction(fn, mutable = mutable, origin = origin)
135+
}
136+
),
137+
138+
public = list(
139+
initialize = function(ydoc, active_transaction) {
140+
private$ydoc <- ydoc
141+
private$active_transaction <- active_transaction
142+
}
64143
)
65144
)
66145

@@ -74,8 +153,8 @@ WidgetBase <- R6::R6Class(
74153
#' @export
75154
YAttrStorage <- R6::R6Class(
76155
"YAttrStorage",
156+
inherit = YActiveTransactionStorage,
77157
private = list(
78-
ydoc = NULL,
79158
attrs = "_attrs",
80159
key = NULL
81160
),
@@ -89,10 +168,19 @@ YAttrStorage <- R6::R6Class(
89168
#' @param ydoc The `yr::Doc`.
90169
#' @param attrs Its attribute map.
91170
#' @param key Attribute key to read/write.
171+
#' @param active_transaction A [TransactionState] shared with the owning
172+
#' widget; when its `transaction` is non-NULL, storage reads/writes join
173+
#' that transaction instead of opening a new one.
92174
#' @param default Default value (Prelim or any R value), or `NULL` to
93175
#' skip the initial write entirely.
94-
initialize = function(ydoc, attrs, key, default = NULL) {
95-
private$ydoc <- ydoc
176+
initialize = function(
177+
ydoc,
178+
attrs,
179+
key,
180+
active_transaction,
181+
default = NULL
182+
) {
183+
super$initialize(ydoc, active_transaction)
96184
private$attrs <- attrs
97185
private$key <- key
98186
self$remote_changed <- Signal$new()
@@ -101,7 +189,7 @@ YAttrStorage <- R6::R6Class(
101189
# It may already be present if joining another widget.
102190
if (!is.null(default)) {
103191
prelim_default <- .as_prelim(default)
104-
private$ydoc$with_transaction(
192+
private$with_transaction(
105193
function(trans) {
106194
if (is.null(private$attrs$get(trans, private$key))) {
107195
private$attrs$insert(trans, private$key, prelim_default)
@@ -115,7 +203,7 @@ YAttrStorage <- R6::R6Class(
115203

116204
#' @description Return the value stored under `key`.
117205
read = function() {
118-
private$ydoc$with_transaction(
206+
private$with_transaction(
119207
function(trans) private$attrs$get(trans, private$key)
120208
)
121209
},
@@ -124,7 +212,7 @@ YAttrStorage <- R6::R6Class(
124212
#' @param value New value.
125213
#' @return `TRUE` iff the value was written.
126214
update = function(value) {
127-
private$ydoc$with_transaction(
215+
private$with_transaction(
128216
function(trans) {
129217
if (identical(private$attrs$get(trans, private$key), value)) {
130218
return(FALSE)
@@ -186,7 +274,13 @@ YAttrWidget <- R6::R6Class(
186274
#' @param default Default value (Prelim or any R value).
187275
#' @return The newly created [YAttrStorage].
188276
register_storage = function(name, default) {
189-
storage <- YAttrStorage$new(self$ydoc, private$.attrs, name, default)
277+
storage <- YAttrStorage$new(
278+
self$ydoc,
279+
private$.attrs,
280+
name,
281+
private$.active_transaction,
282+
default
283+
)
190284
private$.storages[[name]] <- storage
191285
storage
192286
}
@@ -208,6 +302,7 @@ YAttrWidget <- R6::R6Class(
208302
#' @export
209303
YRootStorage <- R6::R6Class(
210304
"YRootStorage",
305+
inherit = YActiveTransactionStorage,
211306
private = list(
212307
ref = NULL,
213308

@@ -233,10 +328,14 @@ YRootStorage <- R6::R6Class(
233328
#' @param name Root name on the doc.
234329
#' @param prelim A `yr::Prelim` whose `is_text/is_map/is_array` selects
235330
#' the root kind. Content is ignored.
236-
initialize = function(ydoc, name, prelim) {
331+
#' @param active_transaction A [TransactionState] shared with the owning
332+
#' widget. Stored for symmetry with [YAttrStorage]; root reads/writes
333+
#' currently go through the ref directly and do not consult it.
334+
initialize = function(ydoc, name, prelim, active_transaction) {
237335
if (!inherits(prelim, "Prelim")) {
238336
stop("YRootStorage requires a yr::Prelim for '", name, "'.")
239337
}
338+
super$initialize(ydoc, active_transaction)
240339
private$ref <- private$insert_root(ydoc, name, prelim)
241340
self$remote_changed <- Signal$new()
242341
sig <- self$remote_changed
@@ -287,7 +386,12 @@ YRootWidget <- R6::R6Class(
287386
#' @param prelim A `yr::Prelim` whose kind selects the root type.
288387
#' @return The newly created [YRootStorage].
289388
register_storage = function(name, prelim) {
290-
storage <- YRootStorage$new(self$ydoc, name, prelim)
389+
storage <- YRootStorage$new(
390+
self$ydoc,
391+
name,
392+
prelim,
393+
private$.active_transaction
394+
)
291395
private$.storages[[name]] <- storage
292396
storage
293397
}

man/CommAttrWidget.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/CommRootWidget.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/WidgetBase.Rd

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/YAttrStorage.Rd

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/YAttrWidget.Rd

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/YRootStorage.Rd

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/YRootWidget.Rd

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)