Skip to content

Instantly share code, notes, and snippets.

@derhuerst
Last active November 10, 2018 16:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derhuerst/2a9e08c961b400695b84e983f5b31534 to your computer and use it in GitHub Desktop.
Save derhuerst/2a9e08c961b400695b84e983f5b31534 to your computer and use it in GitHub Desktop.
making asynchronous JS code synchronous

making asynchronous JS code synchronous

This script demonstrates challenges when inlining async function calls like setTimeout as well as a solution.

inspect-code does this.

Things to be solved:

  • setTimeout needs to return a timer id.
  • clearTimeout
  • async/await
  • fn(...args)
let x = 1
// This mutation of `x` is supposed to happen immeadiately.
const mutateX = (a) => {x = a}
mutateX(2) // `x` is now `2`.
// This mutation of `x` is supposed to happen after all sync code has run.
const obj = {mutateX: () => {x++}}
const defer = setTimeout
defer(obj.mutateX, 10)
defer(obj.mutateX, 5)
assert(x === 2)
// The async code needs to run after all sync code, therefore at the end of the script.
// The async fn calls might be deeply nested inside function scopes.
// We need to declare global "anchor" variables to access them at the end of the script.
let _ref1, _ref2, _ref3, _ref4, _ref5
let x = 1
const mutateX = (a) => {x = a}
_ref1 = mutateX // global "anchor" variable
// `_now` will skip on async functions. Sync functions will just be executed.
_now(_ref1, 2)
const obj = {mutateX: () => {x++}}
const defer = setTimeout
// As `obj` might have a getter fn for `mutateX`, we need to chache the value in a variable.
// Doing that, we will only access it as often as in the original code.
_ref2 = defer
_ref3 = obj.mutateX
_now(_ref2, _ref3, 10)
// second call
_ref4 = defer
_ref5 = obj.mutateX
_now(_ref4, _ref5, 5)
assert(x === 2)
// `_later` will skip on non-async functions. Callbacks of `setTimeout` will be invoked immediately.
// `setTimeout` might be in the "wrong" order. `_later` needs to sort them by delay.
_later([
{fn: _ref1, args: [2]},
{fn: _ref2, args: [_ref3, 10]},
{fn: _ref4, args: [_ref5, 5]}
])
assert(x === 3)
const _now = (fn, ...args) => {
if (fn === setTimeout) return
return fn(...args)
}
const _later = (calls) => {
calls
.filter((call) => call.fn === setTimeout)
.sort((call1, call2) => call1.args[1] - call2.args[1]) // sort by delay
.forEach(({args}) => args[0]()) // call callback synchronously
}

what doesn't work

plain setTimeout(…) -> IIFE (static analysis)

const x = {y: setTimeout}
x['zy'[1]](fn, 1)  // This aliasing process can get arbitrarily complex.

child processes

The child process could run the code inside a vm. The parent process would collect values from the child until the child process closes.

However, the client would have to serialize the values before sending them to the client. There's JS expressions that can't be serialized correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment