-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcui-async1.el
More file actions
268 lines (232 loc) · 10.4 KB
/
Copy pathcui-async1.el
File metadata and controls
268 lines (232 loc) · 10.4 KB
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
;;; cui-async1.el --- Unroll async chains of parallel and sequencial callbacks -*- lexical-binding: t -*-
;; Copyright (c) 2025 github.com/Anoncheg1,codeberg.org/Anoncheg
;; SPDX-License-Identifier: AGPL-3.0-or-later
;; Author: <github.com/Anoncheg1,codeberg.org/Anoncheg>
;; Keywords: tools, async, callback
;; URL: https://github.com/Anoncheg1/emacs-async1
;; Version: 0.1
;; Created: 25 Aug 2025
;;; License
;; This file is not part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;; Licensed under the GNU Affero General Public License, version 3 (AGPLv3)
;; <https://www.gnu.org/licenses/agpl-3.0.en.html>
;;; Commentary:
;; Usage:
;; You define and run pipeline with `cui-async1-start'.
;; You may call own function defined with (data callback) parameters.
;; You may redefine `async-default-aggregator' for parallel
;; calls. There may be only one aggregator for now.
;; :parallel should be at the beginin of list
;; :aggregator may be anywhere in parallel list
;; Deep trees should work also.
;; How this works:
;; Each async records-functions wrapped in lambda that call to next
;; record with result.
;; All lambda functions created as a one lambda and we call it.
;; Examples of usage:
;; 1. Sequential and parallel steps with default template
;; (cui-async1-start nil
;; '((:result "Step 1" :delay 1)
;; (:parallel
;; (:result "Parallel A" :delay 2)
;; (
;; (:result "Sub-seq a" :delay 1)
;; (:result "Sub-seq b" :delay 1)
;; )
;; (:result "Parallel B" :delay 2))
;; (:result "Step 3" :delay -1)))
;; "Final result: {Step 1 -> Sub-seq a -> Sub-seq b, Step 1 -> Parallel B,
;; Step 1 -> Parallel A} -> Step 3"
;; 2. Mixing custom function and parallel steps
;; (defun custom-async-step (data callback)
;; "Custom async function that modifies data differently.
;; CALLBACK is optionall and may be ignored, see `async-create-function'
;; for refence."
;; (run-at-time 1.5 nil callback
;; (concat data " -> Custom Step")))
;; (cui-async1-start nil
;; '((:result "Step 1" :delay 1)
;; (:parallel
;; custom-async-step
;; (:result "Parallel B" :delay 1))
;; (:result "Step 3" :delay 1)))
;; 3. With custom aggregator
;; (defun custom-aggregator (results)
;; "Custom aggregator that joins results with ' & '."
;; (concat "{" (mapconcat 'identity results " & ") "}"))
;; (cui-async1-start nil
;; '((:result "Step 1" :delay 1)
;; (:parallel
;; (:result "Parallel A" :delay 1)
;; (:result "Parallel B" :delay 2)
;; :aggregator #'custom-aggregator)))
;; Output: "Final result: {Step 1 -> Parallel B & Step 1 -> Parallel A}"
;; 4. Use external data in callback and callback with one argument
;; (let* ((var "myvar")
;; (stepcallback)
;; (callback1 (lambda (data)
;; (funcall stepcallback (concat data " -> " var))))
;; (call (lambda (data callback)
;; (setq stepcallback callback)
;; (run-at-time 0 nil callback1
;; (concat data " -> " "Step1"))))
;; )
;; (cui-async1-start nil
;; (list call
;; call
;; call
;; )))
;; Output: "Final result: -> Step1 -> myvar -> Step1 -> myvar -> Step1 -> myvar"
;; 5. Use mutable lambdas
;; (let* ((call (lambda (step)
;; (lambda (data callback)
;; (run-at-time 0 nil callback
;; (concat data " -> " "Step" (number-to-string step)))))
;; ))
;; (cui-async1-start nil
;; (list (funcall call 0)
;; (funcall call 1)
;; (funcall call 2)
;; (funcall call 3))))
;; Output: "Final result: -> Step0 -> Step1 -> Step2 -> Step3"
;; Battlefield example: ehttps://github.com/Anoncheg1/cui/blob/main/cui-prompt.el
;;; TODO:
;; - make :aggregator to be able to set many of them. (or it is not necessary?)
;; - add :catch for error handling. (or it is not necessory?)
;;; Code:
;;;###autoload
(defun cui-async1-default-template (data callback delay result-suffix)
"Default async function template.
Appending RESULT-SUFFIX to DATA after DELAY seconds and call CALLBACK."
(run-at-time delay nil callback
(concat (or (if data (concat data " -> "))
"") result-suffix)))
;;;###autoload
(defun cui-async1-default-aggregator (results)
"Default aggregator for parallel RESULTS, concatenating them with commas."
;; (print "aggregator" results)
(let ((r (mapconcat #'identity results ", ")))
(if (> (length results) 1)
(concat "{" r "}")
r)))
(defun cui-async1-create-function (spec)
"Create an async function from SPEC.
SPEC is either a function that accepts (data, callback), a plist with
:result and :delay, or a list representing a sequential sub-chain."
(cond
((functionp spec) spec)
((and (listp spec) (not (eq (car spec) :parallel)) (listp (car spec)))
;; Treat as a sequential sub-chain
(lambda (data callback)
(cui-async1-start data spec callback)))
(t
;; Handle plist
(let ((result (or (plist-get spec :result) "Result"))
(delay (or (plist-get spec :delay) 1)))
(mapc (lambda (x)
(if (and (symbolp x) (not (member x '(:result :delay))))
(error "Unknown key %s in async function spec" x)))
spec)
(lambda (data callback)
(cui-async1-default-template data callback delay result))))))
(defun cui-async1-plist-remove (plist key)
"Remove KEY and its value from PLIST, returning a new plist.
Used for :aggregator."
(if (memq key plist)
(let ((new-plist (copy-sequence plist)))
(delq (cadr (memq key new-plist)) new-plist)
(delq key new-plist))
plist))
(defun cui-async1-plist-get (plist key &optional default)
"Get value by KEY from PLIST.
If KEY is not found, return DEFAULT.
`plist-get' doesn't work if list has missing values or keys; it doesn't
respect :keywords, only order of key-value.
Used for :aggregator."
(if (memq key plist)
(let ((value (cadr (memq key plist))))
(if (and (listp value) (eql (car value) 'function))
(cadr value) ;; Extract symbol from function
;; else - value found
;; if value is next keyword, return nil
(if (and (symbolp value)
(let ((name (symbol-name value)))
(and (> (length name) 1)
(eq (aref name 0) ?:))))
nil
;; else
value)))
;; KEY not found: return default
default))
;; (if (not (eq (cui-async1-plist-get '(:foo 1 :bar nil :zaza nil) :zaza) nil))
;; (error "Error: cui-async1-plist-get1"))
;; (if (not (eq (cui-async1-plist-get '(:foo 1 :bar nil :zaza) :zaza) nil))
;; (error "Error: cui-async1-plist-get2"))
;; (if (not (eq (cui-async1-plist-get '(:zaza :foo 1 :bar nil) :zaza) nil))
;; (error "Error: cui-async1-plist-get3"))
(defun cui-async1--handle-parallel-step (specs data chain-step current-index)
"Execute parallel SPECS with DATA, aggregate results with AGGREGATOR.
Call CHAIN-STEP with CURRENT-INDEX."
(let* ((aggregator (cui-async1-plist-get specs :aggregator))
(specs (cui-async1-plist-remove specs :aggregator))
(results '())
(pending-calls (length specs)))
(if (zerop pending-calls)
(funcall chain-step data (1+ current-index))
(dolist (spec specs)
(let ((func (cui-async1-create-function spec)))
(funcall func data
(lambda (result)
(push result results)
(when (zerop (setq pending-calls (1- pending-calls)))
(let ((aggregated-result (funcall (or aggregator #'cui-async1-default-aggregator) results)))
(funcall chain-step aggregated-result (1+ current-index)))))))))))
(defun cui-async1--handle-sequential-step (step data chain-step current-index)
"Execute sequential STEP with DATA and call CHAIN-STEP with CURRENT-INDEX."
(let ((func (cui-async1-create-function step)))
(funcall func data
(lambda (result)
(funcall chain-step result (1+ current-index))))))
;;;###autoload
(defun cui-async1-start (initial-data sequence &optional final-callback)
"Execute a SEQUENCE of async functions.
First function receive INITIAL-DATA.
FINAL-CALLBACK is a function with one parameter - data, without callback.
Each spec is either:
1) a function (taking data and callback),
2) a plist with :result and :delay keys,
3) (:parallel spec1 spec2 ...) for parallel execution,
4) a list of specs for a sequential sub-chain.
For parallel steps, execute functions concurrently and combine results
using AGGREGATOR or `async-default-aggregator'.
Each function in SEQUENCE takes DATA and a CALLBACK, passing results to
the next function.
\(chain-step(data 0) -> (funcall func data callback) -> lambda (result)
-> (chain-step(data 1))
Returns result of the first function in the chain."
(letrec ((chain-step
(lambda (data current-index)
(if (< current-index (length sequence))
(let ((step (nth current-index sequence)))
;; (print (list step current-index))
(if (and (listp step) (eq (car step) :parallel))
(cui-async1--handle-parallel-step (cdr step) data chain-step current-index)
(cui-async1--handle-sequential-step step data chain-step current-index)))
;; finally
(if final-callback
(funcall final-callback data)
;; else
(print (format "Final result: %s" data)))))))
(funcall chain-step initial-data 0)))
(provide 'cui-async1)
;;; cui-async1.el ends here