-
Notifications
You must be signed in to change notification settings - Fork 9
/
promise.el
711 lines (595 loc) · 25.2 KB
/
promise.el
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
;;; promise.el --- Promises/A+ -*- lexical-binding: t; -*-
;; Copyright (C) 2016-2017 chuntaro
;; Author: chuntaro <[email protected]>
;; URL: https://github.com/chuntaro/emacs-promise
;; Package-Requires: ((emacs "25.1"))
;; Version: 1.1
;; Keywords: async promise convenience
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;; The original JavaScript code is:
;;
;; Copyright (c) 2014 Forbes Lindesay
;;
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;;
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
;; THE SOFTWARE.
;;; Commentary:
;; This is a simple implementation of Promises/A+.
;;
;; This implementation ported the following Promises/A+ implementation faithfully.
;; https://github.com/then/promise
;;
;; * The same API as JavaScript version Promise can be used.
;; * then, catch, resolve, reject, all, race, etc...
;; * supports "thenable"
;; * supports "Inheritance of Promise"
;; * supports "rejection-tracking"
;;
;; Usage:
;; See `promise-examples.el' for details.
;; https://raw.githubusercontent.com/chuntaro/emacs-promise/master/examples/promise-examples.el
;; You can check the operation while downloading and running it interactively.
;;
;; (require 'promise)
;;
;; ;; Please be sure to enable it when developing.
;; (promise-rejection-tracking-enable '((all-rejections . t)))
;;
;; (defun do-something-async (delay-sec value)
;; "Return `Promise' to resolve the value asynchronously."
;; (promise-new (lambda (resolve _reject)
;; (run-at-time delay-sec
;; nil
;; (lambda ()
;; (funcall resolve value))))))
;;
;; (defun example4 ()
;; "All processes are asynchronous Promise chain."
;; (promise-chain (do-something-async 1 33)
;; (then (lambda (result)
;; (message "first result: %s" result)
;; (do-something-async 1 (* result 2))))
;;
;; (then (lambda (second-result)
;; (message "second result: %s" second-result)
;; (do-something-async 1 (* second-result 2))))
;;
;; (then (lambda (third-result)
;; (message "third result: %s" third-result)))))
;;; Code:
(require 'promise-es6-extensions)
(require 'promise-done)
(require 'promise-finally)
(require 'promise-rejection-tracking)
;;;###autoload
(defmacro promise-chain (promise &rest body)
"Extract PROMISE, BODY include then, catch, done and finally.
Extract the following code...
(promise-chain (promise-new ...)
(then
(lambda (value)
...))
(catch
(lambda (reason)
...))
(done
(lambda (value)
...))
(finally
(lambda () ...))
;; Anaphoric versions of `then' and `catch'.
(thena (message \"result -> %s\" result)
...)
(catcha (message \"error: reason -> %s\" reason)
...))
as below.
(let ((promise (promise-new ...)))
(setf promise (promise-then promise
(lambda (value)
...)))
(setf promise (promise-catch promise
(lambda (value)
...)))
(setf promise (promise-done promise
(lambda (reason)
...)))
(setf promise (promise-finally promise
(lambda ()
...)))
(setf promise (promise-then promise
(lambda (result)
(message \"result -> %s\" result)
...)))
(setf promise (promise-catch promise
(lambda (reason)
(message \"error: reason -> %s\" reason)
...)))
promise)"
(declare (indent 1) (debug t))
`(let ((promise ,promise))
,@(mapcar (lambda (sexp)
(let ((fn (car-safe sexp))
(args (cdr-safe sexp)))
(cl-case fn
(promise-new
`(setf promise ,sexp))
((promise-then
promise-catch
promise-done
promise-finally)
`(setf promise (,fn promise ,@args)))
(catch
`(setf promise (promise-catch promise ,@args)))
(then
`(setf promise (promise-then promise ,@args)))
(done
`(setf promise (promise-done promise ,@args)))
(finally
`(setf promise (promise-finally promise ,@args)))
(thena
`(setf promise (promise-then promise (lambda (result) ,@args))))
(catcha
`(setf promise (promise-catch promise (lambda (reason) ,@args))))
(otherwise
sexp))))
body)
promise))
;;
;; Promise version of various utility functions
;;
(require 'url-http)
(defun promise:run-at-time (time function &rest args)
"Return promise to funcall FUNCTION with ARGS at specified TIME.
Arguments:
- TIME can accept the various formats. See `run-at-time'.
- FUNCTION is funcalled with ARGS.
Resolve:
- The return value from funcalled FUNCTION.
Reject:
- <Never rejected>"
(declare (indent 1))
(promise-new
(lambda (resolve _reject)
(run-at-time time nil
(lambda ()
(funcall resolve (apply function args)))))))
(defun promise:delay (time &optional value)
"Return promise to delay specified TIME.
Arguments:
- TIME can accept the various formats. See `run-at-time'.
- VALUE is return value when resolved this function.
Resolve:
- VALUE
Reject:
- <Never rejected>"
(declare (indent 1))
(promise-new
(lambda (resolve _reject)
(run-at-time time
nil
(lambda ()
(funcall resolve value))))))
(defun promise:time-out (time &optional reason)
"Return promise to reject after specified TIME with REASON.
Arguments:
- TIME an accept various format. See `run-at-time'.
- REASON is return value when rejected this function.
Resolve:
- <Never resolved>
Reject:
- REASON"
(declare (indent 1))
(promise-new
(lambda (_resolve reject)
(run-at-time time nil
(lambda ()
(funcall reject reason))))))
(defun promise:make-process (command)
"Return promise to make new asynchronous COMMAND.
Arguments:
- COMMAND is program and shell arguments list of string.
See `promise:make-process-with-handler' for Resolve and Reject sections."
(funcall #'promise:make-process-with-handler command))
(defun promise:make-process-send-buffer (command buf)
"Return promise to make new asynchronous COMMAND.
Arguments:
- COMMAND is program and shell arguments list of string.
- BUF is buffer, a format that can be accepted by `with-current-buffer'.
`buffer-string' of BUF is sent with EOF after process has been invoked.
See `promise:make-process-with-handler' for Resolve and Reject sections."
(funcall #'promise:make-process-with-handler
command
(lambda (proc)
(with-current-buffer buf
(process-send-region proc (point-min) (point-max))
(process-send-eof proc)))))
(defun promise:make-process-send-string (command string)
"Return promise to make new asynchronous COMMAND.
Arguments:
- COMMAND is program and shell arguments list of string.
- STRING is sent with EOF after process has been invoked.
See `promise:make-process-with-handler' for Resolve and Reject sections."
(funcall #'promise:make-process-with-handler
command
(lambda (proc)
(process-send-string proc string)
(process-send-eof proc))))
(defun promise:make-process-with-handler (command &optional handler merge-stderr)
"Return promise to make new asynchronous COMMAND.
Arguments:
- COMMAND is program and shell arguments list of string.
- HANDLER is function, called with process object after program is invoked.
- MERGE-STDERR is boolean, whether merge stdout and stderr or not.
Resolve:
- A list like as (stdout stderr) when process finish with exitcode 0.
stdout and stderr are string.
Reject:
- A list like as (event stdout stderr) when process doesn't finish exitcode 0.
event, stdout and stderr are string.
The event is documented at https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html"
(promise-new
(lambda (resolve reject)
(let* ((program (car command))
(stdout (generate-new-buffer (concat "*" program "-stdout*")))
(stderr (unless merge-stderr
(generate-new-buffer (concat "*" program "-stderr*"))))
(stderr-pipe (unless merge-stderr
(make-pipe-process
:name (concat "*" program "-stderr-pipe*")
:noquery t
;; use :filter instead of :buffer, to get rid of "Process Finished" lines
:filter (lambda (_ output)
(with-current-buffer stderr
(insert output))))))
(cleanup (lambda ()
(kill-buffer stdout)
(unless merge-stderr
(delete-process stderr-pipe)
(kill-buffer stderr)))))
(condition-case err
(let ((proc (if merge-stderr
(make-process :name program :buffer stdout :command command)
(make-process :name program :buffer stdout :command command :stderr stderr-pipe))))
(set-process-sentinel
proc
(lambda (_process event)
(unwind-protect
(let ((stdout-str (with-current-buffer stdout
(buffer-string)))
(stderr-str (unless merge-stderr
(with-current-buffer stderr
(buffer-string)))))
(if (string= event "finished\n")
(funcall resolve (list stdout-str stderr-str))
(funcall reject (list event stdout-str stderr-str))))
(funcall cleanup))))
(when handler
(funcall handler proc)))
(error (funcall cleanup)
(signal (car err) (cdr err))))))))
(require 'subr-x)
(defun promise:maybe-message (msg)
"Display MSG if non-blank."
(let ((m (string-trim-right msg)))
(when (not (string-empty-p m))
(message "%s" m))))
(require 'seq)
(defun promise:make-process-string (command)
"Return promise to make new asynchronous COMMAND.
Arguments:
- COMMAND is program and shell arguments list of string.
Resolve:
- Process stdout as string when process finish with exitcode 0.
Reject:
- Event as string represented process exit state.
The event is documented at https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html"
(promise-then
(funcall #'promise:make-process command)
(lambda (res)
(seq-let (stdout stderr) res
(promise:maybe-message (propertize stderr 'face '(:foreground "yellow")))
stdout))
(lambda (err)
(seq-let (event stdout stderr) err
(promise:maybe-message (propertize stdout 'face '(:foreground "black" :background "white")))
(promise:maybe-message (propertize stderr 'face '(:foreground "red")))
(promise-reject event)))))
(defun promise:make-shell-command (script &optional dir)
"Return promise to make new asynchronous shell SCRIPT.
Arguments:
- SCRIPT is string, will be passed sh -c.
- DIR is directory path in which SCRIPT will be executed.
See `promise:make-process-string' for Resolve and Reject sections."
(let ((default-directory (or dir default-directory)))
(promise:make-process-string (list shell-file-name shell-command-switch script))))
(defun promise:make-thread (function &rest args)
"Return promise to make new thread via `make-thread'.
Arguments:
- FUNCTION is funcalled with ARGS in new thread.
Resolve:
- Return value from funcalled FUNCTION in the thread.
Reject:
- Error object while running in the thread."
(promise-new
(lambda (resolve reject)
(if (not (fboundp 'make-thread))
(error "`promise:make-thread' needs `make-thread' attached to Emacs-26.1 or above")
(make-thread
(lambda ()
(condition-case err
(funcall resolve (apply function args))
(error (funcall reject err)))))))))
(defun promise:wrap-message (promise)
"Return promise to show debug message after PROMISE resolved.
Arguments:
- PROMISE is any promise object.
Resolve:
- Return original return value when PROMISE resolved.
Reject:
- Return original return value when PROMISE rejected."
(promise-new
(lambda (resolve reject)
(promise-then
promise
(lambda (res)
(message "%s: %s"
(propertize "Result" 'face '(:foreground "green"))
(string-trim-right res))
(funcall resolve res))
(lambda (err)
(message "%s: %s"
(propertize "Error" 'face '(:foreground "red"))
(string-trim-right err))
(funcall reject err))))))
(defun promise:url-retrieve (url)
"Return promise to retrieve response body from URL.
Arguments:
- URL is either a string or a parsed URL. See `url-retrieve'.
Resolve:
- Response body as a string retrieved from the URL.
Reject:
- Error object while retrieving URL."
(promise-new
(lambda (resolve reject)
(url-retrieve url
(lambda (status)
;; All errors are reliably captured and rejected with appropriate values.
(if (plist-get status :error)
(funcall reject (plist-get status :error))
(condition-case err
(if (not (url-http-parse-headers))
(funcall reject (buffer-string))
(search-forward-regexp "\n\\s-*\n" nil t)
(funcall resolve (buffer-substring (point) (point-max))))
(error (funcall reject err)))))))))
(require 'xml) ; for `xml-parse-region'
(defun promise:xml-retrieve (url)
"Return promise to retrieve XML object parsed from contents from URL.
Arguments:
- URL is either a string or a parsed URL. See `url-retrieve'.
Resolve:
- XML object parsed by `xml-parse-region'.
Reject:
- Error object while retrieving URL and parsing contents."
(promise-new
(lambda (resolve reject)
(url-retrieve url
(lambda (status)
;; All errors are reliably captured and rejected with appropriate values.
(if (plist-get status :error)
(funcall reject (plist-get status :error))
(condition-case err
(if (not (url-http-parse-headers))
(funcall reject (buffer-string))
(search-forward-regexp "\n\\s-*\n" nil t)
(funcall resolve (xml-parse-region)))
(error (funcall reject err)))))))))
(defun promise:request (url)
"Return promise to request URL via `request'.
Arguments:
- URL is a target url as string.
Resolve:
- Response body as string.
Reject:
- A string list like as (status-code response-header response-body)"
(promise:request-with-args url nil))
(defun promise:request-post (url data)
"Return promise to POST DATA to URL via `request'.
Arguments:
- URL is a target url as string.
- DATA is post data alist.
Resolve:
- Response body as string.
Reject:
- A string list like as (status-code response-header response-body)"
(declare (indent 1))
(promise:request-with-args url `(:type "POST" :data ',data)))
(declare-function request "request.el" (url &rest settings))
(declare-function request-response-status-code "request.el" (response))
(declare-function request-response--raw-header "request.el" (response))
(declare-function request-response-data "request.el" (response))
(defun promise:request-with-args (url arglist)
"Return promise to request URL via `request' with ARGLIST.
Arguments:
- URL is a target url as string.
Resolve:
- Response body as string.
Reject:
- A string list like as (status-code response-header response-body)"
(declare (indent 1))
(require 'request)
(promise-new
(lambda (resolve reject)
(when (plist-get arglist :success)
(funcall reject "Success callback function is not customizable"))
(when (plist-get arglist :error)
(funcall reject "Error callback function is not customizable"))
(apply #'request url
:success (cl-function
(lambda (&key data &allow-other-keys)
(funcall resolve data)))
:error (cl-function
(lambda (&key response &allow-other-keys)
(funcall reject
(list (request-response-status-code response)
(request-response--raw-header response)
(request-response-data response)))))
arglist))))
(declare-function async-start "async.el" (start-func &optional finish-func))
(declare-function async-when-done "async.el" (proc &optional _change))
(defun promise:async-start (start-func &optional finish-func)
"Return promise to eval function in a new Emacs process via `async-start'.
Arguments:
- START-FUNC is function that will be evaled in new Emacs.
- FINISH-FUNC is function that will be evaled after START-FUNC evaled.
Resolve:
- Return value from START-FUNC in the Emacs.
Reject:
- Error object while evaluating START-FUNC and FINISH-FUNC."
(require 'async)
(promise-new
(lambda (resolve reject)
(set-process-sentinel (async-start start-func
(lambda (result)
(when finish-func
(funcall finish-func result))
(funcall resolve result)))
(lambda (process event)
(condition-case reason
(async-when-done process event)
(error (funcall reject reason))))))))
(defun promise-wait (timeout promise)
"Return promise to wait synchronously until PROMISE is resolved or rejected or TIMEOUT.
Arguments:
- TIMEOUT can accept the various formats. See `run-at-time'.
- PROMISE is any promise object.
Resolve:
- Return (:fullfilled value), value is PROMISE resolved value.
Reject:
- Return (:rejected reason), reason is PROMISE rejected reason.
Timeout:
- Return (:timeouted)."
(declare (indent 1))
(catch 'done
(let* (handled
(timer (run-at-time timeout nil
(lambda ()
(unless handled
(setq handled t)
(throw 'done (promise-reject '(:timeouted))))))))
(promise-then promise
(lambda (value)
(unless handled
(setq handled t)
(cancel-timer timer)
(throw 'done (promise-resolve `(:fullfilled ,value)))))
(lambda (reason)
(unless handled
(setq handled t)
(cancel-timer timer)
(throw 'done (promise-reject `(:rejected ,reason))))))
(while t (accept-process-output)))))
(defun promise-wait-value (promise)
"Return orignal value form PROMISE return value of `promise-wait'."
(seq-let (state value) (promise-_value promise)
(cond
((eq :fullfilled state) value)
((eq :rejected state) (error "Rejected: %s" (prin1-to-string value)))
((eq :timeouted state) (error "Timeouted: %s" (prin1-to-string value))))))
(defun promise-concurrent--internal (concurrent limit promisefn &optional no-reject-immediately-p)
"Internal function of `promise-concurrent'.
Arguments:
- CONCURRENT is limited number of concurrent promises.
- LIMIT is number of PROMISEFN executions.
- PROMISEFN is function should return any promise object.
- If NO-REJECT-IMMEDIATELY-P is non-nil, returned promise is not reject immidiately."
(declare (indent 2))
(let ((pipeline (make-vector concurrent nil))
(results (make-vector limit nil))
(count -1)
reasons)
(dotimes (i concurrent)
(aset pipeline i
(promise-new
(lambda (resolve reject)
(cl-labels
((worker (inx)
(if (not (< inx limit))
(funcall resolve)
(promise-chain (funcall promisefn inx)
(then (lambda (res)
(aset results inx res)
(worker (cl-incf count))))
(catch (lambda (reason)
(if (not no-reject-immediately-p)
(funcall reject reason)
(push `(,inx ,reason) reasons)
(worker (cl-incf count)))))))))
(worker (cl-incf count)))))))
(promise-chain (promise-all pipeline)
(then (lambda (_)
(if (not reasons)
results
(promise-reject `(,results ,reasons))))))))
(defun promise-concurrent (concurrent limit promisefn)
"Return promise to run a limited number of concurrent promises.
This function returns promise which immediately rejected if one
of promises fails. This behavior corresponds to `promise-all'.
See `promise-concurrent-no-reject-immidiately' with no reject immidiately.
Arguments:
- CONCURRENT is limited number of concurrent promises.
- LIMIT is number of PROMISEFN executions.
- PROMISEFN is function should return any promise object.
Resolve:
- Return vector includes values resolved for promise with respect to order.
Reject:
- Return reason for the first rejection."
(declare (indent 2))
(funcall #'promise-concurrent--internal concurrent limit promisefn))
(defun promise-concurrent-no-reject-immidiately (concurrent limit promisefn)
"Return promise to run a limited number of concurrent promises.
This function returns promise which execute the whole promises if
a promise fails. If all promises are fulfilled, only vectors
with resolved values are returned. If one of promise is
rejected, the whole promises are executed and the index and
reason rejected as the second return value is returned after the
whole state has been determined. In this case, the index location
of the first return value is nil.
See `promise-concurrent' with reject immidiately.
Arguments:
- CONCURRENT is limited number of concurrent promises.
- LIMIT is number of PROMISEFN executions.
- PROMISEFN is function should return any promise object.
Resolve:
- Return vector includes values resolved for promise with respect to order.
Reject:
- Return (<vector> <list>)
<vector> includes values resolved for promise with respect to order.
<list> is list of (index reason)."
(declare (indent 2))
(funcall #'promise-concurrent--internal concurrent limit promisefn :no-reject-immediately))
(provide 'promise)
;;; promise.el ends here