Move the batch flush and tp-with-batch-updates up into tp-render

tp--flush-batch-updates existed to re-render buffers, which is
tp-render's whole job - but it lived down in tp-reactive and reached
the renderer through the tp--reactive-flush-function inversion,
complete with a guard that silently DROPPED queued re-renders when no
hook was installed.  A partial load could thus discard updates without
a trace.

Move tp--flush-batch-updates and the public tp-with-batch-updates
macro (same name, same behavior - only the home file changes) into
tp-render.el, next to tp--reactive-flush-entry, which the flush now
calls directly.  Delete the tp--reactive-flush-function defvar, its
silent-drop guard, and the install line.  The queue state
(tp--batch-update-pending, tp--batch-update-active,
tp--reactive-updating) and tp--queue-batch-update stay in tp-reactive;
the relocated macro let-binds them downward, which is legal.

Together with the tp-text handler move this takes the sanctioned
upward hooks from four to two - only the genuine lower-layer event
sources remain (tp--reactive-update-function in tp-reactive,
tp--layer-refresh-function in tp-layer) - and a degraded partial load
now fails honestly with a void-function error instead of silently
discarding queued re-renders.  The ARCH-4 unwind-protect around the
flush tail in tp--reactive-apply-update is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-27 02:06:28 +08:00
parent 2b000b5f0e
commit 6295d5ccbc
2 changed files with 52 additions and 57 deletions

View File

@ -13,9 +13,10 @@
;; Reactive core of tp: storage for variable dependencies, watchers,
;; computed properties and data variables; registration/unregistration;
;; the variable-watcher shell and the batching queue. The actual
;; re-rendering of buffers lives in tp-render.el, which installs
;; itself via `tp--reactive-update-function' / `tp--reactive-flush-function'.
;; the variable-watcher shell and the batching queue state. The
;; actual re-rendering of buffers - including the queue flush and the
;; public `tp-with-batch-updates' macro - lives in tp-render.el, which
;; installs itself via `tp--reactive-update-function'.
;;; Code:
@ -254,11 +255,6 @@ SYMBOL NEWVAL WHERE OVERRIDE-ALIST) after the user watch callbacks
have run. When nil, variable changes only invoke watch callbacks and
no re-rendering happens.")
(defvar tp--reactive-flush-function nil
"Function flushing one pending batched update entry.
Installed by tp-render.el. Called with (LAYER-NAME WHERE
TP-TEXT-AFFECTED).")
(defun tp--reactive-variable-watcher (symbol newval operation where)
"Watcher function called when a reactive variable changes.
SYMBOL is the variable that changed.
@ -321,48 +317,6 @@ NEWVAL is the new value, OLDVAL is the old value."
(error (message "tp: watcher error for %s watching %s: %s"
layer-name watch-sym err))))))))
(defun tp--flush-batch-updates ()
"Flush all pending batch updates.
This processes all updates collected during a `tp-with-batch-updates' form."
(tp-debug-log "Flushing %d pending batch updates" (length tp--batch-update-pending))
(let ((processed-layers nil))
;; Process each pending update, avoiding duplicate layer updates
(dolist (pending (nreverse tp--batch-update-pending))
(let ((layer-name (car pending))
(where (caddr pending))
(tp-text-affected (cadddr pending)))
(unless (memq layer-name processed-layers)
(push layer-name processed-layers)
(tp-debug-log " Batch updating layer %s (tp-text: %s)"
layer-name (if tp-text-affected "yes" "no"))
(when tp--reactive-flush-function
(funcall tp--reactive-flush-function
layer-name where tp-text-affected))))))
(setq tp--batch-update-pending nil))
(defmacro tp-with-batch-updates (&rest body)
"Execute BODY with reactive updates batched.
Multiple variable changes within BODY are collected and applied
together at the end, avoiding redundant buffer modifications.
This is useful when changing multiple reactive variables simultaneously:
(tp-with-batch-updates
(setq my-color \"red\")
(setq my-size 14)
(setq my-text \"Hello\"))
Without batching, each `setq' would trigger a separate buffer update.
With batching, all updates are consolidated and applied once at the end."
(declare (indent 0) (debug t))
`(let ((tp--batch-update-active t)
(tp--batch-update-pending nil))
(tp-debug-log "Starting batch updates")
(unwind-protect
(progn ,@body)
(tp-debug-log "Ending batch updates")
(tp--flush-batch-updates))))
(defun tp--register-layer-watchers (layer-name watchers)
"Register WATCHERS for LAYER-NAME.
WATCHERS is a list of (VAR-SYMBOL CALLBACK) pairs."

View File

@ -13,10 +13,12 @@
;; The reactive update engine: when a reactive variable changes, this
;; module recomputes layer definitions and re-renders every affected
;; buffer region, including live `tp-text' text replacement. It
;; installs itself into tp-reactive.el (update/flush hooks) and
;; tp-layer.el (layer refresh hook), and calls down into tp-ops.el
;; for the `tp-text' helper chain.
;; buffer region, including live `tp-text' text replacement. It also
;; owns the batching flush and the public `tp-with-batch-updates'
;; macro (the queue state lives in tp-reactive.el). It installs
;; itself into tp-reactive.el (update hook) and tp-layer.el (layer
;; refresh hook), and calls down into tp-ops.el for the `tp-text'
;; helper chain.
;;; Code:
@ -500,15 +502,54 @@ installed as `tp--reactive-update-function'."
TP-TEXT-AFFECTED non-nil means the layer's `tp-text' changed and the
text itself must be replaced. Runs after the changed variables have
actually been set, so layer props re-resolve against current
\(buffer-local aware) values. Installed as
`tp--reactive-flush-function'."
\(buffer-local aware) values. This is the per-entry worker of
`tp--flush-batch-updates'."
(if tp-text-affected
(tp--update-reactive-text layer-name where)
(tp--update-layer-regions layer-name where)))
(defun tp--flush-batch-updates ()
"Flush all pending batch updates.
This processes all updates collected during a `tp-with-batch-updates' form."
(tp-debug-log "Flushing %d pending batch updates" (length tp--batch-update-pending))
(let ((processed-layers nil))
;; Process each pending update, avoiding duplicate layer updates
(dolist (pending (nreverse tp--batch-update-pending))
(let ((layer-name (car pending))
(where (caddr pending))
(tp-text-affected (cadddr pending)))
(unless (memq layer-name processed-layers)
(push layer-name processed-layers)
(tp-debug-log " Batch updating layer %s (tp-text: %s)"
layer-name (if tp-text-affected "yes" "no"))
(tp--reactive-flush-entry layer-name where tp-text-affected)))))
(setq tp--batch-update-pending nil))
(defmacro tp-with-batch-updates (&rest body)
"Execute BODY with reactive updates batched.
Multiple variable changes within BODY are collected and applied
together at the end, avoiding redundant buffer modifications.
This is useful when changing multiple reactive variables simultaneously:
(tp-with-batch-updates
(setq my-color \"red\")
(setq my-size 14)
(setq my-text \"Hello\"))
Without batching, each `setq' would trigger a separate buffer update.
With batching, all updates are consolidated and applied once at the end."
(declare (indent 0) (debug t))
`(let ((tp--batch-update-active t)
(tp--batch-update-pending nil))
(tp-debug-log "Starting batch updates")
(unwind-protect
(progn ,@body)
(tp-debug-log "Ending batch updates")
(tp--flush-batch-updates))))
;; Install the engine into the lower modules.
(setq tp--reactive-update-function #'tp--reactive-apply-update)
(setq tp--reactive-flush-function #'tp--reactive-flush-entry)
(setq tp--layer-refresh-function #'tp--update-layer-regions)
(provide 'tp-render)