diff --git a/src/streamsync/app_runner.py b/src/streamsync/app_runner.py index e0c316264..934cc2e9e 100644 --- a/src/streamsync/app_runner.py +++ b/src/streamsync/app_runner.py @@ -177,6 +177,7 @@ def _handle_event(self, session: StreamsyncSession, event: StreamsyncEvent) -> E res_payload = EventResponsePayload( result=result, mutations=mutations, + components=session.session_component_tree.to_dict(), mail=mail ) diff --git a/src/streamsync/core.py b/src/streamsync/core.py index 879cd3fe1..0e1d46fa2 100644 --- a/src/streamsync/core.py +++ b/src/streamsync/core.py @@ -17,6 +17,7 @@ import json import math from streamsync.ss_types import Readable, InstancePath, StreamsyncEvent, StreamsyncEventResult, StreamsyncFileItem +from pydantic import BaseModel, Field class Config: @@ -475,33 +476,22 @@ def call_frontend_function(self, module_key: str, function_name: str, args: List # TODO Consider switching Component to use Pydantic -class Component: - - def __init__(self, id: str, type: str, content: Dict[str, str] = {}): - self.id = id - self.type = type - self.content = content - self.position: int = 0 - self.parentId: Optional[str] = None - self.handlers: Optional[Dict[str, str]] = None - self.visible: Optional[bool] = None - self.binding: Optional[Dict] = None +class Component(BaseModel): + id: str + type: str + content: Dict[str, str] = Field(default_factory=dict) + flag: Optional[str] = None + position: int = 0 + parentId: Optional[str] = None + handlers: Optional[Dict[str, str]] = None + visible: Optional[Union[bool, str]] = None + binding: Optional[Dict] = None def to_dict(self) -> Dict: - c_dict = { - "id": self.id, - "type": self.type, - "content": self.content, - "parentId": self.parentId, - "position": self.position, - } - if self.handlers is not None: - c_dict["handlers"] = self.handlers - if self.binding is not None: - c_dict["binding"] = self.binding - if self.visible is not None: - c_dict["visible"] = self.visible - return c_dict + """ + Wrapper for model_dump to ensure backward compatibility. + """ + return self.model_dump(exclude_none=True) class ComponentTree: @@ -509,7 +499,9 @@ class ComponentTree: def __init__(self) -> None: self.counter: int = 0 self.components: Dict[str, Component] = {} - root_component = Component("root", "root", {}) + root_component = Component( + id="root", type="root", content={} + ) self.attach(root_component) def get_component(self, component_id: str) -> Optional[Component]: @@ -536,13 +528,7 @@ def ingest(self, serialised_components: Dict[str, Any]) -> None: continue self.components.pop(component_id) for component_id, sc in serialised_components.items(): - component = Component( - component_id, sc["type"], sc["content"]) - component.parentId = sc.get("parentId") - component.handlers = sc.get("handlers") - component.position = sc.get("position") - component.visible = sc.get("visible") - component.binding = sc.get("binding") + component = Component(**sc) self.components[component_id] = component def to_dict(self) -> Dict: @@ -550,7 +536,7 @@ def to_dict(self) -> Dict: for id, component in self.components.items(): active_components[id] = component.to_dict() return active_components - + class SessionComponentTree(ComponentTree): @@ -559,15 +545,27 @@ def __init__(self, base_component_tree: ComponentTree): self.base_component_tree = base_component_tree def get_component(self, component_id: str) -> Optional[Component]: - base_component = self.base_component_tree.get_component(component_id) - if base_component: - return base_component - return self.components.get(component_id) + # Check if session component tree contains requested key + session_component_present = component_id in self.components + + if session_component_present: + # If present, return session component (even if it's None) + session_component = self.components.get(component_id) + return session_component + + # Otherwise, try to obtain the base tree component + return self.base_component_tree.get_component(component_id) def to_dict(self) -> Dict: - active_components = {} - for id, component in {**self.components, **self.base_component_tree.components}.items(): - active_components[id] = component.to_dict() + active_components = { + # Collecting serialized base tree components + component_id: base_component.to_dict() + for component_id, base_component + in self.base_component_tree.components.items() + } + for component_id, session_component in self.components.items(): + # Overriding base tree components with session-specific ones + active_components[component_id] = session_component.to_dict() return active_components diff --git a/src/streamsync/ss_types.py b/src/streamsync/ss_types.py index cd3335d09..d48cd5d5c 100644 --- a/src/streamsync/ss_types.py +++ b/src/streamsync/ss_types.py @@ -134,6 +134,7 @@ class EventResponsePayload(BaseModel): result: Any mutations: Dict[str, Any] mail: List + components: Dict class StateEnquiryResponsePayload(BaseModel): diff --git a/ui/src/core/index.ts b/ui/src/core/index.ts index 69c5c3096..2b011c3d6 100644 --- a/ui/src/core/index.ts +++ b/ui/src/core/index.ts @@ -158,6 +158,11 @@ export function generateCore() { }); } + function ingestComponents(newComponents: Record) { + if (!newComponents) return; + components.value = newComponents + } + function clearFrontendMap() { frontendMessageMap.value.forEach(({ callback }) => { callback?.({ ok: false }); @@ -198,6 +203,7 @@ export function generateCore() { ) { ingestMutations(message.payload?.mutations); collateMail(message.payload?.mail); + ingestComponents(message.payload?.components); } const mapItem = frontendMessageMap.value.get(message.trackingId); @@ -429,8 +435,21 @@ export function generateCore() { * @returns */ async function sendComponentUpdate(): Promise { + /* + Ensure that the backend receives only components + created by the frontend (Builder-managed components, BMC), + and not the components it generated (Code-managed components, CMC). + */ + + const builderManagedComponents = {}; + + Object.entries(components.value).forEach(([componentId, component]) => { + if (component.flag === 'cmc') return; + builderManagedComponents[componentId] = component; + }); + const payload = { - components: components.value, + components: builderManagedComponents, }; return new Promise((resolve, reject) => { diff --git a/ui/src/streamsyncTypes.ts b/ui/src/streamsyncTypes.ts index 6d18f31c4..2cafbed30 100644 --- a/ui/src/streamsyncTypes.ts +++ b/ui/src/streamsyncTypes.ts @@ -12,6 +12,7 @@ export type Component = { type: string; position: number; content: Record; + flag?: string; handlers?: Record; visible?: boolean | string; binding?: {