Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Web app

Explanation

Data refresh and change signals

How the data-changed signal in data-bus.js travels from GenApi mutations to lists, stashed screens, the home screen, and bundles.

When a record is created, updated, deactivated, reactivated, or changed by an action, every screen that shows it must refresh without the user moving away and back. The web app solves this with one signal, cluster:data-changed, defined in webapp/js/data-bus.js. This page explains who emits it, who listens, how screens kept in the background catch up, and how cached data is dropped. The desktop counterpart is the signal bus described in the desktop section.

In short

  • GenApi is the single emitter for metadata-driven mutations: every mutating method calls _changed, which drops the module's cached lists and calls emitDataChanged (webapp/js/gen-api.js:44-51).
  • emitDataChanged increments a session counter and dispatches cluster:data-changed with {module, entityType, id, kind, seq} (webapp/js/data-bus.js:11-22).
  • watchData(el, cb) calls back only while the element is on screen and unsubscribes when the element is destroyed (webapp/js/data-bus.js:33-44).
  • A screen kept alive in the background compares the counter when it returns and redraws when data changed meanwhile (webapp/js/home.js:2283-2326).

How it works

Signal path

The flowchart shows the path from a mutation to the screens that refresh.

flowchart LR
  call["GenApi create, update, remove, bulkDelete, reactivate, action"] --> changed["_changed"]
  changed --> inval["invalidateModule"]
  changed --> emit["emitDataChanged"]
  emit --> event["cluster:data-changed"]
  event --> list["makeGenList watchData"]
  event --> meta["meta.js overlay refresh"]
  event --> bundle["gated bundle watchData"]
  emit --> seq["dataSeq counter"]
  seq --> resume["unstashInto on resume"]
  seq --> homeseq["refreshHomeIfStale"]

In words:

  1. A mutation through GenApi (create, update, remove, bulkDelete, reactivate, or a non-GET action) calls _changed(kind, id) (webapp/js/gen-api.js:98-174).
  2. _changed calls invalidateModule(module), which deletes every cached option list and label of that module (webapp/js/gen-api.js:300-311). The comment states that all of the module's entries are dropped rather than guessing which list changed.
  3. _changed then calls emitDataChanged with the module, entity type, id, and kind (webapp/js/gen-api.js:48-51). The kind is one of create, update, deactivate, activate, or action.
  4. A list that is on screen reloads when the change is in its own module and its last load is older than the change (webapp/js/gen-list.js:368-372, webapp/js/gen-list.js:411).
  5. meta.js re-applies the tenant field overlay when the entity type is tenant_field_definition or tenant_field_setting (webapp/js/meta.js:252-256).
  6. Gated bundles receive emitDataChanged, watchData, and dataSeq through the platform context, so they can announce their own mutations and listen to others (webapp/js/custom-widgets.js:85-90). The intranet bundle listens with watchData (gated_apps/intranet/app.js:1175); the analytics, production, and projects bundles use dataSeq to invalidate their caches (gated_apps/analytics/app.js:3944, gated_apps/production/app.js:1065, gated_apps/projects/app.js:696).

Screens in the background

A screen that is not on screen does not refresh at once:

  • watchData skips the callback while the element is detached. If the element's root is a document fragment, the screen is kept alive and the listener stays; otherwise the listener removes itself (webapp/js/data-bus.js:33-44).
  • When an app is moved to the background, stashActive records dataSeq() (webapp/js/home.js:2287-2289).
  • When it returns, unstashInto redraws the top view if the counter changed, if a paint was dropped while it was away, or if a late paint landed on the empty content area. It does not redraw while a form is open, so typed input is not lost (webapp/js/home.js:2293-2326).
  • The home screen records the counter at each render and redraws when the user returns to it after a change (webapp/js/home.js:935-937, webapp/js/home.js:2884-2900).

Dropped paints

An asynchronous paint that finishes while its screen is in the background would otherwise be lost. Painters call canPaint(el) instead of testing el.isConnected; a call on a stashed screen marks the stashed root, and paintDroppedIn(root) reports and clears that mark when the screen returns (webapp/js/data-bus.js:46-74). Module apps, lists, cards, trees, and settings screens use canPaint (webapp/js/module-app.js:82, webapp/js/gen-list.js:456, webapp/js/gen-detail.js:52, webapp/js/gen-tree.js:53, webapp/js/gen-settings.js:34).

Form drafts after a mutation

A saved form clears its draft (webapp/js/gen-form.js:1362). A form left open in the background keeps its input, because unstashInto does not redraw a screen that contains a form (webapp/js/home.js:2324). See generic form.

Reset

Logout resets the counter to zero (webapp/js/data-bus.js:76-79) and clears the option list and label caches (webapp/js/gen-api.js:360-365).

Why it is built this way

  • The header comment of data-bus.js states the rule: no screen calls another screen directly; the one place that mutates emits the signal, and visible surfaces listen. It calls this the web counterpart of the rule against direct calls between modules (webapp/js/data-bus.js:1-9).
  • The comment in webapp/js/data-bus.js:46-56 records a case where a screen stayed on its loading placeholder after a load finished while it was in the background.
  • The project's written engineering rules forbid direct module-to-module communication and route all of it through the EventBus on the server and the signal bus on the desktop. See EventBus-only inter-module communication.

Rules and boundaries

  • Read-only GET actions emit no signal (webapp/js/gen-api.js:170).
  • Browser tests: web-client/_e2e_data_refresh.mjs, web-client/_e2e_keepalive_paint.mjs, web-client/_e2e_form_draft.mjs.

Interaction with other parts

Limits and open points

  • Mutations made with apiFetch directly, outside GenApi, emit no signal unless the caller emits one. Examples are document add and delete in webapp/js/record-docs.js:168-176 and webapp/js/record-docs.js:272, and the role and permission checklists in webapp/js/gen-detail.js:619-775. A search for emitDataChanged in webapp/js/ finds only data-bus.js and gen-api.js.
  • The signal is local to one browser window. A change made in another tab or by another user is seen only on the next load. Not determined from code: any server push channel for data changes.
  • webapp/js/changed.js is the Changed Records screen over the change history, not part of this signal. It is described on generic tree and settings screens.

Terms introduced

Term Code identifier Definition Turkish term Source
data-changed signal cluster:data-changed The document event that announces a mutation to every listening screen. veri değişti sinyali webapp/js/data-bus.js:12
data sequence dataSeq The session counter of mutations, used to detect stale screens. veri sürümü webapp/js/data-bus.js:16
dropped paint canPaint A paint that finished while its screen was in the background and must be redone. düşen boyama webapp/js/data-bus.js:61

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

Verified against commit 92f8051 · 2026-11-13