Table of Contents

Callbacks

Callback style programming is generally how JavaScript has been used outside of Synchronet. In the Synchronet ecosystem however, scripts have been almost universally procedural style. Recently (2021), support was added in Synchronet to support callback style JavaScript programming. It is not as pervasive or well supported as procedural scripting, but the lessons learned over the years have guided the minimum required features.

Some defininitions

callback
a callback is simply a function passed as an argument with the expectation that it will be invoked when specific conditions are met.

polled callbacks
polled callbacks are those that will be invoked if no script is currently running, and the triggering condition is present.

timer callbacks
timer callbacks are invoked after a defined period of time.

immediate callbacks
these are callbacks that will be invoked unconditionally with no blocking after the current script stops executing.

user events
user event callbacks are invoked after the applicable event is dispatched.

run queue
callbacks which are not polled or timer callbacks, which have been triggered but not yet invoked are kept in the run queue.

Callback API

Where an argument is callback this is a callback function. If thisObj is passed, it will be used as the this in the callback invocation. When a method returns an ID, the ID may be passed to the corresponding clear method to stop the callback from being invoked in the future.

To allow callback style programming, the script must set js.do_callbacks. If the variable is not true when the script finishes, the event loop will not run and no callbacks will be invoked.

The following methods are available in the js global object:

js.setInterval(callback, period[, thisObj]) (returns an ID)
Creates a timer callback which will be invoked every period milliseconds.

js.setTimeout(callback, timeout_in_ms[, thisObj]) (returns an ID)
Creates a timer callback which will be invoked only once after period milliseconds.

js.addEventListener(eventName, callback) (returns an ID)
Installs a user callback for the string eventName. eventName is any string defined by the programmer. Care should be taken to ensure the string is unique to the use case and not generic to avoid conflicting with other libraries.

js.dispatchEvent(eventName[, thisObj])
Adds all user callbacks which were installed with the specified eventName to the run queue.

js.setImmediate(callback[, thisObj])
Add the specified callback to the run queue.

js.clearInterval(id)
Clears a callback installed via js.setInterval()

js.clearTimeout(id)
Clears a callback installed via js.setTimeout()

js.removeEventListener(id_or_name)
Removes callbacks installed via js.addEventListener(). If id_or_name is the value returned by js.addEventListener(), only a single listener is removed. If is_or_name is a string, all listeners for that eventName will be cleared.

The following methods are available to instances of the Socket class. They will use sock as the name of the instance. Note that a closed socket can be read without blocking when it has been closed. This means that any socket callback that triggers when the socket can be read should check if the socket has been closed before returning to prevent an infinite loop. The this object in the invocation will be the Socket instance itself. Additional properties can be added to the Socket object to pass additional state to/from the callback.

sock.on(op, callback) (returns an ID)
Installs a polled callback to be invoked whenever the socket will allow op without blocking. op may be 'read' or 'write'.

sock.once(op, callback) (returns an ID)
Installs a polled callback to be invoked once, the next time the socket will allow op without blocking. op may be 'read' or 'write'.

sock.connect(host, port, callback)
Installs a polled callback to be invoked once, after a connect() call either succeeds or fails.

sock.clearOn(op, id)
Clears a callback installed via sock.on(). The op parameter must match the one passed to sock.on().

sock.clearOnce(op, id)
Clears a callback installed via sock.once(). The op parameter must match the one passed to sock.once().

These methods are available to the global console object in the terminal server only. The this object in the invocation will be the console object itself, but this is subject to change as the console object is not a particularly useful this object.

console.on(op, callback_function) (returns an ID)
Installs a polled callback to be invoked whenever the terminal should allow op without blocking (see below). op may be 'read'

console.once('read', callback_function) (returns an ID)
Installs a polled callback to be invoked the next time the terminal should allow op without blocking (see below). op may be 'read'

console.clearOn('read' | 'write', id)
Clears a callback installed via console.on(). The op parameter must match the one passed to console.on().

console.clearOnce('read' | 'write', id)
Clears a callback installed via console.once(). The op parameter must match the one passed to console.once().

The console object currently only allows the 'read' operation, and there is no guarantee that input is actually available. Some protocols such as telnet and SSH may send messages that do not appears as console input, but will trigger the callback. As such, it is not recommended to use a blocking read function in a console read callback.

The Event Loop

The event loops is ran after the script finishes executing while js.do_callbacks is set and there are either timer or polled callbacks installed, or there are any items in the run queue. The event loop will first check for if any polled callbacks need to be invoked. If they is any, one will be chosen and invoked, and the event loop will restart after it completes. If there are no polled events to invoke, timers are checked next. If any timer is pending, one will be invoked, and the event loop will restart after it completes. Finally, if there were no polled or timer callbacks to invoke, the oldest callback on the run queue is invoked.

Note that since polled callbacks are triggered based on current state, only one will be triggered for each such state. If the callback does not clear the state (ie: by calling sock.recv()) it will be invoked again next time through the event loop. If it does clear the state, no callbacks will be invoked in the next pass through the event loop. This does not impact timers because each timer has a separate base time, so there will never be two timer callbacks for the same state.

Basically:

while (js.do_callbacks && (timer_callback_count > 0 || polled_callback_count > 0 || run_queue.length > 0)) {
        timeout = time_to_next_timed_event;
        if (run_queue.length > 0)
                timeout = 0;
        poll_result = poll_callback_states(timeout);
        if (poll_result.polled_callback_ready) {
                polled_callback.call(thisObj);
                continue;
        }
        if (poll_result.timed_callback_ready) {
                timed_callback.call(thisObj);
                continue;
        }
        if (poll_result.run_queue.length > 0) {
                run_queue.shift().call(thisObj);
                continue;
        }
        throw new Error("poll() returned ready, but didn't find anything");
}

See Also