Compare commits

...

3 Commits

Author SHA1 Message Date
willmiao
402d8b07cf docs: auto-update supporters list in README 2026-06-28 14:17:19 +00:00
Will Miao
3e303ab316 chore(release): bump version to v1.1.6 2026-06-28 22:17:02 +08:00
Will Miao
e9e8c31ad1 fix(registry): store nodes per-client to prevent multi-tab race condition
Move NodeRegistry from a single global _nodes dict to a per-client
(_tab_nodes) structure so that multiple ComfyUI browser tabs no
longer overwrite each other's workflow node data during a
lora_registry_refresh cycle.  The merged result is a union of all
known tabs' target nodes, eliminating the non-deterministic failure
where send-to-workflow could randomly target a tab lacking valid
targets.

- NodeRegistry.register_nodes(sid, nodes) replaces per-tab data
  without affecting other tabs.
- NodeRegistry.get_merged_registry() returns the union across all
  connected clients, together with tab_count / per-tab metadata.
- prepare_for_refresh() snapshots the current active sockets; caller
  re-reads before merging so that newly-connected tabs are not pruned.
- workflow_registry.js sends api.clientId in the POST body so the
  backend can identify which tab is registering.
2026-06-28 17:57:58 +08:00
7 changed files with 305 additions and 186 deletions

File diff suppressed because one or more lines are too long

View File

@@ -11,8 +11,8 @@
"Insomnia Art Designs",
"2018cfh",
"Arlecchino Shion",
"Rob Williams",
"Charles Blakemore",
"Rob Williams",
"W+K+White",
"$MetaSamsara",
"wackop",
@@ -21,6 +21,8 @@
"stone9k",
"Rosenthal",
"Francisco Tatis",
"JongWon Han",
"FreelancerZ",
"Polymorphic Indeterminate",
"Skalabananen",
"Marc Whiffen",
@@ -38,24 +40,32 @@
"Estragon",
"J\\B/ 8r0wns0n",
"ClockDaemon",
"KD",
"Omnidex",
"Tyler Trebuchon",
"Release Cabrakan",
"Tobi_Swagg",
"SG",
"James Dooley",
"zenbound",
"Buzzard",
"jmack",
"Andrew Wilson",
"Greybush",
"Mark Corneglio",
"Ricky Carter",
"James Todd",
"JongWon Han",
"Steven Pfeiffer",
"VantAI",
"レプサイ",
"Lisster",
"Michael Wong",
"runte3221",
"Illrigger",
"Tom Corrigan",
"JackieWang",
"FreelancerZ",
"fnkylove",
"Yushio",
"Vik71it",
"Echo",
"Lilleman",
@@ -65,50 +75,48 @@
"Fraser Cross",
"Liam MacDougal",
"Sterilized",
"BadassArabianMofo",
"JORGE+LUIZ+HUSSNI+MESSIAS",
"quarz",
"Greg",
"jean jahren",
"JSST",
"Snaggwort",
"lmsupporter",
"Takkan",
"wfpearl",
"Matt+J",
"Baekdoosixt",
"Jonathan Ross",
"KD",
"Omnidex",
"Jack B Nimble",
"Nazono_hito",
"Melville Parrish",
"daniel dove",
"Lustre",
"Tyler Trebuchon",
"Release Cabrakan",
"JW Sin",
"Alex",
"bh",
"carozzz",
"Marlon Daniels",
"James Dooley",
"Buzzard",
"Starkselle",
"Aaron Bleuer",
"LacesOut!",
"greebles",
"Adam Shaw",
"Mark Corneglio",
"SarcasticHashtag",
"Anthony Rizzo",
"iamresist",
"M Postkasse",
"RedrockVP",
"Wolffen",
"Wicked Choices by ASLPro3D",
"Jacob Hoehler",
"FinalyFree",
"Weasyl",
"Steven Pfeiffer",
"Timmy",
"Johnny",
"Cory Paza",
"Tak",
"Lisster",
"Big Red",
"whudunit",
"Luc Job",
@@ -116,37 +124,36 @@
"Philip Hempel",
"corde",
"nwalker94",
"Yushio",
"Bishoujoker",
"aai",
"Todd Keck",
"Briton Heilbrun",
"Tori",
"wildnut",
"Aleksander Wujczyk",
"AM Kuro",
"BadassArabianMofo",
"Pascal Dahle",
"Sangheili460",
"MagnaInsomnia",
"Akira_HentAI",
"Karl P.",
"lmsupporter",
"andrew.tappan",
"N/A",
"The Spawn",
"graysock",
"Greenmoustache",
"zounic",
"fancypants",
"Eldithor",
"Jack B Nimble",
"Digital",
"JaxMax",
"contrite831",
"Jwk0205",
"Starkselle",
"Bro Xie",
"batblue",
"carey6409",
"Olive",
"greebles",
"太郎 ゲーム",
"Some Guy Named Barry",
"M Postkasse",
"AELOX",
"Gooohokrbe",
"Nicfit23",
@@ -162,12 +169,12 @@
"Serge Bekenkamp",
"Jimmy Ledbetter",
"LeoZero",
"Antonio Pontes",
"nahinahi9",
"Dustin Chen",
"dan",
"aai",
"Mouthlessman",
"otaku fra",
"jean jahren",
"MiraiKuriyamaSy",
"Ran C",
"ViperC",
@@ -176,7 +183,6 @@
"Adam Taylor",
"AbstractAss",
"Weird_With_A_Beard",
"The Spawn",
"Pozadine1",
"Qarob",
"AIGooner",
@@ -187,18 +193,16 @@
"Hasturkun",
"Jon Sandman",
"Ubivis",
"CloudValley",
"linnfrey",
"Jackthemind",
"griffin+dahlberg",
"Joboshy",
"Digital",
"takyamtom",
"Bohemian Corporal",
"Dan",
"Bro Xie",
"yer fey",
"batblue",
"Error_Rule34_Not_found",
"carey6409",
"太郎 ゲーム",
"Roslynd",
"jinxedx",
"Neco28",
@@ -210,22 +214,21 @@
"Frank Nitty",
"Magic Noob",
"DougPeterson",
"Antonio Pontes",
"ApathyJones",
"Jeff",
"Bruce",
"Julian V",
"Steven Owens",
"nahinahi9",
"Kevin John Duck",
"Kevin Christopher",
"Blackfish95",
"dd",
"Paul Kroll",
"Bas Imagineer",
"John Statham",
"yuxz69",
"esthe",
"AlexDuKaNa",
"decoy",
"CloudValley",
"thesoftwaredruid",
"wundershark",
"mr_dinosaur",
@@ -233,15 +236,20 @@
"Ray Wing",
"Ranzitho",
"Gus",
"地獄の禄",
"MJG",
"David LaVallee",
"linnfrey",
"ae",
"Tr4shP4nda",
"capn",
"Joseph",
"Mirko Katzula",
"dan",
"Piccio08",
"kumakichi",
"cppbel",
"IamAyam",
"skaterb949",
"jeaness",
"Brian M",
"Josef Lanzl",
"Nerezza",
@@ -261,37 +269,32 @@
"Eris3D",
"Max Marklund",
"m",
"Pierce McBride",
"Pronredn",
"Mikko Hemilä",
"Jamie Ogletree",
"a _",
"Jeff",
"lh qwe",
"James Coleman",
"Martial",
"conner",
"Ouro Boros",
"Chad Idk",
"dd",
"Princess Bright Eyes",
"Yuji Kaneko",
"Dušan Ryban",
"Felipe dos Santos",
"Rops Alot",
"Sam",
"sjon kreutz",
"Ace Ventura",
"Douglas Gaspar",
"Metryman55",
"AlexDuKaNa",
"George",
"dw",
"地獄の禄",
"Gamalonia",
"WRL_SPR",
"momokai",
"Mirko Katzula",
"dan",
"Piccio08",
"kumakichi",
"cppbel",
"Moon Knight",
"몽타주",
"Kland",
@@ -313,12 +316,15 @@
"kyoumei",
"RadStorm04",
"JohnDoe42054",
"BillyHill",
"emyth",
"gzmzmvp",
"Richard",
"奚明 刘",
"Andrew",
"Robert Wegemund",
"Littlehuggy",
"Gregory Kozhemiak",
"준희 김",
"Brian Buie",
"Sadlip",
@@ -328,16 +334,16 @@
"Tomohiro Baba",
"Mike Simone",
"Noora",
"Pierce McBride",
"Joshua Gray",
"Mattssn",
"Mikko Hemilä",
"Morgandel",
"Jacob McDaniel",
"X",
"Sloan Steddy",
"Temikus",
"Artokun",
"Michael Taylor",
"Derek Baker",
"Martial",
"Michael Anthony Scott",
"Emil Andersson",
"Atilla Berke Pekduyar",
@@ -345,7 +351,6 @@
"CryptoTraderJK",
"Decx _",
"Davaitamin",
"Rops Alot",
"tedcor",
"Fotek Design",
"四糸凜音",
@@ -358,8 +363,6 @@
"starbugx",
"dc7431",
"Crocket",
"BillyHill",
"emyth",
"chriphost",
"KitKatM",
"socrasteeze",
@@ -383,35 +386,40 @@
"g9p0o",
"TheHolySheep",
"Monte Won",
"Gregory Kozhemiak",
"SpringBootisTrash",
"carsten",
"ikok",
"Wolfe7D1",
"Draven T",
"mrjuan",
"elleshar666",
"ACTUALLY_the_Real_Willem_Dafoe",
"Aquatic Coffee",
"Kauffy",
"John J Linehan",
"ethanfel",
"Elliot E",
"Morgandel",
"Theerat Jiramate",
"Focuschannel",
"Edward Kennedy",
"Noah",
"X",
"Sloan Steddy",
"Vane Holzer",
"psytrax",
"hexxish",
"Anthony Faxlandez",
"battu",
"notedfakes",
"Nathan",
"NICHOLAS BAXLEY",
"Michael Scott",
"Pat Hen",
"Xeeosat",
"Saya",
"Ed Wang",
"Jordan Shaw",
"Wes Sims",
"g unit",
"Srdb",
"Filippo Ferrari",
"JC",
"Prompt Pirate",
"uwutismxd",
@@ -427,9 +435,8 @@
"Pkrsky",
"nanana",
"raf8osz",
"SpringBootisTrash",
"carsten",
"ikok",
"FeralOpticsAI",
"Pavlaki",
"Doug+Rintoul",
"Noor",
"Yorunai",
@@ -446,36 +453,34 @@
"cocona",
"ElitaSSJ4",
"David Schenck",
"Wolfe7D1",
"blikkies",
"Chris",
"Time Valentine",
"Shock Shockor",
"ACTUALLY_the_Real_Willem_Dafoe",
"Михал Михалыч",
"Matt",
"Goldwaters",
"Kauffy",
"Zude",
"Frogmilk",
"SPJ",
"Kyler",
"Bryan Rutkowski",
"Justin Blaylock",
"aRtFuL_DodGeR",
"Nick Kage",
"psytrax",
"Cyrus Fett",
"Xenon Xue",
"Edward Ten Eyck",
"Billy Gladky",
"Michael Scott",
"Probis",
"Solixer",
"Wes Sims",
"ItsGeneralButtNaked",
"Donor4115",
"jinksta187",
"Distortik",
"Manu Thetug",
"Filippo Ferrari",
"Karlanx",
"operationancut",
"Youguang",
"andrewzpong",
"BossGame",
@@ -486,11 +491,18 @@
"Kevinj",
"Mitchell Robson",
"POPPIN",
"YassineKhaled",
"Y",
"MatteKey",
"Flob",
"ShiroSenpai",
"Inkognito",
"G",
"Tan+Huynh",
"Bob+Barker",
"D",
"Dark_Pest",
"Eldithor",
"Alex",
"Karru",
"ChaChanoKo",
@@ -503,13 +515,10 @@
"g",
"J",
"Alan+Cano",
"FeralOpticsAI",
"Pavlaki",
"BillyBoy84",
"Buecyb99",
"Welkor",
"John Martin",
"Ink Temptation",
"JBsuede",
"moranqianlong",
"Kalli Core",
@@ -519,13 +528,12 @@
"Dave Abraham",
"Joaquin Hierrezuelo",
"Locrospiel",
"Frogmilk",
"Sean voets",
"Jarrid Lee",
"Kor",
"Joseph Hanson",
"John Rednoulf",
"Kyron Mahan",
"Bryan Rutkowski",
"Boba Smith",
"TBitz33",
"Anonym dkjglfleeoeldldldlkf",
@@ -544,15 +552,12 @@
"Pete Pain",
"Jacob Winter",
"Ryan Presley Ng",
"jinksta187",
"RHopkirk",
"Andrew Wilkinson",
"Karlanx",
"Lyavph",
"Maxim",
"David",
"Meilo",
"operationancut",
"shinonomeiro",
"Snille",
"MaartenAlbers",
@@ -571,6 +576,8 @@
"Scott",
"Muratoraccio",
"D",
"Akkas+Haque",
"Kachac",
"SAVEagleBasement",
"Kevin+Isom",
"Rune+Osnes",
@@ -600,12 +607,6 @@
"Lev+Lanevskiy",
"low9",
"Winged",
"YassineKhaled",
"Y",
"MatteKey",
"Flob",
"ShiroSenpai",
"Inkognito",
"Jacky+Ho",
"generic404",
"abattoirblues",
@@ -614,29 +615,34 @@
"hayden",
"Obsidian.Studios",
"ahoystan",
"Zomba Mann",
"edk",
"Tú Nguyễn Lý Hoàng",
"shira1011",
"Neko Desco",
"Ben D",
"G",
"Vinarus",
"ja s",
"Leslie Andrew Ridings",
"Doug Mason",
"scoreswazey",
"Owen Gwosdz",
"Jarrid Lee",
"Poophead27 Blyat",
"Mythspire",
"AZ Party Oasis",
"Devil Lude",
"David Murcko",
"TheFusion",
"MR.Bear",
"Jack Dole",
"matt",
"somethingtosay8",
"3zS4QNQ4",
"Terminuz",
"ivistorm",
"max blo",
"Ivan Imes",
"CptNeo",
"Jack Lawfield",
"Borte",
@@ -645,7 +651,7 @@
"Sage Himeros",
"Eric Ketchum",
"Kevin Wallace",
"David Spearing",
"Zeeble",
"ChicRic",
"Tigon",
"BastardSama",
@@ -706,6 +712,9 @@
"SelfishMedic",
"adderleighn",
"EnragedAntelope",
"Somebody",
"Jasper",
"megameganck",
"thomasand01",
"Shiba+Sama",
"miduzza",
@@ -730,8 +739,6 @@
"matter",
"SRCRCOSS",
"imer",
"Akkas+Haque",
"Kachac",
"jcx29",
"Drizzly",
"Nebuleux",
@@ -751,24 +758,23 @@
"KUJYAKU",
"Coeur+de+cochon",
"han b",
"Zomba Mann",
"Aquaneo",
"Nico",
"Maximilian Krischan",
"Banana Joe",
"proto merp",
"_ G3n",
"Brandon Thomas",
"Donovan Jenkins",
"Hans Meier",
"sicarius",
"Michael Eid",
"Wolf and Fox Legends",
"beersandbacon",
"Neko Desco",
"Bob barker",
"Ninja Tom",
"karim ben brik",
"Vinarus",
"Elemnt",
"Josh Snyder",
"Michael Zhu",
"Nemisu",
@@ -792,16 +798,15 @@
"Forbidden Atelier",
"Thomas Sankowski",
"DrB",
"Nimhloth",
"Adictedtohumping",
"Snorklebort",
"vinter",
"Towelie",
"TheFusion",
"Jean-françois SEMA",
"3zS4QNQ4",
"Kurt",
"Andrew Ly",
"Matt M.",
"Ivan Imes",
"J M",
"Slacks",
"Bouya shaka",
@@ -837,5 +842,5 @@
"Somebody",
"CK"
],
"totalCount": 834
"totalCount": 839
}

View File

@@ -414,9 +414,10 @@ class PromptServerProtocol(Protocol):
"""Subset of PromptServer used by the handlers."""
instance: "PromptServerProtocol"
sockets: dict # maps clientId (sid) → WebSocketResponse
def send_sync(
self, event: str, payload: dict
self, event: str, payload: dict | None = None, sid: str | None = None
) -> None: # pragma: no cover - protocol
...
@@ -471,90 +472,154 @@ class BackupServiceProtocol(Protocol):
class NodeRegistry:
"""Thread-safe registry for tracking LoRA nodes in active workflows."""
"""Thread-safe registry for tracking LoRA nodes across ComfyUI tabs.
Each connected ComfyUI browser tab (identified by its ``sid`` / ``clientId``)
registers its own set of workflow nodes. Queries merge all known tabs into
a single result so that the calling LM panel always sees *every* available
target node, regardless of which tab responded fastest.
"""
def __init__(self) -> None:
self._lock = asyncio.Lock()
self._nodes: Dict[str, dict] = {}
self._registry_updated = asyncio.Event()
# sid → {unique_id → node_info}
self._tab_nodes: Dict[str, Dict[str, dict]] = {}
self._ready = asyncio.Event()
self._waiting_clients: set[str] = set()
@property
def pending_client_count(self) -> int:
"""Number of clients that have not yet responded in the current refresh cycle."""
return len(self._waiting_clients)
# ------------------------------------------------------------------
# Helpers to build one node dict (extracted so it's reused for each tab)
# ------------------------------------------------------------------
@staticmethod
def _build_node_dict(node: dict) -> dict:
node_id = node["node_id"]
graph_id = str(node["graph_id"])
unique_id = f"{graph_id}:{node_id}"
node_type = node.get("type", "")
type_id = NODE_TYPES.get(node_type, 0)
bgcolor = node.get("bgcolor") or DEFAULT_NODE_COLOR
raw_capabilities = node.get("capabilities")
capabilities: dict = {}
if isinstance(raw_capabilities, dict):
capabilities = dict(raw_capabilities)
raw_widget_names: list | None = node.get("widget_names")
if not isinstance(raw_widget_names, list):
capability_widget_names = capabilities.get("widget_names")
raw_widget_names = (
capability_widget_names
if isinstance(capability_widget_names, list)
else None
)
widget_names: list[str] = []
if isinstance(raw_widget_names, list):
widget_names = [
str(widget_name)
for widget_name in raw_widget_names
if isinstance(widget_name, str) and widget_name
]
if widget_names:
capabilities["widget_names"] = widget_names
else:
capabilities.pop("widget_names", None)
if "supports_lora" in capabilities:
capabilities["supports_lora"] = bool(capabilities["supports_lora"])
comfy_class = node.get("comfy_class")
if not isinstance(comfy_class, str) or not comfy_class:
comfy_class = node_type if isinstance(node_type, str) else None
return {
"id": node_id,
"graph_id": graph_id,
"graph_name": node.get("graph_name"),
"unique_id": unique_id,
"bgcolor": bgcolor,
"title": node.get("title"),
"type": type_id,
"type_name": node_type,
"comfy_class": comfy_class,
"capabilities": capabilities,
"widget_names": widget_names,
"mode": node.get("mode"),
"marker_role": node.get("marker_role"),
}
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
async def register_nodes(self, sid: str, nodes: list[dict]) -> None:
"""Register/replace the node list for a single ComfyUI tab (identified by *sid*)."""
tab_nodes: dict[str, dict] = {}
for node in nodes:
nd = self._build_node_dict(node)
tab_nodes[nd["unique_id"]] = nd
async def register_nodes(self, nodes: list[dict]) -> None:
async with self._lock:
self._nodes.clear()
for node in nodes:
node_id = node["node_id"]
graph_id = str(node["graph_id"])
unique_id = f"{graph_id}:{node_id}"
node_type = node.get("type", "")
type_id = NODE_TYPES.get(node_type, 0)
bgcolor = node.get("bgcolor") or DEFAULT_NODE_COLOR
raw_capabilities = node.get("capabilities")
capabilities: dict = {}
if isinstance(raw_capabilities, dict):
capabilities = dict(raw_capabilities)
self._tab_nodes[sid] = tab_nodes
self._waiting_clients.discard(sid)
if not self._waiting_clients:
self._ready.set()
raw_widget_names: list | None = node.get("widget_names")
if not isinstance(raw_widget_names, list):
capability_widget_names = capabilities.get("widget_names")
raw_widget_names = (
capability_widget_names
if isinstance(capability_widget_names, list)
else None
)
logger.debug("Registered %s nodes from client %s", len(nodes), sid)
widget_names: list[str] = []
if isinstance(raw_widget_names, list):
widget_names = [
str(widget_name)
for widget_name in raw_widget_names
if isinstance(widget_name, str) and widget_name
]
def prepare_for_refresh(self, active_sids: list[str]) -> None:
"""Set the list of client IDs we expect to hear from during the next refresh cycle."""
self._ready.clear()
self._waiting_clients = set(active_sids)
if widget_names:
capabilities["widget_names"] = widget_names
else:
capabilities.pop("widget_names", None)
if "supports_lora" in capabilities:
capabilities["supports_lora"] = bool(capabilities["supports_lora"])
comfy_class = node.get("comfy_class")
if not isinstance(comfy_class, str) or not comfy_class:
comfy_class = node_type if isinstance(node_type, str) else None
self._nodes[unique_id] = {
"id": node_id,
"graph_id": graph_id,
"graph_name": node.get("graph_name"),
"unique_id": unique_id,
"bgcolor": bgcolor,
"title": node.get("title"),
"type": type_id,
"type_name": node_type,
"comfy_class": comfy_class,
"capabilities": capabilities,
"widget_names": widget_names,
"mode": node.get("mode"),
"marker_role": node.get("marker_role"),
}
logger.debug("Registered %s nodes in registry", len(nodes))
self._registry_updated.set()
async def get_registry(self) -> dict:
async with self._lock:
return {
"nodes": dict(self._nodes),
"node_count": len(self._nodes),
}
async def wait_for_update(self, timeout: float = 1.0) -> bool:
self._registry_updated.clear()
async def wait_for_all(self, timeout: float = 2.0) -> bool:
"""Block until every client in the current waiting set has responded
(or *timeout* seconds elapse). Returns ``True`` if all responded."""
if not self._waiting_clients:
return True
try:
await asyncio.wait_for(self._registry_updated.wait(), timeout=timeout)
await asyncio.wait_for(self._ready.wait(), timeout=timeout)
return True
except asyncio.TimeoutError:
return False
async def get_merged_registry(self, active_sids: set[str] | None = None) -> dict:
"""Return the union of all known tab nodes, pruning any tab that is no
longer connected."""
async with self._lock:
# Garbage-collect stale entries (disconnected tabs)
if active_sids is not None:
for sid in list(self._tab_nodes):
if sid not in active_sids:
del self._tab_nodes[sid]
merged: dict[str, dict] = {}
tab_info: dict[str, dict] = {}
for sid, nodes in self._tab_nodes.items():
tab_info[sid] = {
"node_count": len(nodes),
"graph_names": list(
{
n.get("graph_name")
for n in nodes.values()
if n.get("graph_name")
}
),
}
merged.update(nodes)
return {
"nodes": merged,
"node_count": len(merged),
"tab_count": len(self._tab_nodes),
"tabs": tab_info,
}
class HealthCheckHandler:
async def health_check(self, request: web.Request) -> web.Response:
@@ -2995,10 +3060,21 @@ class NodeRegistryHandler:
try:
data = await request.json()
nodes = data.get("nodes", [])
client_id = data.get("client_id")
if not isinstance(nodes, list):
return web.json_response(
{"success": False, "error": "nodes must be a list"}, status=400
)
if not isinstance(client_id, str) or not client_id:
return web.json_response(
{
"success": False,
"error": "Missing client_id parameter",
},
status=400,
)
for index, node in enumerate(nodes):
if not isinstance(node, dict):
return web.json_response(
@@ -3042,7 +3118,7 @@ class NodeRegistryHandler:
else:
node["graph_name"] = str(graph_name)
await self._node_registry.register_nodes(nodes)
await self._node_registry.register_nodes(client_id, nodes)
return web.json_response(
{
"success": True,
@@ -3066,9 +3142,15 @@ class NodeRegistryHandler:
status=503,
)
# Snapshot of currently-connected ComfyUI tabs
active_sids = list(self._prompt_server.instance.sockets.keys())
self._node_registry.prepare_for_refresh(active_sids)
try:
self._prompt_server.instance.send_sync("lora_registry_refresh", {})
logger.debug("Sent registry refresh request to frontend")
logger.debug(
"Sent registry refresh request (expecting %s clients)", len(active_sids)
)
except Exception as exc:
logger.error("Failed to send registry refresh message: %s", exc)
return web.json_response(
@@ -3080,19 +3162,31 @@ class NodeRegistryHandler:
status=500,
)
registry_updated = await self._node_registry.wait_for_update(timeout=1.0)
if not registry_updated:
logger.warning("Registry refresh timeout after 1 second")
if not await self._node_registry.wait_for_all(timeout=2.0):
logger.warning(
"Registry refresh timeout after 2s (%s/%s clients responded)",
len(active_sids) - self._node_registry.pending_client_count,
len(active_sids),
)
# Re-read current sockets after the wait: a tab may have connected
# while we were waiting, and we don't want to garbage-collect it.
current_sids = set(self._prompt_server.instance.sockets.keys())
registry_info = await self._node_registry.get_merged_registry(
active_sids=current_sids
)
if registry_info["node_count"] == 0:
logger.warning("No nodes registered after refresh")
return web.json_response(
{
"success": False,
"error": "Timeout Error",
"message": "Registry refresh timeout - ComfyUI frontend may not be responsive",
"error": "Empty Registry",
"message": "No workflow nodes found — ensure ComfyUI is open and the extension is loaded.",
},
status=408,
)
registry_info = await self._node_registry.get_registry()
return web.json_response({"success": True, "data": registry_info})
except Exception as exc: # pragma: no cover - defensive logging
logger.error("Failed to get registry: %s", exc, exc_info=True)

View File

@@ -1,7 +1,7 @@
[project]
name = "comfyui-lora-manager"
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
version = "1.1.5"
version = "1.1.6"
license = {file = "LICENSE"}
dependencies = [
"aiohttp",

View File

@@ -60,7 +60,9 @@ class FakePromptServer:
sent = []
class Instance:
def send_sync(self, event, payload):
sockets: dict = {}
def send_sync(self, event, payload, sid=None):
FakePromptServer.sent.append((event, payload))
instance = Instance()
@@ -148,7 +150,8 @@ class TestNodeRegistryHandlerSnapshots:
"type": "Lora Loader (LoraManager)",
"title": "Test Loader",
}
]
],
"client_id": "test-client-1",
}
)
@@ -167,7 +170,7 @@ class TestNodeRegistryHandlerSnapshots:
standalone_mode=False,
)
request = FakeRequest(json_data={"nodes": []})
request = FakeRequest(json_data={"nodes": [], "client_id": "test-client-1"})
response = await handler.register_nodes(request)
payload = json.loads(response.text)

View File

@@ -586,7 +586,9 @@ class FakePromptServer:
sent = []
class Instance:
def send_sync(self, event, payload):
sockets: dict = {}
def send_sync(self, event, payload, sid=None):
FakePromptServer.sent.append((event, payload))
instance = Instance()
@@ -601,7 +603,12 @@ async def test_register_nodes_requires_graph_id():
standalone_mode=False,
)
request = FakeRequest(json_data={"nodes": [{"node_id": 1}]})
request = FakeRequest(
json_data={
"nodes": [{"node_id": 1}],
"client_id": "test-client-1",
}
)
response = await handler.register_nodes(request)
payload = json.loads(response.text)
@@ -629,7 +636,8 @@ async def test_register_nodes_stores_graph_identifier():
"type": "Lora Loader (LoraManager)",
"title": "Loader",
}
]
],
"client_id": "test-client-1",
}
)
@@ -638,7 +646,7 @@ async def test_register_nodes_stores_graph_identifier():
assert payload["success"] is True
registry = await node_registry.get_registry()
registry = await node_registry.get_merged_registry()
assert registry["node_count"] == 1
stored_node = next(iter(registry["nodes"].values()))
assert stored_node["graph_id"] == "graph-123"
@@ -664,7 +672,8 @@ async def test_register_nodes_defaults_graph_name_to_none():
"type": "Lora Loader (LoraManager)",
"title": "Root Loader",
}
]
],
"client_id": "test-client-1",
}
)
@@ -673,7 +682,7 @@ async def test_register_nodes_defaults_graph_name_to_none():
assert payload["success"] is True
registry = await node_registry.get_registry()
registry = await node_registry.get_merged_registry()
stored_node = next(iter(registry["nodes"].values()))
assert stored_node["graph_name"] is None
@@ -700,7 +709,8 @@ async def test_register_nodes_includes_capabilities():
"widget_names": ["ckpt_name", "", 42],
},
}
]
],
"client_id": "test-client-1",
}
)
@@ -709,7 +719,7 @@ async def test_register_nodes_includes_capabilities():
assert payload["success"] is True
registry = await node_registry.get_registry()
registry = await node_registry.get_merged_registry()
stored_node = next(iter(registry["nodes"].values()))
assert stored_node["capabilities"] == {
"supports_lora": False,
@@ -724,7 +734,9 @@ async def test_update_node_widget_sends_payload():
class RecordingPromptServer:
class Instance:
def send_sync(self, event, payload):
sockets: dict = {}
def send_sync(self, event, payload, sid=None):
send_calls.append((event, payload))
instance = Instance()
@@ -768,7 +780,9 @@ async def test_update_lora_code_includes_graph_identifier():
class RecordingPromptServer:
class Instance:
def send_sync(self, event, payload):
sockets: dict = {}
def send_sync(self, event, payload, sid=None):
send_calls.append((event, payload))
instance = Instance()

View File

@@ -151,7 +151,10 @@ app.registerExtension({
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ nodes: workflowNodes }),
body: JSON.stringify({
nodes: workflowNodes,
client_id: api.clientId ?? api.initialClientId ?? "",
}),
});
if (!response.ok) {