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});