;
42 | }
43 | ```
44 |
45 | `api.cache()` opts into automatic caching. This is really just an alias for:
46 |
47 | ```ts
48 | function*(ctx, next) {
49 | ctx.cache = true;
50 | yield* next();
51 | }
52 | ```
53 |
54 | The state slice for `cache` is simple, every thunk action has
55 | [special properties](/thunks#anatomy-of-an-action) of which one is a `key` field
56 | that is a hash of the entire user-defined action payload:
57 |
58 | ```js
59 | {
60 | [action.payload.key]: {},
61 | }
62 | ```
63 |
64 | # `timer` supervisor
65 |
66 | This supervisor can help us with how often we refetch data. This will help us
67 | call the same endpoint many times but only fetching the data on an interval.
68 |
69 | [Read more about it in Supervisors](/supervisors#timer)
70 |
71 | This, cominbed with [Automatic caching](#automatic) provides us with the
72 | fundamental features built into `react-query`.
73 |
--------------------------------------------------------------------------------
/docs/posts/controllers.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Controllers
3 | description: How controllers work in starfx
4 | ---
5 |
6 | Why do we call this a micro-mvc framework? Well, our controllers are lighter
7 | weight than traditional MVC frameworks.
8 |
9 | Controllers do not relate to pages, they most often relate to centralized pieces
10 | of business logic. This could be as simple as making a single API endpoint and
11 | caching the results or as complex as making multiple dependent API calls and
12 | combinatory logic.
13 |
14 | Not only do have a centralized place for handling complex business logic,
15 | fetching API data, and updating our FE global state, but we also have a robust
16 | middleware system similar to `express` or `koa`!
17 |
18 | In the following sections we will discuss how to create controllers and the
19 | different use cases for them inside `starfx`.
20 |
--------------------------------------------------------------------------------
/docs/posts/dependent.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dependent Queries
3 | slug: dependent-queries
4 | description: How to call other thunks and endpoints within one
5 | ---
6 |
7 | In this context, thunks and endpoints are identical, so I will just talk about
8 | thunks throughout this guide.
9 |
10 | There are two ways to call a thunk within another thunk.
11 |
12 | # Dispatch the thunk as an action
13 |
14 | Features:
15 |
16 | - Non-blocking
17 | - Thunk is still controlled by supervisor
18 | - Works identical to `dispatch(action)`
19 |
20 | ```ts
21 | import { put } from "starfx";
22 | const fetchMailboxes = api.get("/mailboxes");
23 | const fetchMail = thunks.create("fetch-mail", function* (ctx, next) {
24 | yield* put(fetchMailboxes());
25 | });
26 | ```
27 |
28 | This is the equivalent of using `useDispatch` in your view. As a result, it is
29 | also controlled by the thunk's supervisor task. If that thunk has a supervisor
30 | that might drop the middleware stack from activating (e.g. `takeLeading` or
31 | `timer`) then it might not actually get called. Further, this operation
32 | completes immediately, it does **not** wait for the thunk to complete before
33 | moving to the next yield point.
34 |
35 | If you want to make a blocking call to the thunk and wait for it to complete
36 | then you want to call the thunk's middleware stack directly.
37 |
38 | # Call the middleware stack directly
39 |
40 | Features:
41 |
42 | - Blocking
43 | - Middleware stack guarenteed to run
44 | - Does **not** go through supervisor task
45 |
46 | What do we mean by "middleware stack"? That is the stack of functions that you
47 | define for a thunk. It does **not** include the supervisor task that manages the
48 | thunk. Because a supervisor task could drop, pause, or delay the execution of a
49 | thunk, we need a way to escape hatch out of it and just call the middleware
50 | stack directly.
51 |
52 | ```ts
53 | import { parallel, put } from "starfx";
54 | // imaginary schema
55 | import { schema } from "./schema";
56 |
57 | const fetchMailboxes = api.get("/mailboxes");
58 | const fetchMessages = api.get<{ id: string }>("/mailboxes/:id/messages");
59 | const fetchMail = thunks.create("fetch-mail", function* (ctx, next) {
60 | const boxesCtx = yield* fetchMailboxes.run();
61 | if (!boxesCtx.json.ok) {
62 | return;
63 | }
64 |
65 | const boxes = yield* select(schema.mailboxes.selectTableAsList);
66 | const group = yield* parallel(boxes.map((box) => {
67 | return fetchMessages.run({ id: box.id });
68 | }));
69 | const messages = yield* select(schema.messages.selectTableAsList);
70 | console.log(messages);
71 | });
72 | ```
73 |
--------------------------------------------------------------------------------
/docs/posts/design-philosophy.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Design Philosophy
3 | ---
4 |
5 | - user interaction is a side-effect of using a web app
6 | - side-effect management is the central processing unit to manage user
7 | interaction, app features, and state
8 | - leverage structured concurrency to manage side-effects
9 | - leverage supervisor tasks to provide powerful design patterns
10 | - side-effect and state management decoupled from the view
11 | - user has full control over state management (opt-in to automatic data
12 | synchronization)
13 | - state is just a side-effect (of user interaction and app features)
14 |
--------------------------------------------------------------------------------
/docs/posts/dispatch.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dispatch
3 | description: How to activate controllers
4 | ---
5 |
6 | We use the term `dispatch` when we are emitting an event with a specific type
7 | signature
8 | ([flux standard action](https://github.com/redux-utilities/flux-standard-action)).
9 |
10 | There are two ways to activate a thunk: by dispatching an action or calling it
11 | within another thunk.
12 |
13 | The type signature of `dispatch`:
14 |
15 | ```ts
16 | type Dispatch = (a: Action | Action[]) => any;
17 | ```
18 |
19 | Within `starfx`, the `dispatch` function lives on the store.
20 |
21 | ```ts
22 | const { createSchema, createStore } from "starfx";
23 | const [schema, initialState] = createSchema();
24 | const store = createStore({ initialState });
25 |
26 | store.dispatch({ type: "action", payload: {} });
27 | ```
28 |
29 | # Dispatch in thunk
30 |
31 | ```ts
32 | import { put } from "starfx";
33 |
34 | function* thunk(ctx, next) {
35 | yield* put({ type: "click" });
36 | yield* next();
37 | }
38 | ```
39 |
40 | # Dispatch in react
41 |
42 | You can also use dispatch with a `react` hook:
43 |
44 | ```tsx
45 | import { useDispatch } from "starfx/react";
46 |
47 | function App() {
48 | const dispatch = useDispatch();
49 |
50 | return ;
51 | }
52 | ```
53 |
54 | # Listening to actions
55 |
56 | This is a pubsub system after all. How can we listen to action events?
57 |
58 | ```ts
59 | import { take } from "starfx";
60 |
61 | function* watch() {
62 | while (true) {
63 | const action = yield* take("click");
64 | // -or- const action = yield* take("*");
65 | // -or- const action = yield* take((act) => act.type === "click");
66 | // -or- const action = yield* take(["click", "me"]);
67 | console.log(action.payload);
68 | }
69 | }
70 |
71 | store.run(watch);
72 | ```
73 |
74 | `watch` is what we call a [supervisor](/supervisors). Click that link to learn
75 | more about how they provide powerful flow control mechanisms.
76 |
--------------------------------------------------------------------------------
/docs/posts/error-handling.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Error handling
3 | description: How to manage errors
4 | ---
5 |
6 | By leveraging `effection` and [structured concurrency](/structured-concurrency)
7 | we can let it do most of the heavy lifting for managing errors.
8 |
9 | > Read [error handling](https://frontside.com/effection/docs/errors) doc at
10 | > `effection`!
11 |
12 | There are some tools `starfx` provides to make it a little easier.
13 |
14 | By default in `effection`, if a child task raises an exception, it will bubble
15 | up the ancestry and eventually try to kill the root task. Within `starfx`, we
16 | prevent that from happening with [supervisor](/supervisors) tasks. Having said
17 | that, child tasks can also control how children tasks are managed. Sometimes you
18 | want to kill the child task tree, sometimes you want to recover and restart, and
19 | sometimes you want to bubble the error up the task ancestry.
20 |
21 | If you want to capture a task and prevent it from bubbling an exception up, then
22 | you have two `fx`: `call` and `safe`.
23 |
24 | ```ts
25 | import { call, run, safe } from "starfx";
26 |
27 | function* main() {
28 | try {
29 | // use `call` to enable JS try/catch
30 | yield* call(fetch("api.com"));
31 | } catch (err) {
32 | console.error(err);
33 | }
34 |
35 | // -or- if you don't want to use try/catch
36 | const result = yield* safe(fetch("api.com"));
37 | if (!result.ok) {
38 | console.error(result.err);
39 | }
40 | }
41 |
42 | await run(main);
43 | ```
44 |
45 | Both functions will catch the child task and prevent it from bubbling up the
46 | error.
47 |
--------------------------------------------------------------------------------
/docs/posts/fx.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: fx
3 | description: Utilities to handle complex async flow control
4 | ---
5 |
6 | `fx` (pronounced Effects) are helper functions to make async flow control
7 | easier.
8 |
9 | # parallel
10 |
11 | The goal of `parallel` is to make it easier to cooridnate multiple async
12 | operations in parallel, with different ways to receive completed tasks.
13 |
14 | All tasks are called with `fx.safe` which means they will never throw an
15 | exception. Instead all tasks will return a Result object that the end
16 | development must evaluate in order to grab the value.
17 |
18 | ```ts
19 | import { parallel } from "starfx";
20 |
21 | function* run() {
22 | const task = yield* parallel([job1, job2]);
23 | // wait for all tasks to complete before moving to next yield point
24 | const results = yield* task;
25 | // job1 = results[0];
26 | // job2 = results[1];
27 | }
28 | ```
29 |
30 | Instead of waiting for all tasks to complete, we can instead loop over tasks as
31 | they arrive:
32 |
33 | ```ts
34 | function* run() {
35 | const task = yield* parallel([job1, job2]);
36 | for (const job of yield* each(task.immediate)) {
37 | // job2 completes first then it will be first in list
38 | console.log(job);
39 | yield* each.next();
40 | }
41 | }
42 | ```
43 |
44 | Or we can instead loop over tasks in order of the array provided to parallel:
45 |
46 | ```ts
47 | function* run() {
48 | const task = yield* parallel([job1, job2]);
49 | for (const job of yield* each(task.sequence)) {
50 | // job1 then job2 will be returned regardless of when the jobs
51 | // complete
52 | console.log(job);
53 | yield* each.next();
54 | }
55 | }
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/posts/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: Use starfx with deno, node, or the browser
4 | toc: 1
5 | ---
6 |
7 | # Motivation
8 |
9 | We think we need a react framework and server-side rendering (SSR) because
10 | that's where money is being made. If we are building a highly dynamic and
11 | interactive web application then we probably don't need SSR. These frameworks
12 | sell us that they are an easier way to build web apps, but that's not strictly
13 | true. Just think of it this way: if we can build a web app using **only** static
14 | assets, isn't that simpler than having static assets **and** a react framework
15 | server?
16 |
17 | React hook-based fetching and caching libraries dramatically simplify data
18 | synchronization but are so tightly coupled to a component's life cycle that it
19 | creates waterfall fetches and loading spinners everywhere. We also have the
20 | downside of not being able to normalize our cache which means we have to spend
21 | time thinking about how and when to invalidate our cache.
22 |
23 | Further, all of these data caching libraries don't handle data normalization. In
24 | many similar libraries we are going to see a line like: "Data normalization is
25 | hard and it isn't worth it." Their libraries are not built with data
26 | normalization in mind so they claim it's an anti-feature. Why do we want to
27 | normalize data in the backend but not the frontend? Data normalization is
28 | critically important because it makes CRUD operations automatically update our
29 | web app without having to invalidate our cache.
30 |
31 | So what if we are building a highly interactive web app that doesn't need SEO
32 | and we also need more control over data synchronization and caching?
33 |
34 | Are you frustrated by the following issues in your react app?
35 |
36 | - Prop drilling
37 | - Waterfall fetching data
38 | - Loading spinners everywhere
39 | - Extraneous network calls
40 | - Business logic tightly coupled to react component lifecycle hooks
41 | - State management boilerplate
42 | - Lack of state management
43 | - Lack of async flow control tooling
44 |
45 | We built `starfx` because we looked at the web app landscape and felt like there
46 | was something missing.
47 |
48 | The benefits of using this library:
49 |
50 | - The missing model and controller (MC) in react (V)
51 | - Designed for single-page applications (SPAs)
52 | - Makes data normalization easy and straightforward
53 | - Tools to preload and refresh data
54 | - Has a powerful middleware system similar to `express` to handle requests and
55 | responses
56 | - Reduces state management boilerplate to its absolute essentials
57 | - Has a robust side-effect management system using structured concurrency
58 | - Has data synchronization and caching separated from `react`
59 |
60 | # When to use this library?
61 |
62 | The primary target for this library are SPAs. This is for an app that might be
63 | hosted inside an object store (like s3) or with a simple web server (like nginx)
64 | that serves files and that's it.
65 |
66 | Is your app highly interactive, requiring it to persist data across pages? This
67 | is the sweet spot for `starfx`.
68 |
69 | This library is **not** a great fit for ecommerce, tiny projects, or blogs. This
70 | is for web apps that are generally behind a login screen that require a
71 | desktop-class user experience. This library is designed to scale, so it might
72 | feel a little overwhelming. Just know if you use this library, your code will be
73 | easier to read, easier to write, easier to refactor, all while handling a
74 | massive amount of business complexity.
75 |
76 | # Code
77 |
78 | Here we demonstrate a complete example so you can glimpse at how `starfx` works.
79 | In this example, we will fetch a github repo from an API endpoint, cache the
80 | `Response` json, and then ensure the endpoint only gets called at-most once
81 | every **5 minutes**, mimicking the basic features of `react-query`.
82 |
83 | [Codesanbox](https://codesandbox.io/p/sandbox/starfx-simplest-dgqc9v?file=%2Fsrc%2Findex.tsx)
84 |
85 | ```tsx
86 | import { createApi, createSchema, createStore, mdw, timer } from "starfx";
87 | import { Provider, useCache } from "starfx/react";
88 |
89 | const [schema, initialState] = createSchema();
90 | const store = createStore({ initialState });
91 |
92 | const api = createApi();
93 | // mdw = middleware
94 | api.use(mdw.api({ schema }));
95 | // where `fetchRepo` will be placed inside the middleware stack
96 | api.use(api.routes());
97 | api.use(mdw.fetch({ baseUrl: "https://api.github.com" }));
98 |
99 | const fetchRepo = api.get(
100 | "/repos/neurosnap/starfx",
101 | { supervisor: timer() },
102 | api.cache(),
103 | );
104 |
105 | store.run(api.register);
106 |
107 | function App() {
108 | return (
109 |
110 |
111 |
112 | );
113 | }
114 |
115 | function Example() {
116 | const { isInitialLoading, isError, message, data } = useCache(fetchRepo());
117 | if (isInitialLoading) return "Loading ...";
118 | if (isError) return `An error has occurred: ${message}`;
119 |
120 | return (
121 |
128 | );
129 | }
130 | ```
131 |
132 | # Install
133 |
134 | ```bash
135 | npm install starfx
136 | ```
137 |
138 | ```bash
139 | yarn add starfx
140 | ```
141 |
142 | ```ts
143 | import * as starfx from "https://deno.land/x/starfx@0.13.2/mod.ts";
144 | ```
145 |
146 | # Effection
147 |
148 | This library leverages structured concurrency using
149 | [`effection`](https://frontside.com/effection). It is highly recommended that
150 | you have a brief understanding of how its API because it is used heavily within
151 | `starfx`.
152 |
--------------------------------------------------------------------------------
/docs/posts/home.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: starfx
3 | description: A micro-mvc framework for react apps
4 | slug: index
5 | template: home.page.tmpl
6 | ---
7 |
--------------------------------------------------------------------------------
/docs/posts/learn.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Learn
3 | description: Fundamental concepts in starfx
4 | ---
5 |
6 | # How does `starfx` work?
7 |
8 | `starfx` is a companion framework to `react` that understands how to listen to
9 | user events (e.g. clicks, form inputs, etc.), activate side-effects (e.g. fetch
10 | api data, submit form data, update state), and then intelligently update the
11 | view. If you are familiar with **MVC**:
12 |
13 | - `react` is the **View** layer
14 | - `starfx` are the **Model** and **Controller** layers
15 |
16 | The high-level picture of `starfx` is _essentially_ a glorified pubsub system:
17 |
18 | - The user goes to your app
19 | - The view is generated with `react`
20 | - When a user interacts with your web app, events gets dispatched
21 | - `starfx` listens for events and triggers side-effects (e.g. fetches API data,
22 | updates state, etc.)
23 | - An entirely new version of the state gets created
24 | - `react` surgically updates the view based on changes to the `starfx` state
25 | - Rinse and repeat
26 |
27 | It all happens as a single unidirectional loop.
28 |
29 | # How is `starfx` different?
30 |
31 | `starfx` is different in a number of ways.
32 |
33 | We combine both state and side-effect management into a single cohesive unit.
34 | This streamlines the implementation of your web app.
35 |
36 | Our business logic does not live inside of `react`, rather, it lives inside of
37 | the side-effect system. We are not shackled by `react` lifecycle hooks, in fact,
38 | `starfx` has virtually no concept of `react` at all -- except for a couple of
39 | hooks. The entire system is designed, from the ground up, to not need `react` at
40 | all in order to function. At the end of the day, `starfx` works by subscribing
41 | to and publishing events. Those events could come from `react`, but they could
42 | also come from anywhere.
43 |
44 | We have taken the best part about `express` and `koa` and applied it to fetching
45 | API data on the front-end. What this means is that we have a powerful middleware
46 | system that we can leverage on the front-end.
47 |
48 | We built a state management system leveraging the concept of a database schema.
49 | We took inspiration from [zod](https://zod.dev) to build an ergonomic and
50 | powerful state system leveraging reusable slice helpers. With our schema and
51 | custom built store, we can replace all of boilerplate with a single function
52 | call `createSchema()`.
53 |
54 | # Why does `starfx` use [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator)?
55 |
56 | Generators give us -- the library authors -- more control over how side-effects
57 | are handled within a javascript runtime environment. There are things that we
58 | can do with generators that are just not possible using `async`/`await`. To
59 | provide some specific examples, we need the ability to manage async operations
60 | as a tree of tasks. We need the ability to have
61 | [structured concurrency](https://en.wikipedia.org/wiki/Structured_concurrency)
62 | in order to granularly manipulate, manage, spawn, and teardown tasks.
63 |
64 | Furthermore, `async`/`await` is implemented using generator functions. In
65 | `starfx`, not everything we want to `await` is a `Promise`!
66 |
67 | There is so much more to why generators are awesome but at the end of the day,
68 | to the end developer, you can treat generators the same as `async`/`await`.
69 |
70 | If you are struggling to understand or are getting confused using generator
71 | functions, just use the
72 | [effection async rosetta stone](https://frontside.com/effection/docs/async-rosetta-stone).
73 |
74 | We highly recommend reading the
75 | [Thinking in Effection](https://frontside.com/effection/docs/thinking-in-effection)
76 | page because it should help here.
77 |
78 | # Data strategy: preload then refresh
79 |
80 | The idea is simple:
81 |
82 | > Preload most of your API data in the background and refresh it as the user
83 | > interacts with your web app.
84 |
85 | This strategy removes the need to show loaders throughout your app.
86 |
87 | Preloading is a first-class citizen in `starfx`. It is the primary use case for
88 | using it.
89 |
90 | This is the biggest performance boost to using a single-page app. Since routing
91 | happens all client-side, it's beneficial to first download data in the
92 | background while the user navigates through your web app. While you might be
93 | fetching slow API endpoints, it feels instantaneous because the data was already
94 | loaded before a pager needed to display it.
95 |
96 | When the user lands on your web app, initialize a preload thunk that will sync
97 | the user's database locally, then when they navigate to a page that requires
98 | data, refresh that data as needed.
99 |
100 | For example, let's say the root page `/` requires a list of users while the
101 | `/mailboxes` page requires a list of mailboxes.
102 |
103 | On the root page you would fetch the list of users as well as the lists of
104 | mailboxes. When the user finally decides to click on the "Mailboxes" page, the
105 | page will act as if the data was loaded instantly because it was preloaded. So
106 | the user sees the data immediately, while at the same time you would also
107 | re-fetch the mailboxes.
108 |
--------------------------------------------------------------------------------
/docs/posts/loaders.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Loaders
3 | slug: loaders
4 | description: What are loaders?
5 | ---
6 |
7 | Loaders are general purpose "status trackers." They track the status of a thunk,
8 | an endpoint, or a composite of them. One of the big benefits of decoupled
9 | loaders is you can create as many as you want, and control them however you
10 | want.
11 |
12 | [Read my blog article about it](https://bower.sh/on-decoupled-loaders)
13 |
14 | # Usage
15 |
16 | For endpoints, loaders are installed automatically and track fetch requests.
17 | Loader success is determined by `Response.ok` or if `fetch` throws an error.
18 |
19 | You can also use loaders manually:
20 |
21 | ```ts
22 | import { put } from "starfx";
23 | // imaginary schema
24 | import { schema } from "./schema";
25 |
26 | function* fn() {
27 | yield* put(schema.loaders.start({ id: "my-id" }));
28 | yield* put(schema.loaders.success({ id: "my-id" }));
29 | yield* put(schema.loaders.error({ id: "my-id", message: "boom!" }));
30 | }
31 | ```
32 |
33 | For thunks you can use `mdw.loader()` which will track the status of a thunk.
34 |
35 | ```ts
36 | import { createThunks, mdw } from "starfx";
37 | // imaginary schema
38 | import { initialState, schema } from "./schema";
39 |
40 | const thunks = createThunks();
41 | thunks.use(mdw.loader(schema));
42 | thunks.use(thunks.routes());
43 |
44 | const go = thunks.create("go", function* (ctx, next) {
45 | throw new Error("boom!");
46 | });
47 |
48 | const store = createStore({ initialState });
49 | store.dispatch(go());
50 | schema.loaders.selectById(store.getState(), { id: `${go}` });
51 | // status = "error"; message = "boom!"
52 | ```
53 |
54 | # Shape
55 |
56 | ```ts
57 | export type IdProp = string | number;
58 | export type LoadingStatus = "loading" | "success" | "error" | "idle";
59 | export interface LoaderItemState<
60 | M extends Record = Record,
61 | > {
62 | id: string;
63 | status: LoadingStatus;
64 | message: string;
65 | lastRun: number;
66 | lastSuccess: number;
67 | meta: M;
68 | }
69 |
70 | export interface LoaderState<
71 | M extends AnyState = AnyState,
72 | > extends LoaderItemState {
73 | isIdle: boolean;
74 | isLoading: boolean;
75 | isError: boolean;
76 | isSuccess: boolean;
77 | isInitialLoading: boolean;
78 | }
79 | ```
80 |
81 | # `isLoading` vs `isInitialLoading`
82 |
83 | Why does this distinction exist? Well, when building a web app with `starfx`,
84 | it's very common to have called the same endpoint multiple times. If that loader
85 | has already successfully been called previously, `isInitialLoading` will **not**
86 | flip states.
87 |
88 | The primary use case is: why show a loader if we can already show the user data?
89 |
90 | Conversely, `isLoading` will always be true when a loader is in "loading" state.
91 |
92 | This information is derived from `lastRun` and `lastSuccess`. Those are unix
93 | timestamps of the last "loading" loader and the last time it was in "success"
94 | state, respectively.
95 |
96 | # The `meta` property
97 |
98 | You can put whatever you want in there. This is a useful field when you want to
99 | pass structured data from a thunk into the view on success or failure. Maybe
100 | this is the new `id` for the entity you just created and the view needs to know
101 | it. The `meta` prop is where you would put contextual information beyond the
102 | `message` string.
103 |
104 | Here's an example for how you can update the `meta` property inside an endpoint:
105 |
106 | ```tsx
107 | const fetchUsers = api.get("/users", function* (ctx, next) {
108 | yield* next();
109 | if (!ctx.json.ok) return;
110 | // this will merge with the default success loader state
111 | // so you don't have to set the `status` here as it is done automatically
112 | // with the api middleware
113 | ctx.loader = { meta: { total: ctx.json.value.length } };
114 | });
115 |
116 | function App() {
117 | const loader = useQuery(fetchUsers());
118 | if (loader.isInitialLoading) return
loading ...
;
119 | if (loader.isError) return
error: {loader.message}
;
120 | return
Total number of users: {loader.meta.total}
;
121 | }
122 | ```
123 |
--------------------------------------------------------------------------------
/docs/posts/mdw.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Middleware
3 | slug: middleware
4 | description: The structure of a middleware function
5 | ---
6 |
7 | Here is the most basic middleware (mdw) function in `starfx`:
8 |
9 | ```ts
10 | function* (ctx, next) {
11 | yield* next();
12 | }
13 | ```
14 |
15 | Thunks and endpoints are just thin wrappers around a mdw stack:
16 |
17 | For example, the recommended mdw stack for `createApi()` looks like this:
18 |
19 | ```ts
20 | import { createApi, mdw } from "starfx";
21 | import { schema } from "./schema";
22 |
23 | // this api:
24 | const api = createApi();
25 | api.use(mdw.api({ schema }));
26 | api.use(api.routes());
27 | api.use(mdw.fetch({ baseUrl: "https://api.com" }));
28 |
29 | // looks like this:
30 | [
31 | mdw.err,
32 | mdw.queryCtx,
33 | mdw.customKey,
34 | mdw.nameParser,
35 | mdw.actions,
36 | mdw.loaderApi({ schema }),
37 | mdw.cache({ schema }),
38 | api.routes(),
39 | mdw.composeUrl("https://api.com"),
40 | mdw.payload,
41 | mdw.request,
42 | mdw.json,
43 | ];
44 | ```
45 |
46 | When a mdw function calls `yield* next()`, all it does it call the next mdw in
47 | the stack. When that yield point resolves, it means all the mdw functions after
48 | it have been called. This doesn't necessarily mean all mdw in the stack will be
49 | called, because like `koa`, you can return early inside a mdw function,
50 | essentially cancelling all subsequent mdw.
51 |
52 | # Context
53 |
54 | The context object is just a plain javascript object that gets passed to every
55 | mdw. The type of `ctx` depends ... on the context. But for thunks, we have this
56 | basic structure:
57 |
58 | ```ts
59 | interface Payload
{
64 | name: string;
65 | key: string;
66 | action: ActionWithPayload>;
67 | actionFn: IfAny<
68 | P,
69 | CreateAction,
70 | CreateActionWithPayload, P>
71 | >;
72 | result: Result;
73 | }
74 | ```
75 |
76 | There are **three** very important properties that you should know about:
77 |
78 | - `name` - the name you provided when creating the thunk
79 | - `payload` - the arbitrary data you passed into the thunk
80 | - `key` - a hash of `name` and `payload`
81 |
--------------------------------------------------------------------------------
/docs/posts/models.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Models
3 | description: State management in starfx
4 | ---
5 |
6 | One core component of an MVC framework is the Model.
7 |
8 | Since data normalization is a first-class citizen inside `starfx`, we built a
9 | custom, reactive database for front-end web apps. Like a backend MVC framework,
10 | we want to think of managing the FE store like managing a database. So while
11 | thinking about models as separate entities, you create all your models by
12 | creating a single schema.
13 |
14 | Managing models in `starfx` leverages two primary concepts: schema and store.
15 |
16 | The store is a single, global, and reactive object that was built to make
17 | updating views easy. It is essentially an event emitter with a javascript object
18 | that is updated in a very particular way (via `schema.update`).
19 |
20 | Because the goal of this library is to create scalable web apps, we want users
21 | to create all their models at the same time inside a single schema.
22 |
--------------------------------------------------------------------------------
/docs/posts/react.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: React
3 | description: How to integrate with React
4 | toc: 2
5 | ---
6 |
7 | Even though we are **not** using `redux`, if you are familiar with
8 | [react-redux](https://react-redux.js.org) then this will be an identical
9 | experience because `starfx/react` has an identical API signature.
10 |
11 | `useDispatch`, `useSelector`, and `createSelector` are the bread and butter of
12 | `redux`'s integration with `react` all of which we use inside `starfx`.
13 |
14 | ```tsx
15 | import {
16 | TypedUseSelectorHook,
17 | useApi,
18 | useSelector as useBaseSelector,
19 | } from "starfx/react";
20 | import { schema, WebState } from "./store.ts";
21 | import { fetchUsers } from "./api.ts";
22 |
23 | const useSelector: TypedUseSelectorHook = useBaseSelector;
24 |
25 | function App() {
26 | const users = useSelector(schema.users.selectTableAsList);
27 | const api = useApi(fetchUsers());
28 |
29 | return (
30 |
31 | {users.map((u) =>
{u.name}
)}
32 |
33 |
34 | {api.isLoading ?
Loading ...
: null}
35 |
36 |
37 | );
38 | }
39 | ```
40 |
41 | # Hooks
42 |
43 | ## `useSelector`
44 |
45 | Query your store with this hook.
46 |
47 | ```tsx
48 | import { useSelector } from "starfx";
49 |
50 | function App() {
51 | const data = useSelector((state) => state.data);
52 | return
{data}
;
53 | }
54 | ```
55 |
56 | [See `react-redux` docs](https://react-redux.js.org/api/hooks#useselector)
57 |
58 | ## `useDispatch`
59 |
60 | Call thunks and endpoints with this hook.
61 |
62 | ```tsx
63 | import { useDispatch } from "starfx";
64 |
65 | function App() {
66 | const dispatch = useDispatch();
67 |
68 | return (
69 |
72 | );
73 | }
74 | ```
75 |
76 | [See `react-redux` docs](https://react-redux.js.org/api/hooks#usedispatch)
77 |
78 | ## `useLoader`
79 |
80 | Will accept an action creator or action and return the loader associated with
81 | it.
82 |
83 | ```tsx
84 | import { useLoader } from "starfx/react";
85 |
86 | const log = thunks.create("log");
87 |
88 | function App() {
89 | // this will grab loader for any `log` thunks dispatched
90 | // `action.payload.name`
91 | const loaderAny = useLoader(log);
92 | // this will grab loader a specific `log` thunk dispatched
93 | // `action.payload.key`
94 | const loader = useLoader(log("specific thunk"));
95 | }
96 | ```
97 |
98 | ## `useApi`
99 |
100 | Will take an action creator or action itself and fetch the associated loader and
101 | create a `trigger` function that you can call later in your react component.
102 |
103 | This hook will _not_ fetch the data for you because it does not know how to
104 | fetch data from your redux state.
105 |
106 | ```ts
107 | import { useApi } from 'starfx/react';
108 |
109 | import { api } from './api';
110 |
111 | const fetchUsers = api.get('/users', function*() {
112 | // ...
113 | });
114 |
115 | const View = () => {
116 | const { isLoading, trigger } = useApi(fetchUsers);
117 | useEffect(() => {
118 | trigger();
119 | }, []);
120 | return
140 | }
141 | ```
142 |
143 | ## `useCache`
144 |
145 | Uses [useQuery](#usequery) and automatically selects the cached data associated
146 | with the action creator or action provided.
147 |
148 | ```ts
149 | import { useCache } from 'starfx/react';
150 |
151 | import { api } from './api';
152 |
153 | const fetchUsers = api.get('/users', api.cache());
154 |
155 | const View = () => {
156 | const { isLoading, data } = useCache(fetchUsers());
157 | return
{isLoading ? : 'Loading' : data.length}
158 | }
159 | ```
160 |
161 | ## `useLoaderSuccess`
162 |
163 | Will activate the callback provided when the loader transitions from some state
164 | to success.
165 |
166 | ```ts
167 | import { useApi, useLoaderSuccess } from "starfx/react";
168 |
169 | import { api } from "./api";
170 |
171 | const createUser = api.post("/users", function* (ctx, next) {
172 | // ...
173 | });
174 |
175 | const View = () => {
176 | const { loader, trigger } = useApi(createUser);
177 | const onSubmit = () => {
178 | trigger({ name: "bob" });
179 | };
180 |
181 | useLoaderSuccess(loader, () => {
182 | // success!
183 | // Use this callback to navigate to another view
184 | });
185 |
186 | return ;
187 | };
188 | ```
189 |
--------------------------------------------------------------------------------
/docs/posts/resources.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Resources
3 | description: Some useful links to learn more
4 | ---
5 |
6 | # Quick links
7 |
8 | - [API Docs](https://deno.land/x/starfx@v0.13.2/mod.ts)
9 | - [blog posts about starfx](https://bower.sh/?tag=starfx)
10 | - [examples repo](https://github.com/neurosnap/starfx-examples)
11 | - [production example repo](https://github.com/aptible/app-ui)
12 |
13 | # Talks
14 |
15 | I recently gave a talk about delimited continuations where I also discuss this
16 | library:
17 |
18 |
19 |
20 | > [Delimited continuations are all you need](https://youtu.be/uRbqLGj_6mI?si=Mok0J8Wp0Z-ahFrN)
21 |
22 | Here is another talk I helped facilitate about `effection` with the library
23 | creator:
24 |
25 |
26 |
27 | > [effection with Charles Lowell](https://youtu.be/lJDgpxRw5WA?si=cCHZiKqNO7vIUhPc)
28 |
29 | Here is a deep-dive on how we are using `starfx` in a production App at Aptible:
30 |
31 |
32 |
33 | > [app-ui deep dive](https://youtu.be/3M5VJuIi5fk)
34 |
35 | # Other notable libraries
36 |
37 | This library is not possible without these foundational libraries:
38 |
39 | - [continuation](https://github.com/thefrontside/continuation)
40 | - [effection v3](https://github.com/thefrontside/effection/tree/v3)
41 |
--------------------------------------------------------------------------------
/docs/posts/selectors.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Selectors
3 | description: Deriving data with selectors
4 | ---
5 |
6 | In a typical web app, the logic for deriving data is usually written as
7 | functions we call selectors.
8 |
9 | The basic function signature of a selector:
10 |
11 | ```ts
12 | const selectData = (state: WebState) => state.data;
13 | ```
14 |
15 | Selectors are primarily used to encapsulate logic for looking up specific values
16 | from state, logic for actually deriving values, and improving performance by
17 | avoiding unnecessary recalculations.
18 |
19 | To learn more, redux has excellent docs
20 | [on deriving data with selectors](https://redux.js.org/usage/deriving-data-selectors).
21 |
22 | There is 100% knowledge transfer between selectors in `starfx` and `redux`
23 | because we adhere to the same function signature.
24 |
25 | The only difference is that as part of our API we re-export
26 | [reselect.createSelector](https://reselect.js.org/api/createselector/), which
27 | will memoize functions:
28 |
29 | ```ts
30 | import { createSelector } from "starfx";
31 |
32 | const selectData = (state) => state.data;
33 | const myselector = createSelector(
34 | selectData,
35 | (data) => data.sort((a, b) => a.id - b.id);
36 | );
37 | ```
38 |
39 | Function memoization is just a way to cache a function call. If the dependencies
40 | (e.g. the result of `selectData`) don't change, then `myselector` will not be
41 | called: it will return its previous value.
42 |
--------------------------------------------------------------------------------
/docs/posts/sitemap.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: sitemap
3 | description: starfx doc sitemap
4 | slug: sitemap
5 | template: sitemap.page.tmpl
6 | ---
7 |
--------------------------------------------------------------------------------
/docs/posts/store.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Store
3 | description: An immutable store that acts like a reactive, in-memory database
4 | ---
5 |
6 | Features:
7 |
8 | - A single, global javascript object
9 | - Reactive
10 | - Normalized
11 | - Acts like a database
12 |
13 | We love `redux`. We know it gets sniped for having too much boilerplate when
14 | alternatives like `zustand` and `react-query` exist that cut through the
15 | ceremony of managing state. However, `redux` was never designed to be easy to
16 | use; it was designed to be scalable, debuggable, and maintainable. Yes, setting
17 | up a `redux` store is work, but that is in an effort to serve its
18 | maintainability.
19 |
20 | Having said that, the core abstraction in `redux` is a reducer. Reducers were
21 | originally designed to contain isolated business logic for updating sections of
22 | state (also known as state slices). They were also designed to make it easier to
23 | sustain state immutability.
24 |
25 | Fast forward to `redux-toolkit` and we have `createSlice` which leverages
26 | `immer` under-the-hood to ensure immutability. So we no longer need reducers for
27 | immutability.
28 |
29 | Further, we argue, placing the business logic for updating state inside reducers
30 | (via switch-cases) makes understanding business logic harder. Instead of having
31 | a single function that updates X state slices, we have X functions (reducers)
32 | that we need to piece together in our heads to understand what is being updated
33 | when an action is dispatched.
34 |
35 | With all of this in mind, `starfx` takes all the good parts of `redux` and
36 | removes the need for reducers entirely. We still have a single state object that
37 | contains everything from API data, UX, and a way to create memoized functions
38 | (e.g. [selectors](/selectors)). We maintain immutability (using
39 | [immer](https://github.com/immerjs/immer)) and also have a middleware system to
40 | extend it.
41 |
42 | Finally, we bring the utility of creating a schema (like [zod](https://zod.dev)
43 | or a traditional database) to make it plainly obvious what the state shape looks
44 | like as well as reusable utilities to make it easy to update and query state.
45 |
46 | This gets us closer to treating our store like a traditional database while
47 | still being flexible for our needs on the FE.
48 |
49 | ```ts
50 | import { createSchema, createStore, select, slice } from "starfx";
51 |
52 | interface User {
53 | id: string;
54 | name: string;
55 | }
56 |
57 | // app-wide database for ui, api data, or anything that needs reactivity
58 | const [schema, initialState] = createSchema({
59 | cache: slice.table(),
60 | loaders: slice.loaders(),
61 | users: slice.table(),
62 | });
63 | type WebState = typeof initialState;
64 |
65 | // just a normal endpoint
66 | const fetchUsers = api.get(
67 | "/users",
68 | function* (ctx, next) {
69 | // make the http request
70 | yield* next();
71 |
72 | // ctx.json is a Result type that either contains the http response
73 | // json data or an error
74 | if (!ctx.json.ok) {
75 | return;
76 | }
77 |
78 | const { value } = ctx.json;
79 | const users = value.reduce>((acc, user) => {
80 | acc[user.id] = user;
81 | return acc;
82 | }, {});
83 |
84 | // update the store and trigger a re-render in react
85 | yield* schema.update(schema.users.add(users));
86 |
87 | // User[]
88 | const users = yield* select(schema.users.selectTableAsList);
89 | // User
90 | const user = yield* select(
91 | (state) => schema.users.selectById(state, { id: "1" }),
92 | );
93 | },
94 | );
95 |
96 | const store = createStore(schema);
97 | store.run(api.register);
98 | store.dispatch(fetchUsers());
99 | ```
100 |
101 | # How to update state
102 |
103 | There are **three** ways to update state, each with varying degrees of type
104 | safety:
105 |
106 | ```ts
107 | import { updateStore } from "starfx";
108 |
109 | function*() {
110 | // good types
111 | yield* schema.update([/* ... */]);
112 | // no types
113 | yield* updateStore([/* ... */]);
114 | }
115 |
116 | store.run(function*() {
117 | // no types
118 | yield* store.update([/* ... */]);
119 | });
120 | ```
121 |
122 | `schema.update` has the highest type safety because it knows your state shape.
123 | The other methods are more generic and the user will have to provide types to
124 | them manually.
125 |
126 | # Updater function
127 |
128 | `schema.update` expects one or many state updater functions. An updater function
129 | receives the state as a function parameter. Any mutations to the `state`
130 | parameter will be applied to the app's state using
131 | [immer](https://github.com/immerjs/immer).
132 |
133 | ```ts
134 | type StoreUpdater = (s: S) => S | void;
135 | ```
136 |
137 | > It is highly recommended you read immer's doc on
138 | > [update patterns](https://immerjs.github.io/immer/update-patterns) because
139 | > there are limitations to understand.
140 |
141 | Here's a simple updater function that increments a counter:
142 |
143 | ```ts
144 | function* inc() {
145 | yield* schema.update((state) => {
146 | state.counter += 1;
147 | });
148 | }
149 | ```
150 |
151 | Since the `update` function accepts an array, it's important to know that we
152 | just run those functions by iterating through that array.
153 |
154 | In fact, our store's core state management can _essentially_ be reduced to this:
155 |
156 | ```ts
157 | import { produce } from "immer";
158 |
159 | function createStore(initialState = {}) {
160 | let state = initialState;
161 |
162 | function update(updaters) {
163 | const nextState = produce(state, (draft) => {
164 | updaters.forEach((updater) => updater(draft));
165 | });
166 | state = nextState;
167 | }
168 |
169 | return {
170 | getState: () => state,
171 | update,
172 | };
173 | }
174 | ```
175 |
176 | # Updating state from view
177 |
178 | You cannot directly update state from the view, users can only manipulate state
179 | from a thunk, endpoint, or a delimited continuation.
180 |
181 | This is a design decision that forces everything to route through our
182 | [controllers](/controllers).
183 |
184 | However, it is very easy to create a controller to do simple tasks like updating
185 | state:
186 |
187 | ```ts
188 | import type { StoreUpdater } from "starfx";
189 |
190 | const updater = thunks.create("update", function* (ctx, next) {
191 | yield* updateStore(ctx.payload);
192 | yield* next();
193 | });
194 |
195 | store.dispatch(
196 | updater([
197 | schema.users.add({ [user1.id]: user }),
198 | ]),
199 | );
200 | ```
201 |
--------------------------------------------------------------------------------
/docs/posts/structured-concurrency.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Structured Concurrency
3 | description: What is structured concurrency?
4 | ---
5 |
6 | Resources:
7 |
8 | - [wiki](https://en.wikipedia.org/wiki/Structured_concurrency)
9 | - [await event horizon](https://frontside.com/blog/2023-12-11-await-event-horizon/)
10 | - [Why structured concurrency?](https://bower.sh/why-structured-concurrency)
11 | - [Thinking in Effection](https://frontside.com/effection/docs/thinking-in-effection)
12 | - [Delimited continuation](https://en.wikipedia.org/wiki/Delimited_continuation)
13 | - [Structured Concurrency](https://ericniebler.com/2020/11/08/structured-concurrency/)
14 | - [Structured Concurrency explained](https://www.thedevtavern.com/blog/posts/structured-concurrency-explained/)
15 | - [conc](https://github.com/sourcegraph/conc)
16 |
17 | This is a broad term so I'll make this specific to how `starfx` works.
18 |
19 | Under-the-hood, thunks and endpoints are registered under the root task. Every
20 | thunk and endpoint has their own supervisor that manages them. As a result, what
21 | we have is a single root task for your entire app that is being managed by
22 | supervisor tasks. When the root task receives a signal to shutdown itself (e.g.
23 | `task.halt()` or closing browser tab) it first must shutdown all children tasks
24 | before being resolved.
25 |
26 | When a child task throws an exception (whether intentional or otherwise) it will
27 | propagate that error up the task tree until it is caught or reaches the root
28 | task.
29 |
30 | In review:
31 |
32 | - There is a single root task for an app
33 | - The root task can spawn child tasks
34 | - If root task is halted then all child tasks are halted first
35 | - If a child task is halted or raises exception, it propagates error up the task
36 | tree
37 | - An exception can be caught (e.g. `try`/`catch`) at any point in the task tree
38 |
--------------------------------------------------------------------------------
/docs/posts/supervisors.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Supervisors
3 | description: Learn how supervisor tasks work
4 | ---
5 |
6 | A supervisor task is a way to monitor children tasks and manage their health. By
7 | structuring your side-effects and business logic around supervisor tasks, we
8 | gain interesting coding paradigms that result in easier to read and manage code.
9 |
10 | [Supplemental reading from erlang](https://www.erlang.org/doc/design_principles/des_princ)
11 |
12 | The most basic version of a supervisor is simply an infinite loop that calls a
13 | child task:
14 |
15 | ```ts
16 | import { call } from "starfx";
17 |
18 | function* supervisor() {
19 | while (true) {
20 | try {
21 | yield* call(someTask);
22 | } catch (err) {
23 | console.error(err);
24 | }
25 | }
26 | }
27 |
28 | function* someTask() {
29 | yield* sleep(10 * 1000);
30 | throw new Error("boom!");
31 | }
32 | ```
33 |
34 | Here we `call` some task that should always be in a running and healthy state.
35 | If it raises an exception, we log it and try to run the task again.
36 |
37 | Building on top of that simple supervisor, we can have tasks that always listen
38 | for events and if they fail, restart them.
39 |
40 | ```ts
41 | import { parallel, run, take } from "starfx";
42 |
43 | function* watchFetch() {
44 | while (true) {
45 | const action = yield* take("FETCH_USERS");
46 | console.log(action);
47 | }
48 | }
49 |
50 | function* send() {
51 | yield* put({ type: "FETCH_USERS" });
52 | yield* put({ type: "FETCH_USERS" });
53 | yield* put({ type: "FETCH_USERS" });
54 | }
55 |
56 | await run(
57 | parallel([watchFetch, send]),
58 | );
59 | ```
60 |
61 | Here we create a supervisor function using a helper `take` to call a function
62 | for every `FETCH_USERS` event emitted.
63 |
64 | While inside a `while` loop, you get full access to its powerful flow control.
65 | Another example, let's say we we only want to respond to a login action when the
66 | user isn't logged in and conversely only listen to a logout action when the user
67 | is logged in:
68 |
69 | ```ts
70 | function*() {
71 | while (true) {
72 | const login = yield* take("LOGIN");
73 | // e.g. fetch token with creds inside `login.payload`
74 | const logout = yield* take("LOGOUT");
75 | // e.g. destroy token from `logout.payload`
76 | }
77 | }
78 | ```
79 |
80 | Interesting, we've essentially created a finite state machine within a
81 | while-loop!
82 |
83 | We also built a helper that will abstract the while loop if you don't need it:
84 |
85 | ```ts
86 | import { takeEvery } from "starfx";
87 |
88 | function* watchFetch() {
89 | yield* takeEvery("FETCH_USERS", function* (action) {
90 | console.log(action);
91 | });
92 | }
93 | ```
94 |
95 | However, this means that we are going to make the same request 3 times, we
96 | probably want a throttle or debounce so we only make a fetch request once within
97 | some interval.
98 |
99 | ```ts
100 | import { takeLeading } from "starfx";
101 |
102 | function* watchFetch() {
103 | yield* takeLeading("FETCH_USERS", function* (action) {
104 | console.log(action);
105 | });
106 | }
107 | ```
108 |
109 | That's better, now only one task can be alive at one time.
110 |
111 | Both thunks and endpoints simply listen for
112 | [actions](/thunks#anatomy-of-an-action) being emitted onto a channel -- which is
113 | just an event emitter -- and then call the middleware stack with that action.
114 |
115 | Both thunks and endpoints support overriding the default `takeEvery` supervisor
116 | for either our officially supported supervisors `takeLatest` and `takeLeading`,
117 | or a user-defined supervisor.
118 |
119 | Because every thunk and endpoint have their own supervisor tasks monitoring the
120 | health of their children, we allow the end-developer to change the default
121 | supervisor -- which is `takeEvery`:
122 |
123 | ```ts
124 | const someAction = thunks.create("some-action", { supervisor: takeLatest });
125 | dispatch(someAction()); // this task gets cancelled
126 | dispatch(someAction()); // this task gets cancelled
127 | dispatch(someAction()); // this tasks lives
128 | ```
129 |
130 | This is the power of supervisors and is fundamental to how `starfx` works.
131 |
132 | # poll
133 |
134 | When activated, call a thunk or endpoint once every N millisecond indefinitely
135 | until cancelled.
136 |
137 | ```ts
138 | import { poll } from "starfx";
139 |
140 | const fetchUsers = api.get("/users", { supervisor: poll() });
141 | store.dispatch(fetchUsers());
142 | // fetch users
143 | // sleep 5000
144 | // fetch users
145 | // sleep 5000
146 | // fetch users
147 | store.dispatch(fetchUsers());
148 | // cancelled
149 | ```
150 |
151 | The default value provided to `poll()` is **5 seconds**.
152 |
153 | You can optionally provide a cancel action instead of calling the thunk twice:
154 |
155 | ```ts
156 | import { poll } from "starfx";
157 |
158 | const cancelPoll = createAction("cancel-poll");
159 | const fetchUsers = api.get("/users", {
160 | supervisor: poll(5 * 1000, `${cancelPoll}`),
161 | });
162 | store.dispatch(fetchUsers());
163 | // fetch users
164 | // sleep 5000
165 | // fetch users
166 | // sleep 5000
167 | // fetch users
168 | store.dispatch(cancelPoll());
169 | // cancelled
170 | ```
171 |
172 | # timer
173 |
174 | Only call a thunk or endpoint at-most once every N milliseconds.
175 |
176 | ```ts
177 | import { timer } from "starfx";
178 |
179 | const fetchUsers = api.get("/users", { supervisor: timer(1000) });
180 | store.dispatch(fetchUsers());
181 | store.dispatch(fetchUsers());
182 | // sleep(100);
183 | store.dispatch(fetchUsers());
184 | // sleep(1000);
185 | store.dispatch(fetchUsers());
186 | // called: 2 times
187 | ```
188 |
189 | The default value provided to `timer()` is **5 minutes**. This means you can
190 | only call `fetchUsers` at-most once every **5 minutes**.
191 |
192 | ## clearTimers
193 |
194 | Want to clear a timer and refetch?
195 |
196 | ```ts
197 | import { clearTimers, timer } from "starfx";
198 |
199 | const fetchUsers = api.get("/users", { supervisor: timer(1000) });
200 | store.dispatch(fetchUsers());
201 | store.dispatch(clearTimers(fetchUsers()));
202 | store.dispatch(fetchUsers());
203 | // called: 2 times
204 | store.dispatch(clearTimers("*")); // clear all timers
205 | ```
206 |
--------------------------------------------------------------------------------
/docs/posts/testing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Testing
3 | description: You don't need an HTTP interceptor
4 | ---
5 |
6 | Need to write tests? Use libraries like `msw` or `nock`? Well you don't need
7 | them with `starfx`. If the `mdw.fetch()` middleware detects `ctx.response` is
8 | already filled then it skips making the request. Let's take the update user
9 | endpoint example and provide stubbed data for our tests.
10 |
11 | ```tsx
12 | import { fireEvent, render, screen } from "@testing-library/react";
13 | import { useDispatch, useSelector } from "starfx/react";
14 | import { db } from "./schema.ts";
15 | import { updateUser } from "./user.ts";
16 |
17 | function UserSettingsPage() {
18 | const id = "1";
19 | const dispatch = useDispatch();
20 | const user = useSelector((state) => db.users.selectById(state, { id }));
21 |
22 | return (
23 |
24 |
Name: {user.name}
25 |
28 |
29 | );
30 | }
31 |
32 | describe("UserSettingsPage", () => {
33 | it("should update the user", async () => {
34 | // just for this test -- inject a new middleware into the endpoint stack
35 | updateUser.use(function* (ctx, next) {
36 | ctx.response = new Response(
37 | JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
38 | );
39 | yield* next();
40 | });
41 |
42 | render();
43 |
44 | const btn = await screen.findByRole("button", { name: /Update User/ });
45 | fireEvent.click(btn);
46 |
47 | await screen.findByText(/Name: bobby/);
48 | });
49 | });
50 | ```
51 |
52 | That's it. No need for http interceptors and the core functionality works
53 | exactly the same, we just skip making the fetch request for our tests.
54 |
55 | What if we don't have an API endpoint yet and want to stub the data? We use the
56 | same concept but inline inside the `updateUser` endpoint:
57 |
58 | ```ts
59 | export const updateUser = api.post<{ id: string; name: string }>(
60 | "/users/:id",
61 | [
62 | function* (ctx, next) {
63 | ctx.request = ctx.req({
64 | body: JSON.stringify({ name: ctx.payload.name }),
65 | });
66 | yield* next();
67 | },
68 | function* (ctx, next) {
69 | ctx.response = new Response(
70 | JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
71 | );
72 | yield* next();
73 | },
74 | ],
75 | );
76 | ```
77 |
78 | Wow! Our stubbed data is now colocated next to our actual endpoint we are trying
79 | to mock! Once we have a real API we want to hit, we can just remove that second
80 | middleware function and everything will work exactly the same.
81 |
--------------------------------------------------------------------------------
/docs/posts/thunks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Thunks
3 | description: Thunks are tasks for business logic
4 | ---
5 |
6 | Thunks are the foundational central processing units. They have access to all
7 | the actions being dispatched from the view as well as your global state. They
8 | also wield the full power of structured concurrency.
9 |
10 | > Endpoints are specialized thunks as you will see later in the docs
11 |
12 | Think of thunks as micro-controllers. Only thunks and endpoints have the ability
13 | to update state (or a model in MVC terms). However, thunks are not tied to any
14 | particular view and in that way are more composable. Thunks can call other
15 | thunks and you have the async flow control tools from `effection` to facilitate
16 | coordination and cleanup.
17 |
18 | Every thunk that's created requires a unique id -- user provided string. This
19 | provides 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 |
27 | They 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
29 | for all thunks and endpoints.
30 |
31 | Each run of a thunk gets its own `ctx` object which provides a substrate to
32 | communicate between middleware.
33 |
34 | ```ts
35 | import { call, createThunks, mdw } from "starfx";
36 |
37 | const thunks = createThunks();
38 | // catch errors from task and logs them with extra info
39 | thunks.use(mdw.err);
40 | // where all the thunks get called in the middleware stack
41 | thunks.use(thunks.routes());
42 | thunks.use(function* (ctx, next) {
43 | console.log("last mdw in the stack");
44 | yield* next();
45 | });
46 |
47 | // create a thunk
48 | const log = thunks.create("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 |
60 | store.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 |
69 | Thunks are a composition of middleware functions in a stack. Therefore, every
70 | single 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";
76 | type Next = () => Operation;
77 |
78 | interface ThunkCtx
extends Payload
{
79 | name: string;
80 | key: string;
81 | action: ActionWithPayload>;
82 | actionFn: IfAny<
83 | P,
84 | CreateAction,
85 | CreateActionWithPayload, P>
86 | >;
87 | result: Result;
88 | }
89 |
90 | function* myMiddleware(ctx: ThunkCtx, next: Next) {
91 | yield* next();
92 | }
93 | ```
94 |
95 | Similar to `express` or `koa`, if you do **not** call `next()` then the
96 | middleware stack will stop after the code execution leaves the scope of the
97 | current middleware. This provides the end-user with "exit early" functionality
98 | for even more control.
99 |
100 | # Anatomy of an Action
101 |
102 | When creating a thunk, the return value is just an action creator:
103 |
104 | ```ts
105 | console.log(log("sending log message"));
106 | {
107 | type: "log",
108 | payload: "sending log message"
109 | }
110 | ```
111 |
112 | An action is the "event" being emitted from `startfx` and subscribes to a very
113 | particular type signature.
114 |
115 | A 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 |
121 | For thunks we have a more strict payload type signature with additional
122 | properties:
123 |
124 | ```ts
125 | interface CreateActionPayload
{
126 | name: string; // the user-defined name
127 | options: P; // thunk payload described below
128 | key: string; // hash of entire thunk payload
129 | }
130 |
131 | interface ThunkAction