- 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
Makefile
+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"
+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
+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)
+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+)
+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=
+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+}
+6,
-0
1@@ -0,0 +1,6 @@
2+---
3+title: API
4+description: Our API for public consumption
5+---
6+
7+WIP
+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+```
+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+```
+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+---
+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+```
+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)
+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+```
+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+---
+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+```
+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
+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.
+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.
+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+```
+1,
-0
1@@ -0,0 +1 @@
2+
+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+}
+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}}
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}}
+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}}
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}}
+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"><< 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 >>
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}}
+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}}
+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}}