feat: auto-invalidate cache when installed node set changes

Fingerprints NODE_CLASS_MAPPINGS; on the first object_info request after a
restart, drops the cached object_info if a node was installed/updated/enabled/
removed, so new nodes appear without a manual refresh. First run (no stored
signature) does not invalidate. Unit-tested.

Also documents that the pack has no deps and does not interfere with other
nodes' installs/updates or their own routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 23:16:39 +02:00
parent a8d8b3792c
commit 4a1c2f3a99
2 changed files with 64 additions and 0 deletions
+15
View File
@@ -108,5 +108,20 @@ python main.py --listen --port 8188 --enable-compress-response-body
- An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this - An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this
pack does it in-process so no extra service, container, or port is needed. pack does it in-process so no extra service, container, or port is needed.
## Installing / updating other custom nodes
This pack is a quiet neighbour:
- **No dependencies** — its `requirements.txt` is empty, so it can't cause the
pip version conflicts that break other nodes' installs/updates. It also never
touches other nodes' files or the ComfyUI-Manager installer.
- **Auto-detects node changes** — it fingerprints the installed node set
(`NODE_CLASS_MAPPINGS`) and, on the first page load after a restart, drops the
cache automatically if a node was **installed, updated, enabled or removed**
so new nodes appear with no manual refresh.
- The only thing it gates is the full `/object_info` list (a cached snapshot);
it passes every other request straight through, so other nodes' own routes,
sidebars and refresh buttons are unaffected. For an in-place node tweak that
changes an existing node's inputs *without* adding/removing a node class, use a
refresh button.
## License ## License
MIT — see [LICENSE](LICENSE). MIT — see [LICENSE](LICENSE).
+49
View File
@@ -26,6 +26,7 @@ import os
import gzip import gzip
import json import json
import time import time
import hashlib
import logging import logging
import threading import threading
@@ -45,6 +46,7 @@ _CACHE_DIR = os.path.join(os.path.dirname(__file__), "cache")
_RAW_PATH = os.path.join(_CACHE_DIR, "object_info.json") _RAW_PATH = os.path.join(_CACHE_DIR, "object_info.json")
_GZ_PATH = os.path.join(_CACHE_DIR, "object_info.json.gz") _GZ_PATH = os.path.join(_CACHE_DIR, "object_info.json.gz")
_SNAP_PATH = os.path.join(_CACHE_DIR, "scan_snapshot.json") _SNAP_PATH = os.path.join(_CACHE_DIR, "scan_snapshot.json")
_SIG_PATH = os.path.join(_CACHE_DIR, "node_signature.txt")
_OBJECT_INFO_PATHS = ("/object_info", "/api/object_info") _OBJECT_INFO_PATHS = ("/object_info", "/api/object_info")
_GZIP_LEVEL = 5 _GZIP_LEVEL = 5
@@ -297,6 +299,51 @@ def register_files(folder_name, rel_paths):
return {"added": added, "skipped": skipped, "folder": folder_name, "files": len(filtered)} return {"added": added, "skipped": skipped, "folder": folder_name, "files": len(filtered)}
# --------------------------------------------------------------------------- #
# Auto-invalidate when installed nodes change (install / update / enable / remove)
# --------------------------------------------------------------------------- #
_sig_checked = False
def _current_node_signature():
"""A cheap fingerprint of the available node set. Changes when a node is
installed, removed, enabled or disabled."""
try:
import nodes
keys = sorted(nodes.NODE_CLASS_MAPPINGS.keys())
except Exception:
return None
h = hashlib.sha1()
for k in keys:
h.update(k.encode("utf-8", "replace"))
h.update(b"\x00")
return f"{len(keys)}:{h.hexdigest()}"
def _check_node_signature():
"""Drop the cached object_info if the node set changed since last run, so
newly installed/updated nodes show up without a manual refresh."""
global _sig_checked
_sig_checked = True
try:
cur = _current_node_signature()
if cur is None:
return
old = None
if os.path.exists(_SIG_PATH):
with open(_SIG_PATH) as f:
old = f.read().strip()
if old is not None and old != cur:
log.info("Tenaciousload: installed node set changed -> invalidating object_info cache")
invalidate_object_info_cache()
if old != cur:
os.makedirs(_CACHE_DIR, exist_ok=True)
with open(_SIG_PATH, "w") as f:
f.write(cur)
except Exception as e: # pragma: no cover
log.warning("Tenaciousload: node signature check failed: %s", e)
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# object_info caching middleware # object_info caching middleware
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@@ -324,6 +371,8 @@ async def _object_info_cache_mw(request, handler):
global _disk_loaded global _disk_loaded
if not _disk_loaded: if not _disk_loaded:
_load_from_disk() _load_from_disk()
if not _sig_checked:
_check_node_signature() # auto-drop cache if nodes were installed/updated
if "nocache" not in request.query and _mem["raw"] is not None: if "nocache" not in request.query and _mem["raw"] is not None:
return _serve_cached(request) return _serve_cached(request)