User:Jacek Antonelli/Plugin System/Event Callbacks

Introduction
Event callbacks allow plugins to receive notification when certain events occur, such as:
 * A nearby avatar saying something
 * The user pressing a keyboard key
 * The user receiving an inventory item
 * Or any of a number of other things

This allows the plugin to respond to these events when they happen. For example, a plugin could:
 * Export the incoming chat to a text-to-speech program to help users with limited vision
 * Provide shortcut key activated actions
 * Sort incoming inventory into an "Inbox" directory

Clearly, this is a powerful idea and an important component of the plugin system.

Registering for Events
In order for a plugin to receive events, it must first indicate its interest in particular event types by registering with the viewer for those types. When registering the plugin specifies:


 * 1) Which event type it's interested in.
 * 2) Callback type (listener or hook), as described below.
 * 3) Optionally, which properties of the event it's interested in. (By default, it receives all properties.)
 * 4) Optionally, desired callback order, as described below.

Callback Types
There are two types of event callbacks, listeners and hooks. Both callbacks allow the plugin to receive event notification and act accordingly, but hooks go further, giving the plugin the opportunity to intercept or modify events that it receives.

A plugin can register for as many different event types as they want, but they can't register multiple times for the same event (without unregistering in between). This means that a plugin cannot be registered as both a listener and a hook for the same event type at the same time.

Registering
To register as a listener or hook:

[ "$request_id", "RegisterEventCallback", "$event_type", "$callback_type", $props, "$order" ]

is the name of the event type the plugin wishes to hear about.

is either "listener" or "hook".

(optional) is either an array of names of properties of that event type that the plugin wishes to hear about, or the string "all" to receive all properties. The default if omitted is "all".

(optional) is the desired callback order. It can be one of these strings: "early", "normal", or "late". The default if omitted is "normal".

The viewer will respond with an indicative error in any of these cases:
 * The specified event type isn't supported by the implementation.
 * The callback type is invalid (i.e. not "listener" or "hook").
 * Props is provided but is invalid (i.e. not an array or the string "all").
 * Any of the props in the array are not supported by the implementation.
 * Callback order provided but is invalid (i.e. not "early", "normal" or "late").
 * The plugin is already registered to receive this event. (The plugin must explicitly unregister before it can register with different options.)

If all goes well, the viewer will respond with:

[ "$request_id", "RegisterEventCallback.Response", "$event_type", "$callback_type" ]

Unregistering
To unregister as a listener or hook:

[ "$request_id", "UnregisterEventCallback", "$event_type" ]

The viewer will respond with an indicative error in any of these cases:
 * The given event type doesn't exist.
 * The plugin is not registered to receive that event type.

If all goes well, the viewer will respond with:

[ "$request_id", "UnregisterEventCallback.Response", "$event_type" ]

Event Notices
When an event of the specified type occurs, the viewer will send a message to the plugin:

[ "$event_id", "$notice_type", "$event_type", { "$prop1" : $value1, ... } ]

is an arbitrary string generated by the viewer, used to unambiguously indentify the specific occurence of the event. (This is more important for hooks than listeners, since the hook will be telling the viewer which event to change.) The identifier should be unique enough that no two events within roughly 30 seconds of each other have the same string. An easy way for viewer implementations to do this is to use a large (8 or more digit) random integer as part of the identifier string.

is either "EventListenerNotice" or "EventHookNotice", depending on the callback type.

is the event type name.

is a struct of properties of the event. It will only contain properties that the plugin registered as being interested in.

Hook Actions
When a plugin receives an event through a hook (i.e. an EventHookNotice message), it has the choice to:


 * Let it pass through unmodified
 * Modify the event before passing it along to later callbacks
 * Or, intercept it completely (no later callbacks receive it)

Passing the Event Unmodified
If, after receiving the event, the hook decided it doesn't want to do anything to it, it can tell the viewer to pass the unmodified event to the next callback:

[ "$event_id", "EventHookPass" ]

Of course, even if the hook passes the event on unmodified, it can still perform other actions using the information gained from the event, just as if it were a listener.

Modifying the Event
If the hook wishes to change the properties of the event before giving it back to the viewer, it can tell the viewer the updated property values:

[ "$event_id", "EventHookModify", { "$prop1" : $value1, ... } ]

is a struct of property names and the new values they should have. Any properties that are not included in the struct keep their original value.

The given struct can contain even properties that weren't included in the event as the plugin received it (i.e. the plugin didn't express interest in that property). However, the plugin should be very careful to make sure that the properties it is attempting to set are actually supported by the event type! (Use ListProps for that.)

The viewer will respond with an error message in any of these cases:
 * Any of the given properties are not supported by the event type
 * Any of the given properties are invalid (wrong type, incompatible with other property values, etc.)
 * The timeout has expired.

Intercepting the Event
If the hook wants to prevent later callbacks from receiving the event at all, it can send a message to the viewer to declare that it has intercepted the event:

[ "$event_id", "EventHookIntercept" ]

Intercepting events is useful for events that are intended for one particular plugin, such as a special command entered into chat that isn't meant to be sent to the server as regular chat. However, plugin creators should be extremely careful when designing a plugin that intercepts events, because of the potential for interfering with the proper functioning of other plugins!

The viewer will respond with an error message if the timeout has expired.

Otherwise, if all goes well, the viewer will respond with a success message:

[ "$event_id", "EventHookIntercept.Response" ]

Timeout
The viewer will wait for the plugin's response before taking further action with the event. However, if the plugin does not respond within a certain timeout period, the viewer will assume the plugin is frozen or malfunctioning, and pass the unmodified event to the next callback.

The amount of time that the viewer will wait varies with the event type. If the plugin anticipates that it will take longer than that time to compute an appropriate response, it should indicate that to the viewer by immediately (after receiving the event) asking for an extended timeout:

[ "$event_id", "ExtendHookTimeout", $length ]

(optional) is a number indicating how many seconds the viewer should continue to wait, starting at the time it receives the extension request. If omitted, the default is 5 seconds. The maximum is 20 seconds. Additional extensions can be requested for as long as the timeout has not expired.

However, just because the plugin has requested a time extension does not mean the viewer is obligated to grant it! The viewer is allowed to deny the request, so plugins should be prepared to handle that. The viewer will send a response to the plugin to tell it whether the request was granted or not:

[ "$event_id", "ExtendHookTimeout.Response", $granted ]

is either  or   (Note: those are JSON Boolean values, not strings), and indicates whether the request was granted or not. If not, the plugin might as well abandon its attempt (unless it can finish it before the current time period has passed).

If the timeout is reached (the new timeout, or the original if no extension was granted) and the plugin has not declared an action (pass, modify, or intercept), the viewer sends a timeout notice and passes the event to the next callback. The timeout notice is as follows:

[ "$event_id", "HookTimeoutExpired" ]

If the plugin tries to perform an action anyway, the viewer will respond with an error message and ignore the requested action.

Callback Order
When registering for event callback, the plugin can indicate its desired order relative to other plugins. There are three choices: early, normal, or late; if the plugin does not indicate a desired order, it defaults to normal. The desired callback order and the callback type (listener or hook) determines the order of receiving events, as shown below.

In addition to plugins, the viewer itself has event callbacks for its own purposes. As two examples, it listens for keyboard events to facilitate keyboard shortcuts, and it has a hook for outgoing chat to perform gesture replacement. Wherever possible, "system listeners" should be last in the chain, and "system hooks" in the middle, to give plugins the most opportunity to affect events and affect viewer functionality.

The broad callback order is as follows:


 * 1) Early System Listeners
 * 2) Early Listeners
 * 3) Early Hooks
 * 4) Normal Listeners
 * 5) Normal Hooks
 * 6) (Normal) System Hooks
 * 7) Late Hooks
 * 8) Late Listeners
 * 9) Late System Listeners

That is, the early system listeners receive the event one after another, then the early listeners do, then the early hooks do, then the normal listeners do, and so on. Because hooks are allowed to modify events, only early listeners are certain to receive the original event in all cases. By contrast, late listeners and late system listeners are certain to receive the event only after every hook has had the opportunity to modify it.

The relative order of multiple plugins with the same desired order and callback type is undefined, i.e. it's up to the viewer to decide. Two sensible systems are:


 * First registered, first served.
 * User customizable.

To be clear, the viewer must respect the specified order between callbacks with different types or desired orders. It's only among callbacks which would otherwise have identical priority that the viewer is allowed some flexibility.