Class ClientSessionPushAPI

java.lang.Object
com.iizix.server.push.api.PushSessionAPI
com.iizix.server.push.api.ClientSessionPushAPI
All Implemented Interfaces:
IPushNotificationAPI, IPushNotificationCommonAPI, PushEvents.ISink

public final class ClientSessionPushAPI extends PushSessionAPI implements IPushNotificationAPI
The Push Notification (PN) API of one client session, behind IClientSessionGyro.getPushNotificationAPI() (P4-04).

The engine-bound members are PushSessionAPI's. The client-bound ones need the channel of PUSH-PROPS-DESIGN section 3, PushChannelProps under $env, which the context hands over: queryDeviceState(BiFunction) goes down it as QUERY_STATE and completes on the client's REPLY (P4-06); sendToClient(IPushNotification, String, PushSendOptions, BiFunction) targets the device by the UUID the channel reports, on the device tier through its subscription and on the in-session tier through this session's channel (P4-10); subscribe(String, BiFunction) goes down as SUBSCRIBE and stores what the client answers (P4-07); unsubscribe(String, BiFunction), resetDevice(BiFunction) and resetDeviceEverywhere(BiFunction) remove rows first and then go down as UNSUBSCRIBE or RESET with drop (P4-08). A call the channel cannot carry - no channel (a Designer, Design Viewer or VSViewer session, a client built before P4), no device UUID reported yet, or a disposed channel - fails with PushNotificationException.NO_DEVICE, posted to the client session's worker and never thrown (design 3.2), after the argument checks its declaration makes.

subscribe (design 4.2, 8.5): the client performs the device's part - a Browser subscribes its pushManager under the application's key when the permission is already granted, and answers gesture_required without prompting otherwise (RR-8 A, RR-9 A); a Cordova device asks FirebaseX - and answers the subscription or the token. The server stores it as the session's user, this application, this device and the Topic, through the engine's store directly, with no privilege check (RR-5 A): the application decided. Before anything goes down, what is known here is refused at once: no user (no_user), a device that cannot receive PN (unsupported), an application without the transport the device uses (not_configured: no VAPID key written into the channel for a Browser, no Firebase project for Cordova). A reply that fits its operation badly - a subscription without its endpoint or keys, an empty token, neither or both members - completes bad_request and nothing is stored (design 6.4).

Inside iiziRun Developer (PUSH-CLUSTER-DESIGN 3.3, B-10b, ruled PC-9 with RR-21 C; until B-10b refused by CC-PUSH-ROADMAP row 4 A): a row records the credentials it was made under and is signed with them, so a subscription made under the server's own * credentials is stored for this application too. An FCM token of the server's * Firebase project - a device built as iiziRun Developer - is stored recording that project; one of any other project than the application's is refused key_conflict and nothing is stored. One whose device names no project - iiziRun builds before CC-FIX-PUSHDEV-PROJECT name none - is checked with FCM itself (CC-PUSHDEV-VALIDATE-ONLY): a validate_only request to the token with the application's credentials, off the worker, and when FCM answers SENDER_ID_MISMATCH, a second with *'s; a match stores it under the project that matched, a mismatch of both refuses it key_conflict as a named mismatch is, and a check that cannot tell - no credential, FCM unreachable - stores it with a warning. A Browser already subscribed under the server's * key - the channel's skey - answers that subscription with the key it was made under, vapid_public_key, which is stored when it is the application's or *'s and refused key_conflict otherwise; one subscribed under any other key answers key_conflict itself (RR-21 A).

unsubscribe, resetDevice, resetDeviceEverywhere (design 4.2, RR-15 B, P4-b): the store first, off the worker - the row of the Topic, this application's rows on this device, or every application's rows of the user on this device, the HTTP reset {} - then, from the worker, the request down the channel with drop: whether any row of this application remains for the device, always true after a reset. The device gives up a Browser subscription accordingly and never an FCM token, and no FCM Topic is touched (R-5 A). The callback gets the number of rows removed once the device has answered; a timeout leaves the rows gone. Listeners get onSubscriptionRemoved per row from the store, with unsubscribed or reset.

setSubscribeAction, removeSubscribeAction (design 8.2-8.4, P4-09, RR-6 B, RR-7 A, RR-20 A): the action is written into the channel's act as the button's atom path from the root and the Topic, and the callback stays in the channel's action table; the client subscribes inside the button's DOM click - pushManager.subscribe synchronously, as P4-01 measured on 2026-09-26 - and reports ACTION_RESULT once per click, which is validated and stored exactly as a SUBSCRIBE reply is, through store(String, String, String, String, String, String, PropCnr, BiFunction). The application's VAPID public key is refreshed into the channel when an action is configured (CH-4). A result whose action was removed meanwhile still stores the subscription the device holds, with an INFO line and no callback (design 8.4). Only a UIButton is accepted, and only one of this session's tree.

No user needed by sendToClient(IPushNotification, String, PushSendOptions, BiFunction) (ruled 2026-09-26, CC-PUSH-ROADMAP row 3 B): it names this session, not a user, so it works in a session without an authenticated user - in-session down this session's channel, while the device tier matches no stored subscription, a subscription belonging to a user. The fan-out sends of the application session API stay user-bound. The filter carries the session's user ID as IPushSessionContext.getUserID() answers it, -1 for none: the enumerator reports the same -1 for such a session, and no stored row carries it.

Threading (design 5): the channel calls back on the connection's thread or on the timer's; this class runs the store lookup off it and posts the outcome to the worker, so no callback ever runs on either. Deadlines (RR-11 A): 30 seconds, 5 minutes for a subscribe; setDeadlines(long, long) exists for tests.

Author:
Christopher Mindus
  • Field Details

    • SIGNIN_TOPIC

      public static final String SIGNIN_TOPIC
      The Topic a subscribe at sign-in stores: the application-level subscription, "" (PUSH-WIRE-CONTRACT section 2.4); the .iiziPush model names no Topic (CC-PUSH-SUBSCRIBE-AT-SIGNIN).
      See Also:
  • Constructor Details

    • ClientSessionPushAPI

      public ClientSessionPushAPI(IPushSessionContext ctx)
      Constructor.
      Parameters:
      ctx - The client session.
  • Method Details

    • setDeadlines

      public void setDeadlines(long deadlineMillis, long subscribeDeadlineMillis)
      Sets the deadlines of the client-bound calls, for tests: the ruled values are the defaults.
      Parameters:
      deadlineMillis - The deadline of every client-bound call but a subscribe.
      subscribeDeadlineMillis - The deadline of a subscribe.
    • getDeadlineMillis

      public long getDeadlineMillis()
      Gets the deadline of a client-bound call.
      Returns:
      The deadline in milliseconds.
    • getSubscribeDeadlineMillis

      public long getSubscribeDeadlineMillis()
      Gets the deadline of a subscribe.
      Returns:
      The deadline in milliseconds.
    • queryDeviceState

      public void queryDeviceState(BiFunction<IPushDeviceState, Exception, Void> callback)
      Description copied from interface: IPushNotificationAPI
      Queries what this device can do: whether it supports Push Notification at all, which transport it would use, what the user has granted, and whether it is subscribed for this application.
      Specified by:
      queryDeviceState in interface IPushNotificationAPI
      Parameters:
      callback - Called on the client session's worker thread with the state, or with the failure.
    • subscribe

      public void subscribe(String topic, BiFunction<IPushSubscribeResult, Exception, Void> callback)
      --- subscribe (P4-07) ---
      Specified by:
      subscribe in interface IPushNotificationAPI
      Parameters:
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one (PUSH-WIRE-CONTRACT section 2.4).
      callback - Called on the client session's worker thread with the result - whether a new row was stored, and the subscription as stored - or with the failure.
    • subscribeAtSignIn

      public void subscribeAtSignIn()
      Subscribes this session's device to the application-level Topic (SIGNIN_TOPIC) once a user has signed in, when the application's .iiziPush asks for it (PushProps.isSubscribeAtSignIn(), ruled 2026-10-22 LV-D-2). Called by GyroPushContexts at the client session's creation with a signed-in user and at an in-app sign-in (AppSessionGyro.signInAuthenticatedUser); nothing without the property, a running engine or a signed-in user. Once per session and user: a second call for the same user does nothing, and the store is asked first, off the worker - a device the user already subscribed for this application and Topic is sent no second SUBSCRIBE. Otherwise subscribe(String, BiFunction) is called as the application's Java would call it, with every check it makes.

      A session without a channel to a device (a Designer, Design Viewer or VSViewer session, a client built before P4), a device that reports no transport (a desktop without a Service Worker), an application without the transport the device uses (not_configured) and a Browser whose permission is not granted - the client answers gesture_required and prompts nothing (RR-8 A) - are logged once at FINE and never as an error; a Cordova device asks FirebaseX for the permission when it is undecided (the system dialog, at the first sign-in). A success is logged at INFO; a failure of the store or the engine at WARNING (signInLevel(Exception)).

      A sign-in that comes before the device half has reported - a session created with its user while the client's uuid and transport are still on their way - waits on the channel's events for the report: the client writes uuid and transport in one transaction, uuid first (report() of iz/PushChannelProps.js), and the permission afterwards, so the wait ends at the transport or the perm value event, at which both are in; a report that lands between the check and the listener is caught by a second check. Nothing but the lookups runs on the caller's thread.

    • getSignInOutcome

      public String getSignInOutcome()
      Gets what the last subscribe at sign-in of this session came to, for the rigs and the tests: the text logged, without the application, the user and the session.
      Returns:
      The outcome, or null before any.
    • unsubscribe

      public void unsubscribe(String topic, BiFunction<Integer, Exception, Void> callback)
      Description copied from interface: IPushNotificationAPI
      Removes this device's subscription for this application and a topic: on this server the row of the session's user, this application, the topic and this device - the Java form of the HTTP unsubscribe (PUSH-WIRE-CONTRACT section 5.3), no privilege checked - and then, on the device, the push subscription a Browser holds under this application's key, but only when no row of this application remains for this device: a Browser holds one subscription for every topic of an application (PUSH-PROPS-DESIGN section 4.2, 8.7), and one it holds under another key - the server's own, or another application's - is left alone. A Cordova device keeps its FCM token, which every application on the device shares, and no FCM Topic is unsubscribed (ruled 2026-09-26, R-5 A: the server's rows are the subscription). Listeners of the user's sessions get onSubscriptionRemoved with the reason unsubscribed for each row removed.

      The server's part comes first and stands whatever the device answers: a timeout reported here means the rows are gone and the device may still hold its subscription, which the next subscribe answers as it is. Zero removed is a success: the state the caller wanted already holds. no_user when the session has no authenticated user; bad_request for a topic that is not a valid name.

      Specified by:
      unsubscribe in interface IPushNotificationAPI
      Parameters:
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one.
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, zero when there was none, or with the failure.
    • resetDevice

      public void resetDevice(BiFunction<Integer, Exception, Void> callback)
      Description copied from interface: IPushNotificationAPI
      Removes every subscription of the session's user on this device for this application, every topic (ruled 2026-09-24, RR-15 B), on this server and then on the device as IPushNotificationAPI.unsubscribe(String, BiFunction) does once no row remains: a Browser gives up the subscription it holds under this application's key, a Cordova device keeps its shared FCM token. Other applications' rows on this device, and iiziRun's own (*), survive; listeners of the user's sessions get onSubscriptionRemoved with the reason reset for each row removed. Removing every application's rows is IPushNotificationAPI.resetDeviceEverywhere(BiFunction), the HTTP reset with an empty body (PUSH-WIRE-CONTRACT section 5.4), and iiziRun's Reset.

      The server's part comes first and stands whatever the device answers; zero removed is a success; no_user when the session has no authenticated user.

      Specified by:
      resetDevice in interface IPushNotificationAPI
      Parameters:
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, or with the failure.
    • resetDeviceEverywhere

      public void resetDeviceEverywhere(BiFunction<Integer, Exception, Void> callback)
      Description copied from interface: IPushNotificationAPI
      Removes every subscription of the session's user on this device, for every application and topic, iiziRun's own rows (*) included: the Java form of the HTTP reset with an empty body (PUSH-WIRE-CONTRACT section 5.4), what iiziRun's Reset does on the server (ruled 2026-09-25, P4-b). The one member of this API that reaches beyond this application's subscriptions. On the device a Browser then gives up its push subscription whatever key it was made under - nothing of this user on this device remains to deliver to - and a Cordova device keeps its FCM token, which is never unregistered by the Java API. Listeners of the user's sessions get onSubscriptionRemoved with the reason reset for each row of their own application.

      The server's part comes first and stands whatever the device answers; zero removed is a success; no_user when the session has no authenticated user. Stopping this application's notifications alone is IPushNotificationAPI.resetDevice(BiFunction).

      Specified by:
      resetDeviceEverywhere in interface IPushNotificationAPI
      Parameters:
      callback - Called on the client session's worker thread with the number of subscriptions removed from the server, or with the failure.
    • setSubscribeAction

      public void setSubscribeAction(IGProp<?> component, String topic, BiFunction<IPushSubscribeResult, Exception, Void> callback)
      Description copied from interface: IPushNotificationAPI
      Configures the subscribe action of a button (PUSH-PROPS-DESIGN section 8.2, ruled 2026-09-24 RR-6 B and RR-7 A): the click on component, on the client and inside the user's gesture, subscribes this device for this application and topic - the one place where Push Notification is not driven from the server (PUSH-WIRE-CONTRACT section 9.4a) - and this server then stores the subscription as this session's user, this application, this device and the topic, exactly as IPushNotificationAPI.subscribe(String, BiFunction) does, with no privilege checked (RR-5 A), and calls callback with the outcome. On a Browser the click is what shows the permission prompt: pushManager.subscribe() is called synchronously inside the click, as measured on 2026-09-26 (P4-01) - Firefox, Safari and an iPadOS Home Screen web app show the prompt only there and refuse without a gesture, Chrome prompts either way. On Cordova FirebaseX asks the system's permission when it is undecided, and no gesture is needed.

      A session without an authenticated user answers no_user before any prompt (CC-FIX-PWA-FOLLOWUPS, PWA-D-6): the row a click stores is the user's, so the client refuses the click at once from the channel's auth and no permission is spent on it. Configure the action all the same: the first click after the sign-in asks and subscribes.

      The callback is called once per click, not once per configuration, and may be called several times in one session: the user may click, refuse, and click again. It runs on the client session's worker with the result - whether a new row was stored, and the subscription as stored - or with the failure: permission_denied (the prompt's Block, or a permission already denied: final until the user changes it in the Browser's or the operating system's settings, no code can ask again), dismissed (the prompt closed without an answer: the permission stays undecided and the next click asks again), key_conflict (the Browser already holds a push subscription under another VAPID key, never replaced, RR-21 A; or a Cordova device's FCM token belongs to another Firebase project, CC-PUSH-ROADMAP row 4 A), not_configured (this application offers no transport the device uses), unsupported (the device cannot receive Push Notification at all: on iOS a web page receives it only as a Home Screen web app whose manifest says "display": "standalone", and an ordinary Safari tab reports it), no_user (the session has no authenticated user to store the row for), bad_request (the client answered with a subscription lacking its endpoint or keys, and nothing was stored). gesture_required is not expected from a click and is reported if a Browser refuses inside one. Every failure is a PushNotificationException whose PushNotificationException.getUserText() is a text the user can be shown for it (see IPushNotificationAPI.subscribe(String, BiFunction)).

      The component's own action handler (@OnUIAction) still runs, at once, before the user has answered the prompt (RR-20 A): do not decide anything about the subscription there. The client disables the button while the outcome is pending and enables it again after; every other follow-up - a message, hiding the button, changing the screen - is this callback's (RR-6 B). Setting an action on a component that has one replaces it. A successful click stores the subscription even if you removed the action while the prompt was open: the device holds it, and a row nobody stores is a device the server cannot reach; the callback is then not called and the outcome is logged at INFO.

      Configure it when the panel is created, and show the button only where it can work (IPushNotificationAPI.queryDeviceState(BiFunction)). The application's VAPID public key is refreshed into the client when the action is configured, so a key rotated since the session was created reaches the click. With no channel - a Designer, Design Viewer or VSViewer session - nothing is configured and no_device is posted to the callback once; an invalid topic posts bad_request once.

      Specified by:
      setSubscribeAction in interface IPushNotificationAPI
      Parameters:
      component - The button: a UIButton of this client session's screen (P4 hooks the click of UIButton only), named to the client by its property path (RR-7 A).
      topic - The topic, the empty string for the application-level subscription; a name is validated as the HTTP API validates one (PUSH-WIRE-CONTRACT section 2.4).
      callback - Called on the client session's worker thread, once per click, with the result or with the failure.
    • removeSubscribeAction

      public void removeSubscribeAction(IGProp<?> component)
      Description copied from interface: IPushNotificationAPI
      Removes the subscribe action of a button configured with IPushNotificationAPI.setSubscribeAction(IGProp, String, BiFunction): the next click on it subscribes nothing. A no-op on a component that has none, and in a session without a channel. A click whose prompt was open when the action was removed still stores the subscription the user allowed (see IPushNotificationAPI.setSubscribeAction(IGProp, String, BiFunction)).
      Specified by:
      removeSubscribeAction in interface IPushNotificationAPI
      Parameters:
      component - The button.
    • sendToClient

      public void sendToClient(IPushNotification notification, String topic, PushSendOptions options, BiFunction<IPushSendResult, Exception, Void> callback)
      --- sendToClient (P4-10) ---
      Specified by:
      sendToClient in interface IPushNotificationAPI
      Parameters:
      notification - The notification, built with IPushNotificationMessageBuilder.
      topic - The topic, the empty string for the application-level subscription.
      options - The time-to-live, urgency and delivery mode, or null for every default.
      callback - Called on the client session's worker thread with the result, whose target outcomes name this device, or with the failure.