Scriptable Event Handler with State
Main Style ================================================== {{#include ../links.md}} ```admonish example A runnable example of this implementation is included. See the [_Examples_]({{rootUrl}}/start/examples/rust.md) section for details. ``` Initialize Handler Instance with `Engine::call_fn_with_options` --------------------------------------------------------------- Use `Engine::call_fn_with_options` instead of `Engine::call_fn` in order to retain new [variables] defined inside the custom [`Scope`] when running the `init` function. ```rust impl Handler { // Create a new 'Handler'. pub fn new(path: impl Into) -> Self { let mut engine = Engine::new(); : // Code omitted : // Run the 'init' function to initialize the state, retaining variables. let options = CallFnOptions::new() .eval_ast(false) // do not re-evaluate the AST .rewind_scope(false); // do not rewind scope // In a real application you'd again be handling errors... engine.call_fn_with_options(options, &mut scope, &ast, "init", ()).unwrap(); : // Code omitted : Self { engine, scope, ast } } } ``` Handler Scripting Style ----------------------- Because the stored state is kept in a custom [`Scope`], it is possible for all [functions] defined in the handler script to access and modify these state variables. The API registered with the [`Engine`] can be also used throughout the script. ### Sample script ```js /// Initialize user-provided state (shadows system-provided state, if any). /// Because 'CallFnOptions::rewind_scope' is 'false', new variables introduced /// will remain inside the custom 'Scope'. fn init() { // Add 'bool_state' and 'obj_state' as new state variables let bool_state = false; let obj_state = new_state(0); // Constants can also be added! const EXTRA_CONSTANT = "hello, world!"; } /// Without 'OOP' support, the can only be a function. fn log(value, data) { print(`State = ${value}, data = ${data}`); } /// 'start' event handler fn start(data) { if bool_state { throw "Already started!"; } if obj_state.func1() || obj_state.func2() { throw "Conditions not yet ready to start!"; } bool_state = true; obj_state.value = data; // Constants 'MY_CONSTANT' and 'EXTRA_CONSTANT' // in custom scope are also visible! print(`MY_CONSTANT = ${MY_CONSTANT}`); print(`EXTRA_CONSTANT = ${EXTRA_CONSTANT}`); } /// 'end' event handler fn end(data) { if !bool_state { throw "Not yet started!"; } if !obj_state.func1() && !obj_state.func2() { throw "Conditions not yet ready to end!"; } bool_state = false; obj_state.value = data; } /// 'update' event handler fn update(data) { obj_state.value += process(data); // Without OOP support, can only call function log(obj_state.value, data); } ``` Disadvantages of This Style --------------------------- This style is simple to implement and intuitive to use, but it is not very flexible. New user state [variables] are introduced by evaluating a special initialization script [function] (e.g. `init`) that defines them, and `Engine::call_fn_with_scope` is used to keep them inside the custom [`Scope`] (together with setting `CallFnOptions::rewind_scope` to `false`). However, this has the disadvantage that no other [function] can introduce new state [variables], otherwise they'd simply _[shadow]_ existing [variables] in the custom [`Scope`]. Thus, [functions] are called during events via `Engine::call_fn` which does not retain any [variables]. When there are a large number of state [variables], this style also makes it easy for local [variables] defined in user [functions] to accidentally _[shadow]_ a state [variable] with a [variable] that just happens to be the same name. ```rust // 'start' event handler fn start(data) { let bool_state = false; // <- oops! bad variable name! : // there is now no way to access the : // state variable 'bool_state'... if bool_state { // <- 'bool_state' is not the right thing ... // unless this is what you actually want } : : } ```