Effection Logo

Async Rosetta Stone

When we say "Effection is Structured Concurrency and Effects for JavaScript", we mean "JavaScript" seriously. You shouldn't have to learn an entirely new way of programming just to achieve structured concurrency. That's why the Effection APIs mirror ordinary JavaScript APIs so closely. That way, if you know how to do it in JavaScript, you know how to do it in Effection.

The congruence between vanilla JavaScript constructs and their Effection counterparts is reflected in the “Async Rosetta Stone.”

Async/AwaitEffection
awaityield*
async functionfunction*
PromiseOperation
new Promise()action()
Promise.withResolvers()withResolvers()
for awaitfor yield* each
AsyncIterableStream
AsyncIteratorSubscription

await <=> yield*

Pause a computation and resume it when the value represented by the right hand side becomes available.

Continue once a promise has settled:

await promise;

Continue when operation is complete.

yield* operation;

async function <=> function*

Compose a set of computations together with logic defined by JavaScript syntax:

Count down from 5 to 1 with an async function:

async function countdown() {
  for (let i = 5; i > 1; i--) {
    console.log(`${i}`);
    await sleep(1000);
  }
  console.log('blastoff!');
}

Count down from 5 to 1 with a generator function:

import { sleep } from 'effection';

function* countdown() {
  for (let i = 5; i > 1; i--) {
    console.log(`${i}`);
    yield* sleep(1000);
  }
  console.log('blastoff!');
}

Both will print:

5
4
3
2
1
blastoff!

To call an async function within an operation use call():

import { call } from 'effection';

yield* call(async function() {
  return "hello world";
});

To run an operation from an async function use run() or Scope.run:

import { run } from 'effection';

await run(function*() {
  return "hello world";
});

Promise <=> Operation

The Promise type serves roughly the same purpose as the Operation. It is a abstract value that you can use to pause a computation, and resume when the value has been computed.

To use a promise:

let result = await promise;

To use an operation:

let result = yield* operation;

To convert from a promise to an operation, use call()

import { call } from 'effection';

let operation = call(promise);

to convert from an operation to a promise, use run() or Scope.run

import { run } from 'effection';

let promise = run(operation);

new Promise() <=> action()

Construct a reference to a computation that can be resolved with a callback. In the case of Promise() the value will resolve in the next tick of the run loop.

Create a promise that resolves in ten seconds:

new Promise((resolve) => { setTimeout(resolve, 10000) });

Create an Operation that resolves in ten seconds:

import { action } from 'effection';

action(function*(resolve) { setTimeout(resolve, 10000) });

A key difference is that the promise body will be executing immediately, but the action body is only executed when the action is evaluated. Also, it is executed anew every time the action is evaluated.

Promise.withResolvers() <=> withResolvers()

Both Promise and Operation can be constructed ahead of time without needing to begin the process that will resolve it. To do this with a Promise, use the Promise.withResolvers() function:

async function main() {
  let { promise, resolve } = Promise.withResolvers();

  setTimeout(resolve, 1000);

  await promise;

  console.log("done!")
}

In effection:

import { withResolvers } from "effection";

function* main() {
  let { operation, resolve } = withResolvers();

  setTimeout(resolve, 1000);

  yield* operation;

  console.log("done!");
};

for await <=> for yield* each

Loop over an AsyncIterable with for await:

for await (let item of iterable) {
  //item logic
}

Loop over a Stream with for yield* each

import { each } from 'effection';

for (let item of yield* each(stream)) {
  // item logic
  yield* each.next();
}

See the definition of each() for more detail.

AsyncIterable <=> Stream

A recipe for instantiating a sequence of items that can arrive over time. It is not the sequence itself, just how to create it.

Use an AsyncIterable to create an AsyncIterator:

let iterator = asyncIterable[Symbol.asyncIterator]();

Use a Stream to create a Subscription:

let subscription = yield* stream;

To convert an AsyncIterable to a Stream use the stream() function.

import { stream } from 'effection';

let itemStream = stream(asyncIterable);

AsyncIterator <=> Subscription

A stateful sequence of items that can be evaluated one at a time.

Access the next item in an async iterator:

let next = await iterator.next();
if (next.done) {
  return next.value;
} else {
  console.log(next.value)
}

Access the next item in a subscription:

let next = yield* subscription.next();
if (next.done) {
  return next.value;
} else {
  console.log(next.value);
}

To convert an AsyncIterator to a Subscription, use the subscribe() function.

let subscription = subscribe(asyncIterator);
  • PreviousThinking in Effection
  • NextTutorial