Why structured concurrency?

· erock's devlog

How the front-end can benefit from a task tree

With brevity, my operational view on structured concurrency is the management of a tree structure of tasks. It doesn't matter what color the function is, this task tree can be fully controlled by a parent, child interface. You can cancel children tasks, bubble exceptions up to the root task, catch them from raising exceptions, or even restart them when they fail.

While this might sound a little abstract, I wanted to take some time to reflect on how it can be used effectively inside the front-end.

When I think about front-end web development, I often categorize the type of code I write into two buckets: view and business logic layers. The view layer is what it sounds like: facilitating complex and dynamic web content for users to read and interact with. The view is where the user triggers side-effects.

The view is the substrate where we trigger side-effects. We are a side-effect generating system.

cyclejs image visualizing sources and
sinks

Your application as a pure function main() where inputs are read effects (sources) from the external world and outputs (sinks) are write effects to affect the external world. -- From cycle.js

The business logic layer is just side-effect management, which includes state management. Both of these systems commonly handle data synchronization and caching from API services. The front-end has a tough job, it often deals with RESTful APIs that require complex data marshaling and transformations in order to render data rich web pages. We need to create queries that rival -- and in some cases, exceed -- the complexity of backend services. We need tools to squeeze performance out of network programming.

Other libraries advertise that they can cut through the complexity of async code, for example, from react-query:

Toss out that granular state management, manual re-fetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.

Impressive goals and it largely meets them with high regard from the front-end community. "[E]ndless bowls of async-spaghetti code." This is structured concurrency's strength. With structured concurrency, we can create systems that make complex async behavior easy to reason about with task supervisors.

This, mixed with powerful async flow control tools like parallel, we have something that can handle any complex data synchronization and remove "async-spaghetti code".

Here's a simple, yet powerful example where we create a finite-state machine inside of a supervisor:

1function* auth() {
2  while (true) {
3    const login = yield* take("LOGIN");
4    // e.g. fetch token with creds inside `login.payload`
5    const logout = yield* take("LOGOUT");
6    // e.g. destroy token from `logout.payload`
7  }
8}

take is just a function that subscribes to an event stream and returns the event payload. So in this example, we are waiting for a login event, do some processing and then wait for the logout event. When the user is logged in we don't need to listen for another LOGIN event and conversely when we are logged out we don't need to listen for a LOGOUT event. All of this works within a while-loop: something every developer understands and knows how to manipulate with flow control expressions (e.g. break and continue).

With structured concurrency we can design powerful paradigms for async flow control. We need strong tooling around async flow control because front-end web apps regularly have to deal with data synchronization.

Whether it is clearly defined or not, at the seat of every web app is a side-effect management system, and the goal of structured concurrency is to make that system easy to read, write, and maintain.


I have no idea what I'm doing. Subscribe to my rss feed to read more posts.