From bb6fab1168da126b603e788e7cae5a94a96cae4b Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 11 Aug 2025 12:23:41 +0200 Subject: [PATCH] api: init notification queue --- .../clan-app/clan_app/deps/webview/webview.py | 21 ++++++++++++++- .../clan_app/deps/webview/webview_bridge.py | 1 - pkgs/clan-app/ui/index.d.ts | 9 +++++++ pkgs/clan-app/ui/src/hooks/api.ts | 11 ++++++++ pkgs/clan-cli/clan_lib/api/__init__.py | 26 +++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 pkgs/clan-app/ui/index.d.ts diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index b2dcc72ef..23651459b 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -1,12 +1,14 @@ import functools import json import logging +import threading from collections.abc import Callable from dataclasses import dataclass, field from enum import IntEnum +from time import sleep from typing import TYPE_CHECKING, Any -from clan_lib.api import MethodRegistry +from clan_lib.api import MethodRegistry, message_queue from clan_lib.api.tasks import WebThread from clan_lib.log_manager import LogManager @@ -69,6 +71,22 @@ class Webview: if self.size: self.set_size(self.size) + def __post_init__(self) -> None: + self.setup_notify() # Start the notification loop + + def setup_notify(self) -> None: + def loop() -> None: + while True: + try: + msg = message_queue.get() # Blocks until available + js_code = f"window.notifyBus({json.dumps(msg)});" + self.eval(js_code) + except Exception as e: + print("Bridge notify error:", e) + sleep(0.01) # avoid busy loop + + threading.Thread(target=loop, daemon=True).start() + @property def handle(self) -> Any: """Get the webview handle, creating it if necessary.""" @@ -129,6 +147,7 @@ class Webview: webview=self, middleware_chain=tuple(self._middleware), threads={} ) self._bridge = bridge + return bridge # Legacy methods for compatibility diff --git a/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py b/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py index 4149b40ab..dd92c468a 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py @@ -25,7 +25,6 @@ class WebviewBridge(ApiBridge): def send_api_response(self, response: BackendResponse) -> None: """Send response back to the webview client.""" - serialized = json.dumps( dataclass_to_dict(response), indent=4, ensure_ascii=False ) diff --git a/pkgs/clan-app/ui/index.d.ts b/pkgs/clan-app/ui/index.d.ts new file mode 100644 index 000000000..ae9aa8315 --- /dev/null +++ b/pkgs/clan-app/ui/index.d.ts @@ -0,0 +1,9 @@ +import { ProcessMessage } from "./src/hooks/api"; + +export {}; + +declare global { + interface Window { + notifyBus: (data: ProcessMessage) => void; + } +} diff --git a/pkgs/clan-app/ui/src/hooks/api.ts b/pkgs/clan-app/ui/src/hooks/api.ts index 7a1e7b0b5..6a5c15b7f 100644 --- a/pkgs/clan-app/ui/src/hooks/api.ts +++ b/pkgs/clan-app/ui/src/hooks/api.ts @@ -99,3 +99,14 @@ export const callApi = ( }, }; }; + +export interface ProcessMessage { + topic: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + origin: string | null; +} + +window.notifyBus = (data) => { + console.debug("Channel function called with data:", data); +}; diff --git a/pkgs/clan-cli/clan_lib/api/__init__.py b/pkgs/clan-cli/clan_lib/api/__init__.py index 62df22996..ffbbe0dac 100644 --- a/pkgs/clan-cli/clan_lib/api/__init__.py +++ b/pkgs/clan-cli/clan_lib/api/__init__.py @@ -5,11 +5,13 @@ from collections.abc import Callable from dataclasses import dataclass from functools import wraps from inspect import Parameter, Signature, signature +from queue import Queue from types import ModuleType from typing import ( Annotated, Any, Literal, + TypedDict, TypeVar, get_type_hints, ) @@ -31,6 +33,30 @@ T = TypeVar("T") ResponseDataType = TypeVar("ResponseDataType") +class ProcessMessage(TypedDict): + """ + Represents a message to be sent to the UI. + + Attributes: + - topic: The topic of the message, used to identify the type of message. + - data: The data to be sent with the message. + - origin: The API operation that this message is related to, if applicable. + """ + + topic: str + data: Any + origin: str | None + + +message_queue: Queue[ProcessMessage] = Queue() +""" +A global message queue for sending messages to the UI +This can be used to send notifications or messages to the UI. Before returning a response. + +The clan-app imports the queue as clan_lib.api.message_queue and subscribes to it. +""" + + @dataclass class ApiError: message: str