Jasonette/documentation

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

Opened this issue · 3 comments

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?

maks commented

Mostly LGTM.
With naming events I would suggest $notification.recieved
Actually I think its a little buit confusing that built in actions (eg. $render) as well as events are all $ prefixed now that events won't all necessarily be system events, but thats only a minor niggle.

Main point I wanted to raise though is about background processing. This is Android specific, I don't know enough about how iOS does background processing these days.

Taking push-notifications as a first example, whats not covered yet here is how things will work when a push message is received while Jasonette app is not in the foreground. Normally you would want this (eg. think chat client) to display a std OS notification UI that a new message is available. The user then has the option to press on the notification and expects to be taken to the relevant part of the apps UI.
For this case, Jasonette will need to keep track of which "app" registered for the push message. Implementation-wise I guess we can do this by maintaining a hashmap of registrations to jason doc urls?


In regards to the "problem" I think this should be handled internally by each action that implements an event.
eg. in the case of a websocket for example, if you already have an open connection to a given server and $websocket.connect is called on it again, then it can either be a no-op or the $websocket.onconnected is called again immediately, in either case, the websocket itself does not need to be touched, the action class knows its state and leaves it alone.
This way there is no need for any extra work by the Jasonette json code, it just triggers the $websocket.connect whenever it needs to make sure there is a connection open, irrespective of whether there is an existing connection or not.

With naming events I would suggest $notification.recieved

Wouldn't this be ambiguous? The "registered" event is something we also receive from the server side. Note that we don't have to go with the $notification prefix and come up with a better one if we want, as this API hasn't even been made public yet. Here's an example of how Websockets API https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

Actually I think its a little buit confusing that built in actions (eg. $render) as well as events are all $ prefixed now that events won't all necessarily be system events, but thats only a minor niggle.

I understand it can be a bit confusing, but I did try to make it consistent somewhat, so let me explain. Basically whenever there's a $ prefix, it means it's either an action or an event that has an immutable built-in behavior (could be the default system event, or something supported by an extension). Compare that to custom events you call via trigger. Here's an example:

{
  "$load": {
    "trigger": "draw"
  },
  "draw": {
    "type": "$render"
  }
}

In this example, draw is something we arbitrarily created, it's like a user defined event. It's something you trigger manually. But things like $load or $notification.registered are triggered automatically. You cannot change when these events are triggered. Same way, when it comes to actions like $render, you cannot change what $render does because it's already built in.

Basically I've been using the $ prefix as a way to indicate that it's something that has a fixed behavior (regardless of action or event) and everything else as user defined.

That said, we did experiment with using a @ prefix Jasonette/JASONETTE-iOS#34 (comment) I think it had pros and cons. The benefit was that it made the distinction clear between core events and extension implemented events, but the drawback was that it adds another concept to understand, which is why I thought it would be better if we can go without adding another concept. Hope this clarifies things. If it feels still confusing, or if you prefer the @ prefix type solution, please let me know. It could be that I'm just used to this way of thinking so maybe there's a more intuitive approach.

Main point I wanted to raise though is about background processing. This is Android specific, I don't know enough about how iOS does background processing these days. Taking push-notifications as a first example, whats not covered yet here is how things will work when a push message is received while Jasonette app is not in the foreground. Normally you would want this (eg. think chat client) to display a std OS notification UI that a new message is available. The user then has the option to press on the notification and expects to be taken to the relevant part of the apps UI. For this case, Jasonette will need to keep track of which "app" registered for the push message. Implementation-wise I guess we can do this by maintaining a hashmap of registrations to jason doc urls?

Wow you really thought far ahead into this! I totally forgot to mention this. There are two parts to this question:

  1. what to do when user opens the notification?
  2. Is it possible to customize the push notification behavior while app is in the background?

Let me go through each:

1. what to do when user opens the notification?

OK fortunately we have an clean solution to this by utilizing a couple of already existing features of Jasonette:

  1. We have a call method on both iOS and Android, which we can trigger via NSNotification or Intent. Basically we can feed any JSON action to it and it will execute that JSON.
  2. For both APN and GCM you can attach a JSON payload on the notification itself. (Guess what? it's JSON!)

Combining these two, we can send a push notification with an action payload or href payload which when opened, does whatever that action or href tells it to do. If you think about it for a moment, this means imagination is your limit. An extreme case would be you could even send an entire app over push notification, as payload. Anyway, coming back to normal cases, we could send a push notification with a payload that looks like:

{
  "href": {
    "url": "https://jasonbase.com/things/3nf.json"
  }
}

and it would open that view when the user slides open that notification. Of course, we could express it using action as well:

{
  "action": {
    "type": "$href",
    "options": {
      "url": "https://jasonbase.com/things/3nf.json"
    }
  }
}

This way we don't have to register anything anywhere. In fact, the iOS version has already implemented half of this (just the href part) Jasonette/JASONETTE-iOS@8610338 The reason I didn't implement an action handler was because it felt too powerful and wanted to think about its implications. But more and more I think these are things we should leave up to the developer's judgment instead of having the framework making value judgment, as the upside is huge. What do you think?


2. Is it possible to customize the push notification behavior while app is in the background?

Both iOS and Android have "rich push notifications" which allows for more customized view of the notification itself, as well as allowing users to interact with the notification in custom ways:

rich

From my own personal usage and looking at how most people interact with push notifications, I personally think of these as gimmicky features at this point and may not be worth spending time to standardize into a JASON syntax, especially considering how both Apple and Google have vested interest to create differentiated experiences to the lock screen going forward. That said this may change in the future when the lock screen UI somehow ends up becoming as standardized as typical app UIs AND they provide some significant value for end users. This is why I don't think it's a good idea to implement rich notifications at the core level. Maybe someone can create an extension if they really wanted these features.

Finally, I realize Android has much more powerful features to handle push notifications in custom ways compared to iOS, but this wouldn't be cross platform, which is why the core doesn't implement it. Again this could be added on as an extension in the future if someone wants to do some crazy stuff with push notifications on Android.


So for now, I think it's good enough to implement regular push notifications that:

  1. display plain notification (maybe we could allow an image preview)
  2. execute whatever action or href is attached to the notification as payload.

In regards to the "problem" I think this should be handled internally by each action that implements an event. eg. in the case of a websocket for example, if you already have an open connection to a given server and $websocket.connect is called on it again, then it can either be a no-op or the $websocket.onconnected is called again immediately, in either case, the websocket itself does not need to be touched, the action class knows its state and leaves it alone.
This way there is no need for any extra work by the Jasonette json code, it just triggers the $websocket.connect whenever it needs to make sure there is a connection open, irrespective of whether there is an existing connection or not.

Agreed. Basically abstracting out the exception handling so that the user doesn't have to know, I think that's better for now. The only reason I was cautious about this was maybe sometimes users may want to handle things differently depending on the state, but we can just add the state feature later if this is necessary. For now I can't think of when someone would want to manually handle these things so agree it's better to start simple.

maks commented

@gliechtenstein thanks for such a detailed followup!

Re: "$", sorry I wasnt thinking about extensions correctly from Jasonette users pov, makes perfect sense now.

Re: JSON in notification payloads: excellent idea! Didnt spot that in the iOS implementation. I like supporting actions as well as href.

Re: "Rich Notifications UI" yes agree don't need this right now, BUT...

This makes me think I should point out something that I noticed from working alongside iOS dev's inthe past that seems quite diff between the 2 platforms: on Android the "notification ui" is about much more than just push notifications. Its actual an integral part of apps local functionality, nothing to do with APN,GCM, etc.
This is because on Android, apps running "in the background" is a common paradigm that has been in the OS from the beginning. Thus notification UI is commonly used by apps executing int he background to notify the user of significant events. Also for apps running continuously in the background, there is the concept of on-going notifications for "foreground services", eg. audio playing in background.

So notifications are used a lot, from simple useful cases eg. large file upload continues in background after the app doing the upload leaves the foreground, with the notification showing a progress bar. Through to the annoying: game registers an alarm that triggers after X days and if you haven't played the game, shows a notification remind you to play it.

None of the above have anything to do with push-notifications but things like background audio or file uploading are very useful and I could see myself needing to use them in Jasonette apps right now. On Android ongoing notifications are also linked to lockscreen, but thats only a recent thing on newer versions of Android, on-going notifications have been around much longer.

Hope the above makes sense, like I said, all of the above doesn't directly affect this proposal except to keep it in mind in terms of future functionality for core jasonette as well as extensions.
Like I said I believe this is quite different to iOS, but if the situation is now similiar on iOS, I'd be keen to know how it implements this.