Obteniendo datos asíncronos de una API con Emacs Lisp | Programador Web Valencia

Obteniendo datos asíncronos de una API con Emacs Lisp

3 minutos

Emacs

Emacs Lisp es un lenguaje tremendamente versátil. Puedes usarlo para gestionar el buffer o tareas comunes en otros lenguajes de programación. Entre ellas nos encontramos con la posibilidad de realizar peticiones HTTP a una API. Ahora te voy a plantear un problema muy común y diferentes formas de resolverlo.

Pongámonos en situación: queremos obtener datos de una lista de endpoints. Para ello necesitamos iterar cada uno de ellos en un bucle.

'(https://jsonplaceholder.typicode.com/todos/1
  https://jsonplaceholder.typicode.com/todos/2
  https://jsonplaceholder.typicode.com/todos/3)

En principio no parece una gran tarea. Lo único que necesitamos es hacer una llamada a cada uno de los endpoints y guardar el resultado.

(require 'request)

(dolist (url '(https://jsonplaceholder.typicode.com/todos/1
         https://jsonplaceholder.typicode.com/todos/2
         https://jsonplaceholder.typicode.com/todos/3))
    (request
     url
     :type "GET"
     :parser 'json-read
     :success (cl-function
           (lambda (&key data &allow-other-keys)
         (message "Data fetched: %s" data)))
     :error (lambda (&rest _)
          (message "Error fetching data."))))

Sin embargo hay un detalle que lo cambia todo: Las llamadas son asíncronas. Esto significa que no podemos hacer una llamada tras otra, esperando que termine la anterior. Todas las llamadas se ejecutar de forma simultánea, en paralelo. No podemos saber cuando habrán terminado todas ellas antes de continuar.

Para resolver tienes 2 opciones:

  1. Configurando para que las llamadas sean síncronas con :sync t
(require 'request)

(defun fetch-data ()
  "Fetch data from the queue."
  (dolist (url '(https://jsonplaceholder.typicode.com/todos/1
         https://jsonplaceholder.typicode.com/todos/2
         https://jsonplaceholder.typicode.com/todos/3))
    (request
     url
     :type "GET"
     :sync t
     :parser 'json-read
     :success (cl-function
           (lambda (&key data &allow-other-keys)
         (message "Data fetched: %s" data)))
     :error (lambda (&rest _)
          (message "Error fetching data.")))
    (while request-active))
  (message "All items in the queue have been fetched."))

No es una buena opción. Bloquear el editor mientras se realizar las llamadas. Además, como se realizan de forma secuencial, es lento.

  1. Construir un sistema de colas de tareas.

Una lógica que te informe del estado de cada una de las llamadas (pendiente, en proceso, finalizada, error), guarde los resultados y te informe cuando todas las llamadas hayan terminado.

Con Elisp es bastante sencillo ya que podemos crear hooks para que se ejecuten cuando se actualice la cola o finalice alguna acción.

Por lo tanto los pasos a seguir son:

  1. Crear una lista de tareas a realizar.
  2. Crear una función que recorra la lista de tareas y realice las llamadas de forma asíncrona.
  3. Crear un hook que se ejecute cuando se actualice la cola.
  4. Crear un hook que se ejecute cuando todas las tareas hayan terminado.

Y con esto ya habríamos solucionado el problema.

A continuación te dejo un ejemplo completo:

;;; -*- lexical-binding: t; -*-

(require 'request)

;; Variables

(defvar queue nil) ;; The queue (list of items to fetch)
(defvar queue-update-hook nil) ;; Hook to run when the queue is updated
(defvar fetch-finished-hook nil) ;; Hook to run when all items in the queue have been fetched
(defconst queue-status '(:pending :processing :done :error)) ;; Possible statuses for an item in the queue

;; Functions

(defun update-status-by-url (queue url new-status)
  "Update the status of the item in the queue with the given URL to the new status. Return the updated queue."
  (let ((item (cl-find-if (lambda (i) (string= (cdr (assoc 'url i)) url)) queue)))
    (when item
      (setf (cdr (assoc 'status item)) new-status)))
  queue)

(defun update-response-by-url (queue url new-response)
"Update the response of the item in the queue with the given URL to the new response. Return the updated queue."
  (let ((item (cl-find-if (lambda (i) (string= (cdr (assoc 'url i)) url)) queue)))
    (when item
      (setf (cdr (assoc 'response item)) new-response)))
  queue)


(defun fetch-data ()
  "Fetch data from the queue."
  (dolist (item queue)
    (let ((url (cdr (assoc 'url item))))
      (setq queue (update-status-by-url queue url :processing))
      (request
	url
	:type "GET"
	:parser 'json-read
	:success (cl-function
		  (lambda (&key data &allow-other-keys)
		    ;; Set the status
		    (setq queue (update-status-by-url queue url :done))
		    ;; Set the response
		    (setq queue (update-response-by-url queue url data))
		    (run-hooks 'queue-update-hook)))
	:error (lambda (&rest _)
		 (setq queue (update-status-by-url queue url :error))
		 (run-hooks 'queue-update-hook))))))
;; Hooks

;; Feedback on the queue
(add-hook 'queue-update-hook
	  (lambda ()
	    (let ((in-progress (- (length queue) (length (cl-remove-if-not (lambda (i) (eq (cdr (assoc 'status i)) :done)) queue)))))
	      (when (eq in-progress 0)
		(run-hooks 'fetch-finished-hook)))))

;; Feedback when all items in the queue have been fetched
(add-hook 'fetch-finished-hook
	  (lambda ()
	    (message "All items in the queue have been fetched.")))

;; Start

(setq queue '(
	      (
	       (url . "https://jsonplaceholder.typicode.com/todos/1")
	       (status . :pending)
	       (response . nil)
	       )
	      (
	       (url . "https://jsonplaceholder.typicode.com/todos/2")
	       (status . :pending)
	       (response . nil)
	       )
	      (
	       (url . "https://jsonplaceholder.typicode.com/todos/3")
	       (status . :pending)
	       (response . nil)
	       ))) ;; Set the queue

(fetch-data) ;; Run the fetch data function

Con este código, cada vez que se actualice la cola se ejecutará el hook queue-update-hook. Cuando todas las tareas hayan terminado se ejecutará el hook fetch-finished-hook.

Espero que te sea de ayuda.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me invitas a un café? ☕

Puedes hacerlo usando el terminal.

ssh customer@andros.dev -p 5555

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...