Thunks

Thunks are tasks for business logic


Thunks are the foundational central processing units. They have access to all the actions being dispatched from the view as well as your global state. They also wield the full power of structured concurrency.

Endpoints are specialized thunks as you will see later in the docs

Think of thunks as micro-controllers. Only thunks and endpoints have the ability to update state (or a model in MVC terms). However, thunks are not tied to any particular view and in that way are more composable. Thunks can call other thunks and you have the async flow control tools from effection to facilitate coordination and cleanup.

Every thunk that's created requires a unique id -- user provided string. This provides us with some benefits:

  • User hand-labels each thunk
  • Better traceability
  • Easier to debug async and side-effects
  • Build abstractions off naming conventions (e.g. creating routers /users [GET])

They also come with built-in support for a middleware stack (like express or koa). This provides a familiar and powerful abstraction for async flow control for all thunks and endpoints.

Each run of a thunk gets its own ctx object which provides a substrate to communicate between middleware.

 1import { call, createThunks, mdw } from "starfx";
 2
 3const thunks = createThunks();
 4// catch errors from task and logs them with extra info
 5thunks.use(mdw.err);
 6// where all the thunks get called in the middleware stack
 7thunks.use(thunks.routes());
 8thunks.use(function* (ctx, next) {
 9  console.log("last mdw in the stack");
10  yield* next();
11});
12
13// create a thunk
14const log = thunks.create<string>("log", function* (ctx, next) {
15  const resp = yield* call(
16    fetch("https://log-drain.com", {
17      method: "POST",
18      body: JSON.stringify({ message: ctx.payload }),
19    }),
20  );
21  console.log("before calling next middleware");
22  yield* next();
23  console.log("after all remaining middleware have run");
24});
25
26store.dispatch(log("sending log message"));
27// output:
28// before calling next middleware
29// last mdw in the stack
30// after all remaining middleware have run

Anatomy of thunk middleware #

Thunks are a composition of middleware functions in a stack. Therefore, every single middleware function shares the exact same type signature:

 1// for demonstration purposes we are copy/pasting these types which can
 2// normally be imported from:
 3//   import type { ThunkCtx, Next } from "starfx";
 4type Next = () => Operation<void>;
 5
 6interface ThunkCtx<P = any> extends Payload<P> {
 7  name: string;
 8  key: string;
 9  action: ActionWithPayload<CreateActionPayload<P>>;
10  actionFn: IfAny<
11    P,
12    CreateAction<ThunkCtx>,
13    CreateActionWithPayload<ThunkCtx<P>, P>
14  >;
15  result: Result<void>;
16}
17
18function* myMiddleware(ctx: ThunkCtx, next: Next) {
19  yield* next();
20}

Similar to express or koa, if you do not call next() then the middleware stack will stop after the code execution leaves the scope of the current middleware. This provides the end-user with "exit early" functionality for even more control.

Anatomy of an Action #

When creating a thunk, the return value is just an action creator:

1console.log(log("sending log message"));
2{
3  type: "log",
4  payload: "sending log message"
5}

An action is the "event" being emitted from startfx and subscribes to a very particular type signature.

A thunk action adheres to the flux standard action spec.

While not strictly necessary, it is highly recommended to keep actions JSON serializable

For thunks we have a more strict payload type signature with additional properties:

 1interface CreateActionPayload<P = any, ApiSuccess = any> {
 2  name: string; // the user-defined name
 3  options: P; // thunk payload described below
 4  key: string; // hash of entire thunk payload
 5}
 6
 7interface ThunkAction<P> {
 8  type: string;
 9  payload: CreateActionPayload<P>;
10}

This is the type signature for every action created automatically by createThunks or createApi.

Thunk payload #

When calling a thunk, the user can provide a payload that is strictly enforced and accessible via the ctx.payload property:

1const makeItSo = api.get<{ id: string }>("make-it-so", function* (ctx, next) {
2  console.log(ctx.payload);
3  yield* next();
4});
5
6makeItSo(); // type error!
7makeItSo("123"); // type error!
8makeItSo({ id: "123" }); // nice!

If you do not provide a type for an endpoint, then the action can be dispatched without a payload:

1const makeItSo = api.get("make-it-so", function* (ctx, next) {
2  console.log(ctx.payload);
3  yield* next();
4});
5
6makeItSo(); // nice!

Custom ctx #

End-users are able to provide a custom ctx object to their thunks. It must extend ThunkCtx in order for it to pass, but otherwise you are free to add whatever properties you want:

 1import { createThunks, type ThunkCtx } from "starfx";
 2
 3interface MyCtx extends ThunkCtx {
 4  wow: bool;
 5}
 6
 7const thunks = createThunks<MyCtx>();
 8
 9// we recommend a mdw that ensures the property exists since we cannot
10// make that guarentee
11thunks.use(function* (ctx, next) {
12  if (!Object.hasOwn(ctx, "wow")) {
13    ctx.wow = false;
14  }
15  yield* next();
16});
17
18const log = thunks.create("log", function* (ctx, next) {
19  ctx.wow = true;
20  yield* next();
21});
<< PREV
Controllers
NEXT >>
Endpoints