Created
February 1, 2019 01:37
-
-
Save willmtemple/985706a7d7e1aff0498e83e2ec5d750e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Notes on how to deal with callbacks in pxexec-runtime | |
/** | |
* In pxexec-runtime, program threads run in actual separate threads called | |
* Fibers. Fibers are concurrent in the same way that Operating System threads | |
* are concurrent: each has its own stack, and they are intermittently switched | |
* to give the appearance of multitasking. In some cases, they may run | |
* simultaneously on different processors. | |
* | |
* At the same time, Node events are processed on the global event-loop. All | |
* events (such as all asynchronous callbacks, event triggers, etc.) are | |
* triggered in the event loop. In normal Node programs, the main program body | |
* also runs on the event loop. But there's a problem. If the main body of our | |
* program uses a while (1) {} or other type of divergent behavior, it will | |
* block the event-loop permanently. | |
* | |
* The reason we use Fibers is to keep the main body of the program synchronous, | |
* but to avoid blocking the Node event-loop. By running our synchronous main | |
* program in a Fiber, the node event loop is free to process. However, this | |
* introduces some peculiarities with the processing of callbacks. Sometimes, | |
* in our Fiber, it is necessary to block the program body until a callback | |
* has been issued, or until a promise has been resolved. Additionally, when | |
* user code is invoked _as_ a callback, we must detach that code into a Fiber | |
* of its own. | |
* | |
* In core-exec, I have provided convenience functions for both scenarios that | |
* abstract away the logic of Fibers. The following three scenario guides will | |
* show you how to interface the synchronous code from pxexec user-code and | |
* the asynchronous code from Node libraries. | |
*/ | |
// SCENARIO I - Promises | |
// | |
// When to use: a library returns a promise. | |
// | |
// Consider, a function with this prototype: | |
function foo(): Promise<number> | |
// If we need to call this function in pxexec, we have to have a function that | |
// will block the thread until the promise is resolved. | |
// | |
// This function is: require('/core-exec')._await<T>(Promise<T>) : T | |
// | |
// in English: import _await from core-exec and use it to convert Promise<T> to | |
// T | |
// Wrong | |
export function myFunc() { | |
var x: number = foo(); // <-- Type error! | |
return ...; | |
} | |
// Right | |
import { _await } from './core-exec'; | |
export function myFunc() { | |
var x: number = _await(foo()); | |
return ...; | |
} | |
// SCENARIO II - Waiting on an asynchronous callback | |
// | |
// When to use: a library function takes a callback | |
// | |
// Consider, a function with this prototype: | |
function getAPIValue(key: number, cb: (err: string, value: number) => void) | |
// If we need to wait for the callback to be invoked, we have to block the | |
// calling thread until the callback has completed. We do this by wrapping | |
// the invocation in a Promise (note, only if we _need_ the callback value | |
// parameter in order to return to the user's code!!) | |
// Wrong | |
export function myAPIHandler(key: number) : number { | |
getAPIValue(key, (err, value) => { | |
if (err) { | |
// handle error | |
} else { | |
return value; // <-- This will return _from the callback_, and then | |
} // myAPIHandler() will actually return undefined!! | |
}); | |
} | |
// Right | |
import { _await } from './core-exec'; | |
export function myAPIHandler(key: number): number { | |
return _await(new Promise((resolve, reject) => { | |
getAPIValue(key, (err, value) => { | |
if (err) { | |
// handle error | |
reject("error message"); | |
} else { | |
resolve(value); // <-- resolve the promise with the return value | |
} | |
}); | |
})); | |
} | |
// SCENARIO III - User-code callbacks | |
// | |
// When to use: the user needs to set a callback to be executed later | |
// | |
// Consider an EventEmitter within a library: we want to allow our users to | |
// configure an event handler for that event. | |
// | |
// A lot of our code has to do with letting users configure event handlers, such | |
// as the onButton methods in the Grove library. However, since students are | |
// writing synchronous callbacks, we need to detach these callbacks from the | |
// event loop when they are eventually executed. We do this using another | |
// function that I have provided in core-exec: `_detach(() => void)` | |
// Wrong | |
function onMessage(cb: (message: string) => void) { | |
someEventEmitter.on('event', cb); // <-- We've just set ourselves up for the | |
// event loop to call into user code!! | |
} | |
// Right | |
import { _detach } from './core-exec'; | |
function onMessage(cb: (message: string) => void) { | |
someEventEmitter.on('event', (message: string) => { | |
// This function runs in the event loop. | |
// Then we use _detach to fork a Fiber (technically, a Future) | |
_detach(() => { // <-- _detach can only handle () => void, so wrap | |
// more complicated calls in a simple function. | |
// This function runs in a Fiber | |
cb(message); | |
}) | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment