-
Notifications
You must be signed in to change notification settings - Fork 5
02. Lifecycle
A lifecycle provides initialize, suspend, resume and destroy methods. A lifecycle usually refers to a specific instance. You can provide this instance when creating your lifecycle and refer to it via the read-only target property:
const lifecycle:Lifecycle = new Lifecycle(someManagedObject);
lifecycle.target // returns someManagedObject;
For most developers, the only lifecycle you are interested in will be the context's lifecycle, which is created by the context itself.
A lifecycle can be in any of the following settled states:
- UNINITIALIZED
- ACTIVE
- SUSPENDED
- DESTROYED
During a transition a lifecycle can be in one of the following transitionary states:
- INITIALIZING
- SUSPENDING
- RESUMING
- DESTROYING
Before initialization the lifecycle is UNINITIALIZED
. From here, only initialize()
is a valid transition.
During initialization the lifecycle is INITIALIZING
.
Once initialized the lifecycle becomes ACTIVE
.
From ACTIVE
a lifecycle can be suspended or destroyed.
From SUSPENDED
a lifecycle can be resumed (returning it to ACTIVE), or destroyed.
Once DESTROYED
there is no way back.
Invalid transitions can be captured by listening for the LifecycleEvent.ERROR event. If no listener is attached an Error will be thrown.
The Lifecycle provides 4 distinct hooks: before, when and after any transition, and a callback, passed to the transition function itself, which runs between the when and after hooks. For clarity, the ordering is:
- Before transitioning handlers run
- If there are no errors, the state is changed. If there are errors the callback passed to the transition is run, and the errors are passed to it, and we go no further
- When transitioning handlers run
- The callback passed to the transition is run
- After transitioning handlers run A complete list of the process timing hooks:
beforeInitializing
whenIntitializing
afterIntializing
beforeSuspending
whenSuspending
afterSuspending
beforeResuming
whenResuming
afterResuming
beforeDestroying
whenDestroying
afterDestroying
Timing hooks can be chained, as they return the same interface that you use to access them.
Each of the 4 hooks provides a particular way to hook into the transition.
- before hooks can block the transition from happening.
- when hooks are non-blocking, and run after the state has changed, but before the callback passed to the transition is run.
- after hooks are non-blocking, and run after the callback passed to the transition is run.
The callback passed to the transition can process the errors from the before handlers, and runs whether or not the transition is made. The when a nd after hooks only run if the transition is successful (i.e. there are no before handler errors).
The initialize
and resume transitions run their handlers in the order in which they were added. The suspend
and destroy
transitions run their handlers in reverse, so the last handler added to a particular phase, e.g. whenDestroying, is run first during that phase.
A handler added to beforeSuspending will be run every time the lifecycle is suspended. A callback passed to suspend()will run once only, unless you pass the same callback to the suspend() function when you run it again at a later time.
A lifecycle provides 4 before hooks: beforeInitializing
, beforeSuspending
, beforeResuming
and beforeDestroying
.
Handlers added to these transitions are executed when the transition starts and before any events are dispatched.
A before handler must have one of the following signatures:
handler():Void
handler(phase:String):Void
handler(phase:String, callback:Dynamic):Void
For beforeInitializing()
the phase will be preInitialize
and so on.
If a handler accepts a callback and calls the callback with an error, the transition will be terminated, and the state will be reverted to the pre-transition state. For more background on async handlers in Robotlegs 2 see:
- core.async.readme
- core.messaging.readme
When and After handlers are executed synchronously and must have one of the following signatures:
- handler():Void
- handler(phase:String):Void
For whenInitializing()
the phase will be initialize
and so on.
When and After handlers are not passed callbacks.
A lifecycle dispatches the following events:
- PRE_INITIALIZE
- INITIALIZE
- POST_INITIALIZE
- PRE_SUSPEND
- SUSPEND
- POST_SUSPEND
- PRE_RESUME
- RESUME
- POST_RESUME
- PRE_DESTROY
- DESTROY
- POST_DESTROY
- ERROR
There are two situations in which errors can occur
- When an invalid transition is attempted
- When a before handler calls back with an error
In both cases the lifecycle will dispatch LifecycleEvent.ERROR if a listener for that event has been attached, otherwise an error will be thrown. When attempting a transition into a given state, a user callback may be supplied. If an error occurs, and a listener has been attached as explained above, the error will be supplied to the callback.
A lifecycle manages the validity of transitions - so both initialize and destroy are, via the lifecycle's internal state machine, one-time-only transitions. The only repeatable transitions are suspend and resume. If you need to 'unhook' from these transitions, we recommend you decouple your handlers and use a flag to exit-early from your handlers if the object they would deal with has been cleaned up.
private var _managedExtension:SomeExtension;
public function set managedExtension(value:SomeExtension):Void
{
_managedExtension = value;
}
private function deactivateExtension():Void
{
if (_managedExtension == null) return;
// code that actually does stuff here
}
private function activateExtension():Void
{
if (_managedExtension == null) return;
// code that actually does stuff here
}
private function addContextLifecycleHooks():Void
{
context
.beforeSuspending(deactivateExtension)
.beforeResuming(activateExtension)
}
If the whole Lifecycle is no longer required, just null it out, and any handlers will be released for garbage collection.
The purpose of error reporting in the lifecycle is to debug during development, not to recover run-time problems. When you make use of the lifecycle, your handlers should be returning errors only on the basis of non-mutable properties: problems that were cemented at compile time, not problems that can occur only at runtime such as network availability issues, order of operations, data entry screw ups and so on. In the case of an error in your initialize() process, the next step would be to study that error and make relevant changes in your code base, and then recompile. For this reason it's critical that extension developers write very helpful error messages.
- The correct range of fonts wasn't embedded
- An extension on which this extension depends hasn't been installed
- The code is running in a sandbox that doesn't have the permissions this extension requires
- An essential configuration object hasn't been created (e.g. developer credentials for a licensed extension)
- Couldn't load a module containing critical extension dependencies - e.g. an engine
- Lost contact with the network
- User doesn't have an account
- Couldn't load a module containing non-critical extension dependencies - e.g. a skin
Most developers will only encounter the lifecycle attached to the context. Remember - lifecycle errors are non-recoverable. Their purpose is to provide detailed and meaningful explosions during development in order to ensure that the developer has included all the dependencies and configuration requirements of this extension. Normal run-time errors should be dealt with in the usual ways, not through lifecycles. It is highly unlikely that providing a service with a lifecycle would be a good approach. An extension framework - for example an entity system - might have its own lifecycle to allow other extensions to hook in to its lifecycle phases (particularly suspend / resume).
For extensions accessing the context's own lifecycle: An example usage, for an imaginary extension which provides a developer console to the application.
_context.beforeInitializing( checkEventDispatcherInstalled )
.beforeInitializing( checkEmbeddedFonts )
.whenInitializing( setLocalDateTime )
.whenInitializing( setLocalPaths )
.whenSuspending( grabPauseTime )
.afterSuspending( deactivateConsole )
.whenResuming( calculatePauseInterval )
.afterResuming( reactivateConsole )
.beforeDestroying( offerConsoleDump )
.afterDestroying( destroyConsole );
For managing your own object's lifecycle
inline var awesomeExtension:AwesomeExtension = new AwesomeExtension();
inline var awesomeLifecycle:Lifecycle = new Lifecycle(awesomeExtension);
// you would now need to provide an opportunity for any interested
parties to add their hooks
const callback:Function = function(errors:Dynamic):Void
{
// log the errors
}
awesomeLifecycle.initialize(callback);