Effection Logo

Resources

The third fundamental Effection operation is resource(). It can seem a little complicated at first, but the reason for its existence is rather simple. Sometimes there are operations which meet the following criteria:

  1. They are long running
  2. We want to be able to interact with them while they are running

As an example, let's consider a program that creates a Socket and sends messages to it while it is open. This is fairly simple to write using regular operations like this:

import { main, once } from 'effection';
import { Socket } from 'net';

await main(function*() {
  let socket = new Socket();
  socket.connect(1337, '127.0.0.1');

  yield* once(socket, 'connect');

  socket.write('hello');

  socket.close();
});

This works, but there are a lot of details we need to remember in order to use the socket safely. For example, whenever we use a socket, we don't want to have to remember to close it once we're done. Instead, we'd like that to happen automatically. Our first attempt to do so might look something like this:

import { once, suspend } from 'effection';
import { Socket } from 'net';

export function* useSocket(port, host) {
  let socket = new Socket();
  socket.connect(port, host);

  yield* once(socket, 'connect');

  try {
    yield* suspend();
    return socket;
  } finally {
    socket.close();
  }
}

But when we actually try to call our useSocket operation, we run into a problem: because our useSocket() operation suspends, it will wait around forever and never return control back to our main.

import { main } from 'effection';
import { useSocket } from './use-socket';

await main(function*() {
  let socket = yield* useSocket(1337, '127.0.0.1'); // blocks forever
  socket.write('hello'); // we never get here
});

Remember our criteria from before:

  1. Socket is a long running process
  2. We want to interact with the socket while it is running by sending messages to it

This is a good use-case for using a resource operation. Let's look at how we can rewrite useSocket() as a resource.

import { once, resource } from 'effection';

export function useSocket(port, host) {
  return resource(function* (provide) {
    let socket = new Socket();
    socket.connect(port, host);

    yield* once(socket, 'connect');

    try {
      yield* provide(socket);
    } finally {
      socket.close();
    }
  }
}

Before we unpack what's going on, let's just note that how we call useSocket() has not changed at all, only it now works as expected!

import { main } from 'effection';
import { useSocket } from './use-socket';

await main(function*() {
  let socket = yield* useSocket(1337, '127.0.0.1'); // waits for the socket to connect
  socket.write('hello'); // this works
  // once `main` finishes, the socket is closed
});

The body of a resource is used to initialize a value and make it available to the operation from which it was called. It can do any preparation it needs to, and take as long as it wants, but at some point, it has to "provide" the value back to the caller. This is done with the provide() function that is passed as an argument into each resource constructor. This special operation, when yielded to, passes control back to the caller with our newly minted value as its result.

However, its work is not done yet. The provide() operation will remain suspended until the resource passes out of scope, thus making sure that cleanup is guaranteed.

💡A simple mantra to repeat to yourself so that you remember how resources work is this: resources provide values.

Resources can depend on other resources, so we could use this to make a socket which sends a heart-beat every 10 seconds.

import { main, resource, spawn } from 'effection';
import { useSocket } from './use-socket';

function useHeartSocket(port, host) {
  return resource(function* (provide) {
    let socket = yield* useSocket(port, host);

    yield* spawn(function*() {
      while (true) {
        yield* sleep(10_000);
        socket.send(JSON.stringify({ type: "heartbeat" }));
      }
    });

    yield* provide(socket);
  });
}

await main(function*() {
  let socket = yield* useHeartSocket(1337, '127.0.0.1'); // waits for the socket to connect
  socket.write({ hello: 'world' }); // this works
  // once `main` finishes:
  // 1. the heartbeat is stopped
  // 2. the socket is closed
});

The original socket is created, connected, and set up to pulse every ten seconds, and it's cleaned up just as easily as it was created.

Note that the ensure() operation is an appropriate mechanism to enact cleanup within resources as well. While a matter of preference, the useSocket() resource above could also have been written as:

import { ensure, once, resource } from 'effection';

export function useSocket(port, host) {
  return resource(function* (provide) {
    let socket = new Socket();
    yield* ensure(() => { socket.close(); });

    socket.connect(port, host);

    yield* once(socket, 'connect');
    yield* provide(socket);
  }
}

Resources allow us to create powerful, reusable abstractions which are also able to clean up after themselves.

  • PreviousActions and Suspensions
  • NextSpawn