Skip to content

Task Modifiers

Charles Demers edited this page Aug 20, 2024 · 1 revision

Task Modifiers

By default, Houston tasks run concurrently — if you call myTask.perform(); myTask.perform();, two instances of the task will run at the same time.

Often, you want to guarantee that no more than one instance of a task runs at the same time; for instance, if you have a task that saves model state to the server, you probably don't want that task to run concurrently — you want it to run sequentially, or you might want to ignore attempts to perform the task if it's already running. Manually enforcing these constraints is tricky and often results in redundant, error-prone boilerplate, but Houston makes it easy to rein in this undesired concurrency with the modifiers described below.

Examples

All of the examples below run the same task function (which just pauses for a moment and then completes), but with different task modifiers applied:

const defaultTask = task(function* () { ... });
const restartableTask = task({ restartable: true }, function* () { ... });
const enqueuedTask = task({ enqueue: true }, function* () { ... });
const droppingTask = task({ drop: true }, function* () { ... });
const keepLatestTask = task({ keepLatest: true }, function* () { ... });

Default Behaviour: Tasks Run Concurrently

The lifetimes of each task overlap, and each task runs to completion.

The drop modifier

The drop modifier drops tasks that are .perform()ed while another is already running. Dropped tasks' functions are never even called.

Example use case: submitting a form and dropping other submissions if there’s already one running.

import { task } from '@mirego/houston';

const submitFormTask = task<[data: string]>({ drop: true }, function* (data) {
  yield fetch(someURL, { method: 'post', body: data });
});

someForm.addEventListener('submit', async (event: SubmitEvent) => {
  const serializedData = getDataFromForm(event.currentTarget);

  // Even if the user submits the form multiple times, subsequent calls will simply be canceled.
  await submitFormTask.perform(serializedData);
});

The restartable modifier

The restartable modifier ensures that only one instance of a task is running by canceling any currently-running tasks and starting a new task instance immediately. There is no task overlap, currently running tasks get canceled if a new task starts before a prior one completes.

Example use case: debouncing an action. Paired with the timeout yieldable, a restartable task acts as a debounced function with async capabilities!

import { task, timeout } from '@mirego/houston';

const debounceAutocompleteTask = task<[query: string]>(
  { restartable: true },
  function* (query) {
    yield timeout(200);

    const response = yield fetch(`${someURL}?q=${query}`);
    const json = yield response.json();

    updateUI(json);
  }
);

someInput.addEventListener('input', (event) => {
  debounceAutocompleteTask.perform(event.currentTarget.value);
});

The enqueue modifier

The enqueue modifier ensures that only one instance of a task is running by maintaining a queue of pending tasks and running them sequentially. There is no task overlap, but no tasks are canceled either.

Example use case: sending analytics

import { task, timeout } from '@mirego/houston';

const sendAnalyticsTask = task<[event: AnalyticsEvent]>(
  { enqueue: true },
  function* (event) {
    const response = yield fetch(someURL, { method: 'post', body: event });
  }
);

// Somewhere else in the code
someButton.addEventListener('click', () => {
  sendAnalyticsTask.perform({ type: 'some-button-click' });
});

The keepLatest modifier

The keepLatest will drop all but the most recent intermediate .perform(), which is enqueued to run later.

Example use case: you poll the server in a loop, but during the server request, you get some other indication (say, via websockets) that the data is stale and you need to query the server again when the initial request completed.

import { task, timeout } from '@mirego/houston';

const pollServerTask = task({ keepLatest: true }, function* () {
  const response = yield fetch(someURL);
  const json = yield response.json();

  update(json);
});

setInterval(() => {
  pollServerTask.perform();
}, 10_000);

// Somewhere else in the code
pollServerTask.perform();