|
| 1 | +# My Mental Model of Azoth |
| 2 | + |
| 3 | +*A document where I (the LLM) make explicit my understanding of Azoth, for review and correction.* |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## What I Believe I Understand |
| 8 | + |
| 9 | +### JSX → DOM Literals |
| 10 | + |
| 11 | +When I write `<p>hello</p>` in Azoth, the expression evaluates to an actual `HTMLParagraphElement`. This is not a description or instruction — it's the real DOM node, ready to use. |
| 12 | + |
| 13 | +**Contrast with React:** React JSX returns a plain object (`{ type: 'p', props: {...} }`) that describes what to render. A reconciler later creates/updates actual DOM. |
| 14 | + |
| 15 | +**Implication:** I can immediately use DOM APIs on JSX output: |
| 16 | +```jsx |
| 17 | +const list = <ul><li>one</li><li>two</li></ul>; |
| 18 | +const items = [...list.children]; // Works immediately |
| 19 | +list.querySelector('li').classList.add('first'); // Standard DOM |
| 20 | +``` |
| 21 | + |
| 22 | +### No Framework Layer To Go Through |
| 23 | + |
| 24 | +This is critical: React returns plain objects because **rendering must happen within React's infrastructure**. React needs to control the process so it knows what's going on. All changes and activity must go through the React framework. |
| 25 | + |
| 26 | +In Azoth, **the compiled/transpiled code IS the runtime**. There's no framework layer mediating between your code and the DOM. The JSX compiles to code that directly creates and returns actual DOM objects. You authored JSX, you get DOM. |
| 27 | + |
| 28 | +**Contrast:** React owns the render cycle. Azoth gets out of your way. |
| 29 | + |
| 30 | +### Just JavaScript, Just DOM |
| 31 | + |
| 32 | +The "binding" in Azoth is really just **async subscription** — it's just JavaScript. |
| 33 | + |
| 34 | +This seems counterintuitive after years of state-driven frameworks, but Azoth lets you think in: |
| 35 | +- **JavaScript** — normal values, functions, async patterns |
| 36 | +- **DOM** — the actual web platform API |
| 37 | +- **Web platform primitives** — no proprietary abstractions |
| 38 | + |
| 39 | +**The API around JSX is the DOM:** |
| 40 | +- Outputs are DOM nodes |
| 41 | +- Inputs are DOM nodes, JS values, or JS async data structures |
| 42 | +- Everything can be reasoned about as normal JavaScript |
| 43 | + |
| 44 | +**No boundary** between Azoth and any web platform technology. Use any browser API, any DOM method, any JavaScript pattern — they all just work. |
| 45 | + |
| 46 | +**Contrast with other frameworks:** Even SolidJS has quirky non-standard JavaScript behavior arising from transpilation rules. Azoth avoids this by keeping the abstraction layer minimal and aligned with the platform. |
| 47 | + |
| 48 | +### No Virtual DOM, No Reconciliation |
| 49 | + |
| 50 | +Azoth doesn't maintain a shadow tree to diff against. The DOM I create IS the state. This is consistent with the original vision for HTML in the browser — the DOM is the source of truth. |
| 51 | + |
| 52 | +**Contrast with React:** React diffs virtual trees and batches updates. You work with state, React syncs to DOM. |
| 53 | + |
| 54 | +### Updates Come From Async Data Structures |
| 55 | + |
| 56 | +Azoth accepts asynchronous data structures in JSX child expressions: |
| 57 | +- Promises |
| 58 | +- Async generators |
| 59 | +- Web streams |
| 60 | +- Observables |
| 61 | + |
| 62 | +When these async sources fire (promise resolves, generator yields, stream emits, observable fires), a new HTML payload gets delivered through the transpiled runtime code. |
| 63 | + |
| 64 | +**Concrete example from tests:** |
| 65 | +```jsx |
| 66 | +async function* Items() { |
| 67 | + yield <p>Loading...</p>; // DOM shows: <p>Loading...</p> |
| 68 | + yield <ul><li>one</li></ul>; // DOM shows: <ul>... (replaces previous) |
| 69 | + yield <ul><li>one</li><li>two</li></ul>; // DOM shows: updated list |
| 70 | +} |
| 71 | + |
| 72 | +// In JSX: <div>{Items()}</div> |
| 73 | +``` |
| 74 | + |
| 75 | +Each yield delivers **JSX** (DOM), not just values. The DOM starts with a comment anchor (`<!--0-->`), then each yield **replaces** the previous content. It's not value interpolation — it's DOM replacement. |
| 76 | + |
| 77 | +**The model:** Changes to the UI happen as change instructions over time. This is hypermedia thinking — similar to what HTMX talks about, but more comprehensive and client-side rather than server-oriented. |
| 78 | + |
| 79 | +### The Fundamental Model: Events = Deltas |
| 80 | + |
| 81 | +This is the core of Azoth's architecture. The **event-driven** view and the **hypermedia** view are the same thing: |
| 82 | + |
| 83 | +``` |
| 84 | +ui₀ = initial render (page load event) |
| 85 | +ui₁ = ui₀ + Δ (promise resolved) |
| 86 | +ui₂ = ui₁ + Δ (generator yielded) |
| 87 | +ui₃ = ui₂ + Δ (stream emitted) |
| 88 | +ui₄ = ui₃ + Δ (user clicked button) |
| 89 | +... |
| 90 | +uiₙ = uiₙ₋₁ + Δ (any event) |
| 91 | +``` |
| 92 | + |
| 93 | +**Event sources:** |
| 94 | +1. **Page load** — initial render |
| 95 | +2. **Async data structures** — promises, generators, streams, observables firing |
| 96 | +3. **DOM user events** — clicks, inputs, etc. wired up during render |
| 97 | + |
| 98 | +**The events ARE the deltas.** Each event delivers a change instruction (Δ) to the current UI state. |
| 99 | + |
| 100 | +DOM user events are a major driver of this event-driven architecture. During initial (or subsequent) renders, event handlers get wired to DOM elements. These handlers can: |
| 101 | +- Dispatch new async actions (which deliver future deltas) |
| 102 | +- Make synchronous changes directly (e.g., via observables) |
| 103 | + |
| 104 | +The DOM events themselves come from outside the Azoth boundary — they're just standard DOM event handling. But they integrate naturally because Azoth IS the DOM. |
| 105 | + |
| 106 | +- No full re-render |
| 107 | +- No virtual DOM diffing |
| 108 | +- Just incremental change instructions applied directly to the DOM |
| 109 | + |
| 110 | +The DOM is the app state. The UI evolves through a sequence of deltas, each triggered by an event. |
| 111 | + |
| 112 | +### Alignment with the Original Web Platform |
| 113 | + |
| 114 | +This event-driven, delta-based model isn't novel — **it's how the browser was designed to work from the beginning**. |
| 115 | + |
| 116 | +The web platform has always been event-driven: |
| 117 | +- DOM events fire directly on elements |
| 118 | +- Event handlers trigger changes |
| 119 | +- The DOM is the source of truth |
| 120 | + |
| 121 | +React and similar frameworks introduced an **abstraction layer** on top of this: |
| 122 | +- Virtual DOM as an intermediary |
| 123 | +- Synthetic events wrapping native events |
| 124 | +- Component state separate from DOM state |
| 125 | +- Reconciliation to sync the two |
| 126 | + |
| 127 | +**Azoth rejects this abstraction as an impedance mismatch.** The virtual DOM model adds: |
| 128 | +- Complexity (diffing, reconciliation, batching) |
| 129 | +- Overhead (memory, CPU for virtual tree operations) |
| 130 | +- Cognitive load (reasoning about two states: component state AND DOM state) |
| 131 | +- Boundary friction (escaping the framework to use platform APIs) |
| 132 | + |
| 133 | +By working directly with the DOM, Azoth aligns with: |
| 134 | +- The browser's native event loop |
| 135 | +- Direct DOM manipulation APIs |
| 136 | +- The original vision of HTML as a living document |
| 137 | + |
| 138 | +**This is a return to fundamentals**, not a step backward. Modern browsers are highly optimized for direct DOM operations. The abstractions that made sense in 2013 (cross-browser inconsistencies, performance workarounds) are less necessary today. |
| 139 | + |
| 140 | +### Azoth as "Missing Browser Pieces" |
| 141 | + |
| 142 | +A design philosophy: Azoth fills gaps in the web platform, not replaces it. |
| 143 | + |
| 144 | +**What the browser is missing:** |
| 145 | +1. **DOM literals in JavaScript** — no native syntax for `<p>hello</p>` yielding a DOM node |
| 146 | +2. **Async → DOM integration** — no simple way to pipe async data structures into DOM rendering |
| 147 | + |
| 148 | +Azoth provides exactly these two things. Nothing more. |
| 149 | + |
| 150 | +*Tongue-in-cheek:* Azoth is a suggestion for rounding out the web platform. It takes the **one good idea from React** (JSX as a declarative DOM syntax) and integrates it with the platform rather than building a parallel universe on top of it. |
| 151 | + |
| 152 | +JSX without the baggage. Async without the complexity. DOM without the abstraction. |
| 153 | + |
| 154 | +### HTML Attributes vs DOM Properties |
| 155 | + |
| 156 | +The distinction is about **static vs dynamic**: |
| 157 | + |
| 158 | +- **Static attribute value** (no interpolation) → compiled into the HTML template |
| 159 | +- **Dynamic value** (JSX interpolation `{...}`) → DOM property assignment at runtime |
| 160 | + |
| 161 | +```jsx |
| 162 | +// Static: becomes HTML attribute in template |
| 163 | +<input name="title" required /> |
| 164 | + |
| 165 | +// Dynamic: becomes property assignment at runtime |
| 166 | +<input value={title} /> |
| 167 | +``` |
| 168 | + |
| 169 | +**Current state:** Attributes need attribute names, properties need DOM property names. Future work: translation layer so developers can use either interchangeably. |
| 170 | + |
| 171 | +**Interesting pattern:** You can use BOTH on the same element — static attribute for initial HTML (no flash of unstyled content), plus dynamic property for runtime updates. |
| 172 | + |
| 173 | +**Contrast with React:** React requires `className`/`htmlFor` not because of "programmatic setting" but because React's JSX transpiles to JavaScript where `class` and `for` are reserved keywords. Azoth doesn't need this guard because static attributes go directly to HTML templates, never touching JS runtime as property names. |
| 174 | + |
| 175 | +### Thoth and Maya |
| 176 | + |
| 177 | +- **Thoth** = Compiler. Transforms JSX into templates + binding code. |
| 178 | +- **Maya** = Runtime. Provides composition and rendering services. |
| 179 | + |
| 180 | +### Core Insight: DOM Changes Are Limited |
| 181 | + |
| 182 | +From years of DOM research (including co-maintaining RactiveJS with Rich Harris before Svelte), a key observation: **there are really only two types of DOM changes:** |
| 183 | + |
| 184 | +1. Change attributes/properties on elements |
| 185 | +2. Manage 0-to-n lists of nodes |
| 186 | + |
| 187 | +Everything else is derived from these primitives. |
| 188 | + |
| 189 | +### Maya's Architecture Layers |
| 190 | + |
| 191 | +Maya provides **opt-in levels of sophistication**. React-like concepts exist but you opt INTO them. |
| 192 | + |
| 193 | +These are **compositional building blocks** for state management: |
| 194 | + |
| 195 | +| Layer | Purpose | Complexity | |
| 196 | +| ------------ | -------------------------------------------------------------- | ---------- | |
| 197 | +| **compose** | Value integration, initial render, streaming | Simplest | |
| 198 | +| **replace** | Swap content at anchor | Simple | |
| 199 | +| **blocks** | Replicated templates, list operations (add/remove/update rows) | Medium | |
| 200 | +| **renderer** | Cache DOM, replay bindings, "UI = f(state)" for a section | Advanced | |
| 201 | + |
| 202 | +You combine these as needed for your state management approach. |
| 203 | + |
| 204 | +### Maya's Compose Engine |
| 205 | + |
| 206 | +`compose.js` is the heart of Maya's runtime. It's a **set of rules for any type of value** that resolves inputs to DOM output. |
| 207 | + |
| 208 | +**The resolution chain:** If compose needs a DOM result, it keeps deriving: |
| 209 | +- Function? Call it, compose the result |
| 210 | +- Class? Instantiate it, compose the result |
| 211 | +- Promise? Wait for it, compose the resolved value |
| 212 | +- Async iterator? Subscribe, compose each yielded value |
| 213 | +- Array? Compose each element |
| 214 | +- Node? Insert it |
| 215 | +- String/number? Create text node |
| 216 | + |
| 217 | +**`compose` vs `create`:** |
| 218 | +- `create` = for Component syntax in JSX (class/function components) |
| 219 | +- If top-level JSX is a Component, it creates once and returns the result |
| 220 | +- If a Component is inside a larger JSX snippet with intrinsic elements, it must fully resolve to DOM via compose |
| 221 | + |
| 222 | +**Replace vs Accumulate:** |
| 223 | +- Default behavior: new values **replace** previous content |
| 224 | +- Exception: `ReadableStream` **adds** items (accumulates) |
| 225 | + |
| 226 | +**The anchor mechanism:** |
| 227 | +- Comment nodes (`<!--0-->`) serve as positional anchors |
| 228 | +- `anchor.data` tracks how many nodes were inserted |
| 229 | +- `clear(anchor)` removes previous nodes before replacement |
| 230 | +- `replace(anchor, input)` inserts before anchor and increments count |
| 231 | + |
| 232 | +### Blocks (List Management) |
| 233 | + |
| 234 | +The `blocks/` folder contains anchored blocks, keyed blocks, etc. — strategies for managing lists of nodes with specific operations: |
| 235 | +- Add rows |
| 236 | +- Remove rows |
| 237 | +- Update rows |
| 238 | +- Different keying strategies |
| 239 | + |
| 240 | +This is where you opt into more sophisticated list handling when needed. |
| 241 | + |
| 242 | +### SyncAsync: Initial Value + Async Data Structure |
| 243 | + |
| 244 | +A critical pattern: provide a **synchronous value** for immediate composition, plus an **async data structure** for future values. |
| 245 | + |
| 246 | +```jsx |
| 247 | +SyncAsync.from( |
| 248 | + <p>Loading...</p>, // Value: composed immediately (sync) |
| 249 | + fetchData().then(data => <Results data={data} />) // Async data structure: delivers future replacements |
| 250 | +) |
| 251 | +``` |
| 252 | + |
| 253 | +**The distinction:** |
| 254 | +- First argument: a **value** to compose directly (sync) |
| 255 | +- Second argument: an **async data structure** (Promise, generator, stream, observable) for future values |
| 256 | + |
| 257 | +**Why this matters:** |
| 258 | +- Many frameworks have async/sync tension (React is mostly async with sync opt-ins) |
| 259 | +- High UI/UX work often needs synchronous behavior |
| 260 | +- Azoth's library mentality: render sync NOW, follow up with async later |
| 261 | +- This is the hypermedia "swap" pattern |
| 262 | + |
| 263 | +**SDK naming:** Current naming (`SyncAsync`) needs improvement — a future refactoring target. |
| 264 | + |
| 265 | +**Future potential:** Compose could accept custom signals for behaviors beyond just replace/add — developer-controlled rendering behavior. |
| 266 | + |
| 267 | +### Renderer (Cached DOM + Replay) |
| 268 | + |
| 269 | +The renderer can **cache DOM nodes and replay bindings**. This is opting into "UI = f(state)" but for a **section** of DOM, not the whole app. |
| 270 | + |
| 271 | +**How it works:** Literally reuses the **same binding function** that applied initial values. No reconciliation process — just re-running the bindings on the cached DOM structure. |
| 272 | + |
| 273 | +**Limitations (by design):** |
| 274 | +- Meant for sections of DOM with data updates |
| 275 | +- Not for heavy recomposition of DOM blocks |
| 276 | +- Must reliably rebind to the initial DOM structure |
| 277 | + |
| 278 | +**Inverse of React's limitation:** |
| 279 | +- React: must keep `useState` calls in same order (invocation counting for consistency) |
| 280 | +- Azoth: must reliably rebind to initial DOM structure created on first render |
| 281 | + |
| 282 | +Different tradeoffs for different models. |
| 283 | + |
| 284 | +**Key distinction from traditional frameworks:** |
| 285 | +- Traditional frameworks = "spreadsheet editors" — any CRUD anywhere, framework manages all state ubiquitously |
| 286 | +- Azoth = DOM is state, you **choose** the appropriate strategy for each piece of functionality |
| 287 | + |
| 288 | +You pick the state management approach that fits your use case, rather than having one imposed globally. |
| 289 | + |
| 290 | +### Intentional Data Flow Design |
| 291 | + |
| 292 | +A consequence of Azoth's architecture: **you must understand your application's data flows**. |
| 293 | + |
| 294 | +In generic state management frameworks, you do state management the same way everywhere. In Azoth, you need to think about: |
| 295 | +- How will this part of the application be used? |
| 296 | +- How does data flow through it? |
| 297 | +- What types of changes are expected? |
| 298 | +- How should changes be applied? |
| 299 | + |
| 300 | +**This is intentional.** Solutions emerge that are: |
| 301 | +- Well thought out |
| 302 | +- Appropriate for how they're being used |
| 303 | +- Specific to the problem at hand |
| 304 | + |
| 305 | +This requires more upfront thinking but produces more efficient, purpose-built solutions rather than one-size-fits-all approaches. |
| 306 | + |
| 307 | +--- |
| 308 | + |
| 309 | +## Questions I Have |
| 310 | + |
| 311 | +1. ~~**Updates:** When data changes, how does the DOM update?~~ **ANSWERED:** Updates come from async data structures firing events (promise resolution, generator yield, stream/observable emission). The transpiled code handles delivering the new HTML payload. |
| 312 | + |
| 313 | +2. ~~**Bindings:**~~ **ANSWERED from tests:** If `name` is a primitive string, it's inserted once. If it's a Promise, the content appears when it resolves. If it's an async generator, each yield replaces the previous content. The "binding" is really just the async subscription. |
| 314 | + |
| 315 | +3. **Components:** I know components exist but we haven't touched them. Are they functions? Classes? Something else? |
| 316 | + |
| 317 | +4. ~~**Fragments:**~~ **ANSWERED:** Nothing special. `<>...</>` returns a standard DocumentFragment. Thoth may collapse unnecessary fragments during compilation, but otherwise they work as expected per the DOM spec. |
| 318 | + |
| 319 | +5. ~~**The runtime exports:**~~ **ANSWERED:** `compose` = resolve any value to DOM and insert at anchor. `createComponent` = instantiate a component and return result. `composeComponent` = instantiate and compose into an anchor. `renderer` = cache DOM + replay bindings for "UI = f(state)" sections. |
| 320 | + |
| 321 | +--- |
| 322 | + |
| 323 | +## Where My React Bias Might Be Showing |
| 324 | + |
| 325 | +1. ~~**Thinking in re-renders:**~~ **REFRAMED:** The right question isn't "when does it re-render?" — Azoth is **event-driven**. Page load is the initial event. All subsequent DOM changes are caused by async activity set in motion. There's no render cycle, just events triggering DOM updates. |
| 326 | + |
| 327 | +2. **Component lifecycle:** I assume there's mount/unmount behavior, but maybe DOM elements just... exist and get removed? |
| 328 | + |
| 329 | +3. **State management:** I assume something manages state, but you said there's "no direct tie to state management" — just primitives and async constructs. |
| 330 | + |
| 331 | +4. ~~**Expecting a framework boundary:**~~ **CONFIRMED:** Azoth has no such boundary. There's no "inside Azoth" vs "escaping Azoth" — it's all just JavaScript and DOM. Use any web platform API directly. |
| 332 | + |
| 333 | +--- |
| 334 | + |
| 335 | +## Inconsistencies I Notice In My Understanding |
| 336 | + |
| 337 | +1. ~~I said "DOM is the state" but also there's a "binding mechanism"~~ **RESOLVED:** The compiled code sets up listeners for async sources. When they fire, the DOM gets updated. DOM is still the state — the bindings are just the plumbing that delivers updates. |
| 338 | + |
| 339 | +2. Template extraction is "invisible to developers" but I keep mentioning it — should I stop thinking about it? *Probably yes for developer docs, but it's relevant for understanding compilation.* |
| 340 | + |
| 341 | +3. ~~I'm unclear if Azoth is "no framework" or "minimal framework"~~ **CLARIFIED:** The transpiled code IS the runtime. Maya provides services that the transpiled code uses, but there's no framework layer you "go through." It's more like a library the compiled output calls. |
| 342 | + |
| 343 | +--- |
| 344 | + |
| 345 | +*Last updated: Based on smoke.test.tsx work and pivot-feasibility planning* |
0 commit comments