tcmts.server

server

The server API of a tcm app, as a library: the request, cookies, sessions and rate limits, typed tables and transactions, and background jobs, as ordinary definitions you can read, pin and upgrade. tcm ships it, so tcm lib add tcm server adds the release your tcm has, with no network (and tcm lib upgrade server the one a newer tcm has). It's a tcm library, not an npm package: it holds everything its code uses, so a codebase that adds it has every definition named.

import { HttpError, enqueue, every, request, table, transaction } from "#codebase/app/lib/server";

export type Note = { title: string; body: string; author: string };
export const notes = table<Note>("notes", { index: ["author"] });

export async function addNote(title: string, body: string): Promise<string> {
  const author = request().headers.get("x-user") ?? "";
  if (!author) throw new HttpError(401, "sign in first");
  return transaction(async () => {
    const id = await notes.insert({ title, body, author });
    enqueue(notify, author, id);
    return id;
  });
}
export async function notify(author: string, id: string): Promise<void> {
  console.log(`${author} wrote ${id}`);
}
export const nightly = every("0 3 * * *", async () => console.log(await notes.count()));

What it has

tcm's runtime

It runs on tcm's server host, #tcm/host: the call being answered, the app's SQLite files, the store's lock and transactions, and the job queue, one per process however many libraries use it. That's version 1 of tcm's host protocol, as its manifest says: a tcm that doesn't serve that version won't build an app using it. Its manifest gives table and every their roles, so a table declared with it keeps its row type's schema, and a definition every makes runs on its schedule, wherever it's mounted.

Its versions follow semver from its API, as any package's do (releases).

test/
8 definitions

Types

CookieOptions type
FindOptions type
HttpError typeof HttpError
A failure meant for the caller: its status and message reach the browser (other errors are hidden in production). It runs anywhere, so browser code can tell one apart (`error.name === "HttpError"`, `error.status`).
RecurringJob type
RowSchema type
The schema tcm attaches to a table where it's declared (`Symbol.for("tcm.schema")`): its row type's, with that schema's hash, and each migrate step's parameter type's.
ServerRequest type
The HTTP request a server function is answering.
Table typeof Table
A typed, durable table: rows of `T` in .tcm/data/store.sqlite, by id. The row type is erased at runtime, but tcm knows it where the table is declared and attaches its schema (with a hash) to the table. Every write is checked against it and records the schema it was written with. A read of a row written under another schema checks that it still fits, else migrates it, else refuses: stored data never silently disagrees with the code reading it. Rows from newer code mean the caller is a stale page (410).
TableOptions type

Jobs

every (spec: string, fn: () => unknown) => RecurringJob
A recurring job: `export const nightly = every("0 3 * * *", cleanup)` (five cron fields, UTC) or an interval like `every("15m", sync)`. tcm start / tcm dev run the jobs in the app's namespace, once per slot.

Server

callNow (what: string) => ServerCall
The call being answered, or why `what` can't be used here.
clearSession () => void
End the caller's session.
compareFields (a: unknown, b: unknown) => number
How `find` orders by a field: missing first, then numbers, dates, strings, anything else by its text.
cookie (name: string) => string | undefined
A cookie the request came with.
deleteCookie (name: string, opts?: { path?: string | undefined; }) => void
enqueue <A extends unknown[]>(fn: (...args: A) => unknown, ...args: A) => string
Run `fn(...args)` in the background, soon, with retries (5 attempts, exponential backoff); returns the job id. `fn` must be a codebase function: the job records its hash, so it runs the code version that enqueued it. Arguments must be plain data. In a transaction, the job is queued when it commits.
enqueueAfter <A extends unknown[]>(seconds: number, fn: (...args: A) => unknown, ...args: A) => string
Like enqueue, after `seconds`.
request () => ServerRequest
The HTTP request this server function is answering.
requestNow (what: string) => ServerCall & { request: ServerRequest; }
The call being answered, which must be a request (a background job isn't one).
revokeSessions (match: (data: Record<string, unknown>) => boolean) => number
End every session whose data matches, e.g. `revokeSessions((s) => s.user === "ada")`; returns how many.
runJobs (opts?: { all?: boolean | undefined; }) => Promise<{ done: number; failed: number; }>
For tests: run the queued jobs that are due (all of them with `{ all: true }`), inline.
session () => Record<string, unknown>
The caller's session data (empty if none, expired or revoked).
sessionOf (c: ServerCall) => { id: string; data: Record<string, unknown>; } | undefined
The session the call's request names, if it's live.
setCookie (name: string, value: string, opts?: CookieOptions) => void
Send a cookie with the answer: HttpOnly and SameSite=Lax unless `opts` says, for the app's base path.
setSession (data: Record<string, unknown>, maxAge?: number) => void
Start a new session with this data for `maxAge` seconds (default 30 days). The data is stored server-side (.tcm/data/sessions.sqlite); the cookie holds only a fresh, unguessable id.
sqlValue (v: unknown) => string | number | null
What `json_extract` gives for a stored value: scalars as themselves (true as 1), anything else as its text.
table <T>(name: string, opts?: TableOptions<T> | undefined) => Table<T>
A typed, durable table, declared at the top level: `export const users = table<User>("users")`. tcm attaches the row type's schema: writes are checked against it, and rows written under an older type are checked, migrated with `migrate` (if given) or refused when read. Data lives in .tcm/data/store.sqlite.
transaction <R>(fn: () => R | Promise<R>) => Promise<R>
Run `fn` as one transaction over every table: all its writes happen, or, if it throws, none do. While it runs, other requests' table calls wait, so none sees its uncommitted rows or writes in between. Jobs it enqueues are queued when it commits, and dropped if it rolls back. A transaction inside one is a savepoint. Keep it short (it holds the store), and don't await anything inside that itself waits for a table.
withRequest <T>(init: { method?: string | undefined; url?: string | undefined; headers?: Record<string, string>…
Run `fn` as if answering a request, for tests of server functions: no network (sessions kept in memory).

Shared

SESSION_COOKIE "tcm_sid"
The cookie a session's id is in: the data stays on the server, so a session can be revoked.
fieldSql (f: string) => string
A table row field as SQL: indexes are on exactly this expression (fields are checked, so it's inlined).
isScalar (v: unknown) => boolean
A value SQLite compares as itself: a string, a finite number, a boolean or null.
manifest { name: string; npm: boolean; host: number; roles: { table: string; every: string; }; }
The server API of a tcm app, as a library: the request, cookies, sessions, rate limits, typed tables, transactions and background jobs. It runs on tcm's server host (`#tcm/host`, host protocol 1); `table` and `every` have the roles of those names, so a table declared with `table` keeps its row type's schema, and a definition `every` makes runs on its schedule.
rateLimit (key: string, opts: { max: number; per: number; }) => void
Allow `max` calls per `per` seconds for `key` (e.g. "login:" + request().ip); beyond that, throws a 429.
rateWindows Map<string, number[]>
Rate limits' sliding windows, by key: in this process's memory.

Files

README file