Package su.onno.ui

Class UiEventPublisher

java.lang.Object
su.onno.ui.UiEventPublisher

public class UiEventPublisher extends Object
Pushes entity-change notifications to browser SseEmitters for live UI updates. It is one listener of the framework's EntityChangedEvent — the single funnel both write paths (the generic controllers and repository.save) publish to (issues #28, #29) — so the live stream reflects programmatic saves too, not just back-office edits.

Per-subscriber RBAC (#190). Each stream is keyed by the read-authorities its viewer held at subscribe time, and an event is delivered only when those roles grant read access to the event's entity — so a viewer never receives change- or presence-notifications for entities their role can't read. Roles are captured up front because the fan-out runs off the request thread (the event-publishing thread, or the ClusterUiBridge relay for peer-node events), where SecurityContextHolder no longer holds the subscriber's authentication; see UiAccessService.canReceiveEvent(java.util.Set, String, String).

  • Constructor Details

    • UiEventPublisher

      public UiEventPublisher(UiAccessService access)
    • UiEventPublisher

      public UiEventPublisher(UiAccessService access, boolean devMode)
  • Method Details

    • subscribe

      public org.springframework.web.servlet.mvc.method.annotation.SseEmitter subscribe(Set<String> roles)
      Open a stream for a viewer holding roles (capture them with UiAccessService.roles(java.security.Principal) on the request thread). The role set is snapshotted: every event is filtered against it for the life of the connection.
    • subscribe

      public org.springframework.web.servlet.mvc.method.annotation.SseEmitter subscribe(Set<String> roles, String userId)
      Open a stream for a viewer holding roles and identified for notification routing by userId (their identity record id, or username for an unlinked login; may be null for a guest, whose stream then receives no per-user notifications). Roles are snapshotted and filter every broadcast event; userId routes notification events addressed to it.
    • subscribe

      public org.springframework.web.servlet.mvc.method.annotation.SseEmitter subscribe(Set<String> roles, String userId, String username)
      Open a stream with separate identity-record and authentication usernames. Notifications route by userId; process task assignments route by the principal username.
    • isDevMode

      public boolean isDevMode()
      Whether this publisher was wired for a live-development server.
    • publishReload

      public void publishReload()
      Dev-mode live reload on demand: tells every connected browser to full-reload right now, without waiting for a context restart. Complements the automatic bootId reload — use it when something changed that no restart announces (static assets, a rebuilt widget bundle) or to refresh immediately after a batch of edits. No-op outside dev mode, so nothing can yank a production page out from under a user.
    • onEntityChanged

      @EventListener public void onEntityChanged(EntityChangedEvent event)
      Fans an EntityChangedEvent out to every open SSE stream whose viewer may read it. Registered as a Spring @EventListener, so anything that publishes the event (both write paths) reaches the browser — no direct coupling to the controllers.
    • onProcessTasksChanged

      @EventListener public void onProcessTasksChanged(ProcessTasksChangedEvent event)
      Route a committed process-task inbox invalidation only to affected users, roles, and admins.
    • publishProcessTasksChanged

      public void publishProcessTasksChanged(String instanceId, Set<String> audienceUsers, Set<String> audienceRoles)
      Push a payload-free task inbox invalidation. Candidate assignments remain server-side; an eligible browser learns only that its own authenticated /api/tasks view is stale.
    • publish

      public void publish(String type, String entityType, String entityName, Object id)
    • publish

      public void publish(String type, String entityType, String entityName, Object id, String naturalKey)
    • publishPresence

      public void publishPresence(String kind, String entityName, String id, List<Map<String,String>> viewers)
      Fans the current viewer set of one record out as a presence event, to every open stream whose viewer may read that record — record-level collaboration markers. Each viewer is a {userId, displayName} map. Sent only when a record's viewer set changes (a join or a leave), never on a bare heartbeat.

      Its entityType is the distinct sentinel "presence", not the record's catalog/document kind — exactly as comment events use "comment" — so the list/detail/dashboard surfaces, which refetch on a row change to their entity, never mistake a presence ping for one. The record's route kind (catalogs/documents) and entityName ride alongside so the ambient-presence store can map a viewed record to its nav item and list rows; the marker surfaces match on id (globally unique). The read check uses that kind/entityName (the presence sentinel is not a real entity type), so a viewer is never told that someone is viewing a record in an entity they can't open (#190).

    • publishNotification

      public void publishNotification(String recipientId, Map<String,Object> payload)
      Pushes one notification event to every open stream owned by recipientId — the per-user counterpart of the role-filtered publish(java.lang.String, java.lang.String, java.lang.String, java.lang.Object) fan-out. A notification is addressed to exactly one person (it was raised for them and already persisted), so delivery routes on the subscriber's userId rather than an entity read check: the recipient may have several streams open (tabs/devices) and all of them light up; nobody else's does.

      Its entityType is the distinct sentinel "notification" — like presence and comment — so the list/detail/dashboard surfaces never mistake it for a row change to one of their entities. The payload is the wire shape the client's notification store consumes (id, type, title, link, …); when its display fields are absent (a peer-node event trimmed to fit the cluster payload cap) the client refetches the feed.