Skip to content

Commit 2f4fdfe

Browse files
committed
start readme adventure
1 parent 152271b commit 2f4fdfe

File tree

1 file changed

+92
-2
lines changed

1 file changed

+92
-2
lines changed

README.md

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,95 @@
1-
# HIFI-CRUD
1+
# HIFI CRUD
22

3-
> Hyperlith Is For Immediate CRUD
3+
> **H**yperlith **I**s **F**or **I**mmediate **CRUD**
44
5+
This is an experiment in building backend-driven web apps where `view = f(state)` is strictly enforced.
56

7+
It is built on top of [`hyperlith`](https://github.com/andersmurphy/hyperlith), the opinionated fullstack [Datastar](https://data-star.dev/) framework. It extends its opinions in radical directions.
8+
9+
*What is this for?* HIFI CRUD is an exploration of *simple* yet scalable ways to build business applications. By "business" applications I mean not only back-office dashboards, admin apps, but also customer-facing B2B applications with zero to (tens of) thousands of users.
10+
11+
*Who is this for?* HIFI CRUD explores how solo developers (or very small teams) can leverage Clojure's superpowers to rapidly build applications from day one without a complex stack and without having to throw everything away after the prototype phase.
12+
13+
## Architecture Patterns
14+
15+
HIFI CRUD distinguishes itself from your standard Clojure CRUD web application with the patterns:
16+
17+
- **Immediate mode** - Changes to state re-renders the client.
18+
- **State in the backend** - State that would traditionally (post-2013) live on the client, lives in the backend instead.
19+
- **Strict CQRS** - Commands are strictly separate from the page render functions.
20+
- **Strict FC/IS** - Functional Core/Imperative Shell. Your functional core cannot[^1] cause side effects, all core functions are pure. The shell contains no logic.
21+
22+
Pages are rendered with a single top-level function that takes state as input
23+
24+
## The View
25+
26+
The view is a pure function of state. What is a view? In HIFI CRUD a view is a page. `/login`, `/dashboard`, `/invoices`, these are all views.
27+
28+
Each view has a single render function whose arguments are *values*. They return a hiccup representing the HTML for the page.
29+
30+
There are no "partials" or "components" in the traditional sense. On every change the entire view is recomputed.
31+
32+
Of course views are just functions calling functions, so you can pull common functionality (dialogs, forms, buttons, etc) into functions accessible from a shared namespace.
33+
34+
## The State
35+
36+
There are two sources of backend state:
37+
38+
1. The database
39+
2. Ephemeral per-tab ui state
40+
41+
The example uses [datahike][datahike], an in-process Datomic like database. Why not datalevin? Because the database must implement value semantics, this is non-negotiable [^2].
42+
43+
Entity state, jobs, and sessions, are stored in the database. Every time the database changes, all clients pages are re-rendered and pushed to the client if they changed. This sounds dumb. But rendering the views is fast, and you can always add smart culling later on. This requires a big shift in UX thinking, the page can change underneath the user. You must design appropriately.
44+
45+
Ephemeral UI state is stored in an in-memory map (atom), this is useful for things like modals, popups, and other UI elements that are not part of the page. This state is not persisted to the database, and is not shared between tabs.
46+
47+
I waffle on this feature. The state stored in this map could easily be moved to the database, but it doesn't add much complexity to the code and keeps potentially noisy txs out of the database.
48+
49+
## The Command
50+
51+
The command is an action issued by a user, a robot, or a background job with the intent of changing the state of the application (and possible the outside world!). Commands are not a part of the view, they are separate, but their code can be co-located for developer convenience.
52+
53+
Commands are issued to the `/cmd` endpoint which takes a query parameter `cmd` and a body. The body is a JSON object that contains command data. The command is a keyword that identifies the command to be executed. Client commands are triggered by datastar `@post()` actions.
54+
55+
A command handler is a pure function that takes the command data, and possibly other coeffects such as the current database value, the current time, etc. The command handler returns a map of outcomes that describe the side effects that should be performed as a result of the command. The command handler does not change anything! It merely computes what should be changed and conveys that to the shell as data.
56+
57+
## The Effect
58+
59+
If the command returns a description of what *should* be done, the effect is the responsible for *doing* it. Every effect has an [effect handler][src-fx]. The effect handler touches the world and makes things happen.
60+
61+
Examples of effects are:
62+
63+
- `:db/transact` - submits a transaction to the database
64+
- `:d*/merge-signals` - sends a datastar merge-signals SSE event
65+
- `:email/send` - attempts to send an email
66+
67+
## The Co-effect
68+
69+
~~Sometimes~~ Often a command handler needs a value that is not part of the State and not part of the command data, but is nevertheless required to compute the outcome of the command. Obtaining this value requires an impure operation.
70+
71+
These values are called co-effects. The command declares what co-effects it requires. They are prepared and passed to the command handler as values along with the State and command data.
72+
73+
Examples of co-effects are:
74+
75+
- The current time
76+
- A random uuid
77+
- The current user
78+
79+
## The Functional Core
80+
81+
The functional core is the part of the application that is pure. It does not touch the world. Values go into the core, and values come out. The core contains the domain, the business logic, the rules, the invariants, and the functions for manipulating and calculating the state.
82+
83+
The functional core should be where 80+% of your SLOC lives. The functional core can be reasoned about, tested, and understood in isolation.
84+
85+
The entrypoints to the functional core are the View and the Command.
86+
87+
## The Imperative Shell
88+
89+
The imperative shell is the engine that drives the application forward. It mediates with the outside world to accept inputs, pipe them through your functional core, and finally execute effects. The shell contains no knowledge of your domain.
90+
91+
92+
[^1]: well, this is isn't Haskell, if you call `(random-uuid)` in your pure function, that's on you. Shame!
93+
[^2]: ... for me! Please don't take anything personally. If you disagree I would love to hear your thoughts.
94+
[datahike]: https://github.com/replikativ/datahike
95+
[src-fx]: ./src/app/effects.clj

0 commit comments

Comments
 (0)