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```