Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Desktop client

Explanation

Signal bus and watch_entity

How the desktop SignalBus carries change, session, and navigation signals, how ApiClient emits change signals, and how screens subscribe with watch_entity.

The SignalBus in src/cluster_erp/desktop/core/signal_bus.py is the desktop client's only channel between screens. A mutation sent through ApiClient with an entity descriptor emits a change signal, and every screen that declared a dependency with watch_entity refreshes. This page explains the signals, the mutation-to-signal path, and the subscription rules. The server-side counterpart for modules is the event bus; the web client's counterpart is on data refresh and change signals.

In short

  • SignalBus is a QObject with 12 signals, counted as the Signal( class attributes in src/cluster_erp/desktop/core/signal_bus.py:18-44.
  • ApiClient.post, put, patch, delete, post_multipart, and put_multipart emit entity_created, entity_updated, or entity_deleted after a successful call when an entity descriptor is passed (src/cluster_erp/desktop/core/api_client.py:213-294).
  • watch_entity(entity, callback, owner=...) calls callback for any change of that module and entity type, and disconnects itself when the owner widget is destroyed (src/cluster_erp/desktop/core/signal_bus.py:55-85).
  • FkCache listens to the same three signals and drops the cached option lists of the changed module (src/cluster_erp/desktop/core/fk_cache.py:51-77).

How it works

Signals

Signal Arguments Emitted by Received by
navigate_to module, section, params GenericDetailView._follow_link, the warehouse and MRP dashboards (src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:767, src/cluster_erp/desktop/modules/inventory/warehouse_dashboard.py:91, src/cluster_erp/desktop/modules/procurement/mrp/dashboard.py:94) MainWindow._on_navigate_to (src/cluster_erp/desktop/widgets/main_window.py:66)
open_entity module, entity type, id, title the scheduling board (src/cluster_erp/desktop/modules/production/scheduling_board.py:2220) MainWindow._on_open_entity (src/cluster_erp/desktop/widgets/main_window.py:67)
entity_created module, entity type, id ApiClient.emit_entity_change (src/cluster_erp/desktop/core/api_client.py:213-218) watch_entity callbacks, FkCache, two panels
entity_updated module, entity type, id ApiClient.emit_entity_change watch_entity callbacks, FkCache, two panels
entity_deleted module, entity type, id ApiClient.emit_entity_change watch_entity callbacks, FkCache, one panel
theme_changed theme name no emitter in the desktop folder no receiver in the desktop folder
language_changed language code no emitter in the desktop folder no receiver in the desktop folder
connection_status_changed connected flag StatusBarWidget._set_status (src/cluster_erp/desktop/widgets/status_bar.py:65-70) no receiver in the desktop folder
user_logged_in user dictionary main() and MainWindow._on_session_expired (src/cluster_erp/desktop/main.py:192, src/cluster_erp/desktop/widgets/main_window.py:264) StatusBarWidget (src/cluster_erp/desktop/widgets/status_bar.py:44)
user_logged_out none MainWindow._do_logout and _on_session_expired (src/cluster_erp/desktop/widgets/main_window.py:241, src/cluster_erp/desktop/widgets/main_window.py:267) StatusBarWidget (src/cluster_erp/desktop/widgets/status_bar.py:45)
session_expired none ApiClient._emit_session_expired (src/cluster_erp/desktop/core/api_client.py:207-211) MainWindow._on_session_expired (src/cluster_erp/desktop/widgets/main_window.py:65)
accessibility_changed colour-blind mode a lambda in main() on AccessibilityManager.settings_changed (src/cluster_erp/desktop/main.py:173-175) BomExplorer (src/cluster_erp/desktop/modules/bom/bom_explorer.py:1118)

The rows were found by searching src/cluster_erp/desktop/ for each signal name. The comments in src/cluster_erp/desktop/core/signal_bus.py:31-35 state that theme and language changes go through ThemeEngine.theme_changed and Translator.language_changed directly.

Mutation-to-signal path

The diagram shows a save from a generic form until a list in another tab refreshes.

sequenceDiagram
  participant F as GenericFormDialog
  participant W as ApiWorker
  participant G as GenericApi
  participant A as ApiClient
  participant B as SignalBus
  participant C as FkCache
  participant L as GenericListView
  F->>W: call_api(_do_save)
  W->>G: create(data)
  G->>A: post(path, json, entity=descriptor)
  A-->>G: result with id
  A->>B: entity_created(module, entity_type, id)
  B->>C: _on_entity_change drops module entries
  B->>L: _on_change filter matches
  L->>L: refresh

In words:

  1. The form runs _do_save in a worker (src/cluster_erp/desktop/widgets/generic/generic_form_dialog.py:230-237). _do_save calls GenericApi.create or GenericApi.update (src/cluster_erp/desktop/widgets/generic/generic_form_dialog.py:262-269).
  2. GenericApi passes its EntityDescriptor to ApiClient (src/cluster_erp/desktop/core/generic_api.py:67-84).
  3. After the server answers, ApiClient.post reads id from the result and calls emit_entity_change with the action created (src/cluster_erp/desktop/core/api_client.py:230-234). emit_entity_change looks up the signal entity_<action> and emits module, entity type, and id (src/cluster_erp/desktop/core/api_client.py:213-218).
  4. FkCache._on_entity_change removes every cached option list whose key starts with the module name (src/cluster_erp/desktop/core/fk_cache.py:65-77).
  5. Each watch_entity closure compares module and entity type and calls its callback on a match (src/cluster_erp/desktop/core/signal_bus.py:69-71). A list view refreshes its rows (src/cluster_erp/desktop/widgets/generic/generic_list_view.py:333-334).

Subscribing with watch_entity

watch_entity connects one closure to all three change signals. With an owner, _track stores the connections and connects the owner's destroyed signal to _disconnect_owner, which disconnects them (src/cluster_erp/desktop/core/signal_bus.py:77-110). unwatch_entity(owner) does the same on demand (src/cluster_erp/desktop/core/signal_bus.py:87-89).

Eleven call sites use watch_entity, counted by searching src/cluster_erp/desktop/ for the name outside signal_bus.py and a comment in tab_manager.py. They are the generic list, tree, settings, sub-panel, and detail views, the capability panel, the BOM explorer and builder, and the scheduling board (for example src/cluster_erp/desktop/widgets/generic/generic_list_view.py:334, src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:455-459, src/cluster_erp/desktop/modules/production/scheduling_board.py:1521).

GenericDetailView reloads on a change only when its form has no unsaved edits. The comment states that a save of another record of the same type would otherwise overwrite the user's edits (src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:452-459, src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:564-569).

Session expiry

A failed token refresh emits session_expired (src/cluster_erp/desktop/core/api_client.py:173-211). MainWindow._on_session_expired handles only the first emission until the user signs in again. It shows a warning toast, clears the session, and opens the sign-in dialog. A successful sign-in emits user_logged_in; a cancel emits user_logged_out and closes the window (src/cluster_erp/desktop/widgets/main_window.py:244-268).

Why it is built this way

  • The project's written engineering rules state that screens declare their data dependencies with watch_entity, that the API client emits the signals automatically, and that this replaces direct calls between modules.
  • Owner tracking: the docstring in src/cluster_erp/desktop/core/signal_bus.py:62-67 states that automatic disconnection prevents signal leaks and crashes.
  • Option-list invalidation: the comment in src/cluster_erp/desktop/core/fk_cache.py:51-54 states that a new record appears in drop-downs at once instead of after the cache lifetime.

Rules and boundaries

  • A screen passes entity= to ApiClient for a mutation that other screens may display. GenericApi does this for create, update and delete, and emits the signal itself through emit_entity_change for bulk delete and POST actions (src/cluster_erp/desktop/core/generic_api.py:67-121).
  • When a duplicate tab is detected, TabManager.open_tab deletes the newly built widget, so its watch_entity registration ends with it (src/cluster_erp/desktop/core/tab_manager.py:97-107). See tabs, panes, and layout.

Interaction with other parts

  • MainWindow turns navigate_to and open_entity into tabs; see tabs, panes, and layout.
  • FkCache subscribes in its constructor when main() passes the bus (src/cluster_erp/desktop/main.py:224); see metadata store and foreign-key cache.
  • The signals live only inside one client process. Other clients see changes when they reload data. Server-side events between modules use the event bus.

Limits and open points

  • CompletenessPanel and PurchaseRfqComparisonPanel connect to the raw change signals without a module filter and without owner tracking (src/cluster_erp/desktop/widgets/generic/completeness_panel.py:65-67, src/cluster_erp/desktop/widgets/generic/purchase_rfq_comparison_panel.py:92-93).
  • MRP screens post bulk actions without an entity descriptor, so they emit no change signal (src/cluster_erp/desktop/modules/procurement/mrp/planned_orders.py:411-417, src/cluster_erp/desktop/modules/procurement/mrp/exception_workbench.py:527-533).
  • The comment above navigate_to describes it as the navigation request that MainWindow answers by opening the module and section (src/cluster_erp/desktop/core/signal_bus.py:19-20). The signal is connected and emitted, as the table above shows.
  • Mutations started by call_api run in a worker thread, so emit_entity_change is called on that thread (src/cluster_erp/desktop/core/api_worker.py:43-46). On which thread each connected callback runs: Not determined from code. No connection type is set in src/cluster_erp/desktop/, found by searching for ConnectionType.

Terms introduced

Term Code identifier Definition Turkish term Source
signal bus SignalBus The desktop QObject that carries change, session, navigation, and accessibility signals between screens. sinyal veri yolu src/cluster_erp/desktop/core/signal_bus.py:18
entity watch watch_entity A subscription that calls a callback when a record of one module and entity type changes. varlık izleme src/cluster_erp/desktop/core/signal_bus.py:55
change signal entity_created, entity_updated, entity_deleted The signals that ApiClient emits after a successful mutation. değişiklik sinyali src/cluster_erp/desktop/core/signal_bus.py:27-29

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13