Files
rose-ash/sx/sx/protocols.sx
giles 36a0bd8577 Move sx docs markup from Python to .sx files (Phase 2)
Migrate ~2,500 lines of SX markup from Python string concatenation in
essays.py to proper .sx defcomp definitions:

- docs-content.sx: 8 defcomps for docs pages (intro, getting-started,
  components, evaluator, primitives, css, server-rendering, home)
- protocols.sx: 6 defcomps for protocol documentation pages
- essays.sx: 9 essay defcomps (pure content, no params)
- examples.sx: template defcomp receiving data values, calls highlight
  internally — Python passes raw code strings, never SX
- reference.sx: 6 defcomps for data-driven reference pages

essays.py reduced from 2,699 to 619 lines. Docs/protocol/essay
functions become one-liners returning component names. Example functions
use sx_call to pass data values to the template. Reference functions
pass data-built component trees via SxExpr.

renders.py: removed _code, _example_code, _placeholder,
_clear_components_btn (now handled by .sx templates).
helpers.py: removed inline hero code building, uses ~sx-home-content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:22:17 +00:00

99 lines
7.4 KiB
Plaintext

;; Protocol documentation pages — fully self-contained
(defcomp ~protocol-wire-format-content ()
(~doc-page :title "Wire Format"
(~doc-section :title "The text/sx content type" :id "content-type"
(p :class "text-stone-600"
"sx responses use content type text/sx. The body is s-expression source code. The client parses and evaluates it, then renders the result into the DOM.")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash")))
(~doc-section :title "Request lifecycle" :id "lifecycle"
(p :class "text-stone-600"
"1. User interacts with an element that has sx-get/sx-post/etc.")
(p :class "text-stone-600"
"2. sx.js fires sx:beforeRequest, then sends the HTTP request with SX-Request: true header.")
(p :class "text-stone-600"
"3. Server builds s-expression tree, scans CSS classes, prepends missing component definitions.")
(p :class "text-stone-600"
"4. Client receives text/sx response, parses it, evaluates it, renders to DOM.")
(p :class "text-stone-600"
"5. sx.js fires sx:afterSwap and sx:afterSettle.")
(p :class "text-stone-600"
"6. Any sx-swap-oob elements are swapped into their targets elsewhere in the DOM."))
(~doc-section :title "Component definitions" :id "components"
(p :class "text-stone-600"
"On full page loads, component definitions are in <script type=\"text/sx\" data-components>. On subsequent sx requests, missing definitions are prepended to the response body. The client caches definitions in localStorage keyed by a content hash."))))
(defcomp ~protocol-fragments-content ()
(~doc-page :title "Cross-Service Fragments"
(~doc-section :title "Fragment protocol" :id "protocol"
(p :class "text-stone-600"
"Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments that other services compose into their pages. Fragment endpoints return text/sx or text/html.")
(p :class "text-stone-600"
"The frag resolver is an I/O primitive in the render tree:")
(~doc-code :code (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp")))
(~doc-section :title "SxExpr wrapping" :id "wrapping"
(p :class "text-stone-600"
"When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly in the render tree. When it returns text/html, it's wrapped in a ~rich-text component that inserts the HTML via raw!. This allows transparent composition across service boundaries."))
(~doc-section :title "fetch_fragments()" :id "fetch"
(p :class "text-stone-600"
"The Python helper fetch_fragments() fetches multiple fragments in parallel via asyncio.gather(). Fragments are cached in Redis with short TTLs. Each fragment request is HMAC-signed for authentication."))))
(defcomp ~protocol-resolver-io-content ()
(~doc-page :title "Resolver I/O"
(~doc-section :title "Async I/O primitives" :id "primitives"
(p :class "text-stone-600"
"The sx resolver identifies I/O nodes in the render tree, groups them, executes them in parallel via asyncio.gather(), and substitutes results back in.")
(p :class "text-stone-600"
"I/O primitives:")
(ul :class "space-y-2 text-stone-600"
(li (span :class "font-mono text-violet-700" "frag") " — fetch a cross-service fragment")
(li (span :class "font-mono text-violet-700" "query") " — read data from another service")
(li (span :class "font-mono text-violet-700" "action") " — execute a write on another service")
(li (span :class "font-mono text-violet-700" "current-user") " — resolve the current authenticated user")))
(~doc-section :title "Execution model" :id "execution"
(p :class "text-stone-600"
"The render tree is walked to find I/O nodes. All nodes at the same depth are gathered and executed in parallel. Results replace the I/O nodes in the tree. The walk continues until no more I/O nodes are found. This typically completes in 1-2 passes."))))
(defcomp ~protocol-internal-services-content ()
(~doc-page :title "Internal Services"
(~doc-note
(p "Honest note: the internal service protocol is JSON, not sx. Sx is the composition layer on top. The protocols below are the plumbing underneath."))
(~doc-section :title "HMAC-signed HTTP" :id "hmac"
(p :class "text-stone-600"
"Services communicate via HMAC-signed HTTP requests with short timeouts:")
(ul :class "space-y-2 text-stone-600 font-mono text-sm"
(li "GET /internal/data/{query} — read data (3s timeout)")
(li "POST /internal/actions/{action} — execute write (5s timeout)")
(li "POST /internal/inbox — ActivityPub-shaped event delivery")))
(~doc-section :title "fetch_data / call_action" :id "fetch"
(p :class "text-stone-600"
"Python helpers fetch_data() and call_action() handle HMAC signing, serialization, and error handling. They resolve service URLs from environment variables (INTERNAL_URL_BLOG, etc) and fall back to public URLs in development."))))
(defcomp ~protocol-activitypub-content ()
(~doc-page :title "ActivityPub"
(~doc-note
(p "Honest note: ActivityPub wire format is JSON-LD, not sx. This documents how AP integrates with the sx rendering layer."))
(~doc-section :title "AP activities" :id "activities"
(p :class "text-stone-600"
"Rose Ash services communicate cross-domain writes via ActivityPub-shaped activities. Each service has a virtual actor. Activities are JSON-LD objects sent to /internal/inbox endpoints. RSA signatures authenticate the sender."))
(~doc-section :title "Event bus" :id "bus"
(p :class "text-stone-600"
"The event bus dispatches activities to registered handlers. Handlers are async functions that process the activity and may trigger side effects. The bus runs as a background processor in each service."))))
(defcomp ~protocol-future-content ()
(~doc-page :title "Future Possibilities"
(~doc-note
(p "This page is speculative. Nothing here is implemented. It documents ideas that may or may not happen."))
(~doc-section :title "Custom protocol schemes" :id "schemes"
(p :class "text-stone-600"
"sx:// and sxs:// as custom URI schemes for content addressing or deep linking. An sx:// URI could resolve to an sx expression from a federated registry. This is technically feasible but practically unnecessary for a single-site deployment."))
(~doc-section :title "Sx as AP serialization" :id "ap-sx"
(p :class "text-stone-600"
"ActivityPub objects could be serialized as s-expressions instead of JSON-LD. S-expressions are more compact and easier to parse. The practical barrier: the entire AP ecosystem expects JSON-LD."))
(~doc-section :title "Sx-native federation" :id "federation"
(p :class "text-stone-600"
"Federated services could exchange sx fragments directly — render a remote user's profile card by fetching its sx source from their server. This requires trust and standardization that doesn't exist yet."))
(~doc-section :title "Realistic assessment" :id "realistic"
(p :class "text-stone-600"
"The most likely near-term improvement is sx:// deep linking for client-side component resolution. Everything else requires ecosystem adoption that one project can't drive alone."))))