diff --git a/README.md b/README.md index 1972def..605c321 100644 --- a/README.md +++ b/README.md @@ -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 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 MIT — see [LICENSE](LICENSE). diff --git a/__init__.py b/__init__.py index f6cbb0a..d7a9caf 100644 --- a/__init__.py +++ b/__init__.py @@ -26,6 +26,7 @@ import os import gzip import json import time +import hashlib import logging 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") _GZ_PATH = os.path.join(_CACHE_DIR, "object_info.json.gz") _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") _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)} +# --------------------------------------------------------------------------- # +# 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 # --------------------------------------------------------------------------- # @@ -324,6 +371,8 @@ async def _object_info_cache_mw(request, handler): global _disk_loaded if not _disk_loaded: _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: return _serve_cached(request)