Now build it
Exercises
Each lesson's key functions with the bodies removed and the tests left in. Read the post, then make the tests pass.
The exercise copies live in clojure/ex/exercises/lessonNN and
go/exercises/lessonNN; each blank is a TODO that throws, with a hint above
it. At a REPL: clj -A:ex, then (require 'exercises.lessonNN.ns-test :reload) and
(clojure.test/run-tests 'exercises.lessonNN.ns-test). The reference lessons are the answer key.
Part 1 · HTML over the wire
cd clojure && clojure -M:ex -d ex -n exercises.lesson01.<ns>-test
cd go && go test ./exercises/lesson01/
sse-eventClojure clojure/ex/exercises/lesson01/hypermedia.clj
Return the text of one SSE event: "event: <type>", then "data: <line>" for each data line, each line ended by a newline, and one empty line at the end.hello-responseClojure clojure/ex/exercises/lesson01/hypermedia.clj
Return a Ring response map: status 200, Content-Type text/event-stream, Cache-Control no-cache, and a body that is one datastar-patch-elements event whose single data line is "elements " followed by the greeting HTML.
Part 2 · The event stream
cd clojure && clojure -M:ex -d ex -n exercises.lesson02.<ns>-test
cd go && go test ./exercises/lesson02/
send-eventClojure clojure/ex/exercises/lesson02/event_stream.clj
Return the event text in this order: "event: <type>", then "id: <id>" when id is given, then "retry: <ms>" when retry is given and is not 1000, then "data: <line>" for each data line, then one empty line. Every line ends with "\n".patch-elementsClojure clojure/ex/exercises/lesson02/event_stream.clj
Build the data lines, leaving out defaults: "selector …" if given, "mode …" unless outer, "useViewTransition true" if true, "viewTransitionSelector …" if given with a view transition, "namespace …" unless html, then "elements <line>" for each line of the HTML (none when html is nil). Send them with send-event.patch-signalsClojure clojure/ex/exercises/lesson02/event_stream.clj
Encode signals as JSON unless it is already a string. Send "onlyIfMissing true" first when that option is true, then "signals <line>" for each line of the JSON.execute-scriptClojure clojure/ex/exercises/lesson02/event_stream.clj
Make a <script> tag with each attribute as name="value", plus data-effect="el.remove()" when auto-remove is on. Send it as a patch-elements event that appends to the selector body.
Part 3 · Signals
cd clojure && clojure -M:ex -d ex -n exercises.lesson03.<ns>-test
cd go && go test ./exercises/lesson03/
price-centsClojure clojure/ex/exercises/lesson03/signals.clj
Add the drink's base price, the size delta, and the milk delta (no milk adds nothing).read-signalsClojure clojure/ex/exercises/lesson03/signals.clj
Take the JSON text from the "datastar" query parameter for GET and DELETE, and from the request body for other methods. Parse it with keyword keys. Return {} when there is nothing to read, when the text is not valid JSON, or when the JSON is not an object.quote-responseClojure clojure/ex/exercises/lesson03/signals.clj
Make a draft from the signals. If draft-error finds a problem, render [:p#quote.error message]. Otherwise render [:p#quote "Server price: " [:strong total]] where total is price × qty in dollars. Return a one-shot SSE response with one patch-elements event.
Part 4 · When to use a signal
cd clojure && clojure -M:ex -d ex -n exercises.lesson04.<ns>-test
cd go && go test ./exercises/lesson04/
drink-options-viewClojure clojure/ex/exercises/lesson04/where_state_lives.clj
Return hiccup for div#drink-options. It holds select#size (data-bind:size) with one option per size of the drink, and select#milk (data-bind:milk) with one option per milk, or [:p.muted "No milk"] when the drink takes no milk. Mark the chosen size and milk as selected. A sold-out milk is disabled and its label ends with " (sold out)".drink-options-responseClojure clojure/ex/exercises/lesson04/where_state_lives.clj
Read drink, size and milk from the signals. Find the valid choice. Send two events: first patch #drink-options with the view in replace mode, then patch only the signals that changed (size, milk). A missing milk is sent as "" so the bound select keeps a string.add-to-cartClojure clojure/ex/exercises/lesson04/where_state_lives.clj
Return nil when draft-error finds a problem (use the state's sold-out set). Otherwise add the line {:drink :size :milk :qty} to the end of the cart for sid and return the new state.
Part 5 · Shaping the backend
cd clojure && clojure -M:ex -d ex -n exercises.lesson05.<ns>-test
cd go && go test ./exercises/lesson05/
handle-commandClojure clojure/ex/exercises/lesson05/routes.clj
Return the next state for each command type: :add-to-cart (append line to sid's cart when line-error is nil), :remove-from-cart (drop the line at idx), :place-order (turn a non-empty cart into a :placed order for a non-blank customer, then empty the cart), :advance (move an order one step along next-status; an order that reaches :picked-up leaves :orders), :toggle-stock (add or remove milk from :sold-out).command-responseClojure clojure/ex/exercises/lesson05/routes.clj
Return a one-shot SSE response that patches exactly the regions this command type changed (see the docstring), rendered from the new state.wrap-datastar-requestClojure clojure/ex/exercises/lesson05/routes.clj
Return a handler that calls `handler` only when the request header datastar-request is "true", and answers 403 otherwise.
Part 6 · One stream, many viewers
cd clojure && clojure -M:ex -d ex -n exercises.lesson06.<ns>-test
cd go && go test ./exercises/lesson06/
view-forClojure clojure/ex/exercises/lesson06/live.clj
Return the hiccup for <main id="app"> for this viewer: a customer tab sees its own draft and error, its browser's cart and only its browser's orders; the board sees every order and the milk stock. (customer-view and board-view are already written.)broadcastClojure clojure/ex/exercises/lesson06/live.clj
Lock the hub, then read the current state from !state. For every subscriber, render (view-for state viewer) to an HTML string. If it differs from the subscriber's :last, call its send-fn with a patch-elements event of that HTML and record it as :last, but only if the subscriber is still in the hub: its stream may close mid-broadcast. If one subscriber throws, print the error to *err* and skip it. Return the ids that were sent to.stream-lifecycleClojure clojure/ex/exercises/lesson06/live.clj
Make a fresh id for this stream. On open: for a viewer with a :tabid, run an :open-tab command (sid, tabid, this stream's id, the draft); subscribe send-fn with the viewer (without :draft); send it the whole current view right away (a reconnect needs it too). On close: unsubscribe, then run :close-tab for that tab and this stream.
Part 7 · Replacing the React Native app (optional)
cd clojure && clojure -M:ex -d ex -n exercises.lesson07.<ns>-test
cd go && go test ./exercises/lesson07/
shell-requestClojure clojure/ex/exercises/lesson07/native_shell.clj
The shell appends `BrewShell/1` to its User-Agent. Return true when the request's user-agent header contains it, false otherwise (including no header).bridge-scriptClojure clojure/ex/exercises/lesson07/native_shell.clj
Return an execute-script event whose script calls window.BrewShell.post with msg as a JSON object literal. The JSON must be safe inside a <script> tag: write every < as \u003c, so text such as "</script>" or "<!--<script>" in a customer's name cannot end or confuse the tag.ready-notificationsClojure clojure/ex/exercises/lesson07/native_shell.clj
Return one {:sid … :message …} per order that is :ready in new but was not :ready in old, in order-id order. The message reads "Ada, your order #1 is ready."