SSR / Pages that work without JavaScript

State between two posts

A post starts from init. Whatever must outlive it lives where the next request reads it: domains-plus keeps its cart in a cookie, through Mirror.cookie (foldkit-mirror), which the server and the scripted page share:

ts
export const CartInCookie = Mirror.cookie(App, {
  name: 'cart',
  fields: [App.model.cart],
  keys: { cart: { codec: Cart.CartCookie } },
})

// The server, for every page: the cart's names in the request's cookie.
const held = CartInCookie.keysOf(request.headers.get('cookie'))
// The server, answering a post: the Set-Cookie for the cart the post reached.
CartInCookie.setCookie(reached, request)

The scripted page brings CartInCookie.wiring() into its assembly, and its Subscription writes document.cookie as the cart changes, so a cart filled with scripts is there on a reload without them, and the other way round.

  • Reading it is validating it. The cookie is the visitor's to edit. The mirror reads only its own keys, and nothing from a cookie past 4096 bytes; the server decodes its names with a Schema, drops repeats, keeps at most 50, and prices each again from its own data; a taken or unknown name is dropped.
  • The posted Message is validated three times: its Schema (a name has a length), the plan (an active Surface must list it), and update, which adds a name only if the Model read it as available, at the price it read.
  • Writing it needs the Model the post reached. SSR.entry's headers(request, model) is given it, and domains-plus answers a post with CartInCookie.setCookie(model, request). headers is called for every answer, a GET and a refusal too, so it writes the cookie only for a POST.
  • A page drawn with someone's cart is theirs. Answer it cache-control: private, no-store.
  • Answer a post with a redirect. answerPost: 'redirect' answers it 303 to the address it was posted to, so the page the reader sees is a GET, and a reload asks for it again instead of posting twice. What the post changed reaches that GET through the cookie headers wrote; the page shows nothing else of the post. A refused post is still a 400.

The cookie keeps the cart's names alone, apart by a +: a name holds no price, so the cart's codec (Cart.CartCookie) encodes the names and refuses to decode them, and the server reads them with Cart.CartNames and prices each. 50 names of the longest a search makes (a 63-character label and the longest extension) take 3,959 of the cookie's 4096 bytes; past that the mirror writes nothing and reports CookieTooLarge, and the cookie keeps the cart it held. The server reads at most 50 names, however short.