repos / starfx

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

commit
2ab8297
parent
95bee2f
author
Eric Bower
date
2024-02-14 21:12:50 +0000 UTC
docs site (#39)

29 files changed,  +1164, -465
A .github/workflows/docs.yml
+32, -0
 1@@ -0,0 +1,32 @@
 2+name: docs
 3+on:
 4+  push:
 5+    branches:
 6+      - main
 7+jobs:
 8+  static:
 9+    runs-on: ubuntu-latest
10+    steps:
11+      - uses: actions/checkout@v3
12+        with:
13+          # need entire history
14+          fetch-depth: 0
15+      - uses: actions/setup-go@v4
16+        with:
17+          go-version: '1.20'
18+      - name: generate site
19+        run: |
20+          go mod tidy
21+          make ssg
22+      - name: Set outputs
23+        id: vars
24+        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
25+      - name: publish to pgs
26+        uses: picosh/pgs-action@v3
27+        with:
28+          user: erock
29+          key: ${{ secrets.PRIVATE_KEY }}
30+          src: './docs/public/'
31+          project: "starfx-docs-${{ steps.vars.outputs.sha_short }}"
32+          promote: "starfx-prod"
33+          retain: "starfx-docs"
M .gitignore
+3, -1
1@@ -4,4 +4,6 @@ node_modules/
2 npm/
3 .DS_Store
4 *.bak
5-.vscode
6+.vscode
7+docs/public/*
8+!docs/public/.gitkeep
A Makefile
+17, -0
 1@@ -0,0 +1,17 @@
 2+clean:
 3+	rm -rf ./docs/public/*
 4+	echo "" > ./docs/public/.gitkeep
 5+.PHONY: clean
 6+
 7+ssg: clean
 8+	cd docs && go run ./main.go
 9+	cp ./docs/static/* ./docs/public
10+.PHONY: ssg
11+
12+local: ssg
13+	rsync -vr ./docs/public/ erock@pgs.sh:/starfx-local
14+.PHONY: dev
15+
16+prod: ssg
17+	rsync -vr ./docs/public/ erock@pgs.sh:/starfx-prod
18+.PHONY: prod
M README.md
+1, -464
  1@@ -9,467 +9,4 @@ and state management.
  2 > This project is under active development, there are zero guarantees for API
  3 > stability.
  4 
  5-- [blog posts about starfx](https://bower.sh/?tag=starfx)
  6-- [discord](https://discord.gg/frontside)
  7-
  8-# features
  9-
 10-- task tree side-effect management system (like `redux-saga`)
 11-- task management and fault-tolerance via supervisors
 12-- simple immutable data store (like `redux`)
 13-- traceability throughout the entire system (event logs via dispatching actions)
 14-- data synchronization and caching for `react` (like `react-query`, `rtk-query`)
 15-
 16-# design philosophy
 17-
 18-- user interaction is a side-effect of using a web app
 19-- side-effect management is the central processing unit to manage user
 20-  interaction, app features, and state
 21-- leverage structured concurrency to manage side-effects
 22-- leverage supervisor tasks to provide powerful design patterns
 23-- side-effect and state management decoupled from the view
 24-- user has full control over state management (opt-in to automatic data
 25-  synchronization)
 26-- state is just a side-effect (of user interaction and app features)
 27-
 28-# example: thunks are tasks for business logic
 29-
 30-Thunks are the foundational central processing units. They have access to all
 31-the actions being dispatched from the view as well as your global state. They
 32-also wield the full power of structured concurrency.
 33-
 34-As I've been developing these specialized thunks, I'm starting to think of them
 35-more like micro-controllers. Only thunks and endpoints have the ability to
 36-update state. However, thunks are not tied to any particular view and in that
 37-way are more composable. Thunks can call other thunks and you have the async
 38-flow control tools from effection to facilitate coordination.
 39-
 40-Every thunk that is created requires a unique id -- user provided string. This
 41-provides us with a handful of benefits:
 42-
 43-- User hand-labels each thunk created
 44-- Better traceability (via labels)
 45-- Easier to debug async and side-effects in general (via labels)
 46-- Build abstractions off naming conventions (e.g. creating routers
 47-  `/users [GET]`)
 48-
 49-They also come with built-in support for a middleware stack (like `express`).
 50-This provides a familiar and powerful abstraction for async flow control for all
 51-thunks and endpoints.
 52-
 53-Each run of a thunk gets its own `ctx` object which provides a substrate to
 54-communicate between middleware.
 55-
 56-```ts
 57-import { call, createThunks, mdw } from "starfx";
 58-
 59-const thunks = createThunks();
 60-// catch errors from task and logs them with extra info
 61-thunks.use(mdw.err);
 62-// where all the thunks get called in the middleware stack
 63-thunks.use(thunks.routes());
 64-thunks.use(function* (ctx, next) {
 65-  console.log("last mdw in the stack");
 66-  yield* next();
 67-});
 68-
 69-// create a thunk
 70-const log = thunks.create<string>("log", function* (ctx, next) {
 71-  const resp = yield* call(
 72-    fetch("https://log-drain.com", {
 73-      method: "POST",
 74-      body: JSON.stringify({ message: ctx.payload }),
 75-    }),
 76-  );
 77-  console.log("before calling next middleware");
 78-  yield* next();
 79-  console.log("after all remaining middleware have run");
 80-});
 81-
 82-store.dispatch(log("sending log message"));
 83-// output:
 84-// before calling next middleware
 85-// last mdw in the stack
 86-// after all remaining middleware have run
 87-```
 88-
 89-# example: endpoints are tasks for managing HTTP requests
 90-
 91-Building off of `createThunks` we have a way to easily manage http requests.
 92-
 93-```ts
 94-import { createApi, mdw } from "starfx";
 95-
 96-const api = createApi();
 97-// composition of handy middleware for createApi to function
 98-api.use(mdw.api());
 99-api.use(api.routes());
100-// calls `window.fetch` with `ctx.request` and sets to `ctx.response`
101-api.use(mdw.fetch({ baseUrl: "https://jsonplaceholder.typicode.com" }));
102-
103-// automatically cache Response json in datastore as-is
104-export const fetchUsers = api.get("/users", api.cache());
105-
106-// create a POST HTTP request
107-export const updateUser = api.post<{ id: string; name: string }>(
108-  "/users/:id",
109-  function* (ctx, next) {
110-    ctx.request = ctx.req({
111-      body: JSON.stringify({ name: ctx.payload.name }),
112-    });
113-    yield* next();
114-  },
115-);
116-
117-store.dispatch(fetchUsers());
118-// now accessible with useCache(fetchUsers)
119-
120-// lets update a user record
121-store.dispatch(updateUser({ id: "1", name: "bobby" }));
122-```
123-
124-# example: an immutable store that acts like a reactive, in-memory database
125-
126-I love `redux`. I know it gets sniped for having too much boilerplate when
127-alternatives like `zustand` and `react-query` exist that cut through the
128-ceremony of managing state. However, `redux` was never designed to be easy to
129-use, it was designed to be scalable, debuggable, and maintainable. Yes, setting
130-up a `redux` store is work, but that is in an effort to serve its
131-maintainability.
132-
133-Having said that, the core abstraction in `redux` is a reducer. Reducers were
134-originally designed to contain isolated business logic for updating sections of
135-state (also known as state slices). They were also designed to make it easier to
136-sustain state immutability.
137-
138-Fast forward to `redux-toolkit` and we have `createSlice` which leverages
139-`immer` under-the-hood to ensure immutability. So we no longer need reducers for
140-immutability.
141-
142-Further, I argue, placing the business logic for updating state inside reducers
143-(via switch-cases) makes understanding business logic harder. Instead of having
144-a single function that updates X state slices, we have X functions (reducers)
145-that we need to piece together in our heads to understand what is being updated
146-when an action is dispatched.
147-
148-With all of this in mind, `starfx/store` takes all the good parts of `redux` and
149-removes the need for reducers entirely. We still have a single state object that
150-contains everything from API data, UX, and a way to create memoized functions
151-(e.g. selectors). We maintain immutability (using `immer`) and also have a
152-middleware system to extend it.
153-
154-Finally, we bring the utility of creating a schema (like `zod` or a traditional
155-database) to make it plainly obvious what the state shape looks like as well as
156-reusable utilities to make it easy to update and query state.
157-
158-This gets us closer to treating our store like a traditional database while
159-still being flexible for our needs on the FE.
160-
161-```ts
162-import { configureStore, createSchema, select, slice } from "starfx/store";
163-
164-interface User {
165-  id: string;
166-  name: string;
167-}
168-
169-// app-wide database for ui, api data, or anything that needs reactivity
170-const { db, initialState, update } = createSchema({
171-  users: slice.table<User>(),
172-  cache: slice.table(),
173-  loaders: slice.loader(),
174-  // -- more slice examples --
175-  // token: slice.str(),
176-  // nav: slice.obj<{ collapsed: boolean }>(),
177-  // counter: slice.num(0),
178-  // userIds: slice.any<string[]>(),
179-});
180-type AppState = typeof initialState;
181-
182-// just a normal endpoint
183-const fetchUsers = api.get<never, User[]>(
184-  "/users",
185-  function* (ctx, next) {
186-    // make the http request
187-    yield* next();
188-
189-    // ctx.json is a Result type that either contains the http response
190-    // json data or an error
191-    if (!ctx.json.ok) {
192-      return;
193-    }
194-
195-    const { value } = ctx.json;
196-    const users = value.reduce<Record<string, User>>((acc, user) => {
197-      acc[user.id] = user;
198-      return acc;
199-    }, {});
200-
201-    // update the store and trigger a re-render in react
202-    yield* update(db.users.add(users));
203-
204-    // User[]
205-    const users = yield* select(db.users.selectTableAsList);
206-    // User
207-    const user = yield* select(
208-      (state) => db.users.selectById(state, { id: "1" }),
209-    );
210-  },
211-);
212-
213-const store = configureStore({ initialState });
214-store.dispatch(fetchUsers());
215-```
216-
217-# example: the view
218-
219-```tsx
220-import { useApi, useSelector } from "starfx/react";
221-import { db } from "./store.ts";
222-import { fetchUsers } from "./api.ts";
223-
224-function App() {
225-  const users = useSelector(db.users.selectTableAsList);
226-  const api = useApi(fetchUsers());
227-
228-  return (
229-    <div>
230-      {users.map((u) => <div key={u.id}>{u.name}</div>)}
231-      <div>
232-        <button onClick={() => api.trigger()}>fetch users</button>
233-        {api.isLoading ? <div>Loading ...</div> : null}
234-      </div>
235-    </div>
236-  );
237-}
238-```
239-
240-# usage
241-
242-- [examples repo](https://github.com/neurosnap/starfx-examples)
243-- [production example repo](https://github.com/aptible/app-ui)
244-
245-# when to use this library?
246-
247-The primary target for this library are single-page apps (SPAs). This is for an
248-app that might be hosted inside an object store (like s3) or with a simple web
249-server that serves files and that's it.
250-
251-Is your app highly interactive, requiring it to persist data across pages? This
252-is the sweet spot for `starfx`.
253-
254-You can use this library as general purpose structured concurrency, but
255-[effection](https://github.com/thefrontside/effection) serves those needs well.
256-
257-You could use this library for SSR, but I don't heavily build SSR apps, so I
258-cannot claim it'll work well.
259-
260-# what is structured concurrency?
261-
262-This is a broad term so I'll make this specific to how `starfx` works.
263-
264-Under-the-hood, thunks and endpoints are registered under the root task. Every
265-thunk and endpoint has their own supervisor that manages them. As a result, what
266-we have is a single root task for your entire app that is being managed by
267-supervisor tasks. When the root task receives a signal to shutdown itself (e.g.
268-`task.halt()` or closing browser tab) it first must shutdown all children tasks
269-before being resolved.
270-
271-When a child task throws an exception (whether intentional or otherwise) it will
272-propagate that error up the task tree until it is caught or reaches the root
273-task.
274-
275-In review:
276-
277-- There is a single root task for an app
278-- The root task can spawn child tasks
279-- If root task is halted then all child tasks are halted first
280-- If a child task is halted or raises exception, it propagates error up the task
281-  tree
282-- An exception can be caught (e.g. `try`/`catch`) at any point in the task tree
283-
284-# what is a supervisor task?
285-
286-[Supplemental reading from erlang](https://www.erlang.org/doc/design_principles/des_princ)
287-
288-A supervisor task is a way to monitor children tasks and probably most
289-importantly, manage their health. By structuring your side-effects and business
290-logic around supervisor tasks, we gain very interesting coding paradigms that
291-result is easier to read and manage code.
292-
293-The most basic version of a supervisor is simply an infinite loop that calls a
294-child task:
295-
296-```ts
297-function* supervisor() {
298-  while (true) {
299-    try {
300-      yield* call(someTask);
301-    } catch (err) {
302-      console.error(err);
303-    }
304-  }
305-}
306-```
307-
308-Here we call some task that should always be in a running and healthy state. If
309-it raises an exception, we log it and try to run the task again.
310-
311-Building on top of that simple supervisor, we can have tasks that always listen
312-for events and if they fail, restart them.
313-
314-```ts
315-import { parallel, run, takeEvery } from "starfx";
316-
317-function* watchFetch() {
318-  yield* takeEvery("FETCH_USERS", function* (action) {
319-    console.log(action);
320-  });
321-}
322-
323-function* send() {
324-  yield* put({ type: "FETCH_USERS" });
325-  yield* put({ type: "FETCH_USERS" });
326-  yield* put({ type: "FETCH_USERS" });
327-}
328-
329-await run(
330-  parallel([watchFetch, send]),
331-);
332-```
333-
334-Here we create a supervisor function using a helper `takeEvery` to call a
335-function for every `FETCH_USERS` event emitted.
336-
337-However, this means that we are going to make the same request 3 times, we
338-probably want a throttle or debounce to prevent this behavior.
339-
340-```ts
341-import { takeLeading } from "starfx";
342-
343-function* watchFetch() {
344-  yield* takeLeading("FETCH_USERS", function* (action) {
345-    console.log(action);
346-  });
347-}
348-```
349-
350-That's better, now only one task can be alive at one time.
351-
352-Both thunks and endpoints simply listen for particular actions being emitted
353-onto the `ActionContext` -- which is just an event emitter -- and then call the
354-middleware stack with that action.
355-
356-Both thunks and endpoints support overriding the default `takeEvery` supervisor
357-for either our officially supported supervisors `takeLatest` and `takeLeading`,
358-or a user-defined supervisor.
359-
360-Because every thunk and endpoint have their own supervisor tasks monitoring the
361-health of their children, we allow the end-developer to change the default
362-supervisor -- which is `takeEvery`:
363-
364-```ts
365-const someAction = thunks.create("some-action", { supervisor: takeLatest });
366-dispatch(someAction()); // this task gets cancelled
367-dispatch(someAction()); // this task gets cancelled
368-dispatch(someAction()); // this tasks lives
369-```
370-
371-This is the power of supervisors and is fundamental to how `starfx` works.
372-
373-# example: test that doesn't need an http interceptor
374-
375-Need to write tests? Use libraries like `msw` or `nock`? Well you don't need
376-them with `starfx`. If the `mdw.fetch()` middleware detects `ctx.response` is
377-already filled then it skips making the request. Let's take the update user
378-endpoint example and provide stubbed data for our tests.
379-
380-```tsx
381-import { fireEvent, render, screen } from "@testing-library/react";
382-import { useDispatch, useSelector } from "starfx/react";
383-import { db } from "./schema.ts";
384-import { updateUser } from "./user.ts";
385-
386-function UserSettingsPage() {
387-  const id = "1";
388-  const dispatch = useDispatch();
389-  const user = useSelector((state) => db.users.selectById(state, { id }));
390-
391-  return (
392-    <div>
393-      <div>Name: {user.name}</div>
394-      <button onClick={() => dispatch(updateUser({ id, name: "bobby" }))}>
395-        Update User
396-      </button>
397-    </div>
398-  );
399-}
400-
401-describe("UserSettingsPage", () => {
402-  it("should update the user", async () => {
403-    // just for this test -- inject a new middleware into the endpoint stack
404-    updateUser.use(function* (ctx, next) {
405-      ctx.response = new Response(
406-        JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
407-      );
408-      yield* next();
409-    });
410-
411-    render(<UserSettingsPage />);
412-
413-    const btn = await screen.findByRole("button", { name: /Update User/ });
414-    fireEvent.click(btn);
415-
416-    await screen.findByText(/Name: bobby/);
417-  });
418-});
419-```
420-
421-That's it. No need for http interceptors and the core functionality works
422-exactly the same, we just skip making the fetch request for our tests.
423-
424-What if we don't have an API endpoint yet and want to stub the data? We use the
425-same concept but inline inside the `updateUser` endpoint:
426-
427-```ts
428-export const updateUser = api.post<{ id: string; name: string }>(
429-  "/users/:id",
430-  [
431-    function* (ctx, next) {
432-      ctx.request = ctx.req({
433-        body: JSON.stringify({ name: ctx.payload.name }),
434-      });
435-      yield* next();
436-    },
437-    function* (ctx, next) {
438-      ctx.response = new Response(
439-        JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
440-      );
441-      yield* next();
442-    },
443-  ],
444-);
445-```
446-
447-Wow! Our stubbed data is now colocated next to our actual endpoint we are trying
448-to mock! Once we have a real API we want to hit, we can just remove that second
449-middleware function and everything will work exactly the same.
450-
451-# talk
452-
453-I recently gave a talk about delimited continuations where I also discuss this
454-library:
455-
456-[![Delimited continuations are all you need](http://img.youtube.com/vi/uRbqLGj_6mI/0.jpg)](https://youtu.be/uRbqLGj_6mI?si=Mok0J8Wp0Z-ahFrN)
457-
458-Here is another talk I helped facilitate about `effection` with the library
459-creator:
460-
461-[![effection with Charles Lowell](http://img.youtube.com/vi/lJDgpxRw5WA/0.jpg)](https://youtu.be/lJDgpxRw5WA?si=cCHZiKqNO7vIUhPc)
462-
463-# resources
464-
465-This library is not possible without these foundational libraries:
466-
467-- [continuation](https://github.com/thefrontside/continuation)
468-- [effection v3](https://github.com/thefrontside/effection/tree/v3)
469+[Read the docs](https://starfx.bower.sh)
A docs/go.mod
+15, -0
 1@@ -0,0 +1,15 @@
 2+module github.com/neurosnap/starfx/docs
 3+
 4+go 1.22.0
 5+
 6+require github.com/picosh/pdocs v0.0.0-20240205045212-d44525ffbbf5
 7+
 8+require (
 9+	github.com/alecthomas/chroma v0.10.0 // indirect
10+	github.com/dlclark/regexp2 v1.10.0 // indirect
11+	github.com/yuin/goldmark v1.6.0 // indirect
12+	github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 // indirect
13+	github.com/yuin/goldmark-meta v1.1.0 // indirect
14+	go.abhg.dev/goldmark/anchor v0.1.1 // indirect
15+	gopkg.in/yaml.v2 v2.4.0 // indirect
16+)
A docs/go.sum
+32, -0
 1@@ -0,0 +1,32 @@
 2+github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
 3+github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
 4+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 5+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 6+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 7+github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 8+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
 9+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
10+github.com/picosh/pdocs v0.0.0-20240205045212-d44525ffbbf5 h1:AXSQbwThiGubFwnE1Sav6UoahzV/Ya/p4g/hcg2Z3Ew=
11+github.com/picosh/pdocs v0.0.0-20240205045212-d44525ffbbf5/go.mod h1:4Ei1K68K1qYyqSx5shNdw4++PN1Ws3QhD1u5eJYBmsA=
12+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
17+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
18+github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
19+github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
20+github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
21+github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg=
22+github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
23+github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
24+github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
25+go.abhg.dev/goldmark/anchor v0.1.1 h1:NUH3hAzhfeymRqZKOkSoFReZlEAmfXBZlbXEzpD2Qgc=
26+go.abhg.dev/goldmark/anchor v0.1.1/go.mod h1:zYKiaHXTdugwVJRZqInVdmNGQRM3ZRJ6AGBC7xP7its=
27+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
28+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
29+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
30+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
31+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
32+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
33+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A docs/main.go
+99, -0
  1@@ -0,0 +1,99 @@
  2+package main
  3+
  4+import (
  5+	"github.com/picosh/pdocs"
  6+)
  7+
  8+func main() {
  9+	pager := pdocs.Pager("./posts")
 10+	sitemap := []*pdocs.Sitemap{
 11+		{
 12+			Text: "Home",
 13+			Href: "/",
 14+			Page: pager("home.md"),
 15+		},
 16+		{
 17+			Text: "Sitemap",
 18+			Href: "/sitemap",
 19+			Page: pager("sitemap.md"),
 20+		},
 21+		{
 22+			Text: "Getting started",
 23+			Href: "/getting-started",
 24+			Page: pager("getting-started.md"),
 25+			Tag:  "Info",
 26+		},
 27+		{
 28+			Text: "Thunks",
 29+			Href: "/thunks",
 30+			Page: pager("thunks.md"),
 31+			Tag:  "Side Effects",
 32+		},
 33+		{
 34+			Text: "Endpoints",
 35+			Href: "/endpoints",
 36+			Page: pager("endpoints.md"),
 37+			Tag:  "Side Effects",
 38+		},
 39+		{
 40+			Text: "Store",
 41+			Href: "/store",
 42+			Page: pager("store.md"),
 43+			Tag:  "Store",
 44+		},
 45+		{
 46+			Text: "React",
 47+			Href: "/react",
 48+			Page: pager("react.md"),
 49+			Tag:  "View",
 50+		},
 51+		{
 52+			Text: "Schema",
 53+			Href: "/schema",
 54+			Page: pager("schema.md"),
 55+			Tag:  "Store",
 56+		},
 57+		{
 58+			Text: "Structured Concurrency",
 59+			Href: "/structured-concurrency",
 60+			Page: pager("structured-concurrency.md"),
 61+			Tag:  "Info",
 62+		},
 63+		{
 64+			Text: "Supervisors",
 65+			Href: "/supervisors",
 66+			Page: pager("supervisors.md"),
 67+			Tag:  "Advanced",
 68+		},
 69+		{
 70+			Text: "Testing",
 71+			Href: "/testing",
 72+			Page: pager("testing.md"),
 73+			Tag:  "Advanced",
 74+		},
 75+		{
 76+			Text: "API",
 77+			Href: "/api",
 78+			Page: pager("api.md"),
 79+			Tag:  "Info",
 80+		},
 81+		{
 82+			Text: "Resources",
 83+			Href: "/resources",
 84+			Page: pager("resources.md"),
 85+			Tag:  "Info",
 86+		},
 87+	}
 88+
 89+	config := &pdocs.DocConfig{
 90+		Sitemap:  sitemap,
 91+		Out:      "./public",
 92+		Tmpl:     "./tmpl",
 93+		PageTmpl: "post.page.tmpl",
 94+	}
 95+
 96+	err := config.GenSite()
 97+	if err != nil {
 98+		panic(err)
 99+	}
100+}
A docs/posts/api.md
+6, -0
1@@ -0,0 +1,6 @@
2+---
3+title: API
4+description: Our API for public consumption
5+---
6+
7+WIP
A docs/posts/endpoints.md
+37, -0
 1@@ -0,0 +1,37 @@
 2+---
 3+title: Endpoints
 4+description: endpoints are tasks for managing HTTP requests
 5+---
 6+
 7+Building off of `createThunks` we have a way to easily manage http requests.
 8+
 9+```ts
10+import { createApi, mdw } from "starfx";
11+
12+const api = createApi();
13+// composition of handy middleware for createApi to function
14+api.use(mdw.api());
15+api.use(api.routes());
16+// calls `window.fetch` with `ctx.request` and sets to `ctx.response`
17+api.use(mdw.fetch({ baseUrl: "https://jsonplaceholder.typicode.com" }));
18+
19+// automatically cache Response json in datastore as-is
20+export const fetchUsers = api.get("/users", api.cache());
21+
22+// create a POST HTTP request
23+export const updateUser = api.post<{ id: string; name: string }>(
24+  "/users/:id",
25+  function* (ctx, next) {
26+    ctx.request = ctx.req({
27+      body: JSON.stringify({ name: ctx.payload.name }),
28+    });
29+    yield* next();
30+  },
31+);
32+
33+store.dispatch(fetchUsers());
34+// now accessible with useCache(fetchUsers)
35+
36+// lets update a user record
37+store.dispatch(updateUser({ id: "1", name: "bobby" }));
38+```
A docs/posts/getting-started.md
+97, -0
 1@@ -0,0 +1,97 @@
 2+---
 3+title: Getting Started
 4+description: Use starfx with deno, node, or the browser
 5+---
 6+
 7+# design philosophy
 8+
 9+- user interaction is a side-effect of using a web app
10+- side-effect management is the central processing unit to manage user
11+  interaction, app features, and state
12+- leverage structured concurrency to manage side-effects
13+- leverage supervisor tasks to provide powerful design patterns
14+- side-effect and state management decoupled from the view
15+- user has full control over state management (opt-in to automatic data
16+  synchronization)
17+- state is just a side-effect (of user interaction and app features)
18+
19+# when to use this library?
20+
21+The primary target for this library are single-page apps (SPAs). This is for an
22+app that might be hosted inside an object store (like s3) or with a simple web
23+server that serves files and that's it.
24+
25+Is your app highly interactive, requiring it to persist data across pages? This
26+is the sweet spot for `starfx`.
27+
28+You can use this library as general purpose structured concurrency, but
29+[effection](https://github.com/thefrontside/effection) serves those needs well.
30+
31+You could use this library for SSR, but I don't heavily build SSR apps, so I
32+cannot claim it'll work well.
33+
34+# install
35+
36+```bash
37+npm install starfx
38+```
39+
40+```bash
41+yarn add starfx
42+```
43+
44+```ts
45+import * as starfx from "https://deno.land/x/starfx@0.7.0/mod.ts";
46+```
47+
48+# the simplest example
49+
50+Here we demonstrate a complete example so you can get a glimpse of how `starfx`
51+works. The rest of our docs will go into more detail for how all the pieces
52+work.
53+
54+```tsx
55+import ReactDOM from "react-dom/client";
56+import { createApi, mdw } from "starfx";
57+import { configureStore, createSchema, slice } from "starfx/store";
58+import { Provider, useCache } from "starfx/react";
59+
60+const api = createApi();
61+api.use(mdw.api());
62+api.use(api.routes());
63+api.use(mdw.fetch({ baseUrl: "https://jsonplaceholder.typicode.com" }));
64+
65+const fetchUsers = api.get("/users", api.cache());
66+
67+const schema = createSchema({
68+  loaders: slice.loader(),
69+  cache: slice.table(),
70+});
71+const store = configureStore(schema);
72+type WebState = typeof store.initialState;
73+
74+store.run(api.bootup);
75+
76+function App() {
77+  const { isLoading, data: users } = useCache(fetchUsers());
78+
79+  if (isLoading) {
80+    return <div>Loading ...</div>;
81+  }
82+
83+  return (
84+    <div>
85+      {users.map(
86+        (user) => <div key={user.id}>{user.name}</div>,
87+      )}
88+    </div>
89+  );
90+}
91+
92+const root = document.getElementById("root") as HTMLElement;
93+ReactDOM.createRoot(root).render(
94+  <Provider schema={schema} store={store}>
95+    <App />
96+  </Provider>,
97+);
98+```
A docs/posts/home.md
+6, -0
1@@ -0,0 +1,6 @@
2+---
3+title: starfx
4+description: Structured concurrency for your FE apps with a modern approach to side-effect and state management.
5+slug: index
6+template: home.page.tmpl
7+---
A docs/posts/react.md
+39, -0
 1@@ -0,0 +1,39 @@
 2+---
 3+title: React
 4+description: How to integrate with React
 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 that's what we are using under-the-hood to integrate with
10+`react`.
11+
12+`useDispatch`, `useSelector`, and `createSelector` are the bread and butter of
13+`redux`'s integration with `react` all of which we use inside `starfx`.
14+
15+```tsx
16+import {
17+  TypedUseSelectorHook,
18+  useApi,
19+  useSelector as useBaseSelector,
20+} from "starfx/react";
21+import { schema, WebState } from "./store.ts";
22+import { fetchUsers } from "./api.ts";
23+
24+const useSelector: TypedUseSelectorHook<WebState> = useBaseSelector;
25+
26+function App() {
27+  const users = useSelector(schema.users.selectTableAsList);
28+  const api = useApi(fetchUsers());
29+
30+  return (
31+    <div>
32+      {users.map((u) => <div key={u.id}>{u.name}</div>)}
33+      <div>
34+        <button onClick={() => api.trigger()}>fetch users</button>
35+        {api.isLoading ? <div>Loading ...</div> : null}
36+      </div>
37+    </div>
38+  );
39+}
40+```
A docs/posts/resources.md
+29, -0
 1@@ -0,0 +1,29 @@
 2+---
 3+title: Resources
 4+description: Some useful links to learn more
 5+---
 6+
 7+# quick links
 8+
 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+# talk
14+
15+I recently gave a talk about delimited continuations where I also discuss this
16+library:
17+
18+[![Delimited continuations are all you need](http://img.youtube.com/vi/uRbqLGj_6mI/0.jpg)](https://youtu.be/uRbqLGj_6mI?si=Mok0J8Wp0Z-ahFrN)
19+
20+Here is another talk I helped facilitate about `effection` with the library
21+creator:
22+
23+[![effection with Charles Lowell](http://img.youtube.com/vi/lJDgpxRw5WA/0.jpg)](https://youtu.be/lJDgpxRw5WA?si=cCHZiKqNO7vIUhPc)
24+
25+# other notable libraries
26+
27+This library is not possible without these foundational libraries:
28+
29+- [continuation](https://github.com/thefrontside/continuation)
30+- [effection v3](https://github.com/thefrontside/effection/tree/v3)
A docs/posts/schema.md
+72, -0
 1@@ -0,0 +1,72 @@
 2+---
 3+title: Schema
 4+description: Learn more about schamas and slices
 5+---
 6+
 7+- `any`
 8+- `loader`
 9+- `num`
10+- `obj`
11+- `str`
12+- `table`
13+
14+`createSchema` requires two slices by default in order for it and everything
15+inside `starfx` to function properly: `cache` and `loader`.
16+
17+# Build your own slice
18+
19+We will build a `counter` slice to demonstrate how to build your own slices.
20+
21+```ts
22+import type { AnyState } from "starfx";
23+import { BaseSchema, select } from "starfx/store";
24+
25+export interface CounterOutput<S extends AnyState> extends BaseSchema<number> {
26+  schema: "counter";
27+  initialState: number;
28+  increment: (by?: number) => (s: S) => void;
29+  decrement: (by?: number) => (s: S) => void;
30+  reset: () => (s: S) => void;
31+  select: (s: S) => number;
32+}
33+
34+export function createCounter<S extends AnyState = AnyState>(
35+  { name, initialState = 0 }: { name: keyof S; initialState?: number },
36+): CounterOutput<S> {
37+  return {
38+    name: name as string,
39+    schema: "counter",
40+    initialState,
41+    increment: (by = 1) => (state) => {
42+      (state as any)[name] += by;
43+    },
44+    decrement: (by = 1) => (state) => {
45+      (state as any)[name] -= by;
46+    },
47+    reset: () => (state) => {
48+      (state as any)[name] = initialState;
49+    },
50+    select: (state) => {
51+      return (state as any)[name];
52+    },
53+  };
54+}
55+
56+export function counter(initialState?: number) {
57+  return (name: string) => createCounter<AnyState>({ name, initialState });
58+}
59+
60+const schema = createSchema({
61+  counter: counter(100),
62+});
63+const store = configureStore(schema);
64+
65+store.run(function* () {
66+  yield* schema.update([
67+    schema.counter.increment(),
68+    schema.counter.increment(),
69+  ]);
70+  const result = yield* select(schema.counter.select);
71+  console.log(result); // 102
72+});
73+```
A docs/posts/sitemap.md
+6, -0
1@@ -0,0 +1,6 @@
2+---
3+title: sitemap
4+description: starfx doc sitemap 
5+slug: sitemap
6+template: sitemap.page.tmpl
7+---
A docs/posts/store.md
+91, -0
 1@@ -0,0 +1,91 @@
 2+---
 3+title: Store
 4+Description: An immutable store that acts like a reactive, in-memory database
 5+---
 6+
 7+I love `redux`. I know it gets sniped for having too much boilerplate when
 8+alternatives like `zustand` and `react-query` exist that cut through the
 9+ceremony of managing state. However, `redux` was never designed to be easy to
10+use, it was designed to be scalable, debuggable, and maintainable. Yes, setting
11+up a `redux` store is work, but that is in an effort to serve its
12+maintainability.
13+
14+Having said that, the core abstraction in `redux` is a reducer. Reducers were
15+originally designed to contain isolated business logic for updating sections of
16+state (also known as state slices). They were also designed to make it easier to
17+sustain state immutability.
18+
19+Fast forward to `redux-toolkit` and we have `createSlice` which leverages
20+`immer` under-the-hood to ensure immutability. So we no longer need reducers for
21+immutability.
22+
23+Further, I argue, placing the business logic for updating state inside reducers
24+(via switch-cases) makes understanding business logic harder. Instead of having
25+a single function that updates X state slices, we have X functions (reducers)
26+that we need to piece together in our heads to understand what is being updated
27+when an action is dispatched.
28+
29+With all of this in mind, `starfx/store` takes all the good parts of `redux` and
30+removes the need for reducers entirely. We still have a single state object that
31+contains everything from API data, UX, and a way to create memoized functions
32+(e.g. selectors). We maintain immutability (using `immer`) and also have a
33+middleware system to extend it.
34+
35+Finally, we bring the utility of creating a schema (like `zod` or a traditional
36+database) to make it plainly obvious what the state shape looks like as well as
37+reusable utilities to make it easy to update and query state.
38+
39+This gets us closer to treating our store like a traditional database while
40+still being flexible for our needs on the FE.
41+
42+```ts
43+import { configureStore, createSchema, select, slice } from "starfx/store";
44+
45+interface User {
46+  id: string;
47+  name: string;
48+}
49+
50+// app-wide database for ui, api data, or anything that needs reactivity
51+const schema = createSchema({
52+  cache: slice.table(),
53+  loaders: slice.loader(),
54+  users: slice.table<User>(),
55+});
56+type WebState = typeof schema.initialState;
57+
58+// just a normal endpoint
59+const fetchUsers = api.get<never, User[]>(
60+  "/users",
61+  function* (ctx, next) {
62+    // make the http request
63+    yield* next();
64+
65+    // ctx.json is a Result type that either contains the http response
66+    // json data or an error
67+    if (!ctx.json.ok) {
68+      return;
69+    }
70+
71+    const { value } = ctx.json;
72+    const users = value.reduce<Record<string, User>>((acc, user) => {
73+      acc[user.id] = user;
74+      return acc;
75+    }, {});
76+
77+    // update the store and trigger a re-render in react
78+    yield* schema.update(schema.users.add(users));
79+
80+    // User[]
81+    const users = yield* select(schema.users.selectTableAsList);
82+    // User
83+    const user = yield* select(
84+      (state) => schema.users.selectById(state, { id: "1" }),
85+    );
86+  },
87+);
88+
89+const store = configureStore(schema);
90+store.run(api.bootup);
91+store.dispatch(fetchUsers());
92+```
A docs/posts/structured-concurrency.md
+26, -0
 1@@ -0,0 +1,26 @@
 2+---
 3+title: Structured Concurrency
 4+description: What is structured concurrency?
 5+---
 6+
 7+This is a broad term so I'll make this specific to how `starfx` works.
 8+
 9+Under-the-hood, thunks and endpoints are registered under the root task. Every
10+thunk and endpoint has their own supervisor that manages them. As a result, what
11+we have is a single root task for your entire app that is being managed by
12+supervisor tasks. When the root task receives a signal to shutdown itself (e.g.
13+`task.halt()` or closing browser tab) it first must shutdown all children tasks
14+before being resolved.
15+
16+When a child task throws an exception (whether intentional or otherwise) it will
17+propagate that error up the task tree until it is caught or reaches the root
18+task.
19+
20+In review:
21+
22+- There is a single root task for an app
23+- The root task can spawn child tasks
24+- If root task is halted then all child tasks are halted first
25+- If a child task is halted or raises exception, it propagates error up the task
26+  tree
27+- An exception can be caught (e.g. `try`/`catch`) at any point in the task tree
A docs/posts/supervisors.md
+91, -0
 1@@ -0,0 +1,91 @@
 2+---
 3+title: Supervisors
 4+description: Learn how supervisor tasks work
 5+---
 6+
 7+[Supplemental reading from erlang](https://www.erlang.org/doc/design_principles/des_princ)
 8+
 9+A supervisor task is a way to monitor children tasks and probably most
10+importantly, manage their health. By structuring your side-effects and business
11+logic around supervisor tasks, we gain very interesting coding paradigms that
12+result is easier to read and manage code.
13+
14+The most basic version of a supervisor is simply an infinite loop that calls a
15+child task:
16+
17+```ts
18+function* supervisor() {
19+  while (true) {
20+    try {
21+      yield* call(someTask);
22+    } catch (err) {
23+      console.error(err);
24+    }
25+  }
26+}
27+```
28+
29+Here we call some task that should always be in a running and healthy state. If
30+it raises an exception, we log it and try to run the task again.
31+
32+Building on top of that simple supervisor, we can have tasks that always listen
33+for events and if they fail, restart them.
34+
35+```ts
36+import { parallel, run, takeEvery } from "starfx";
37+
38+function* watchFetch() {
39+  yield* takeEvery("FETCH_USERS", function* (action) {
40+    console.log(action);
41+  });
42+}
43+
44+function* send() {
45+  yield* put({ type: "FETCH_USERS" });
46+  yield* put({ type: "FETCH_USERS" });
47+  yield* put({ type: "FETCH_USERS" });
48+}
49+
50+await run(
51+  parallel([watchFetch, send]),
52+);
53+```
54+
55+Here we create a supervisor function using a helper `takeEvery` to call a
56+function for every `FETCH_USERS` event emitted.
57+
58+However, this means that we are going to make the same request 3 times, we
59+probably want a throttle or debounce to prevent this behavior.
60+
61+```ts
62+import { takeLeading } from "starfx";
63+
64+function* watchFetch() {
65+  yield* takeLeading("FETCH_USERS", function* (action) {
66+    console.log(action);
67+  });
68+}
69+```
70+
71+That's better, now only one task can be alive at one time.
72+
73+Both thunks and endpoints simply listen for particular actions being emitted
74+onto the `ActionContext` -- which is just an event emitter -- and then call the
75+middleware stack with that action.
76+
77+Both thunks and endpoints support overriding the default `takeEvery` supervisor
78+for either our officially supported supervisors `takeLatest` and `takeLeading`,
79+or a user-defined supervisor.
80+
81+Because every thunk and endpoint have their own supervisor tasks monitoring the
82+health of their children, we allow the end-developer to change the default
83+supervisor -- which is `takeEvery`:
84+
85+```ts
86+const someAction = thunks.create("some-action", { supervisor: takeLatest });
87+dispatch(someAction()); // this task gets cancelled
88+dispatch(someAction()); // this task gets cancelled
89+dispatch(someAction()); // this tasks lives
90+```
91+
92+This is the power of supervisors and is fundamental to how `starfx` works.
A docs/posts/testing.md
+80, -0
 1@@ -0,0 +1,80 @@
 2+---
 3+title: Testing
 4+description: You don't need an HTTP interceptor
 5+---
 6+
 7+Need to write tests? Use libraries like `msw` or `nock`? Well you don't need
 8+them with `starfx`. If the `mdw.fetch()` middleware detects `ctx.response` is
 9+already filled then it skips making the request. Let's take the update user
10+endpoint example and provide stubbed data for our tests.
11+
12+```tsx
13+import { fireEvent, render, screen } from "@testing-library/react";
14+import { useDispatch, useSelector } from "starfx/react";
15+import { db } from "./schema.ts";
16+import { updateUser } from "./user.ts";
17+
18+function UserSettingsPage() {
19+  const id = "1";
20+  const dispatch = useDispatch();
21+  const user = useSelector((state) => db.users.selectById(state, { id }));
22+
23+  return (
24+    <div>
25+      <div>Name: {user.name}</div>
26+      <button onClick={() => dispatch(updateUser({ id, name: "bobby" }))}>
27+        Update User
28+      </button>
29+    </div>
30+  );
31+}
32+
33+describe("UserSettingsPage", () => {
34+  it("should update the user", async () => {
35+    // just for this test -- inject a new middleware into the endpoint stack
36+    updateUser.use(function* (ctx, next) {
37+      ctx.response = new Response(
38+        JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
39+      );
40+      yield* next();
41+    });
42+
43+    render(<UserSettingsPage />);
44+
45+    const btn = await screen.findByRole("button", { name: /Update User/ });
46+    fireEvent.click(btn);
47+
48+    await screen.findByText(/Name: bobby/);
49+  });
50+});
51+```
52+
53+That's it. No need for http interceptors and the core functionality works
54+exactly the same, we just skip making the fetch request for our tests.
55+
56+What if we don't have an API endpoint yet and want to stub the data? We use the
57+same concept but inline inside the `updateUser` endpoint:
58+
59+```ts
60+export const updateUser = api.post<{ id: string; name: string }>(
61+  "/users/:id",
62+  [
63+    function* (ctx, next) {
64+      ctx.request = ctx.req({
65+        body: JSON.stringify({ name: ctx.payload.name }),
66+      });
67+      yield* next();
68+    },
69+    function* (ctx, next) {
70+      ctx.response = new Response(
71+        JSON.stringify({ id: ctx.payload.id, name: ctx.payload.name }),
72+      );
73+      yield* next();
74+    },
75+  ],
76+);
77+```
78+
79+Wow! Our stubbed data is now colocated next to our actual endpoint we are trying
80+to mock! Once we have a real API we want to hit, we can just remove that second
81+middleware function and everything will work exactly the same.
A docs/posts/thunks.md
+63, -0
 1@@ -0,0 +1,63 @@
 2+---
 3+title: Thunks
 4+description: Thunks are tasks for business logic
 5+---
 6+
 7+Thunks are the foundational central processing units. They have access to all
 8+the actions being dispatched from the view as well as your global state. They
 9+also wield the full power of structured concurrency.
10+
11+As I've been developing these specialized thunks, I'm starting to think of them
12+more like micro-controllers. Only thunks and endpoints have the ability to
13+update state (think MVC). However, thunks are not tied to any particular view
14+and in that way are more composable. Thunks can call other thunks and you have
15+the async flow control tools from `effection` to facilitate coordination.
16+
17+Every thunk that's created requires a unique id -- user provided string. This
18+provides us with a handful of benefits:
19+
20+- User hand-labels each thunk created
21+- Better traceability
22+- Easier to debug async and side-effects
23+- Build abstractions off naming conventions (e.g. creating routers
24+  `/users [GET]`)
25+
26+They also come with built-in support for a middleware stack (like `express` or
27+`koa`). This provides a familiar and powerful abstraction for async flow control
28+for all thunks and endpoints.
29+
30+Each run of a thunk gets its own `ctx` object which provides a substrate to
31+communicate between middleware.
32+
33+```ts
34+import { call, createThunks, mdw } from "starfx";
35+
36+const thunks = createThunks();
37+// catch errors from task and logs them with extra info
38+thunks.use(mdw.err);
39+// where all the thunks get called in the middleware stack
40+thunks.use(thunks.routes());
41+thunks.use(function* (ctx, next) {
42+  console.log("last mdw in the stack");
43+  yield* next();
44+});
45+
46+// create a thunk
47+const log = thunks.create<string>("log", function* (ctx, next) {
48+  const resp = yield* call(
49+    fetch("https://log-drain.com", {
50+      method: "POST",
51+      body: JSON.stringify({ message: ctx.payload }),
52+    }),
53+  );
54+  console.log("before calling next middleware");
55+  yield* next();
56+  console.log("after all remaining middleware have run");
57+});
58+
59+store.dispatch(log("sending log message"));
60+// output:
61+// before calling next middleware
62+// last mdw in the stack
63+// after all remaining middleware have run
64+```
A docs/public/.gitkeep
+1, -0
1@@ -0,0 +1 @@
2+
A docs/static/main.css
+110, -0
  1@@ -0,0 +1,110 @@
  2+.sitemap {
  3+  column-count: 3;
  4+}
  5+
  6+.link-alt-adj,
  7+.link-alt-adj:visited,
  8+.link-alt-adj:visited:hover,
  9+.link-alt-adj:hover {
 10+  color: var(--link-color);
 11+  text-decoration: none;
 12+}
 13+
 14+.link-alt-adj:visited:hover,
 15+.link-alt-adj:hover {
 16+  text-decoration: underline;
 17+}
 18+
 19+.link-alt-hover,
 20+.link-alt-hover:visited,
 21+.link-alt-hover:visited:hover,
 22+.link-alt-hover:hover {
 23+  color: var(--hover);
 24+  text-decoration: none;
 25+}
 26+
 27+.link-alt-hover:visited:hover,
 28+.link-alt-hover:hover {
 29+  text-decoration: underline;
 30+}
 31+
 32+.link-alt,
 33+.link-alt:visited,
 34+.link-alt:visited:hover,
 35+.link-alt:hover {
 36+  color: var(--white);
 37+  text-decoration: none;
 38+}
 39+
 40+.link-alt:visited:hover,
 41+.link-alt:hover {
 42+  text-decoration: underline;
 43+}
 44+
 45+.hero {
 46+  padding: 5rem 0;
 47+}
 48+
 49+.features {
 50+  display: grid;
 51+  grid-template-columns: repeat(auto-fit, minmax(225px, 1fr));
 52+  gap: 1rem;
 53+}
 54+
 55+.features h3 {
 56+  border: none;
 57+}
 58+
 59+.mk-nav {
 60+  padding: 1rem;
 61+}
 62+
 63+.text-hdr {
 64+  color: var(--hover);
 65+}
 66+
 67+.text-underline-hdr {
 68+  border-bottom: 3px solid var(--hover);
 69+  padding-bottom: 3px;
 70+}
 71+
 72+.toc ul, ol {
 73+  list-style: none;
 74+  padding: 0 0 0 0.5rem;
 75+}
 76+
 77+.toc li {
 78+  margin: 0 0 0.15rem 0;
 79+}
 80+
 81+.toc li:first-child {
 82+  margin-top: 0;
 83+}
 84+
 85+.toc a {
 86+  color: var(--grey-light);
 87+}
 88+
 89+.current {
 90+  background-color: var(--blockquote-bg) !important;
 91+  border-right: 5px solid var(--blockquote);
 92+}
 93+
 94+.current a {
 95+  color: var(--white);
 96+}
 97+
 98+.current-page a {
 99+  color: var(--white);
100+}
101+
102+.pager {
103+  min-width: 150px;
104+}
105+
106+
107+@media only screen and (max-width: 600px) {
108+  .sitemap {
109+    column-count: 2;
110+  }
111+}
A docs/tmpl/base.layout.tmpl
+21, -0
 1@@ -0,0 +1,21 @@
 2+{{define "base"}}
 3+<!doctype html>
 4+<html lang="en">
 5+  <head>
 6+    <title>{{template "title" .}}</title>
 7+    <meta charset='utf-8'>
 8+    <meta name="viewport" content="width=device-width, initial-scale=1" />
 9+    <meta name="keywords" content="ngrok, sish, ssh, tunnel, self-hosted" />
10+    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
11+    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
12+    <link rel="stylesheet" href="https://pico.sh/syntax.css" />
13+    <link rel="stylesheet" href="https://pico.sh/smol.css" />
14+    <link rel="stylesheet" href="/main.css" />
15+    {{template "meta" .}}
16+  </head>
17+
18+  <body {{template "attrs" .}}>
19+    {{template "body" .}}
20+  </body>
21+</html>
22+{{end}}
A docs/tmpl/footer.partial.tmpl
+7, -0
1@@ -0,0 +1,7 @@
2+{{define "footer"}}
3+<footer>
4+  <div>
5+    Built by <a href="https://bower.sh">Eric Bower</a>
6+  </div>
7+</footer>
8+{{end}}
A docs/tmpl/home.page.tmpl
+58, -0
 1@@ -0,0 +1,58 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}{{.Data.Title}}{{end}}
 5+
 6+{{define "meta"}}
 7+{{end}}
 8+
 9+{{define "attrs"}}class="container"{{end}}
10+
11+{{define "body"}}
12+{{template "nav" .}}
13+
14+<main>
15+  <div class="flex flex-col gap-2">
16+    <div class="flex items-center justify-center hero">
17+      <div class="flex flex-col items-center gap-2">
18+        <h1 class="logo-header text-2xl">starfx</h1>
19+        <div class="text-center text-lg">A modern approach to side-effect and state management for web apps.</div>
20+        <a href="/getting-started" class="btn-link">GET STARTED</a>
21+      </div>
22+    </div>
23+
24+    <article class="features">
25+      <div class="box">
26+        <h3 class="m-0 p-0 text-lg">
27+          Task tree side-effect system
28+        </h3>
29+        <p>Leverage structured concurrency to express any async flow control logic</p>
30+      </div>
31+
32+      <div class="box">
33+        <h3 class="m-0 p-0 text-lg">
34+          An immutable and reactive data store
35+        </h3>
36+        <p>Redux meets zod for state management</p>
37+      </div>
38+
39+      <div class="box">
40+        <h3 class="m-0 p-0 text-lg">
41+          Data synchronization and caching
42+        </h3>
43+        <p>A powerful middleware system to handle all front-end business requirements</p>
44+      </div>
45+
46+      <div class="box">
47+        <h3 class="m-0 p-0 text-lg">
48+         React integration
49+        </h3>
50+        <p>Designed with React in mind</p>
51+      </div>
52+    </article>
53+  </div>
54+
55+  <hr class="my-4" />
56+
57+  {{template "footer" .}}
58+</main>
59+{{end}}
A docs/tmpl/nav.partial.tmpl
+18, -0
 1@@ -0,0 +1,18 @@
 2+{{define "nav"}}
 3+<nav class="mk-nav">
 4+  <div class="flex items-center justify-center gap-2 text-md">
 5+    <a href="/" class="link-alt">
 6+      starfx
 7+    </a>
 8+    <a href="/sitemap" class="link-alt">
 9+      sitemap
10+    </a>
11+    <a href="https://github.com/neurosnap/starfx" class="link-alt">
12+      github
13+    </a>
14+    <a href="https://discord.gg/frontside" class="link-alt">
15+      discord
16+    </a>
17+  </div>
18+</nav>
19+{{end}}
A docs/tmpl/post.page.tmpl
+62, -0
 1@@ -0,0 +1,62 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}{{.Data.Title}}{{end}}
 5+
 6+{{define "meta"}}{{end}}
 7+
 8+{{define "attrs"}}class="container-sm"{{end}}
 9+
10+{{define "body"}}
11+{{template "nav" .}}
12+
13+<main class="post">
14+  <h1 class="text-2xl text-underline-hdr text-hdr inline-block">{{.Data.Title}}</h1>
15+  <h2 class="text-xl">{{.Data.Description}}</h2>
16+
17+  <hr />
18+
19+  <article class="md">
20+    {{.Data.Html}}
21+  </article>
22+
23+  <div class="flex justify-between gap-2 my-4">
24+    {{if .Prev}}
25+      <div class="pager max-w-half flex items-center">
26+        <div class="flex flex-col items-start">
27+          <div class="text-sm font-grey-light">&lt;&lt; PREV</div>
28+          <a href="{{.Prev.GenHref}}" class="text-xl link-alt-adj">{{.Prev.Text}}</a>
29+        </div>
30+      </div>
31+    {{end}}
32+
33+    {{if .Next}}
34+      <div class="pager max-w-half flex items-center justify-end">
35+        <div class="flex flex-col items-end">
36+          <div class="text-sm font-grey-light">
37+            NEXT &gt;&gt;
38+          </div>
39+          <a href="{{.Next.GenHref}}" class="text-xl align-right link-alt-adj">{{.Next.Text}}</a>
40+        </div>
41+      </div>
42+    {{end}}
43+  </div>
44+</main>
45+
46+<div class="sitemap text-sm mb-4 text-center">
47+{{range .Sitemap -}}
48+  <div>
49+  {{- if (and $.Prev (eq $.Prev.GenHref .GenHref)) -}}
50+  <a href="{{.GenHref}}" class="link-alt-adj">{{.Text}}</a>
51+  {{- else if (and $.Next (eq $.Next.GenHref .GenHref)) -}}
52+  <a href="{{.GenHref}}" class="link-alt-adj">{{.Text}}</a>
53+  {{- else if (eq $.Href .GenHref) -}}
54+  <a href="{{.GenHref}}" class="link-alt-hover">{{.Text}}</a>
55+  {{- else -}}
56+  <a href="{{.GenHref}}" class="link-alt">{{.Text}}</a>
57+  {{- end -}}
58+  </div>
59+{{- end}}
60+</div>
61+
62+{{template "footer" .}}
63+{{end}}
A docs/tmpl/sitemap.page.tmpl
+22, -0
 1@@ -0,0 +1,22 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}{{.Data.Title}}{{end}}
 5+
 6+{{define "meta"}}{{end}}
 7+
 8+{{define "attrs"}}class="container-sm"{{end}}
 9+
10+{{define "body"}}
11+{{template "nav" .}}
12+
13+<main>
14+  <h1 class="text-2xl text-underline-hdr text-hdr inline-block">{{.Data.Title}}</h1>
15+  <h2 class="text-xl">{{.Data.Description}}</h2>
16+
17+  <hr />
18+
19+  {{template "toc" .}}
20+</main>
21+
22+{{template "footer" .}}
23+{{end}}
A docs/tmpl/toc.partial.tmpl
+23, -0
 1@@ -0,0 +1,23 @@
 2+{{define "toc"}}
 3+<div>
 4+  {{range $key, $value := .SitemapByTag}}
 5+    <div class="box my">
 6+      <h2 class="text-xl">{{$key}}</h2>
 7+      <ul>
 8+        {{range $value}}
 9+          <li>
10+            <a href="{{.GenHref}}">{{.Text}}</a>
11+            {{if .Children}}
12+              <ul>
13+              {{range .Children}}
14+                <li><a href="{{.ParentHref}}{{.GenHref}}">{{.Text}}</a></li>
15+              {{end}}
16+              </ul>
17+            {{end}}
18+          </li>
19+        {{end}}
20+      </ul>
21+    </div>
22+  {{end}}
23+</div>
24+{{end}}