A reading list built on octane/data, rendered three ways from one set of
contracts and one server, built with Vite or with Rspack:
| Page | Renderer | Binding |
|---|---|---|
/ |
Octane (.tsrx), server-rendered |
octane/data/octane |
/react.html |
React 19 (.tsx), server-rendered |
octane/data/react |
/mixed.html |
Octane, hosting a React island through ReactCompat, browser-rendered |
both, sharing one store |
The Data guide walks through every piece used here. The Octane and React pages differ only in their view code and binding import. Everything else is shared:
src/shared/contracts.data.tsdeclares the reads and writes with Zod schemas. Declaring them does no I/O, so the server and both front ends import the same module. Its.data.tsname opts it into the build's contract check: Vite and Rspack fail the build if a contract the pages use has no handler, and generateoctane/data/manifest, whosecontractsandwritesthe session passes to the store.src/shared/server.tsserves them withcreateDataHandler. Each write records its operation ID with its effect, so a lookup can say whether it landed.src/shared/data.tsrxexportsdata, amodule serverfunction that answers every store request, andevents, which streams the shelf's changes. In the browser each is the compiler's stub, which calls the server; there is no data route.src/shared/session.tsopens a store per account overserverTransport(data), injects network faults, and settles uncertain writes by receipt lookup. Its account cookie stands in for a sign-in.src/server/index.tsanswers calls todataand renders the pages, throughcreateServerFunctionsfrom@octanejs/app-core/server-functions. One middleware authenticates both. A page renders with its own store, whosedatacalls run in process under the page request's account.store.dehydrate()goes into the page, and the browser's store starts from it, so hydration loads nothing. A page also takes POST:handler.forms(request, context)runs a form's native submission, then redirects back to the page or renders it again with the form's issues.
-
Load
/or/react.htmlwith JavaScript turned off. The shelf and its counts are in the HTML. With JavaScript on, the Network panel stays empty until your first edit: hydration reuses the server's data. -
Toggle a book. The checkbox changes, and the book moves into or out of Finished, before the server replies. Both lists and the React island's list are views (
view()in the contracts), so the store keeps each one sorted and complete under any edit. The Network panel shows one request: the write, whose reply carries the saved book for the views and the counts it affected. -
Add a title already on the shelf. The new row appears, then disappears when the server refuses it. The title stays in the box, with the reason under it.
-
Add and remove books with JavaScript turned off. Both are native forms (
useFormover a write that declaresformfields). Each submission posts to the page, and the server redirects back or shows the refusal. With JavaScript on, the same forms submit through the store instead. -
Lose the next reply, then add a book. The server saves it but the reply is dropped. The row stays, marked as uncertain, until Check saved result asks the server. Nothing is sent twice.
-
Go offline and toggle a book. The edit stays until a lookup, once back online, shows it never arrived. Then it is removed.
-
Reload
/mixed.html. The lists come from the store's encrypted IndexedDB cache (persistinsession.ts), so only the counts, which are never fresh, load again. A real app takes the cache key from its signed-in session. -
Go offline, toggle a book, then reload. The writes are durable (
journalinsession.ts): the edit comes back after the reload, and a receipt lookup rolls it back, because the server never got it. Nothing is sent twice. -
Switch account. The old account's store is retired: its values clear, its cache is deleted, and late replies are ignored.
-
Open the same page in a second tab, then edit the shelf in one. The other tab shows the change at once, counts included, and its Network panel stays empty: each page listens to
shelfChanges(useFeed), and each event carries the changed book and the new counts. -
On the mixed page, mark a book read from the React island. The Octane list updates in the same moment, and a rollback reaches both.
pnpm --filter shelf-example dev # Vite, http://127.0.0.1:5239
pnpm --filter shelf-example dev:rspack # Rspack, rebuilt and restarted on each change
pnpm --filter shelf-example typecheck
pnpm --filter shelf-example test:e2e # both production buildsBoth builds run the same server, createShelfServer().fetch. A small Vite
plugin in vite.config.ts mounts it: from source through
Vite's SSR loader in development, and from dist/server in preview.
rspack.mjs builds the pages and
src/server/node.ts, a Node server over the same
fetch, into dist-rspack.