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¶
GenApiis the single emitter for metadata-driven mutations: every mutating method calls_changed, which drops the module's cached lists and callsemitDataChanged(webapp/js/gen-api.js:44-51).emitDataChangedincrements a session counter and dispatchescluster:data-changedwith{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:
- A mutation through
GenApi(create,update,remove,bulkDelete,reactivate, or a non-GETaction) calls_changed(kind, id)(webapp/js/gen-api.js:98-174). _changedcallsinvalidateModule(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._changedthen callsemitDataChangedwith the module, entity type, id, and kind (webapp/js/gen-api.js:48-51). The kind is one ofcreate,update,deactivate,activate, oraction.- 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). meta.jsre-applies the tenant field overlay when the entity type istenant_field_definitionortenant_field_setting(webapp/js/meta.js:252-256).- Gated bundles receive
emitDataChanged,watchData, anddataSeqthrough 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 withwatchData(gated_apps/intranet/app.js:1175); the analytics, production, and projects bundles usedataSeqto 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:
watchDataskips 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,
stashActiverecordsdataSeq()(webapp/js/home.js:2287-2289). - When it returns,
unstashIntoredraws 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.jsstates 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-56records 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
GETactions 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¶
- The server records every change in the change history independently of this signal; see change history.
- Running apps and keep-alive are described on module apps and app scope.
Limits and open points¶
- Mutations made with
apiFetchdirectly, outsideGenApi, emit no signal unless the caller emits one. Examples are document add and delete inwebapp/js/record-docs.js:168-176andwebapp/js/record-docs.js:272, and the role and permission checklists inwebapp/js/gen-detail.js:619-775. A search foremitDataChangedinwebapp/js/finds onlydata-bus.jsandgen-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.jsis 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 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.