repos / starfx

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

commit
7754e1f
parent
a6babda
author
Eric Bower
date
2024-02-15 04:08:55 +0000 UTC
docs: more pages
11 files changed,  +405, -9
M docs/main.go
+24, -0
 1@@ -53,6 +53,30 @@ func main() {
 2 			Page: pager("schema.md"),
 3 			Tag:  "Store",
 4 		},
 5+		{
 6+			Text: "Caching",
 7+			Href: "/caching",
 8+			Page: pager("caching.md"),
 9+			Tag:  "Store",
10+		},
11+		{
12+			Text: "Dependent Queries",
13+			Href: "/dependent-queries",
14+			Page: pager("dependent.md"),
15+			Tag:  "Side Effects",
16+		},
17+		{
18+			Text: "Middleware",
19+			Href: "/middleware",
20+			Page: pager("mdw.md"),
21+			Tag:  "Side Effects",
22+		},
23+		{
24+			Text: "Loaders",
25+			Href: "/loaders",
26+			Page: pager("loader.md"),
27+			Tag:  "Side Effects",
28+		},
29 		{
30 			Text: "Structured Concurrency",
31 			Href: "/structured-concurrency",
A docs/posts/caching.md
+53, -0
 1@@ -0,0 +1,53 @@
 2+---
 3+title: Caching
 4+description: How to store data in starfx 
 5+---
 6+
 7+There are two primary ways to store data in `starfx`:
 8+
 9+- Manual
10+- Automatic
11+
12+# Manual
13+
14+You have full control over how data is stored in your app, however, the cost is
15+managing it.
16+
17+For anything beyond the simplest of apps, actively managing your state is going
18+to promote a more robust and managable codebase. When you are performing CRUD
19+operations and want to store those records in a database table that is strongly
20+typed, you probably want manually managed.
21+
22+The good news this is really easy in `starfx` because we can leverage
23+[schemas](/schema) to do most of the heavy lifting.
24+
25+# Automatic
26+
27+This one is simpler to setup, easy for it to "just work" and is more like
28+`react-query`.
29+
30+When using an endpoint, this method simply stores whatever is put inside
31+`ctx.json`. Then you can access that data via `useCache`.
32+
33+```tsx
34+const fetchUsers = api.get("/users", api.cache());
35+```
36+
37+`api.cache()` opts into automatic caching. This is really just an alias for:
38+
39+```ts
40+function*(ctx, next) {
41+  ctx.cache = true;
42+  yield* next();
43+}
44+```
45+
46+# `timer` supervisor
47+
48+This supervisor can help us with how often we refetch data. This will help us
49+call the same endpoint many times but only fetching the data on an interval.
50+
51+[Read more about it in Supervisors](/supervisors#timer)
52+
53+This, cominbed with [Automatic caching](#automatic) provides us with the
54+fundamental features built into `react-query`.
A docs/posts/dependent.md
+72, -0
 1@@ -0,0 +1,72 @@
 2+---
 3+title: Dependent Queries
 4+slug: dependent-queries
 5+description: How to call other thunks and endpoints within one
 6+---
 7+
 8+In this context, thunks and endpoints are identical, so I will just talk about
 9+thunks throughout this guide.
10+
11+There are two ways to call a thunk within another thunk.
12+
13+# Dispatch the thunk as an action
14+
15+Features:
16+
17+- Non-blocking
18+- Thunk is still controlled by supervisor
19+- Works identical to `dispatch(action)`
20+
21+```ts
22+import { put } from "starfx";
23+const fetchMailboxes = api.get("/mailboxes");
24+const fetchMail = thunks.create("fetch-mail", function* (ctx, next) {
25+  yield* put(fetchMailboxes());
26+});
27+```
28+
29+This is the equivalent of using `useDispatch` in your view. As a result, it is
30+also controlled by the thunk's supervisor task. If that thunk has a supervisor
31+that might drop the middleware stack from activating (e.g. `takeLeading` or
32+`timer`) then it might not actually get called. Further, this operation
33+completes immediately, it does **not** wait for the thunk to complete before
34+moving to the next yield point.
35+
36+If you want to make a blocking call to the thunk and wait for it to complete
37+then you want to call the thunk's middleware stack directly.
38+
39+# Call the middleware stack directly
40+
41+Features:
42+
43+- Blocking
44+- Middleware stack guarenteed to run
45+- Does **not** go through supervisor task
46+
47+What do we mean by "middleware stack"? That is the stack of functions that you
48+define for a thunk. It does **not** include the supervisor task that manages the
49+thunk. Because a supervisor task could drop, pause, or delay the execution of a
50+thunk, we need a way to escape hatch out of it and just call the middleware
51+stack directly.
52+
53+```ts
54+import { parallel, put } from "starfx";
55+// imaginary schema
56+import { schema } from "./schema";
57+
58+const fetchMailboxes = api.get("/mailboxes");
59+const fetchMessages = api.get<{ id: string }>("/mailboxes/:id/messages");
60+const fetchMail = thunks.create("fetch-mail", function* (ctx, next) {
61+  const boxesCtx = yield* fetchMailboxes.run();
62+  if (!boxesCtx.json.ok) {
63+    return;
64+  }
65+
66+  const boxes = yield* select(schema.mailboxes.selectTableAsList);
67+  const group = yield* parallel(boxes.map((box) => {
68+    return fetchMessages.run({ id: box.id });
69+  }));
70+  const messages = yield* select(schema.messages.selectTableAsList);
71+  console.log(messages);
72+});
73+```
M docs/posts/endpoints.md
+24, -1
 1@@ -3,7 +3,9 @@ title: Endpoints
 2 description: endpoints are tasks for managing HTTP requests
 3 ---
 4 
 5-Building off of `createThunks` we have a way to easily manage http requests.
 6+An endpoint is just a specialized thunk designed to manage http requests. It has
 7+a supervisor, it has a middleware stack, and it hijacks the unique id for our
 8+thunks and turns it into a router.
 9 
10 ```ts
11 import { createApi, mdw } from "starfx";
12@@ -35,3 +37,24 @@ store.dispatch(fetchUsers());
13 // lets update a user record
14 store.dispatch(updateUser({ id: "1", name: "bobby" }));
15 ```
16+
17+# The same API endpoints but different logic
18+
19+It is very common to have the same endpoint with different business logic
20+associated with it.
21+
22+For example, sometimes I need a simple `fetchUsers` endpoint as well as a
23+`fetchUsersPoll` endpoint, essentially the same endpoint, but different
24+supervisor tasks.
25+
26+Since the router is defined by a thunk id that must be unique, we have to
27+support a workaround:
28+
29+```ts
30+const fetchUsers = api.get("/users");
31+const fetchUsersPoll = api.get(["/users", "poll"], { supervisors: poll() });
32+```
33+
34+The first part of the array is what is used for the router, everything else is
35+unused. This lets you create as many different variations of calling that
36+endpoint that you need.
M docs/posts/getting-started.md
+2, -4
 1@@ -24,7 +24,7 @@ server that serves files and that's it.
 2 Is your app highly interactive, requiring it to persist data across pages? This
 3 is the sweet spot for `starfx`.
 4 
 5-You can use this library as general purpose structured concurrency, but
 6+You can use this library for general purpose structured concurrency, but
 7 [effection](https://github.com/thefrontside/effection) serves those needs well.
 8 
 9 You could use this library for SSR, but I don't heavily build SSR apps, so I
10@@ -46,9 +46,7 @@ import * as starfx from "https://deno.land/x/starfx@0.7.0/mod.ts";
11 
12 # the simplest example
13 
14-Here we demonstrate a complete example so you can get a glimpse of how `starfx`
15-works. The rest of our docs will go into more detail for how all the pieces
16-work.
17+Here we demonstrate a complete example so you glimpse at how `starfx` works.
18 
19 ```tsx
20 import ReactDOM from "react-dom/client";
A docs/posts/loader.md
+61, -0
 1@@ -0,0 +1,61 @@
 2+---
 3+title: Loaders
 4+slug: loaders
 5+description: What are loaders?
 6+---
 7+
 8+Loaders are general purpose "status trackers." They track the status of a thunk,
 9+an endpoint, or a composite of them. One of the big benefits of decoupled
10+loaders is you can create as many as you want, and control them however you
11+want.
12+
13+[Read my blog article about it](https://bower.sh/on-decoupled-loaders)
14+
15+# Usage
16+
17+For endpoints, when you use `storeMdw.store()`, loaders automatically track
18+fetch requests.
19+
20+For thunks you can use `storeMdw.loader()` which will track the status of a
21+thunk.
22+
23+You can also use loaders manually:
24+
25+```ts
26+import { put } from "starfx";
27+// imaginary schema
28+import { schema } from "./schema";
29+
30+function* fn() {
31+  yield* put(schema.loaders.start({ id: "my-id" }));
32+  yield* put(schema.loaders.success({ id: "my-id" }));
33+  yield* put(schema.loaders.error({ id: "my-id", message: "boom!" }));
34+}
35+```
36+
37+# Shape
38+
39+```ts
40+export type IdProp = string | number;
41+export type LoadingStatus = "loading" | "success" | "error" | "idle";
42+export interface LoaderItemState<
43+  M extends Record<string, unknown> = Record<IdProp, unknown>,
44+> {
45+  id: string;
46+  status: LoadingStatus;
47+  message: string;
48+  lastRun: number;
49+  lastSuccess: number;
50+  meta: M;
51+}
52+
53+export interface LoaderState<
54+  M extends AnyState = AnyState,
55+> extends LoaderItemState<M> {
56+  isIdle: boolean;
57+  isLoading: boolean;
58+  isError: boolean;
59+  isSuccess: boolean;
60+  isInitialLoading: boolean;
61+}
62+```
A docs/posts/mdw.md
+81, -0
 1@@ -0,0 +1,81 @@
 2+---
 3+title: Middleware
 4+slug: middleware
 5+description: The structure of a middleware function
 6+---
 7+
 8+Here is the most basic middleware (mdw) function in `starfx`:
 9+
10+```ts
11+function* (ctx, next) {
12+  yield* next();
13+}
14+```
15+
16+Thunks and endpoints are just thin wrappers around a mdw stack:
17+
18+For example, the recommended mdw stack for `createApi()` looks like this:
19+
20+```ts
21+import { createApi, mdw } from "starfx";
22+import { storeMdw } from "starfx/store";
23+
24+// this api:
25+const api = createApi();
26+api.use(mdw.api());
27+api.use(storeMdw.store());
28+api.use(api.routes());
29+api.use(mdw.fetch({ baseUrl: "https://api.com" }));
30+
31+// looks like this:
32+[
33+  mdw.err,
34+  mdw.queryCtx,
35+  mdw.customKey,
36+  mdw.nameParser,
37+  storeMdw.actions,
38+  storeMdw.loaderApi(),
39+  storeMdw.cache(props.cache),
40+  api.routes(),
41+  mdw.composeUrl("https://api.com"),
42+  mdw.payload,
43+  mdw.request,
44+  mdw.json,
45+];
46+```
47+
48+When a mdw function calls `yield* next()`, all it does it call the next mdw in
49+the stack. When that yield point resolves, it means all the mdw functions after
50+it have been called. This doesn't necessarily mean all mdw in the stack will be
51+called, because like `koa`, you can return early inside a mdw function,
52+essentially cancelling all subsequent mdw.
53+
54+# Context
55+
56+The context object is just a plain javascript object that gets passed to every
57+mdw. The type of `ctx` depends ... on the context. But for thunks, we have this
58+basic structure:
59+
60+```ts
61+interface Payload<P> {
62+  payload: P;
63+}
64+
65+interface ThunkCtx<P = any> extends Payload<P> {
66+  name: string;
67+  key: string;
68+  action: ActionWithPayload<CreateActionPayload<P>>;
69+  actionFn: IfAny<
70+    P,
71+    CreateAction<ThunkCtx>,
72+    CreateActionWithPayload<ThunkCtx<P>, P>
73+  >;
74+  result: Result<void>;
75+}
76+```
77+
78+There are **three** very important properties that you should know about:
79+
80+- `name` - the name you provided when creating the thunk
81+- `payload` - the arbitrary data you passed into the thunk
82+- `key` - a hash of `name` and `payload`
M docs/posts/schema.md
+24, -0
 1@@ -3,6 +3,24 @@ title: Schema
 2 description: Learn more about schamas and slices
 3 ---
 4 
 5+A schema has two primary features:
 6+
 7+- A fully typed state shape
 8+- Reusable pieces of state management logic
 9+
10+A schema must be an object. It is composed of slices of state. Slices can
11+represent any data type, however, we recommend keeping it as JSON serializable
12+as possible. Slices not only hold a value, but associated with that value are
13+ways to:
14+
15+- Update the value
16+- Query for data within the value
17+
18+# Built-in slices
19+
20+As a result, the following slices should cover the most common data types and
21+associated logic:
22+
23 - `any`
24 - `loader`
25 - `num`
26@@ -10,9 +28,15 @@ description: Learn more about schamas and slices
27 - `str`
28 - `table`
29 
30+# Schema assumptions
31+
32 `createSchema` requires two slices by default in order for it and everything
33 inside `starfx` to function properly: `cache` and `loader`.
34 
35+Why do we require those slices? Because if we can assume those exist, we can
36+build a lot of useful middleware and supervisors on top of that assumption. It's
37+a place for `starfx` and third-party functionality to hold their state.
38+
39 # Build your own slice
40 
41 We will build a `counter` slice to demonstrate how to build your own slices.
M docs/posts/supervisors.md
+62, -2
 1@@ -3,13 +3,13 @@ title: Supervisors
 2 description: Learn how supervisor tasks work
 3 ---
 4 
 5-[Supplemental reading from erlang](https://www.erlang.org/doc/design_principles/des_princ)
 6-
 7 A supervisor task is a way to monitor children tasks and probably most
 8 importantly, manage their health. By structuring your side-effects and business
 9 logic around supervisor tasks, we gain very interesting coding paradigms that
10 result is easier to read and manage code.
11 
12+[Supplemental reading from erlang](https://www.erlang.org/doc/design_principles/des_princ)
13+
14 The most basic version of a supervisor is simply an infinite loop that calls a
15 child task:
16 
17@@ -89,3 +89,63 @@ dispatch(someAction()); // this tasks lives
18 ```
19 
20 This is the power of supervisors and is fundamental to how `starfx` works.
21+
22+# poll
23+
24+When activated, call a thunk or endpoint once every N millisecond indefinitely
25+until cancelled.
26+
27+```ts
28+import { poll } from "starfx";
29+
30+const fetchUsers = api.get("/users", { supervisor: poll() });
31+store.dispatch(fetchUsers());
32+// fetch users
33+// sleep 5000
34+// fetch users
35+// sleep 5000
36+// fetch users
37+store.dispatch(fetchUsers());
38+// cancelled
39+```
40+
41+The default value provided to `poll()` is **5 seconds**.
42+
43+You can optionally provide a cancel action instead of calling the thunk twice:
44+
45+```ts
46+import { poll } from "starfx";
47+
48+const cancelPoll = createAction("cancel-poll");
49+const fetchUsers = api.get("/users", {
50+  supervisor: poll(5 * 1000, `${cancelPoll}`),
51+});
52+store.dispatch(fetchUsers());
53+// fetch users
54+// sleep 5000
55+// fetch users
56+// sleep 5000
57+// fetch users
58+store.dispatch(cancelPoll());
59+// cancelled
60+```
61+
62+# timer
63+
64+Only call a thunk or endpoint at-most once every N milliseconds.
65+
66+```ts
67+import { timer } from "starfx";
68+
69+const fetchUsers = api.get("/users", { supervisor: timer(1000) });
70+store.dispatch(fetchUsers());
71+store.dispatch(fetchUsers());
72+// sleep(100);
73+store.dispatch(fetchUsers());
74+// sleep(1000);
75+store.dispatch(fetchUsers());
76+// called: 2 times
77+```
78+
79+The default value provided to `timer()` is **5 minutes**. This means you can
80+only call `fetchUsers` at-most once every **5 minutes**.
M query/mdw.ts
+1, -1
1@@ -97,7 +97,7 @@ export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
2  * This middleware is a composition of many middleware used to faciliate
3  * the {@link createApi}.
4  *
5- * It is not required, however,
6+ * It is not required, however, it is battle-tested and highly recommended.
7  */
8 export function api<Ctx extends ApiCtx = ApiCtx>() {
9   return compose<Ctx>([
M supervisor.ts
+1, -1
1@@ -8,7 +8,7 @@ const MS = 1000;
2 const SECONDS = 1 * MS;
3 const MINUTES = 60 * SECONDS;
4 
5-export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
6+export function poll(parentTimer: number = 5 * SECONDS, cancelType?: string) {
7   return function* poller<T>(
8     actionType: string,
9     op: (action: AnyAction) => Operation<T>,