From ce74e30562dfac3f599a94a45ac16c3c2319ab41 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 28 Jun 2026 04:06:12 -0400 Subject: fix: drop stale async weather responses that overwrite a newer query wttrin-query reuses the single *wttr.in* buffer, so requesting Paris then Berlin and having Paris return last overwrote the Berlin buffer. Each query now stamps a monotonic request id on the buffer; the callback displays only when its captured id still matches, so a superseded response is ignored. --- tests/test-wttrin-query-request-id.el | 56 +++++++++++++++++++++++++++++++++++ wttrin.el | 16 ++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/test-wttrin-query-request-id.el 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))))))) -- cgit v1.2.3