repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / docs / posts
Eric Bower · 26 Aug 24

thunks.md

  1---
  2title: Thunks
  3description: Thunks are tasks for business logic
  4---
  5
  6Thunks are the foundational central processing units. They have access to all
  7the actions being dispatched from the view as well as your global state. They
  8also wield the full power of structured concurrency.
  9
 10> Endpoints are specialized thunks as you will see later in the docs
 11
 12Think of thunks as micro-controllers. Only thunks and endpoints have the ability
 13to update state (or a model in MVC terms). However, thunks are not tied to any
 14particular view and in that way are more composable. Thunks can call other
 15thunks and you have the async flow control tools from `effection` to facilitate
 16coordination and cleanup.
 17
 18Every thunk that's created requires a unique id -- user provided string. This
 19provides us with some benefits:
 20
 21- User hand-labels each thunk
 22- Better traceability
 23- Easier to debug async and side-effects
 24- Build abstractions off naming conventions (e.g. creating routers
 25  `/users [GET]`)
 26
 27They also come with built-in support for a middleware stack (like `express` or
 28`koa`). This provides a familiar and powerful abstraction for async flow control
 29for all thunks and endpoints.
 30
 31Each run of a thunk gets its own `ctx` object which provides a substrate to
 32communicate between middleware.
 33
 34```ts
 35import { call, createThunks, mdw } from "starfx";
 36
 37const thunks = createThunks();
 38// catch errors from task and logs them with extra info
 39thunks.use(mdw.err);
 40// where all the thunks get called in the middleware stack
 41thunks.use(thunks.routes());
 42thunks.use(function* (ctx, next) {
 43  console.log("last mdw in the stack");
 44  yield* next();
 45});
 46
 47// create a thunk
 48const log = thunks.create<string>("log", function* (ctx, next) {
 49  const resp = yield* call(
 50    fetch("https://log-drain.com", {
 51      method: "POST",
 52      body: JSON.stringify({ message: ctx.payload }),
 53    }),
 54  );
 55  console.log("before calling next middleware");
 56  yield* next();
 57  console.log("after all remaining middleware have run");
 58});
 59
 60store.dispatch(log("sending log message"));
 61// output:
 62// before calling next middleware
 63// last mdw in the stack
 64// after all remaining middleware have run
 65```
 66
 67# Anatomy of thunk middleware
 68
 69Thunks are a composition of middleware functions in a stack. Therefore, every
 70single middleware function shares the exact same type signature:
 71
 72```ts
 73// for demonstration purposes we are copy/pasting these types which can
 74// normally be imported from:
 75//   import type { ThunkCtx, Next } from "starfx";
 76type Next = () => Operation<void>;
 77
 78interface ThunkCtx<P = any> extends Payload<P> {
 79  name: string;
 80  key: string;
 81  action: ActionWithPayload<CreateActionPayload<P>>;
 82  actionFn: IfAny<
 83    P,
 84    CreateAction<ThunkCtx>,
 85    CreateActionWithPayload<ThunkCtx<P>, P>
 86  >;
 87  result: Result<void>;
 88}
 89
 90function* myMiddleware(ctx: ThunkCtx, next: Next) {
 91  yield* next();
 92}
 93```
 94
 95Similar to `express` or `koa`, if you do **not** call `next()` then the
 96middleware stack will stop after the code execution leaves the scope of the
 97current middleware. This provides the end-user with "exit early" functionality
 98for even more control.
 99
100# Anatomy of an Action
101
102When creating a thunk, the return value is just an action creator:
103
104```ts
105console.log(log("sending log message"));
106{
107  type: "log",
108  payload: "sending log message"
109}
110```
111
112An action is the "event" being emitted from `startfx` and subscribes to a very
113particular type signature.
114
115A thunk action adheres to the
116[flux standard action spec](https://github.com/redux-utilities/flux-standard-action).
117
118> While not strictly necessary, it is highly recommended to keep actions JSON
119> serializable
120
121For thunks we have a more strict payload type signature with additional
122properties:
123
124```ts
125interface CreateActionPayload<P = any, ApiSuccess = any> {
126  name: string; // the user-defined name
127  options: P; // thunk payload described below
128  key: string; // hash of entire thunk payload
129}
130
131interface ThunkAction<P> {
132  type: string;
133  payload: CreateActionPayload<P>;
134}
135```
136
137This is the type signature for every action created automatically by
138`createThunks` or `createApi`.
139
140# Thunk payload
141
142When calling a thunk, the user can provide a payload that is strictly enforced
143and accessible via the `ctx.payload` property:
144
145```ts
146const makeItSo = api.get<{ id: string }>("make-it-so", function* (ctx, next) {
147  console.log(ctx.payload);
148  yield* next();
149});
150
151makeItSo(); // type error!
152makeItSo("123"); // type error!
153makeItSo({ id: "123" }); // nice!
154```
155
156If you do not provide a type for an endpoint, then the action can be dispatched
157without a payload:
158
159```ts
160const makeItSo = api.get("make-it-so", function* (ctx, next) {
161  console.log(ctx.payload);
162  yield* next();
163});
164
165makeItSo(); // nice!
166```
167
168# Custom `ctx`
169
170End-users are able to provide a custom `ctx` object to their thunks. It must
171extend `ThunkCtx` in order for it to pass, but otherwise you are free to add
172whatever properties you want:
173
174```ts
175import { createThunks, type ThunkCtx } from "starfx";
176
177interface MyCtx extends ThunkCtx {
178  wow: bool;
179}
180
181const thunks = createThunks<MyCtx>();
182
183// we recommend a mdw that ensures the property exists since we cannot
184// make that guarentee
185thunks.use(function* (ctx, next) {
186  if (!Object.hasOwn(ctx, "wow")) {
187    ctx.wow = false;
188  }
189  yield* next();
190});
191
192const log = thunks.create("log", function* (ctx, next) {
193  ctx.wow = true;
194  yield* next();
195});
196```