/
then.R
157 lines (151 loc) · 6.72 KB
/
then.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#' Access the results of a promise
#'
#' Use the `then` function to access the eventual result of a promise (or, if the operation fails, the reason for that failure). Regardless of the state of the promise, the call to `then` is non-blocking, that is, it returns immediately; so what it does *not* do is immediately return the result value of the promise. Instead, you pass logic you want to execute to `then`, in the form of function callbacks (or formulas, see Details). If you provide an `onFulfilled` callback, it will be called upon the promise's successful resolution, with a single argument `value`: the result value. If you provide an `onRejected` callback, it will be called if the operation fails, with a single argument `reason`: the error that caused the failure.
#'
#' @section Formulas:
#'
#' For convenience, the `then()`, `catch()`, and `finally()` functions use
#' [rlang::as_function()] to convert `onFulfilled`, `onRejected`, and
#' `onFinally` arguments to functions. This means that you can use formulas to
#' create very compact anonymous functions, using `.` to access the value (in
#' the case of `onFulfilled`) or error (in the case of `onRejected`).
#'
#' @section Chaining promises:
#'
#' The first parameter of `then` is a promise; given the stated purpose of the
#' function, this should be no surprise. However, what may be surprising is that
#' the return value of `then` is also a (newly created) promise. This new
#' promise waits for the original promise to be fulfilled or rejected, and for
#' `onFulfilled` or `onRejected` to be called. The result of (or error raised
#' by) calling `onFulfilled`/`onRejected` will be used to fulfill (reject) the
#' new promise.
#'
#' ```
#' promise_a <- get_data_frame_async()
#' promise_b <- then(promise_a, onFulfilled = head)
#' ```
#'
#' In this example, assuming `get_data_frame_async` returns a promise that
#' eventually resolves to a data frame, `promise_b` will eventually resolve to
#' the first 10 or fewer rows of that data frame.
#'
#' Note that the new promise is considered fulfilled or rejected based on
#' whether `onFulfilled`/`onRejected` returns a value or throws an error, not on
#' whether the original promise was fulfilled or rejected. In other words, it's
#' possible to turn failure to success and success to failure. Consider this
#' example, where we expect `some_async_operation` to fail, and want to consider
#' it an error if it doesn't:
#'
#' ```
#' promise_c <- some_async_operation()
#' promise_d <- then(promise_c,
#' onFulfilled = function(value) {
#' stop("That's strange, the operation didn't fail!")
#' },
#' onRejected = function(reason) {
#' # Great, the operation failed as expected
#' NULL
#' }
#' )
#' ```
#'
#' Now, `promise_d` will be rejected if `promise_c` is fulfilled, and vice
#' versa.
#'
#' **Warning:** Be very careful not to accidentally turn failure into success,
#' if your error handling code is not the last item in a chain!
#'
#' ```
#' some_async_operation() %>%
#' catch(function(reason) {
#' warning("An error occurred: ", reason)
#' }) %>%
#' then(function() {
#' message("I guess we succeeded...?") # No!
#' })
#' ```
#'
#' In this example, the `catch` callback does not itself throw an error, so the
#' subsequent `then` call will consider its promise fulfilled!
#'
#' @section Convenience functions:
#'
#' For readability and convenience, we provide `catch` and `finally` functions.
#'
#' The `catch` function is equivalent to `then`, but without the `onFulfilled`
#' argument. It is typically used at the end of a promise chain to perform error
#' handling/logging.
#'
#' The `finally` function is similar to `then`, but takes a single no-argument
#' function (or formula) that will be executed upon completion of the promise,
#' regardless of whether the result is success or failure. It is typically used
#' at the end of a promise chain to perform cleanup tasks, like closing file
#' handles or database connections. Unlike `then` and `catch`, the return value
#' of `finally` is ignored; however, if an error is thrown in `finally`, that
#' error will be propagated forward into the returned promise.
#'
#' @section Visibility:
#'
#' `onFulfilled` functions can optionally have a second parameter `visible`,
#' which will be `FALSE` if the result value is [invisible][base::invisible()].
#'
#' @param promise A promise object. The object can be in any state.
#'
#' @param onFulfilled A function (or a formula--see Details) that will be
#' invoked if the promise value successfully resolves. When invoked, the
#' function will be called with a single argument: the resolved value.
#' Optionally, the function can take a second parameter `.visible` if you care
#' whether the promise was resolved with a visible or invisible value. The
#' function can return a value or a promise object, or can throw an error;
#' these will affect the resolution of the promise object that is returned
#' by `then()`.
#'
#' @param onRejected A function taking the argument `error` (or a formula--see
#' Details). The function can return a value or a promise object, or can throw
#' an error. If `onRejected` is provided and doesn't throw an error (or return
#' a promise that fails) then this is the async equivalent of catching an
#' error.
#'
#' @export
then <- function(promise, onFulfilled = NULL, onRejected = NULL) {
promise <- as.promise(promise)
if (!is.null(onFulfilled))
onFulfilled <- rlang::as_function(onFulfilled)
if (!is.null(onRejected))
onRejected <- rlang::as_function(onRejected)
invisible(promise$then(onFulfilled = onFulfilled, onRejected = onRejected))
}
#' @param tee If `TRUE`, ignore the return value of the callback, and use the
#' original value instead. This is useful for performing operations with
#' side-effects, particularly logging to the console or a file. If the
#' callback itself throws an error, and `tee` is `TRUE`, that error will still
#' be used to fulfill the the returned promise (in other words, `tee` only has
#' an effect if the callback does not throw).
#' @rdname then
#' @export
catch <- function(promise, onRejected, tee = FALSE) {
promise <- as.promise(promise)
if (!is.null(onRejected))
onRejected <- rlang::as_function(onRejected)
if (!tee) {
return(promise$catch(onRejected))
} else {
promise$catch(function(reason) {
onRejected(reason)
stop(reason)
})
}
}
#' @rdname then
#'
#' @param onFinally A function with no arguments, to be called when the async
#' operation either succeeds or fails. Usually used for freeing resources that
#' were used during async operations.
#'
#' @export
finally <- function(promise, onFinally) {
promise <- as.promise(promise)
if (!is.null(onFinally))
onFinally <- rlang::as_function(onFinally)
promise$finally(onFinally)
}