Deploying: by moving a name — tcm-ts

30. Deploying: by moving a name

  • tcm build <app> writes an immutable, content-addressed build to .tcm/apps/<app hash>/ (an app with a pwa, the hash of both). An existing build is reused unless --force, or another tcm made it (what tcm puts beside the app, its runtime, is part of the build: a new tcm builds the app again). --static <dir> writes it out as a static site, for any static host, every page rendered as it's built, when the app calls no server functions (static sites). A React component app's build also holds a server-rendering bundle (server/, never served), which tcm start renders each page's first paint with (React).
  • Builds at once don't clash. Each build is made in a directory of its own (.tcm/build/<hash>.<pid>-<random>/) and put in its place whole, by one rename. Two builds of one app at once (two verifies, a tcm build while tcm start deploys) both succeed: the first in place stays and the other drops its own, the same build (with --force, or another tcm's in place, it's replaced, the old one moved aside first). A server reads the build before or the build after, never one half-written; there's no lock around builds. tcm dev's files are its own too (.tcm/web/<pid>-<random>/, removed when it stops).
  • Browser code ships in a few chunks, kept across deploys. React, other npm packages, tcm's runtime and the app's own code are each a chunk, named by its content, in .tcm/apps/assets/ (served under /_app/assets/, immutable), which every build shares. Chunks import each other through an import map in the page, so a chunk that didn't change keeps its URL even when one it imports did: a deploy that changes the app's code re-downloads only its chunk. So is each stylesheet <Stylesheet> links to (React), named by its hash. Assets are stored compressed (brotli and gzip) and served as the browser accepts.
  • An updated chunk arrives as the difference from the one the browser has. Each chunk is offered as a compression dictionary for its next version (Use-As-Dictionary), so a browser that has the app's previous chunk asks for the new one with it (Available-Dictionary), and gets it compressed against it (dcb): after a small change, a few hundred bytes instead of the chunk. Each difference is made once and kept (.tcm/apps/deltas/, pruned with the builds). Browsers without it (and anything in between that drops the headers) get plain brotli.
  • tcm start <app> serves the build the app's name points to now. When the name moves (tcm update, mv, reset, pull), it builds the new version if needed and switches to it. Deploying and rolling back are both moving a name, like Unison's service names pointing at service hashes. It also runs the job worker and schedules, and serves HTTP routes with --http.
  • There is no version skew. Earlier builds stay served, and their server functions stay callable. A tab opened before a deploy keeps loading its own assets and calling the exact code it was built against, until it reloads. tcmts.cli.test.e2e.prod deploys v2 and checks the v1 tab still works, then rolls back.
  • tcm apps lists builds; tcm apps prune --keep N removes old ones. It keeps the current build plus the N newest others; tcm start --keep N does this after every deploy. A tab still on a pruned build, on a kept build whose code the codebase no longer has (as the web UI's after tcm changes), or on a build this server never had (served from another copy of the codebase, or before a deploy that started afresh) gets 410 Gone from its next server call, and the browser runtime reloads the page onto the current version: tcm start answers any hash it doesn't serve with a 410. Pruning leaves what's in use, in any process: the build a running tcm start serves, and a build being made and its assets. Each holds a lease (.tcm/build/lease.<pid>-<random>.json), gone when it's done or its process ends.
  • Pushing is deploying. tcm push prod to a server running tcm start sends only the definitions it lacks. The server verifies them by re-hashing, then its tcm start sees the name move, builds and switches. There's no deploy script and no restart.
  • TLS. tcm start --tls serves HTTPS; cookies then default to Secure.
  • Errors. Unexpected errors are logged on the server and reach the browser only as "internal error" (server API). A call that fails is that call's 500: the server keeps running.
  • Data changes are covered in data: old pages that read rows written by newer code get a 410 and reload.

30.1 Under a path of a site

tcm start <app> --base /examples/notes serves the app under that path, beside other things on one site (tcm's docs site runs its examples so, behind tcm serve --proxy):

  • Requests outside the path aren't the app's (404), and the path itself redirects to it with its slash.
  • The page's assets, its server function calls (<base>/__tcm/…) and its plain HTTP routes are under the path. The build doesn't know where it's served: the page is told (__TCM_BASE__) as it's sent, so one build serves any path, and its chunks stay the same.
  • Cookies are for the path unless they say otherwise (setCookie(name, value, { path })), so apps on one site keep their sessions apart.
  • The react library's paths are within it: useLocation().path is / at the base, navigate("/about") goes to <base>/about, and href("/about") is a link's address (<a href>, or React Aria's RouterProvider useHref). Server rendering sees the same, so links in the first paint are right.
  • A plain HTTP route's Request has the address the caller used: under the path, and https: and the host a proxy in front says (x-forwarded-proto, x-forwarded-host).

built from c482a14e