aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/test-wttrin-query-request-id.el56
-rw-r--r--wttrin.el16
2 files changed, 70 insertions, 2 deletions
diff --git a/tests/test-wttrin-query-request-id.el b/tests/test-wttrin-query-request-id.el
new file mode 100644
index 0000000..cf3c371
--- /dev/null
+++ b/tests/test-wttrin-query-request-id.el
@@ -0,0 +1,56 @@
+;;; test-wttrin-query-request-id.el --- Stale async response guard -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024-2026 Craig Jennings
+
+;;; Commentary:
+;; wttrin-query reuses the single *wttr.in* buffer, so an async response for an
+;; earlier query that completes after a later one must not overwrite the newer
+;; buffer. A per-buffer request id, captured in the callback closure, drops the
+;; stale response.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+(ert-deftest test-wttrin-query-normal-current-callback-displays ()
+ "Normal: a query's own callback (matching request id) displays its result."
+ (let ((cb nil) (displayed nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_loc c) (setq cb c)))
+ ((symbol-function 'switch-to-buffer)
+ (lambda (buf &rest _) (set-buffer buf)))
+ ((symbol-function 'wttrin--display-weather)
+ (lambda (query &rest _) (setq displayed query))))
+ (wttrin-query "Paris")
+ (funcall cb "paris-data")
+ (should (equal displayed "Paris")))
+ (when (get-buffer "*wttr.in*") (kill-buffer "*wttr.in*")))))
+
+(ert-deftest test-wttrin-query-error-stale-callback-does-not-overwrite-newer ()
+ "Error: a slow response for an earlier query does not overwrite a newer query.
+Paris then Berlin share the *wttr.in* buffer; the Paris callback arriving after
+Berlin must be ignored."
+ (let ((callbacks nil) (displayed nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_loc cb) (push cb callbacks)))
+ ((symbol-function 'switch-to-buffer)
+ (lambda (buf &rest _) (set-buffer buf)))
+ ((symbol-function 'wttrin--display-weather)
+ (lambda (query &rest _) (setq displayed query))))
+ (wttrin-query "Paris")
+ (wttrin-query "Berlin")
+ (let ((paris-cb (cadr callbacks)) ; first pushed
+ (berlin-cb (car callbacks))) ; last pushed
+ (funcall berlin-cb "berlin-data")
+ (should (equal displayed "Berlin"))
+ (funcall paris-cb "paris-data")
+ (should (equal displayed "Berlin"))))
+ (when (get-buffer "*wttr.in*") (kill-buffer "*wttr.in*")))))
+
+(provide 'test-wttrin-query-request-id)
+;;; test-wttrin-query-request-id.el ends here
diff --git a/wttrin.el b/wttrin.el
index f109ed2..e34fdb5 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -1216,22 +1216,34 @@ coordinates but can name the place)."
(setq-local wttrin--current-address address)
(wttrin--debug-mode-line-info)))))
+(defvar wttrin--request-counter 0
+ "Monotonic counter for weather requests, used to drop stale async responses.")
+
+(defvar-local wttrin--current-request-id nil
+ "Request id of the most recent query for this weather buffer.")
+
(defun wttrin-query (query &optional display address)
"Asynchronously query weather for QUERY, display the result when ready.
QUERY is what weather is fetched by (and the cache key). Optional DISPLAY is
the name shown in the header (a saved-location name); when nil it falls back to
QUERY. Optional ADDRESS is shown on a \"Location:\" line, used when QUERY is raw
coordinates from a geolocation command."
- (let ((buffer (get-buffer-create (format "*wttr.in*"))))
+ (let ((buffer (get-buffer-create (format "*wttr.in*")))
+ (request-id (setq wttrin--request-counter (1+ wttrin--request-counter))))
(switch-to-buffer buffer)
(setq buffer-read-only nil)
(erase-buffer)
(insert "Loading weather for " (or display query) "...")
(setq buffer-read-only t)
+ (setq-local wttrin--current-request-id request-id)
(wttrin--get-cached-or-fetch
query
(lambda (raw-string &optional error-msg)
- (when (buffer-live-p buffer)
+ ;; The single *wttr.in* buffer is reused, so ignore a response whose
+ ;; query was superseded by a newer one before it returned.
+ (when (and (buffer-live-p buffer)
+ (= request-id
+ (buffer-local-value 'wttrin--current-request-id buffer)))
(with-current-buffer buffer
(wttrin--display-weather query raw-string error-msg display address)))))))