Skip to content

Custom Events (push notification, websocket, geolocation, etc) #77

@gliechtenstein

Description

@gliechtenstein

Currently on Jasonette, actions can be triggered by:

  1. User input (when you touch a button, the action attached to the same button is run)
  2. Another action (via "trigger")
  3. System events ($load, $show, $pull, $foreground)

This post is related to 3.

Preface: How Events Work Today

The only way to make use of system events is to define their handlers under $jason.head.actions. For example:

{
  "$jason": {
    "head": {
      "actions": {
        "$load": {
          "type": "$network.request",
          "options": {
            "url": "https://..."
          },
          "success": {
            "type": "$render"
          }
        }
      }
    }
  }
}

Here we are listening for the $load system event and have attached an action handler, which makes a $network.request and $renders its content.

There are limited number of these built-in system events ($load, $show, $foreground, $pull) at the moment.

Going Beyond Default System Events

Being able to listen to events is very useful, but what if we could go further and listen to other types of events?

We could listen to other background events that we aren't capturing at the moment, such as location (geofencing), time (local notifications), and remote events (remote push notifications), etc.

Custom Events

Unlike events such as $show and $foreground which are automatically triggered by the system, when it comes to custom events, the system has no prior knowledge, and it may be wasteful to listen to all of those events even when not in use, so we need to manually register to listen to them. Normally a lifecycle for custom events looks like the following:

  1. Register: We first need to register to listen to some event in the future
  2. Listen: Then the app goes back to doing its thing, and the event listener just keeps listening for the event in the background.
  3. Trigger: When some event occurs, the registered handler is triggered and we can call actions or do whatever. We may need to listen to more than one events to cover all aspects of a feature set. For example, push notification involves listening to "registered" event as well as actual notification events after the initial registration.
  4. Unregister: Unregister the event when not needed anymore. (Not always necessary)

In fact we already have an implementation for this, which is the push notification module. This feature is only implemented on iOS at the moment and not even documented because I wanted to take some time to decide if this was the best way to handle this, but I think it's time that we establish an official way to implement these custom events and move forward, officially rolling out all the long-overdue features such as push notification.

Push Notification Example

I'll use the push notification example to describe each step.

Basically We can package all of the four steps into a single Jason*Action class.

1. Register

$notification.register: This is not an event, but an action. Sends a token registration request to APNS server.

"actions": {
	"$load": {
		"type": "$notification.register"
	}
}

We can implement this as a register() method under JasonNotificationAction class.

2. Listen (for register success event)

Then it waits for a response back from APNS.

3. Trigger (on register success)

The first thing that happens after the registration is APNS returns with a "$notification.registered" event. This is a one time event that only gets triggered right after $notification.register action to APNS returns success. It returns the device token assigned by APNS. We need to store this token somewhere so we can reuse it later (normally on a server).

"actions": {
	"$notification.registered": {
	    "type": "$network.request",
	    "options": {
	        "url": "https://myserver.com/register_device.json",
	        "data": {
	            "device_token": "{{$jason.device_token}}"
	        }
	    }
	},
	...
}

4. Listen (for actual notifications)

Now everything is set to go. The app goes back to doing its thing, but this time it keeps listening to APNS for any incoming push notifications.

5. Trigger (on every new push notification message)

When someone sends a POST request to APNS pointing to this device's device_token, APNS will send a push notification to this device, and a "$notification.remote" event will be triggered on this device.

We can attach any handler action here to handle the notification, as seen below.

(May want to change the event name to something that's more intuitive, like $notification.onmessage or $notification.message. Feedback appreciated)

"$notification.remote": {
    "type": "$util.banner",
    "options": {
        "title": "Message",
        "description": "{{JSON.stringify($jason)}}"
    }
}

6. Unregister

In case of push notification this is not really necessary so this part is ommitted. But I can imagine some action/events requiring this, such as location tracking.


Example Scenario

Jasonette is a view-oriented framework--we write a JSON markup to describe a view, just like how web browsers work. This means there currently isn't a way to describe something outside the context of a view.

So far with push notifications this hasn't been a big problem because we can simply attach different event handlers for different views.

For example if we have a chat app, we may have

  1. Chatroom view (chat UI)
  2. Lobby view (no chat UI, just displays the chatrooms available)

Whenever there's an incoming push notification from APNS or GCM,

A. In the chatroom view we may want to:

  1. Look at the notification to see if the message is directed at the current chatroom
  2. If it is, make a refresh and call $render.
  3. If it's not, it means the notification is sent to another chatroom, so the current chatroom content doesn't need to refresh. We just need a notification, so simply display a $util.banner

B. In the lobby view:

We probably just want to dispaly $util.banner all the time, since there is no chat going on in the lobby itself.


So far from experience, it looks like attaching event handlers to the view (instead of globally) seems to cover all use cases while keeping it consistent with the existing, view-oriented JASON syntax. (But please share feedback if there seems to be a hole)


Implementation Spec

Bringing all this together here's a spec for implementing these custom events:

A. 4 phases

There are four steps to dealing with custom events:

  1. Register: Register to listen to some event in the future. This exists as an action.
  2. Listen: The event listener now keeps listening for the event in the background. The event handler can be attached either on an app level (like $notification.registered), or on a view level (like $load).
  3. Trigger: When the event occurs, the handler is triggered.
  4. Unregister: Unregister the event when not needed anymore. (Not always necessary)

B. Implement as a single Jason*Action class.

Sometimes this may not be 100% feasible but we should strive to put everything into a single class as much as possible in order to keep features modular.

This class would consist of:

  1. Register action: (example: "type": "$notification.register", which calls JasonNotificationAction.register())
  2. Event handlers that trigger call(iOS / Android). Then all that's left is to just define an event handler under $jason.head.actions that will handle these events.

Problem

What I described above kind of works, with a caveat.

I talked about the $notification.register action as if it works without any problem, but there is a small issue I didn't mention--"registration" is not something we should be doing all the time.

The code above would register for notification every time this view is loaded. In some cases this may not be desirable because it's redundant.

Using a more relevant case to explain this, let's say we're using this method to implement websocket. We may want to start a session once when the app launches and keep that session around throughout the entire lifecycle of the app instead of reinitializing the session everytime a view is loaded.

Solutions?

We need some way to deal with this problem. (The current iOS push notification implementation doesn't address this problem) Some ideas I have so far involve keeping track of state (registered vs. not registered), probably keep it as a member variable of say, JasonWebsocketAction.

Solution 1. Action based

Maybe implement an action called $notification.token which returns as either success or error callback (if the app already has registered for push, the $notification.register action should have stored it somewhere and would return the token through success callback, as $jason)

{
  "actions": {
    "$load": {
      "type": "$notification.token",
      "success": {
        "type": "$render"
      },
      "error": {
        "type": "$websocket.connect",
        "options": {
          ...
        }
      }
    }
  }
}

This seems clean because we make use of the success and error callbacks,

Solution 2. Variable based

Introduce a new variable for each of these cases (for example $notification.token) and use template expression to selectively execute.

{
  "actions": {
    "$load": [{
      "{{#if 'token' in $notification}}": {
        "type": "$render"
      }
    }, {
      "{{#else}}": {
        "type": "$websocket.connect",
        "options": {
          ...
        }
      }
    }]
  }
}

Implementation-wise this is more complex because now we have to touch variables and templates. Might be challenging to keep it modular.

Solution 3?

Any ideas or suggestions?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions