branch: externals/futur
commit 097918a9eb5b36b35006d68422b08c59c48c3079
Author: Stefan Monnier <[email protected]>
Commit: Stefan Monnier <[email protected]>
Tweak names and add new functions for race, sit-for, and url-retrieve
* futur.el (futur--failed): Rename from `futur--error`.
(futur-failed): Rename from `futur-error`.
(futur-bind): Make sure FUN is always run outside of the current
dynamic context.
(futur--run-continuation): Use `nil` instead of `elisp` for the blocker.
(futur--register-callback): Rename from `futur-register-callback`.
(futur--ize): Rename from `futur-ize`.
(futur-blocker-abort) <timer>: Fix last change.
(futur-race, futur-sit-for, futur-url-retrieve): New functions.
(futur-blocker-abort) <url-retrieve>: New method.
---
futur-tests.el | 8 +-
futur.el | 256 +++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 172 insertions(+), 92 deletions(-)
diff --git a/futur-tests.el b/futur-tests.el
index f02abccd88..cb96910036 100644
--- a/futur-tests.el
+++ b/futur-tests.el
@@ -36,7 +36,7 @@
(ert-deftest futur-simple ()
(should (equal (futur-blocking-wait-to-get-result (futur-done 5)) 5))
- (let ((p (futur-error '(scan-error "Oops"))))
+ (let ((p (futur-failed '(scan-error "Oops"))))
(should-error (futur-blocking-wait-to-get-result p) :type 'scan-error))
(should (equal
@@ -47,7 +47,7 @@
12))
(let ((p (futur-let* ((x1 5)
- (x2 <- (futur-error '(scan-error "Oops"))))
+ (x2 <- (futur-failed '(scan-error "Oops"))))
(list (+ x1 x2) (error "Wow!")))))
(should-error (futur-blocking-wait-to-get-result p) :type 'scan-error)))
@@ -102,7 +102,7 @@
(should (< (- (float-time) start) (* timescale 4)))
(should (pcase fut6 ((futur--waiting _) t)))
(should (pcase fut2 ((futur--done 'nil) t)))
- (should (pcase fut22 ((futur--error '(futur-aborted)) t)))
+ (should (pcase fut22 ((futur--failed '(futur-aborted)) t)))
(should (pcase fut4 ((futur--waiting _) t) ((futur--done 'nil) t)))
(should (pcase futB ((futur--waiting _) t)))
(should (equal '(nil) (futur-blocking-wait-to-get-result futB)))
@@ -115,7 +115,7 @@
(_ (write-region "Emacs" nil tmpfile nil 'silent))
(futur
(futur-let* ((exitcode
- <- (futur-process-make
+ <- (futur--process-make
:name "futur-hexl"
:command (list (expand-file-name
"hexl" exec-directory)
diff --git a/futur.el b/futur.el
index bdb8e3e617..b725aa06c2 100644
--- a/futur.el
+++ b/futur.el
@@ -46,9 +46,9 @@
;;;; Low level API
-;; - (futur-done VAL) to create a trivial future returning VAL.
-;; - (futur-error ERR) to create a trivial failed future.
-;; - (futur-new FUN) to create a non-trivial future.
+;; - (futur-done VAL): Create a trivial future returning VAL.
+;; - (futur-failed ERR): Create a trivial failed future.
+;; - (futur-new FUN): Create a non-trivial future.
;; FUN is called with one argument (the new `futur' object) and should
;; return the "blocker" that `futur' is waiting for (used mostly
;; when aborting a future).
@@ -57,7 +57,7 @@
;; successfully with VAL, and runs the clients waiting for that event.
;; - (futur-deliver-failure FUTUR ERROR): Mark FUTUR as having failed
;; with ERROR, and runs the clients waiting for that event.
-;; - (futur-register-callback FUTUR FUN): Register FUN as a client.
+;; - (futur--register-callback FUTUR FUN): Register FUN as a client.
;; Will be called with two arg (the ERROR and the VAL) when FUTURE completes.
;; - (futur-blocking-wait-to-get-result FUTUR): Busy-wait for FUTUR to complete
;; and return its value. Better use `futur-bind' or `futur-let*' instead.
@@ -76,6 +76,8 @@
;; delayed until FUTUR completes.
;; - (futur-list &rest FUTURS): Run FUTURS concurrently and return the
;; resulting list of values.
+;; - (futur-race &rest FUTURS): Run FUTURS concurrently, return the
+;; first result, and discard the rest.
;;;; Related packages
@@ -125,6 +127,10 @@
;; Since 1.0:
+;; - New functions: `futur-race', `futur-sit-for', `futur-url-retrieve'.
+;; - Rename `futur-error' to `futur-failed'.
+;; - Rename `futur-register-callback' to `futur--register-callback'.
+;; - Rename `futur-ize' to `futur--ize'.
;; - Fix compatibility with Emacs<31.
;; - Minor bug fixes.
@@ -225,7 +231,7 @@ time or order of execution."
(:constructor nil)
(:constructor futur--done (value &aux (clients 't))
"Return a new `futur' that just returns VALUE.")
- (:constructor futur--error (value &aux (clients 'error))
+ (:constructor futur--failed (value &aux (clients 'error))
"Return a new `futur' that signals error VALUE")
(:constructor futur--waiting (&optional blocker clients
&aux (value blocker))
@@ -233,7 +239,7 @@ time or order of execution."
"A promise/future.
A futur has 3 possible states:
- (futur-done VAL): in that state, `clients' is `t', and `value' holds VAL.
-- (futur-error ERR): in that state, `clients' is `error', and `value' holds
ERR.
+- (futur-failed ERR): in that state, `clients' is `error', and `value' holds
ERR.
- (futur-waiting BLOCKER CLIENTS): in that state, `clients' is a list
of \"callbacks\" waiting for the value or the error, and `value' holds
the BLOCKER that will deliver the value (can be another future,
@@ -247,7 +253,7 @@ A futur has 3 possible states:
(app futur--clients 't)
(app futur--value ,result)))
-(pcase-defmacro futur--error (error-object)
+(pcase-defmacro futur--failed (error-object)
`(and (pred futur--p)
(app futur--clients 'error)
(app futur--value ,error-object)))
@@ -298,18 +304,19 @@ A futur has 3 possible states:
"Build a trivial `futur' which returns VAL."
(futur--done val))
-(defun futur-error (error-object)
+(defun futur-failed (error-object)
"Build a trivial `futur' which just signals ERROR-OBJECT."
- (futur--error error-object))
+ (futur--failed error-object))
(defun futur-new (builder)
"Build a future.
BUILDER is a function that will be called with one argument
\(the new `futur' object, not yet fully initialized) and it should
-return the object on which the future is waiting.
+return the \"blocker\", i.e. the object on which the future is waiting.
The code creating this future needs to call `futur-deliver-value'
-when the object has done the needed work.
-The object can be any object for which there is a `futur-blocker-wait' method."
+of `futur-deliver-failure' on its argument when the future has completed.
+The blocker can be any object for which there are `futur-blocker-wait'
+and `futur-blocker-abort' methods. `nil' is a valid blocker."
(let* ((f (futur--waiting))
(x (funcall builder f)))
(cl-assert (null (futur--blocker f)))
@@ -328,7 +335,7 @@ The error is `futur-aborted'. Does nothing if FUTUR was
already complete."
;;;; Composing futures
-(defun futur-register-callback (futur fun)
+(defun futur--register-callback (futur fun)
"Call FUN when FUTUR completes.
Calls it with two arguments (ERR VAL), where only one of the two is non-nil,
and throws away the return value. If FUTUR fails ERR is the error object,
@@ -338,11 +345,11 @@ If FUTUR already completed, FUN is called immediately."
(pcase futur
((futur--waiting _ clients)
(setf (futur--clients futur) (cons fun clients)))
- ((futur--error err) (funcall fun err nil))
+ ((futur--failed err) (funcall fun err nil))
((futur--done val) (funcall fun nil val)))
nil)
-(defun futur-ize (val)
+(defun futur--ize (val)
"Make sure VAL is a `futur'. If not, make it a trivial one that returns
VAL."
(if (futur--p val) val (futur--done val)))
@@ -352,52 +359,31 @@ That future calls FUN with the return value of FUTUR and
returns
the same value as the future returned by FUN.
If ERROR-FUN is non-nil, it should be a function that will be called instead of
FUN when FUTUR fails. It is called with a single argument (the error object).
-By default any error in FUTUR is propagated to the returned future."
- ;; This should behave like:
- ;;
- ;; (let ((new (futur--waiting futur)))
- ;; (futur-register-callback futur
- ;; (lambda (err val)
- ;; (if err (futur-deliver-failure new err)
- ;; (futur--run-continuation new fun (list val)))))
- ;; new)
- ;;
- ;; But we try to skip the `new' futur if `futur' is already completed.
- (pcase-exhaustive futur
- ((futur--waiting _ clients)
- (let ((new (futur--waiting futur)))
- (setf (futur--clients futur)
- (cons
- (lambda (err val)
- ;; If NEW is not waiting any more (e.g. it's been aborted),
- ;; don't bother running the continuation.
- (pcase new
- ((futur--waiting)
- (if err (futur-deliver-failure new err)
- (futur--run-continuation new fun (list val))))))
- clients))
- new))
- ((and (futur--error _) (guard (null error-fun))) futur)
- ((or (futur--done value) (futur--error err1))
- (condition-case-unless-debug err2
- (let ((res (if err1 (funcall error-fun err1) (funcall fun value))))
- (futur-ize res))
- (t (futur-error err2))))))
+By default any error in FUTUR is propagated to the returned future.
+ERROR-FUN and FUN can also return non-future values,"
+ (let ((new (futur--waiting futur)))
+ (futur--register-callback
+ futur (lambda (err val)
+ (cond
+ ((null err) (futur--run-continuation new fun (list val)))
+ (error-fun (futur--run-continuation new error-fun (list err)))
+ (t (futur-deliver-failure new err)))))
+ new))
(defun futur--run-continuation (futur fun args)
;; The thing FUTUR was waiting for is completed, maybe we'll soon be waiting
- ;; for another future, but for now, we're waiting for some piece of ELisp
- ;; (namely FUN) to terminate.
- (setf (futur--blocker futur) 'elisp)
+ ;; for another future, but for now, there's no blocker object,
+ ;; we're just waiting for some piece of ELisp (namely FUN) to terminate.
+ (setf (futur--blocker futur) nil)
(condition-case-unless-debug err
(let ((res (apply fun args)))
(if (not (futur--p res))
(futur-deliver-value futur res)
(setf (futur--blocker futur) res)
- (futur-register-callback res
- (lambda (err val)
- (if err (futur-deliver-failure futur err)
- (futur-deliver-value futur val))))))
+ (futur--register-callback
+ res (lambda (err val)
+ (if err (futur-deliver-failure futur err)
+ (futur-deliver-value futur val))))))
(t (futur-deliver-failure futur err))))
(defun futur--resignal (error-object)
@@ -429,12 +415,12 @@ its result, or (re)signals the error if ERROR-FUN is nil."
(let* ((mutex (make-mutex "futur-wait"))
(condition (make-condition-variable mutex)))
(with-mutex mutex
- (futur-register-callback futur (lambda (_err _val)
- (with-mutex mutex
- (condition-notify condition))))
+ (futur--register-callback futur (lambda (_err _val)
+ (with-mutex mutex
+ (condition-notify condition))))
(condition-wait condition))))
(pcase-exhaustive futur
- ((futur--error err) (funcall (or error-fun #'futur--resignal) err))
+ ((futur--failed err) (funcall (or error-fun #'futur--resignal) err))
((futur--done val) val)))
(defmacro futur-let* (bindings &rest body)
@@ -492,11 +478,11 @@ exactly when FUN is called, other than not before FUTUR
completes."
;; sure not to forget to run FUN. Maybe we should register FUTUR+FUN
;; on some global list somewhere that we can occasionally scan, in case
;; something happened that prevented running FUN?
- (let ((futur (futur-ize futur)))
+ (let ((futur (futur--ize futur)))
;; Use `futur--aux' to let `futur--multi-clients-p' know not to count
;; this function as a "real" client.
- (futur-register-callback futur (oclosure-lambda (futur--aux) (_ _)
- (funcall fun)))
+ (futur--register-callback futur (oclosure-lambda (futur--aux) (_ _)
+ (funcall fun)))
futur))
(defmacro futur-unwind-protect (form &rest forms)
@@ -567,8 +553,8 @@ If it had not been computed yet, then make it fail with
ERROR.")
(when (cl-typep client 'futur--aux)
(futur--funcall client error nil))))))
-(cl-defmethod futur-blocker-abort ((_ (eql 'elisp)) _error)
- ;; FIXME: No idea how to do that!
+(cl-defmethod futur-blocker-abort ((_ (eql nil)) _error)
+ ;; No blocker to abort.
nil)
;;;; Postpone
@@ -584,6 +570,28 @@ If IDLE is non-nil, then wait for that amount of idle
time."
(lambda (futur) (futur-deliver-value futur nil))
futur)))))
+(defun futur-sit-for (time)
+ "Return a `futur' that completes after TIME or upon user-input.
+Similar to `sit-for' but non-blocking.
+Returns non-nil if it waited the full TIME."
+ (futur-new
+ (lambda (futur)
+ ;; FIXME: This implementation relies on `pre-command-hook' to detect
+ ;; user input, which can be "slow" (e.g. when the user types `C-x C-s'
+ ;; `pre-command-hook' is run only after the `C-s').
+ (letrec ((timer (run-with-timer
+ time nil
+ (lambda (futur)
+ (remove-hook 'pre-command-hook abort)
+ (futur-deliver-value futur t))
+ futur))
+ (abort (lambda ()
+ (remove-hook 'pre-command-hook abort)
+ (cancel-timer timer)
+ (futur-deliver-value futur nil))))
+ (add-hook 'pre-command-hook abort)
+ (cons 'timer timer))))) ;; FIXME: Make timers proper structs instead!
+
(cl-defmethod futur-blocker-wait ((timer (head timer)))
(setq timer (cdr timer))
;; No support for repeated timers (yet?).
@@ -600,9 +608,10 @@ If IDLE is non-nil, then wait for that amount of idle
time."
t))
(cl-defmethod futur-blocker-abort ((timer (head timer)) _error)
+ (setq timer (cdr timer))
;; Older versions of Emacs signal errors if we try to cancel a timer
;; that's already run (or been canceled).
- (unless (timer--triggered timer) (cancel-timer (cdr timer))))
+ (unless (timer--triggered timer) (cancel-timer timer)))
;;;; Processes
@@ -621,7 +630,7 @@ If IDLE is non-nil, then wait for that amount of idle time."
(if (< (length futur--process-active) futur-process-max)
(let ((new (apply #'funcall args)))
(push new futur--process-active)
- (futur-register-callback
+ (futur--register-callback
new (oclosure-lambda (futur--aux) (_ _) (futur--process-next new)))
new)
(let ((new (futur--waiting 'waiting)))
@@ -636,7 +645,7 @@ If IDLE is non-nil, then wait for that amount of idle time."
(pcase fut
((futur--waiting)
(let ((new (apply #'futur--process-bounded call)))
- (futur-register-callback
+ (futur--register-callback
new (lambda (err val) (futur--deliver new err val)))
(cl-return))))))))
@@ -760,31 +769,49 @@ that have not yet completed."
(failed nil)
(args (make-list count :futur--waiting-for-result))
(i 0))
- (dolist (futur futurs)
- (futur-register-callback futur
- (let ((cell (nthcdr i args)))
- (lambda (err val)
- (cl-assert (eq :futur--waiting-for-result (car cell)))
- (cond
- (failed nil)
- (err
- (setq failed t)
- (futur-deliver-failure new err)
- ;; Abort the remaining ones.
- (let ((abort-error (list 'futur-aborted)))
- (futur-blocker-abort futurs abort-error)))
- (t
- (setf (car cell) val)
- (setq count (1- count))
- (when (zerop count)
- (pcase new
- ;; We don't unbind ourselves from some FUTURs
- ;; when aborting, so ignore their delivery here.
- ((futur--error '(futur-aborted)) nil)
- (_ (futur-deliver-value new args)))))))))
- (setq i (1+ i)))
+ (dolist (futur futurs)
+ (futur--register-callback
+ futur
+ (let ((cell (nthcdr i args)))
+ (lambda (err val)
+ (cl-assert (eq :futur--waiting-for-result (car cell)))
+ (cond
+ (failed nil)
+ (err
+ (setq failed t)
+ (futur-deliver-failure new err)
+ ;; Abort the remaining ones.
+ (let ((abort-error (list 'futur-aborted)))
+ (futur-blocker-abort futurs abort-error)))
+ (t
+ (setf (car cell) val)
+ (setq count (1- count))
+ (when (zerop count)
+ (pcase new
+ ;; We don't unbind ourselves from some FUTURs
+ ;; when aborting, so ignore their delivery here.
+ ((futur--failed '(futur-aborted)) nil)
+ (_ (futur-deliver-value new args)))))))))
+ (setq i (1+ i)))
new)))
+(defun futur-race (&rest futurs)
+ "Build a `futur' that returns the value of the first of FUTURS that
completes.
+If the first future among FUTURS completes with a failure, then the new
+future also completes with that same failure."
+ (let* ((new (futur--waiting futurs)))
+ (dolist (futur futurs)
+ (futur--register-callback
+ futur
+ (lambda (err val)
+ (pcase new
+ ((futur--waiting)
+ (futur--deliver new err val)
+ ;; Abort the remaining ones.
+ (let ((abort-error (list 'futur-aborted)))
+ (futur-blocker-abort futurs abort-error)))))))
+ new))
+
(cl-defmethod futur-blocker-wait ((_blockers cons))
;; FIXME: The loop below can misbehave when there's an early-exit
;; because of an error: we may remain waiting for the first blocker
@@ -802,6 +829,59 @@ that have not yet completed."
(dolist (futur futurs)
(futur-blocker-abort futur error)))
+;;;; URL futures
+
+(defun futur-url-retrieve (url &optional silent inhibit-cookies)
+ "Retrieve URL asynchronously.
+Thin wrapper around `url-retrieve'.
+URL is either a string or a parsed URL. If it is a string
+containing characters that are not valid in a URI, those
+characters are percent-encoded; see `url-encode-url'.
+
+The future returns nil if the process has already completed (i.e. URL
+was a mailto URL or similar). Otherwise, it returns (BUFFER . STATUS),
+where BUFFER contains the object, and any MIME headers associated with
+it, and STATUS is a plist representing what happened during the request,
+with most recent events first, or an empty list if no events have
+occurred. Each pair is one of:
+
+\(:redirect REDIRECTED-TO) - the request was redirected to this URL.
+
+\(:error (error type . DATA)) - an error occurred. TYPE is a
+symbol that says something about where the error occurred, and
+DATA is a list (possibly nil) that describes the error further.
+
+The variables `url-request-data', `url-request-method' and
+`url-request-extra-headers' can be dynamically bound around the
+request; dynamic binding of other variables doesn't necessarily
+take effect.
+
+If SILENT, then don't message progress reports and the like.
+If INHIBIT-COOKIES, cookies will neither be stored nor sent to
+the server.
+If URL is a multibyte string, it will be encoded as utf-8 and
+URL-encoded before it's used."
+ (futur-new
+ (lambda (f)
+ (let ((res (url-retrieve url
+ (lambda (status)
+ (futur-deliver-value
+ f (cons (current-buffer) status)))
+ nil silent inhibit-cookies)))
+ (if (null res)
+ (futur-deliver-value f res)
+ (cons 'url-retrieve res))))))
+
+;; (cl-defmethod futur-blocker-wait ((blocker (head url-retrieve))) nil)
+
+(cl-defmethod futur-blocker-abort ((blocker (head url-retrieve)) _error)
+ ;; AFAIK the URL library doesn't provide support for aborting
+ ;; a request, so this is a best-effort attempt.
+ (when (buffer-live-p (cdr blocker))
+ (with-current-buffer (cdr blocker)
+ (let ((proc (get-buffer-process (current-buffer))))
+ (when proc (delete-process proc))))))
+
;;;; Other helpers
(defmacro futur-with-temp-buffer (&rest body)