Documentation
¶
Overview ¶
Package loom is a live-first web framework: pages render on the server from compiled .hx templates, and every change to a page's state is sent to the browser as a small diff over Server-Sent Events.
type TodoPage struct{ todos []templates.Todo }
func (p *TodoPage) Mount(c *loom.Ctx) {}
func (p *TodoPage) Assigns(c *loom.Ctx) templates.TodoListAssigns {
return templates.TodoListAssigns{Todos: p.todos}
}
func (p *TodoPage) Events() loom.Events {
return loom.Events{"add": p.add}
}
func (p *TodoPage) add(c *loom.Ctx, payload loom.Payload) {
p.todos = append(p.todos, templates.Todo{Title: payload["title"]})
}
func main() {
app := loom.New()
app.Live("/", &TodoPage{}, templates.TodoList)
app.Run(":4000")
}
Each page load gets its own copy of the page value, owned by one goroutine, so Mount and the event handlers never run concurrently for a page.
Index ¶
- Constants
- Variables
- func Get[T any](ctx context.Context, q Querier, query string, args ...any) (T, error)
- func InstalledName(pkg, file string) string
- func IsSealed(s string) bool
- func LayoutOf[T any](c *Ctx) T
- func LocalPath(p string) bool
- func Migrate(ctx context.Context, db *DB, fsys fs.FS) ([]data.Applied, error)
- func MigrateCommand(fsys fs.FS, args []string, stdout, stderr io.Writer) int
- func Migrations() fs.FS
- func RateLimit(limits ...Limit) rateLimitOption
- func Redirect(url string) error
- func RenderComponent(name string, assigns []byte) (*RenderResult, *Error)
- func Select[T any](ctx context.Context, q Querier, query string, args ...any) ([]T, error)
- func SubAppPackage(sub SubApp) string
- func User[T any](c *Ctx) T
- func Users[T any](app *App, load func(ctx context.Context, id string) (T, error))
- type App
- func (a *App) BaseURL() string
- func (a *App) Broadcast(topic, event string, payload map[string]string) error
- func (a *App) ClearSentMail()
- func (a *App) Cron(expr string, job jobs.Job)
- func (a *App) DB() *DB
- func (a *App) Dev() bool
- func (a *App) Emit(ctx context.Context, name string, attrs ...any)
- func (a *App) ErrorPage(tmpl render.Template, assigns any)
- func (a *App) Group(opts ...Option) *Group
- func (a *App) Handle(pattern string, h http.Handler)
- func (a *App) HasSecret() bool
- func (a *App) Inspect(sid string) (*SessionDetail, *Error)
- func (a *App) JobRunner() *jobs.Runner
- func (a *App) JobStore(s jobs.Store)
- func (a *App) Jobs(js ...jobs.Job)
- func (a *App) Live(path string, page Page, tmpl render.Template, opts ...RouteOption)
- func (a *App) Logger() *slog.Logger
- func (a *App) Mailer() Mailer
- func (a *App) Manifest() *Manifest
- func (a *App) Mount(prefix string, sub SubApp, opts ...Option)
- func (a *App) NewView(r *http.Request, sid string, layouts bool) (*View, error)
- func (a *App) NewViewFrom(from *View, r *http.Request, sid string, layouts bool) (*View, error)
- func (a *App) Open(purpose, sealed string) ([]byte, error)
- func (a *App) PubSub() pubsub.PubSub
- func (a *App) Queries() QueriesResult
- func (a *App) Queues(qs ...jobs.Queue)
- func (a *App) RemountView(from *View, r *http.Request, sid string, layouts bool) (*View, error)
- func (a *App) ReportError(err error)
- func (a *App) RootLayout(tmpl render.Template, assigns any)
- func (a *App) Routes() []RouteInfo
- func (a *App) Run(addr string) error
- func (a *App) Seal(purpose string, plaintext []byte) (string, error)
- func (a *App) SendEvent(sid, event string, payload Payload) (*EventResult, *Error)
- func (a *App) SentMail() []SentMail
- func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (a *App) SessionCookie(ctx context.Context, userID string) (*http.Cookie, error)
- func (a *App) Sessions() []SessionInfo
- func (a *App) SetLimitClock(now func() time.Time)
- func (a *App) SignIn(w http.ResponseWriter, r *http.Request, userID string) error
- func (a *App) SignOut(w http.ResponseWriter, r *http.Request) error
- func (a *App) SignOutEverywhere(ctx context.Context, userID string) error
- func (a *App) StartupCheck(check func(ctx context.Context) error)
- func (a *App) Stats() StatsResult
- func (a *App) Storage() storage.Bucket
- func (a *App) Stream() *trace.Stream
- func (a *App) Title() string
- func (a *App) Topics() map[string][]string
- func (a *App) URL(r *http.Request, p string) string
- func (a *App) UseDB(db *DB)
- func (a *App) UseJobRunner(r *jobs.Runner)
- func (a *App) UseMailer(m Mailer)
- func (a *App) Validate(ctx context.Context) error
- type Assigns
- type AuthFunc
- type AuthRequired
- type Config
- type Ctx
- func (c *Ctx) Asset(name string) string
- func (c *Ctx) CancelTimer(event string)
- func (c *Ctx) Context() context.Context
- func (c *Ctx) Fail(err error)
- func (c *Ctx) Get(key string) any
- func (c *Ctx) Meta(m Meta)
- func (c *Ctx) Navigate(url string)
- func (c *Ctx) NotFound()
- func (c *Ctx) Patch(url string, opts ...PatchOption)
- func (c *Ctx) Path(p string) string
- func (c *Ctx) SendAfter(d time.Duration, event string, payload Payload)
- func (c *Ctx) Set(key string, value any)
- func (c *Ctx) SignIn(userID, next string)
- func (c *Ctx) SignOut(next string)
- func (c *Ctx) Subscribe(topic string)
- func (c *Ctx) Title(title string)
- func (c *Ctx) TitleOr(def string) string
- func (c *Ctx) Topics() []string
- func (c *Ctx) URL(p string) string
- func (c *Ctx) UserID() string
- type DB
- type DBPoolInfo
- type DefaultRootAssigns
- type DevMode
- type Error
- type ErrorInfo
- type EventResult
- type Events
- type EventsResult
- type Group
- func (g *Group) App() *App
- func (g *Group) Group(opts ...Option) *Group
- func (g *Group) Handle(pattern string, h http.Handler)
- func (g *Group) Live(path string, page Page, tmpl render.Template, opts ...RouteOption)
- func (g *Group) Mount(prefix string, sub SubApp, opts ...Option)
- func (g *Group) Prefix() string
- type HasMigrations
- type Health
- type InfoHandler
- type JobsMode
- type Keeper
- type Limit
- type LimitError
- type LimitKey
- type Mail
- type Mailer
- type Manifest
- type ManifestEvent
- type ManifestFlag
- type ManifestHandler
- type ManifestLayout
- type ManifestMigration
- type ManifestMount
- type ManifestRoute
- type ManifestSecurity
- type Message
- type Meta
- type MigrateResult
- type Option
- func Assets(fsys fs.FS) Option
- func Auth(check AuthFunc) Option
- func DevOnly() Option
- func Layout(tmpl render.Template, assigns any) Option
- func LiveLayout(tmpl render.Template, state Page) Option
- func Middleware(mw func(http.Handler) http.Handler) Option
- func RootLayout(tmpl render.Template, assigns any) Option
- func SignedIn(signInPath string) Option
- type Page
- type PageError
- type ParamsHandler
- type PatchOption
- type Payload
- type PreviewInfo
- type Querier
- type QueriesResult
- type RedirectError
- type RenderResult
- type RouteInfo
- type RouteOption
- type RuntimeInfo
- type SentMail
- type SessionDetail
- type SessionInfo
- type StatsResult
- type StorageList
- type SubApp
- type Timer
- type TimerInfo
- type Tx
- type ValidateResult
- type View
- func (v *View) Assigns() Assigns
- func (v *View) Close()
- func (v *View) Document() (string, error)
- func (v *View) Event(name string, payload Payload) (map[string]any, error)
- func (v *View) Failures() []error
- func (v *View) HTML() string
- func (v *View) HandlesParams() bool
- func (v *View) Info(msg Message) (map[string]any, error)
- func (v *View) Kept() string
- func (v *View) LayoutAssigns() Assigns
- func (v *View) Meta() Meta
- func (v *View) Navigation() string
- func (v *View) NewTopics() []string
- func (v *View) NextTimer() (time.Time, bool)
- func (v *View) Patch(target string) (map[string]any, error)
- func (v *View) Patched() (url string, replace bool)
- func (v *View) Refresh() (map[string]any, error)
- func (v *View) Restored() string
- func (v *View) Route() string
- func (v *View) RunTimers() (map[string]any, error)
- func (v *View) SetClock(now func() time.Time)
- func (v *View) Tally() *trace.Tally
- func (v *View) Timers() []Timer
- func (v *View) Title() string
- func (v *View) Topics() []string
- func (v *View) Trace() string
- func (v *View) Tree() map[string]any
- func (v *View) URL() string
- func (v *View) Version() int
- func (v *View) Watches() []string
Constants ¶
const ( DefaultDBMaxConns = 20 DefaultStatementTimeout = 30 * time.Second DefaultSessionTimeout = 2 * time.Minute DefaultSignInFor = 30 * 24 * time.Hour DefaultMaxSessions = 10_000 DefaultSlowQuery = 500 * time.Millisecond )
Defaults for Config's zero values.
const ( RT401 = "RT401" // a page's assigns don't fit its component RT402 = "RT402" // Mount, an event handler, HandleInfo, Assigns or rendering panicked RT403 = "RT403" // a list has duplicate :key values RT404 = "RT404" // unknown or expired session RT405 = "RT405" // malformed event request RT406 = "RT406" // invalid page passed to App.Live RT407 = "RT407" // too many events waiting for a page RT408 = "RT408" // dev tools: unknown component RT409 = "RT409" // dev tools: assigns JSON doesn't fit the component RT410 = "RT410" // a page subscribed to a topic but has no HandleInfo RT411 = "RT411" // App.UseDB called too late RT412 = "RT412" // a layout's assigns func doesn't return its assigns type RT413 = "RT413" // a component can't be used as that kind of layout RT414 = "RT414" // no live route matches a path (App.NewView, loomtest.Mount) RT415 = "RT415" // a sub-app that requires auth is mounted without loom.Auth RT416 = "RT416" // invalid mount prefix, route path or assets RT417 = "RT417" // dev, browser: element does not match a behavior's requires RT418 = "RT418" // dev, browser: behavior emitted an undeclared event RT419 = "RT419" // a page reported an error with Ctx.Fail RT420 = "RT420" // dev, browser: an uncaught error or console.error on a page RT421 = "RT421" // loomtest: generated *_hx.go files are out of date with their templates RT422 = "RT422" // an event the page's markup sends has no handler in its Events RT423 = "RT423" // a page got an event it has no handler for RT424 = "RT424" // the server is restarting, or can't tell whether an event ran; the browser resends it RT425 = "RT425" // c.SendAfter names an event the page has no handler for RT426 = "RT426" // Ctx.Patch to a URL that isn't of the page's route RT427 = "RT427" // Ctx.Patch on a page with no HandleParams RT428 = "RT428" // a page's HandleParams has the wrong signature RT429 = "RT429" // a browser sent a POST (or other unsafe request) from another site RT430 = "RT430" // loom.User with no one signed in, or signing in an empty user id RT431 = "RT431" // loom.User's type isn't the one loom.Users registered, or there is no loom.Users RT432 = "RT432" // the sign-in session store failed RT433 = "RT433" // a mounted sub-app's migrations aren't installed and applied RT434 = "RT434" // a page and its live layout both handle an event RT435 = "RT435" // LayoutOf asked for a live layout the page doesn't have RT436 = "RT436" // too many attempts: over a rate limit (loom.RateLimit) RT437 = "RT437" // an invalid rate limit RT438 = "RT438" // an event no element on the page binds with on:* RT439 = "RT439" // LOOM_DEV=1 without LOOM_DEV_TOKEN: dev mode outside loom dev RT440 = "RT440" // dev mode on an address other machines can reach RT441 = "RT441" // an invalid loom.Config value RT442 = "RT442" // BaseURL is https but a request came over plain http from an untrusted sender RT443 = "RT443" // LOOM_ADDR set outside dev mode RT444 = "RT444" // too many live pages open: Config.MaxSessions RT445 = "RT445" // an event handler ran for a long time, holding its page RT446 = "RT446" // app.ErrorPage was given the wrong component or assigns func RT447 = "RT447" // a broadcast too large for Postgres NOTIFY reached only this node RT448 = "RT448" // c.NotFound called after Mount, where it can't answer 404 RT449 = "RT449" // c.Meta changed after the page loaded, where nothing reads it RT450 = "RT450" // c.Meta's StructuredData can't be encoded as JSON RT451 = "RT451" // loom dev can't tell which main package to run RT452 = "RT452" // a page's or live layout's Keep doesn't return a pointer to a JSON-encodable struct RT453 = "RT453" // pages keep state (Keep) but the app has no Config.Secret to seal it with RT454 = "RT454" // the state a page keeps is too large for a remount to send back )
Runtime error codes.
const BrowserCookie = "loom_browser"
BrowserCookie is the cookie that binds a page's session to the browser that loaded it. It is HttpOnly and lasts until the browser closes.
const DefaultCSP = "" /* 135-byte string literal not displayed */
DefaultCSP is Config.CSP's default. Style attributes are allowed (style={...} is part of the template language, and CSS can't run script); scripts, connections, frames and forms are the app's own.
const DevStorageDir = ".loom/storage"
DevStorageDir is where an app under `loom dev` with no Config.Storage keeps its files, relative to the directory it runs in, so they last across rebuilds.
const DevTokenHeader = "X-Loom-Dev-Token"
DevTokenHeader carries LOOM_DEV_TOKEN on requests through loom dev's proxy, which adds it to every request it forwards.
const HealthPath = "/_loom/health"
HealthPath answers load balancers and orchestrators: 200 while the app can serve, 503 once it is draining for shutdown or can't reach its database. It is served in production, unlike /_loom/dev/.
const MailTopic = "loom:mail"
MailTopic is the PubSub topic the dev outbox broadcasts on, event "sent", each time it keeps a mail, and event "cleared" when ClearSentMail empties it: a dev mailbox page subscribes to it to show new mail as it arrives.
const MaxKept = 6 << 10
MaxKept is the most bytes of sealed state a page and its live layout may keep: it travels in a request header, which proxies limit.
const MinSecretLen = 32
MinSecretLen is the shortest Config.Secret New accepts.
const SessionCookie = "loom_session"
SessionCookie is the name of the cookie holding the sign-in token.
Variables ¶
var ( ErrNotFound = data.ErrNotFound // DB604: Get found no row ErrUniqueViolation = data.ErrUniqueViolation // DB606 ErrForeignKeyViolation = data.ErrForeignKeyViolation // DB607 ErrNotNullViolation = data.ErrNotNullViolation // DB608 )
Errors to test for with errors.Is.
var ByIP = LimitKey{/* contains filtered or unexported fields */}
ByIP counts attempts per client IP address: the connection's, or the one a trusted proxy forwarded (Config.TrustedProxies).
var ByUser = LimitKey{/* contains filtered or unexported fields */}
ByUser counts attempts per signed-in user. Attempts with no one signed in aren't counted.
var DefaultRoot = render.Register(render.Component[DefaultRootAssigns]{Name: "DefaultRoot", Package: "loom", RootLayout: true, Render: renderDefaultRoot})
DefaultRoot is the component declared at default_root.hx:1:11.
var ErrForbidden = errors.New("forbidden")
ErrForbidden is returned by an auth check to refuse a request: a page load gets 403 Forbidden.
var ErrNoSecret = errors.New("loom: the app has no secret (loom.Config.Secret)")
ErrNoSecret is returned by Seal and Open when Config.Secret is empty.
var ErrPageNotFound = errors.New("page not found")
ErrPageNotFound is the error NewView returns for a page whose Mount called Ctx.NotFound.
var ErrSealed = errors.New("loom: the value can't be opened with this secret and purpose")
ErrSealed is returned by Open for a value that wasn't sealed with this secret and purpose: the secret changed, or the value was altered.
Functions ¶
func Get ¶
Get runs a query for one row and scans it into a T, as Select does. No row is an error matching ErrNotFound; more than one is DB605.
todo, err := loom.Get[Todo](ctx, db, `select * from todos where id = $1`, id)
func InstalledName ¶
InstalledName is the name (without number) a sub-app migration file has once installed: "0001_users.sql" of package .../auth is "auth_users". A name that already starts with the package's ("0001_loom_clients.sql" of Loom) keeps it.
func IsSealed ¶
IsSealed reports whether s is a value Seal made, rather than one stored before the app had a secret.
func LayoutOf ¶
LayoutOf returns the state of the page's live layout: the copy mounted for this page load, which the page may read and change in Mount, its handlers and Assigns. It panics with RT435 if the page's layout isn't a live layout of type T.
func (p *TeamPage) Mount(c *loom.Ctx) {
shell := loom.LayoutOf[*Shell](c)
p.user = shell.User
}
func LocalPath ¶
LocalPath reports whether p is a path on this site, so redirecting to it (a ?next= page, say) can't send anyone to another site. Browsers read "//host" and "/\host" as other sites, and drop tabs and newlines before reading, so "/\t/host" is one too: a local path starts with one slash and has no control characters or backslashes. Sub-apps that redirect to a page they were given check it with this.
func Migrate ¶
Migrate applies the pending migrations in fsys to db, in order, each in its own transaction, under an advisory lock so two deploys can't apply them at once, and returns those it applied. An edited migration that was already applied is DB614. See docs/data.md.
func MigrateCommand ¶
MigrateCommand runs `migrate [status] [--json] [--db URL]` against the migrations in fsys, as `loom migrate` does, writing to stdout and stderr, and returns the exit code: 0 done, 1 a migration or status problem (printed as text, or as {"code", "message", "fix"} with --json), 2 a usage or connection error. The database is --db, else $DATABASE_URL.
func Migrations ¶
Migrations returns Loom's own migrations, the files `loom db install github.com/moomerman/loom` copies into an app's migrations directory.
func RateLimit ¶
func RateLimit(limits ...Limit) rateLimitOption
RateLimit limits a group's routes (as an Option) or one route (as a RouteOption to Live). A group's limits count across all its routes and apply to its handlers; checks accumulate outer to inner, like Auth. An invalid limit panics with RT437.
func Redirect ¶
Redirect returns an error for an auth check that sends the browser to url, e.g. a login page, instead of serving the page.
func RenderComponent ¶
func RenderComponent(name string, assigns []byte) (*RenderResult, *Error)
RenderComponent renders any compiled component by name ("TodoApp" or "templates.TodoApp") with assigns given as JSON with template field names.
func Select ¶
Select runs a query and scans each row into a T: a struct whose fields take the columns of their db tag or snake_case name, or a single value for one column. See data.Select.
todos, err := loom.Select[Todo](ctx, db, `select * from todos where user_id = $1 order by id`, userID)
func SubAppPackage ¶
SubAppPackage returns the import path of a sub-app's package, as `loom db install` takes it.
func User ¶
User returns the signed-in user, loaded with the func given to Users, once per event. Use it on routes that require sign-in (SignedIn); on others check c.UserID() first. No one signed in is RT430; a T other than the one given to Users, or no Users at all, is RT431.
me := loom.User[*User](c)
func Users ¶
Users registers the app's user type and how to load one by id, so pages and layouts read the signed-in user typed, with User[T]. The SignedIn check loads the user too: a user the loader can't find (ErrNotFound) counts as signed out.
loom.Users(app, func(ctx context.Context, id string) (*User, error) {
return loom.Get[*User](ctx, app.DB(), `select * from users where id = $1`, id)
})
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is a Loom application.
func New ¶
New returns an App configured by cfg, at most one; see Config. It panics with RT441 on an invalid value.
app := loom.New(loom.Config{DB: os.Getenv("DATABASE_URL")})
func (*App) BaseURL ¶
BaseURL is the app's public address (Config.BaseURL), without a trailing slash; "" if not set.
func (*App) Broadcast ¶
Broadcast publishes a message to the pages subscribed to topic, as a job's c.Broadcast does. Call it in tx.AfterCommit to tell pages about a committed change:
tx.AfterCommit(func() { app.Broadcast("todos:"+userID, "changed", nil) })
func (*App) ClearSentMail ¶
func (a *App) ClearSentMail()
ClearSentMail empties the dev outbox and broadcasts "cleared" on MailTopic. IDs keep counting up, so a link to a cleared mail never shows a newer one.
func (*App) Cron ¶
Cron enqueues job on a cron schedule, once per cluster; see jobs.Runner.Cron. It panics on an invalid expression (JB504).
func (*App) DB ¶
DB returns the app's database: the one from Config.DB or UseDB, the job store's, or a local DB. Enqueue jobs in its transactions:
app.DB().Tx(ctx, func(tx loom.Tx) error {
_, err := jobs.Enqueue(tx, Export{UserID: id})
return err
})
func (*App) Emit ¶
Emit records an event on the app's stream, with its trace taken from ctx (a page's c.Context(), a job's ctx). attrs alternate keys and values, like slog's. Loom's own names are in docs/events.md; a sub-app's events use names of their own, prefixed with its name.
app.Emit(c.Context(), "billing.charge_failed", "user", userID, "code", code)
func (*App) ErrorPage ¶
ErrorPage sets the component a failed page load shows: a page that doesn't exist (404), an auth check that refuses (403), a page that fails to mount or render (500), too many attempts (429, RT436), or too many open pages (503). It renders inside the root layout of the route that failed (the app's, for a URL no route matches), with that route's assets, as a plain page with no session. assigns is a func(loom.PageError) A, where A is the component's assigns type; a wrong one panics with RT446 at startup.
app.ErrorPage(templates.Oops, func(e loom.PageError) templates.OopsAssigns {
return templates.OopsAssigns{Title: e.Title, Code: e.Code}
})
Without one, a plain page with the same sentence is shown. In dev, a failure shows the error itself instead, with its fix.
func (*App) Group ¶
Group returns a route group: routes added with its Live share its options. Groups nest; the nearest group's layout applies, and layouts don't stack (an inner layout that wants the outer one calls it). Auth checks and middleware accumulate, outer first (see mount.go).
g := app.Group(loom.Layout(templates.App, appAssigns))
g.Live("/", &TodoPage{}, templates.TodoApp)
func (*App) Handle ¶
Handle serves a plain HTTP handler at pattern, like Group.Handle, outside any group.
func (*App) HasSecret ¶
HasSecret reports whether the app has a secret (Config.Secret), so Seal and Open work.
func (*App) Inspect ¶
func (a *App) Inspect(sid string) (*SessionDetail, *Error)
Inspect returns a session's current assigns and HTML.
func (*App) JobRunner ¶
JobRunner returns the app's job runner, creating it if needed; set its fields to tune it before Run.
func (*App) JobStore ¶
JobStore sets where jobs are stored. It must be called before any other jobs method. The store's DB becomes the app's DB (app.DB()); if the app already has one, it must be the same. The default is a jobs.MemoryStore on app.DB().
func (*App) Jobs ¶
Jobs registers job types; see jobs.Runner.Register. It panics on an invalid type (JB502, JB503), at startup.
func (*App) Live ¶
Live serves a live page at path, in the root layout and with no app layout (see Group for app layouts). Each page load gets a shallow copy of page, so page should be a pointer to a struct, and any fields set on it (a database handle, say) are shared by every copy.
func (*App) Logger ¶
Logger is where the app logs (Config.Logger), for sub-apps and plain handlers that log alongside it.
func (*App) Mailer ¶
Mailer returns the app's mailer. Without one it is nil, except in dev (App.Dev), where it is an outbox that logs each mail and keeps the last ones for `loom mail`.
func (*App) Manifest ¶
Manifest describes the app's behaviour for review; see loom.manifest and docs/manifest.md.
func (*App) Mount ¶
Mount mounts a sub-app at prefix, like Group.Mount, in a group of the app's top level.
func (*App) NewView ¶
NewView mounts and renders the live page routed at r's path, as a GET would, without starting a session. With layouts false the page renders alone, without its app layout or root layout. loomtest.Mount uses it.
func (*App) NewViewFrom ¶
NewViewFrom mounts and renders the live page routed at r's path as a live navigation from the page from would: if both pages are in the same live layout (and root layout), the layout's state moves to the new View, unmounted again, and from does nothing more. If the new page fails to mount, from keeps its layout. loomtest's Navigate uses it.
func (*App) Open ¶
Open decrypts a value Seal made for purpose: ErrNoSecret without Config.Secret, ErrSealed if it wasn't sealed with this secret and purpose.
func (*App) PubSub ¶
PubSub is the app's PubSub (Config.PubSub): publish on it to reach pages subscribed with Ctx.Subscribe.
func (*App) Queries ¶
func (a *App) Queries() QueriesResult
Queries counts the statements the app's database ran since it started, the busiest first. It is empty without a database.
func (*App) RemountView ¶
RemountView mounts the page routed at r's path again, as a remount after a restart would: a fresh copy of the page and its live layout, given the state from kept (Keep) before Mount. from does nothing more. loomtest's Remount uses it.
func (*App) ReportError ¶
ReportError logs err and keeps it for `loom errors`, for a plain handler that answers the browser itself but must not hide what went wrong: auth's OAuth callback when a provider refuses its code, say. A *Error keeps its code and fix.
func (*App) RootLayout ¶
RootLayout sets the root layout of every route, except in groups that set their own with the loom.RootLayout option. assigns is a func(c *loom.Ctx) A, where A is the layout's assigns type: it runs once per page load, after Mount. Pass only request-scoped data, never per-user data, so the document stays cacheable (see docs/layouts.md).
app.RootLayout(templates.Root, func(c *loom.Ctx) templates.RootAssigns {
return templates.RootAssigns{Title: c.TitleOr("Todos")}
})
func (*App) Run ¶
Run serves the app on addr, e.g. ":4000", and runs its background jobs. Under `loom dev`, it serves on the address loom dev gives in LOOM_ADDR instead, behind the dev server. On SIGINT or SIGTERM it shuts down gracefully, losing no user's work: /_loom/health answers 503 at once, and it keeps serving for Config.DrainDelay while load balancers notice; then it stops accepting events (browsers resend them to the restarted server), lets every page finish the events already queued and running jobs finish, up to the job runner's ShutdownGrace, releases unfinished jobs to their queue, then closes the pages' streams, and browsers remount the pages when the server is back.
func (*App) Seal ¶
Seal encrypts plaintext for purpose (e.g. "auth.totp") with the app's secret. The result is text, safe to store in a column. ErrNoSecret without Config.Secret.
func (*App) SendEvent ¶
func (a *App) SendEvent(sid, event string, payload Payload) (*EventResult, *Error)
SendEvent sends an event to a session, as its browser would, and returns the page afterwards with the errors the event caused. Browsers showing the page get the diff as usual. It backs `loom event` and the send_event MCP tool.
func (*App) SentMail ¶
SentMail returns the mail the dev outbox kept, oldest first: the last 50 the app sent while it ran in dev with no mailer. It is empty outside dev, and when the app has a mailer. The devmail package shows it in the browser; `loom mail` shows it in the terminal.
func (*App) ServeHTTP ¶
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP serves the app's pages and the /_loom/ endpoints, gzipped when worth it (see compress.go).
func (*App) SessionCookie ¶
SessionCookie starts a session for userID and returns its cookie, without a browser: for tests (loomtest.SignIn) and tools. Apps sign people in with Ctx.SignIn or App.SignIn.
func (*App) Sessions ¶
func (a *App) Sessions() []SessionInfo
Sessions lists the running page sessions, oldest first.
func (*App) SetLimitClock ¶
SetLimitClock sets the clock rate limits count by, instead of time.Now. It is for tests: loomtest.LimitClock sets a fake one.
func (*App) SignIn ¶
SignIn signs userID in from a plain handler, such as an OAuth callback: it starts a session and sets its cookie on w. Redirect afterwards. Live pages use Ctx.SignIn.
func (*App) SignOut ¶
SignOut signs the request's user out from a plain handler: it ends the session and clears the cookie. Live pages use Ctx.SignOut.
func (*App) SignOutEverywhere ¶
SignOutEverywhere ends every session of userID, after a password change, say. Their open pages end at their next event.
func (*App) StartupCheck ¶
StartupCheck adds a check that Run makes before it serves, and Validate (so loomtest.Mount) makes too, after Loom's own. A sub-app uses it for what can only be checked once the app is configured, such as auth needing a mailer: an error stops the app from starting.
func (*App) Stats ¶
func (a *App) Stats() StatsResult
Stats counts the app's events since it started.
func (*App) Storage ¶
Storage returns the app's bucket: Config.Storage, or without one, a directory (DevStorageDir) under `loom dev`, memory in other dev mode (tests and tools, Config.Dev: DevOn), and outside dev a bucket whose every call fails with SG001.
func (*App) Stream ¶
Stream returns the app's event stream: the latest events, a live subscription, and counts per name.
func (*App) Topics ¶
Topics maps each PubSub topic the open pages subscribe to to the sessions subscribed, for /_loom/dev/topics.
func (*App) URL ¶
URL makes a path ("/c64/monty") an absolute URL, on BaseURL or, without one, the origin r came to, as link previews, sitemaps and mail need. A URL with a scheme, or "", is returned as it is. Pages use c.URL, which also resolves the path against the page's mount.
func (*App) UseDB ¶
UseDB sets the app's database to one already open, such as a test's loomtest.DB(t); apps give the URL in Config.DB instead. It must be called before any jobs method, so the app's job store binds to it.
func (*App) UseJobRunner ¶
UseJobRunner makes r the app's job runner, so app.Jobs, app.Queues and app.Cron (a sub-app's included) register on it, its store's DB becomes the app's DB as with JobStore, and its PubSub, if set, the app's. It is for tests that run jobs themselves (loomtest's JobQueue.Attach); Run starts r as it would its own. It must be called before any other jobs method.
func (*App) Validate ¶
Validate runs the checks Run makes before it serves, without serving or starting anything: the jobs configuration (JB510, JB501), the tables routes watch (DB622), sub-apps' migrations (RT433), the storage bucket (SG002 to SG005) and the checks added with StartupCheck. loomtest.Mount runs it, so a test fails where Run would.
type Assigns ¶
type Assigns = any
Assigns are the values a page renders its template with: the template's generated assigns struct, e.g. templates.TodoListAssigns.
type AuthFunc ¶
AuthFunc is an auth check: it returns nil to allow the request, ErrForbidden to refuse it, or Redirect(url) to send the browser elsewhere (to a login page, say). See Auth.
type AuthRequired ¶
AuthRequired is implemented by sub-apps that must not be mounted without an auth check, such as an admin dashboard. Mounting one in a group chain with no loom.Auth is a startup error (RT415). A sub-app that is public by design, such as a sign-in page, returns false, and the manifest doesn't flag it as mounted with no auth check.
type Config ¶
type Config struct {
// DB is the URL of the app's Postgres database, such as
// postgres://user:password@localhost:5432/myapp. Run checks the
// connection (DB601), and jobs are stored in it (pgjobs) unless
// app.JobStore says otherwise. Empty means no database: app.DB() is
// a local DB, which can't run SQL (DB602), and jobs are kept in
// memory.
DB string
// DBMaxConns caps the database connections the app opens. Table
// watch and the jobs runner each hold one for LISTEN, and the jobs
// leader one for its lock. Defaults to 20.
DBMaxConns int
// StatementTimeout ends any statement that runs longer, so a hung
// query fails instead of holding a page or a job runner forever.
// A job with a slow query raises it for its transaction with
// SET LOCAL statement_timeout. Defaults to 30 seconds.
StatementTimeout time.Duration
// Title is the document title of pages that don't set their own, in
// the default root layout. Defaults to "Loom".
Title string
// BaseURL is the app's public address, such as
// https://app.example.com: where links in mail point and OAuth
// providers send the browser back. Outside dev, auth needs it
// (AU902).
BaseURL string
// TrustedProxies are the reverse proxies whose X-Forwarded-For and
// X-Forwarded-Proto headers the app believes: the client's address
// and whether it used https. Loopback addresses are always trusted,
// for a proxy on the same machine; any other sender's forwarded
// headers are removed before a handler sees them.
TrustedProxies []netip.Prefix
// Secret encrypts values the app keeps but can't hash, such as
// two-factor secrets (App.Seal). Give it at least 32 random
// characters from the environment, the same on every node, e.g.
// os.Getenv("LOOM_SECRET") made with `openssl rand -base64 32`.
// Changing it makes what was sealed with the old one unreadable.
Secret string
// CSP is the Content-Security-Policy every response carries;
// DefaultCSP unless set. The default allows only the app's own
// scripts, styles and connections, so injected markup can't run
// script. Loosen it for what the app needs, e.g. DefaultCSP +
// "; img-src 'self' data: https://images.example.com" (the last
// directive of a name wins over none: write the whole directive).
CSP string
// Logger receives the app's events (docs/events.md) at the levels
// it enables: errors, warnings and lifecycle events at Info; the
// per-click and per-query events at Debug. Defaults to text on
// stderr at Info.
Logger *slog.Logger
// OnEvent, if set, receives every event, whatever Logger enables: the
// bridge to an error tracker, metrics or tracing system. It runs on
// the goroutine that emitted the event, so it must be quick and
// must not block; hand slow work to a channel.
OnEvent func(trace.Event)
// SlowQuery is how long a SQL statement runs before it is reported
// as db.slow. Defaults to 500ms.
SlowQuery time.Duration
// PubSub delivers messages to pages that call Ctx.Subscribe, and from
// jobs' Broadcast. By default messages reach this node's pages at once
// and, with a Postgres database, every other node's by NOTIFY
// (docs/deploy.md); without one they stay in-process.
PubSub pubsub.PubSub
// Storage is the bucket the app keeps files in (docs/storage.md):
// storage.S3(...) for an S3-compatible service such as AWS S3,
// Cloudflare R2 or MinIO; storage.Dir(path) for a directory on one
// server; storage.Memory() in tests. Run checks it can be used
// (SG002 to SG005). Nil means none: app.Storage() keeps files in
// .loom/storage under `loom dev`, and outside dev fails with SG001.
Storage storage.Bucket
// SessionTimeout is how long a page's session lives without a
// connected browser before it is stopped. Defaults to two minutes.
SessionTimeout time.Duration
// SignInFor is how long a sign-in lasts (users.go): the session
// cookie's lifetime. Defaults to 30 days.
SignInFor time.Duration
// MaxSessions caps the live pages open at once. A page load beyond
// it answers 503 (RT444), so a crawler can't exhaust memory.
// Defaults to 10,000.
MaxSessions int
// ShutdownGrace is how long shutdown waits for pages' queued events
// and running jobs. Defaults to jobs.DefaultShutdownGrace.
ShutdownGrace time.Duration
// DrainDelay is how long shutdown keeps serving after /_loom/health
// starts answering 503, so a load balancer stops sending requests
// before the server stops taking them. Zero (the default) suits one
// node with no balancer.
DrainDelay time.Duration
// Dev turns dev mode on or off: the /_loom/dev/ endpoints the loom
// CLI uses, the preview gallery, DevOnly groups and detailed error
// pages. DevFromEnv (the default) means on when LOOM_DEV is "1", as
// under `loom dev`, which also sets LOOM_DEV_TOKEN: then dev
// requests must carry that token, which loom dev's proxy adds
// (dev.go). DevOn is for tests and tools; never use it in
// production.
Dev DevMode
// InspectToken lets the loom CLI and MCP tools ask a deployed app
// about itself: requests carrying it as "Authorization: Bearer
// TOKEN" may read the /_loom/dev/ endpoints that show what the app
// does, from anywhere, with dev mode off (stats, queries, events,
// errors, sessions, routes, jobs, queues, nodes, cron), but not
// those that show a page's state or change anything. Empty (the
// default) means LOOM_INSPECT_TOKEN, and none if that is unset. At
// least MinSecretLen characters: make one with `openssl rand -hex 32`.
InspectToken string
// Jobs is what this node does with background jobs, so web and
// worker nodes can be deployed apart: JobsRun (the default) serves
// pages and runs jobs and cron; JobsEnqueueOnly serves pages and
// only enqueues, for web nodes when other nodes run the jobs;
// JobsOnly runs jobs and cron and answers only /_loom/health, for
// worker nodes. It is a string, so a deploy can set it from the
// environment: loom.JobsMode(os.Getenv("LOOM_JOBS")).
Jobs JobsMode
// Server, if set, adjusts the http.Server Run serves with: TLS, or
// other limits. Run sets ReadHeaderTimeout (10s), IdleTimeout (120s)
// and MaxHeaderBytes (1 MB) first. Leave WriteTimeout at zero: it
// would cut the pages' event streams.
Server func(*http.Server)
}
Config configures an App; pass it to New. It is the one place an app is configured: every field has a default, so the zero Config is an in-memory app for development.
app := loom.New(loom.Config{
DB: os.Getenv("DATABASE_URL"),
BaseURL: os.Getenv("BASE_URL"),
Title: "Tally",
})
New checks the values (RT441), and Run checks them against the environment it serves in (RT439, RT440, RT443).
type Ctx ¶
type Ctx struct {
SID string // the page's session id
Request *http.Request // the request that loaded the page
// contains filtered or unexported fields
}
Ctx is the context of a page call.
func (*Ctx) Asset ¶
Asset returns the URL of a file, or of a directory when name ends in "/", embedded by the page's group with loom.Assets: the Go twin of the asset template builtin. An unknown name panics.
func (*Ctx) CancelTimer ¶
CancelTimer cancels the page's pending timer for event, if any.
func (*Ctx) Context ¶
Context returns a context that lives as long as the page: it is cancelled when the page's session ends or the page restarts. Use it, not c.Request.Context(), for work in handlers and HandleInfo: the request that loaded the page has finished by then, and its context is cancelled. It carries the values of the request's context.
func (*Ctx) Fail ¶
Fail reports an error the page handled without panicking, such as a database write that failed in an event handler. The error is logged and listed by `loom errors` and the recent_errors MCP tool (RT419, or the error's own code, such as a DB6xx), and loomtest's Mount, Event and Info fail the test. The page keeps running with the assigns it returns, so return assigns that tell the user what happened:
if err := p.store.Add(ctx, title); err != nil {
c.Fail(err)
return p.assigns(c, "Couldn't save the todo; try again.")
}
For a bug the page can't recover from, panic instead: the page is restarted with a fresh Mount (RT402).
func (*Ctx) Meta ¶
Meta sets the page's metadata. Call it in Mount (or the HandleParams that runs with Mount): it is rendered with the document, on a page load or a live navigation, for link previews and search engines, which never see a later change. Changing it after the page has loaded is RT449, and StructuredData that encoding/json can't marshal is RT450; both are reported like c.Fail.
c.Meta(loom.Meta{Description: tune.Title + " by " + tune.Credit, Image: c.Asset("cards/c64.png")})
func (*Ctx) Navigate ¶
Navigate sends the browser to url after the current Mount, event handler or HandleInfo returns: a live navigation if url is a page with the same root layout, else an ordinary page load. In Mount, the page load answers with a redirect instead. The page's session ends when the browser leaves it.
func (p *NewIssuePage) create(c *loom.Ctx, payload loom.Payload) {
id := p.save(c, payload)
c.Navigate(c.Path("/issues/" + id))
}
func (*Ctx) NotFound ¶
func (c *Ctx) NotFound()
NotFound ends the page load with 404 Not Found and the app's error page, for a page whose URL names nothing, such as /tracks/{id} with no such track. Call it in Mount (or the HandleParams that runs with Mount) and return; Assigns doesn't run. A live navigation to the page loads the error page like a link would. Called later, in a handler, it is RT448: the page has loaded and can't answer 404.
func (p *TrackPage) Mount(c *loom.Ctx) {
t, err := p.store.Track(c.Context(), c.Request.PathValue("slug"))
if errors.Is(err, loom.ErrNotFound) {
c.NotFound()
return
}
...
}
func (*Ctx) Patch ¶
func (c *Ctx) Patch(url string, opts ...PatchOption)
Patch changes the page's URL to url, the same path with another query (relative URLs like "?view=active" resolve against the current one), after the current event handler or HandleInfo returns: the page's HandleParams runs with the new URL, the page re-renders, and the browser pushes the URL to its history (or replaces the current entry, with loom.Replace()). The session and the rest of the page's state stay as they are. Build the URL from the state it should hold, and let HandleParams set that state:
func (p *TeamPage) pick(c *loom.Ctx, payload loom.Payload) {
q := p.query()
q.Set("priority", payload["value"])
c.Patch(c.Path("/team/" + p.key + "?" + q.Encode()))
}
A URL of another route or with other path values is RT426 and a page without HandleParams is RT427; either way the page reports the error and navigates to url instead. In Mount or HandleParams on a page load, Patch redirects the load to url, as Navigate does.
func (*Ctx) Path ¶
Path returns p, a path within the page's mount, as a path from the site root: the Go twin of the path template builtin. In a sub-app mounted at /admin/jobs, c.Path("/jobs/1") is "/admin/jobs/jobs/1". Outside any mount it returns p.
func (*Ctx) SendAfter ¶
SendAfter runs the page's handler for event after d, as if the browser had sent event with payload: the handler runs on the page's goroutine, then Assigns, and the page re-renders. A page has at most one pending timer per event, so calling SendAfter again for an event replaces its timer, which is a debounce:
"typed": func(c *loom.Ctx, payload loom.Payload) {
p.draft = payload["value"]
c.SendAfter(2*time.Second, "save", nil) // saves once typing stops for 2s
},
A handler that calls SendAfter for its own event polls. The event must be one of the page's Events; another name is RT425, reported like c.Fail, and schedules nothing. Timers are part of the page's memory: they end with the session, and a restart or remount starts with none, so a Mount that needs one schedules it.
func (*Ctx) Set ¶
Set stores a value for the page's app layout, whose assigns func reads it with Get. Layout assigns are recomputed on every render, including after a handler that only sets a value.
c.Set("unread", 3) // in a handler; the layout func reads c.Get("unread")
func (*Ctx) SignIn ¶
SignIn signs userID in, from a live page's Mount or event handler, once the page knows who they are: a checked password, a new account. A live page can't set a cookie, so it sends the browser to /_loom/signin/<token>, which sets the session cookie and goes on to the page in the current URL's ?next= (as SignedIn sends), or else to next, a path in the app. The token works once, within a minute.
c.SignIn(user.ID, "/")
func (*Ctx) SignOut ¶
SignOut signs the page's user out at once, from a live page's event handler: their session ends (every open page of it ends at its next event), and the browser goes to /_loom/signout, which clears the cookie, then to next, a path in the app.
func (*Ctx) Subscribe ¶
Subscribe makes the page receive messages published on topic, in HandleInfo, for as long as the page is open. The page must implement InfoHandler (RT410). Subscribing twice to a topic does nothing.
func (p *ExportPage) Mount(c *loom.Ctx) {
c.Subscribe("exports:" + p.userID)
...
}
A live layout subscribes the same way and gets the messages on its topics in its own HandleInfo; a topic both subscribe to reaches both.
func (*Ctx) Title ¶
Title sets the page's title, which the root layout's <.live_title> shows. Set it in Mount or a handler; when it changes, the browser's title is updated along with the next diff.
type DB ¶
DB is a database handle: Postgres from OpenDB, or a local DB with no database (NewLocalDB) whose transactions still commit and roll back jobs.
func NewLocalDB ¶
func NewLocalDB() *DB
NewLocalDB returns a DB with no database behind it, for in-memory apps: its transactions run AfterCommit hooks and keep or drop the jobs enqueued in them, but SQL fails (DB602).
type DBPoolInfo ¶
type DBPoolInfo struct {
Max int `json:"max"` // Config.DBMaxConns
Open int `json:"open"`
InUse int `json:"in_use"`
Idle int `json:"idle"`
Waits int64 `json:"waits"` // times a statement waited for a connection, since the start
WaitMS float64 `json:"wait_ms"` // how long those waits took, in all
}
DBPoolInfo is database/sql's view of the app's connection pool.
type DefaultRootAssigns ¶
DefaultRootAssigns holds the parameters of component DefaultRoot.
type Error ¶
type Error struct {
Code string // e.g. "RT402"
Msg string
Where string // file:line of the user code involved, if known
Fix string
}
Error is a runtime error in a live page. Like compile errors, it says what went wrong, where, and how to fix it, and has a stable code documented in docs/errors.md.
type ErrorInfo ¶
type ErrorInfo struct {
Time time.Time `json:"time"`
Source string `json:"source"` // "server", or "browser" for one a page's scripts reported
SID string `json:"sid,omitempty"`
URL string `json:"url,omitempty"` // for a browser error, the page's URL
Code string `json:"code,omitempty"`
Message string `json:"message"`
Where string `json:"where,omitempty"`
Fix string `json:"fix,omitempty"`
Route string `json:"route,omitempty"` // the page's route, for a server error in a page
Trace string `json:"trace,omitempty"` // the action that caused it: `loom trace ID`
}
ErrorInfo is a runtime error recorded for dev tools.
type EventResult ¶
type EventResult struct {
SessionDetail
Errors []ErrorInfo `json:"errors"` // logged while handling the event, e.g. RT402 or RT419
Ended bool `json:"ended"` // the session ended (an auth check failed, or it failed too often)
// Trace is the event's trace id: `loom trace ID` shows what it
// caused later, such as its jobs running.
Trace string `json:"trace"`
// Events are what the event did while it ran: the handler's
// page.event (ms, diff bytes), its commits, jobs enqueued and
// broadcasts, oldest first.
Events []trace.Event `json:"events"`
}
EventResult is the page after an event sent with App.SendEvent: its assigns and HTML, the errors the event caused, and what it did.
type Events ¶
Events maps the events a page's template sends to the funcs that handle them. Each func changes the page's state; the runtime then calls Assigns, renders and sends the difference. Every event the page's template, the components it calls and its app layout send must have a handler, checked when the route is registered (RT422).
func (p *TodoPage) Events() loom.Events {
return loom.Events{"add": p.add, "toggle": p.toggle, "delete": p.delete}
}
Events is called on each page load's copy of the page, so method values like p.add act on that copy. It must only build the map.
type EventsResult ¶
type EventsResult struct {
Events []trace.Event `json:"events"` // oldest first
// Next is the cursor for the next call (?since=): the sequence of
// the latest event the stream had, matching or not.
Next uint64 `json:"next"`
}
EventsResult is a page of the event stream, as /_loom/dev/events and `loom events --json` give it.
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
Group is a set of routes sharing options: layouts, auth checks, middleware and assets. A group made by Mount also has a path prefix.
func (*Group) App ¶
App returns the app the group belongs to, so a sub-app can reach the app's services, such as its job runner and PubSub.
func (*Group) Handle ¶
Handle serves a plain HTTP handler (a form post, a webhook, a sign-in that sets a cookie) at pattern, a method and a path such as "POST /login", with the group's auth checks and middleware. In a mounted group the path is relative to the mount prefix. An auth check that refuses answers as for a page load: 403 or a redirect.
app.Handle("POST /logout", http.HandlerFunc(logout))
func (*Group) Live ¶
Live serves a live page at path, like App.Live, with the group's options. In a mounted group, path is relative to the mount prefix: "/" is the prefix itself.
func (*Group) Mount ¶
Mount mounts a sub-app at prefix (e.g. "/admin/jobs"), in a new group nested in g with the given options. The sub-app inherits g's auth checks and middleware, and registers its routes relative to the prefix.
admin := app.Group(loom.Auth(requireAdmin))
admin.Mount("/admin/jobs", jobsweb.New(jobsweb.Config{}))
type HasMigrations ¶
HasMigrations is implemented by sub-apps that keep data in the app's database. Migrations returns the package's migrations/ directory, embedded; `loom db install <package>` copies them into the app's own migrations, so `loom migrate`, `loom dev`, `migrate status` and `db schema` apply and report them like the app's. Run and loomtest check that each is applied (RT433).
//go:embed migrations
var migrations embed.FS
func (a *App) Migrations() fs.FS { m, _ := fs.Sub(migrations, "migrations"); return m }
type Health ¶
type Health struct {
Status string `json:"status"` // "ok", "draining" or "unavailable"
DB string `json:"db"` // "ok", "none" or the error
Jobs string `json:"jobs"` // "running", "enqueue-only" (Config.Jobs), "none" or the error starting them
Leader bool `json:"leader"` // this node runs cron and rescue for the cluster
// Node is this node in the jobs' node registry, from memory: its
// name (as jobsweb and `loom nodes` show it), queues with running
// jobs, and its latest heartbeat. Null unless it runs jobs.
Node *jobs.Node `json:"node"`
// Watch is the table watch's LISTEN: "ok", "idle" (opened when a
// page watching a table first mounts), "reconnecting", or "none"
// without a database.
Watch string `json:"watch"`
// PubSub is the PubSub's LISTEN for other nodes' broadcasts: "ok",
// "idle" (opened when a page first subscribes), "reconnecting", or
// "none" without a database or with Config.PubSub.
PubSub string `json:"pubsub"`
Sessions int `json:"sessions"` // live pages open on this node
Version string `json:"version,omitempty"` // the build's VCS revision, if known
}
Health is the body of GET /_loom/health.
type InfoHandler ¶
type InfoHandler interface {
// HandleInfo runs for each message, on the page's goroutine like an
// event handler, and changes the page's state; the runtime then calls
// Assigns and renders.
HandleInfo(c *Ctx, msg Message)
}
InfoHandler is a page that receives messages published on topics it subscribed to with Ctx.Subscribe: from background jobs (jobs.Ctx's Broadcast) or other code that publishes on App.PubSub.
type Keeper ¶
type Keeper interface {
Keep() any
}
Keeper is implemented by a page or live layout whose state a remount keeps. Keep returns a pointer to that state: a struct of exported, JSON-encodable fields, usually a field of the page itself. It is restored before Mount, so Mount sees it and loads what it names.
type Player struct {
store *Store
playing Playing // kept: what a restart mustn't lose
tracks map[int]Track
}
type Playing struct {
Station string
History []int // track ids, oldest first
At int
}
func (p *Player) Keep() any { return &p.playing }
type Limit ¶
type Limit struct {
Key LimitKey // whose attempts are counted: ByIP, ByUser, ByPayload or KeyBy
Max int // attempts allowed in any window of Per
Per time.Duration // the window, e.g. time.Minute
On []string // the events counted; none means everything
}
Limit is one rate limit: at most Max attempts per Per for each value of Key. With On, it counts only those events of the route; without, it counts every page load, event and handler request.
type LimitError ¶
type LimitError struct {
Err *Error
RetryAfter time.Duration // when the next attempt will be allowed
}
LimitError is RT436: an attempt over a rate limit. Nothing ran.
func (*LimitError) Error ¶
func (e *LimitError) Error() string
func (*LimitError) Unwrap ¶
func (e *LimitError) Unwrap() error
type LimitKey ¶
type LimitKey struct {
// contains filtered or unexported fields
}
LimitKey says whose attempts a Limit counts. An attempt whose key is "" (no one signed in for ByUser, no such field for ByPayload) isn't counted.
type Mail ¶
type Mail struct {
To string `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"` // the plain-text body
HTML string `json:"html,omitempty"` // an HTML body, if any
}
Mail is an email to send.
type Mailer ¶
Mailer sends email. Implement it around the app's mail service (SMTP with net/smtp, or an HTTP API) and register it with App.UseMailer. Send is called from background jobs: an error retries the job.
type Manifest ¶
type Manifest struct {
Routes []ManifestRoute `json:"routes"`
Layouts []ManifestLayout `json:"layouts"`
Handlers []ManifestHandler `json:"handlers"`
Mounts []ManifestMount `json:"mounts"`
Jobs jobs.Setup `json:"jobs"`
// PubSub is how broadcasts reach pages: "postgres" for the default
// (every node's, by NOTIFY, when the app has a database; in-process
// without one), or the Go type of Config.PubSub.
PubSub string `json:"pubsub"`
// Security is the headers every response carries (headers.go).
Security ManifestSecurity `json:"security"`
// Migrations are set by AddMigrations; App.Manifest doesn't know
// where they are.
Migrations []ManifestMigration `json:"migrations"`
Flags []ManifestFlag `json:"flags"`
}
Manifest describes an app's behaviour for review.
func (*Manifest) AddMigrations ¶
AddMigrations adds the migrations in fsys (as for `loom migrate`) to the manifest, each summarised by its statements' first lines, so a reviewer sees a schema change next to the behaviour it enables.
type ManifestEvent ¶
type ManifestEvent struct {
Name string `json:"name"`
// Handler is set on a route's event its live layout handles rather
// than the page, e.g. (*issues.Shell).Events.
Handler string `json:"handler,omitempty"`
On []string `json:"on"` // the DOM or behaviour events, e.g. click
Vals []string `json:"vals"` // val-* names sent with it
At []string `json:"at"` // template positions, e.g. todo_app.hx:26:59
}
ManifestEvent is a server event a page sends, with every place that sends it.
type ManifestFlag ¶
type ManifestFlag struct {
Subject string `json:"subject"` // e.g. "mount /admin/jobs"
Message string `json:"message"`
}
ManifestFlag is something a reviewer should look at: not an error, since it may be intended, but easy to miss in the code.
type ManifestHandler ¶
type ManifestHandler struct {
Pattern string `json:"pattern"` // e.g. POST /login
Auth []string `json:"auth"`
RateLimit []string `json:"rate_limit"`
}
ManifestHandler is a plain HTTP handler (Group.Handle).
type ManifestLayout ¶
type ManifestLayout struct {
Component string `json:"component"`
Handler string `json:"handler,omitempty"`
Events []ManifestEvent `json:"events"`
Keeps []string `json:"keeps,omitempty"` // the fields a live layout keeps across a remount (Keep)
}
ManifestLayout is an app layout with the events it sends. A live layout (LiveLayout) handles them itself, named in Handler; otherwise each page in it does, and Handler is "".
type ManifestMigration ¶
type ManifestMigration struct {
File string `json:"file"`
Statements []string `json:"statements"` // each statement's first line
Tracks []string `json:"tracks"`
}
ManifestMigration is a migration file: what its statements do, and the tables it turns change tracking on for.
type ManifestMount ¶
type ManifestMount struct {
Prefix string `json:"prefix"`
Type string `json:"type"`
Auth []string `json:"auth"`
Routes []string `json:"routes"`
// RateLimits are the limits on the sub-app's routes: "every route: "
// and the limit for one that covers them all, else the route and the
// limit, e.g. "/sign-in: 5 per minute by email on sign_in".
RateLimits []string `json:"rate_limits"`
// DevOnly is set when the sub-app's pages are served only in dev
// (loom.DevOnly), such as devmail's.
DevOnly bool `json:"dev_only"`
}
ManifestMount is a mounted sub-app. Its own pages are its package's business; the app decides where it lives and who may reach it.
type ManifestRoute ¶
type ManifestRoute struct {
Path string `json:"path"`
Page string `json:"page"` // the Go type handling the page's events
Component string `json:"component"` // e.g. templates.TodoApp
Layout string `json:"layout"` // the app layout; "" for none
Auth []string `json:"auth"` // auth checks, outer first
RateLimit []string `json:"rate_limit"` // rate limits, outer first (loom.RateLimit)
Events []ManifestEvent `json:"events"` // what the page and its layout send
Handler string `json:"handler"` // what handles them
Watch []string `json:"watch"` // tables whose changes re-render the page
Params string `json:"params"` // what reads the URL's state (HandleParams); "" for none
Keeps []string `json:"keeps"` // the fields the page keeps across a remount (Keep)
DevOnly bool `json:"dev_only"` // served only in dev (loom.DevOnly)
}
ManifestRoute is a page route outside any mount.
type ManifestSecurity ¶
ManifestSecurity is the security headers every response carries: the Content-Security-Policy (Config.CSP) and whether Strict-Transport-Security is sent (Config.BaseURL is https).
type Message ¶
Message is a message published on a topic: an event name and a string payload, like a browser event.
type Meta ¶
type Meta struct {
Description string // a sentence or two: what the page is
Image string // a picture for link previews, ideally 1200×630
Canonical string // the page's one URL, when it can be reached by several
// StructuredData is schema.org JSON-LD for search engines: any value
// encoding/json marshals, usually a map with "@context" and "@type".
// Its URLs aren't rewritten, so make them absolute with c.URL.
StructuredData any
// NoIndex asks search engines not to index the page (robots
// noindex), for pages such as search results that only repeat
// others. Its links are still followed.
NoIndex bool
}
Meta is what a page says about itself to link previews and search engines, rendered into the document's <head> by the root layout's <.live_meta />. Image and Canonical may be paths, such as c.Asset("cards/c64.png") or c.Path("/c64/" + slug); they become absolute URLs on Config.BaseURL (or, without one, the request's host).
type MigrateResult ¶
MigrateResult is the JSON of `migrate --json`: the migrations applied, in order (empty when none were pending).
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option is a route group option: Layout, RootLayout, Auth, Middleware or Assets.
func Assets ¶
Assets is a group option embedding static files (CSS, images) for the group's templates, served under the group's prefix at /_assets/<name>.<hash>.<ext> with long cache headers. Templates link them with the asset builtin, asset("app.css"), which gives the hashed URL, so a new version of a file has a new URL. fsys is usually an embed.FS, narrowed with fs.Sub to the assets directory.
A name ending in "/" is a directory, served whole at /_assets/<dir>.<hash>/ with one hash over everything in it: asset("player/") gives that URL. Files in it keep their names, so they can load each other by relative URL (an ES module's imports, a worker, a worklet, a .wasm file), and any change to any of them gives the whole directory a new URL.
//go:embed assets var files embed.FS assetsFS, _ := fs.Sub(files, "assets") g = g.Group(loom.Assets(assetsFS))
func Auth ¶
Auth is a group option adding an auth check. Checks run before Mount on every page load, and again before every event and message in the live session, outer groups' checks first. A check that fails on load answers 403 (ErrForbidden) or a redirect (Redirect); one that fails later ends the session, and the browser reloads and gets the same answer. So a user who loses permission loses the page at their next event.
func requireAdmin(c *loom.Ctx) error {
if !currentUser(c.Request).Admin {
return loom.ErrForbidden
}
return nil
}
admin := app.Group(loom.Auth(requireAdmin))
func DevOnly ¶
func DevOnly() Option
DevOnly is a group option for dev tools served as ordinary pages, such as the devmail mailbox: the group's page loads, assets and handlers answer 404 unless the app runs in dev (App.Dev), and 403 to requests from other machines, like /_loom/dev/. It is checked on each request, so it holds however the app sets Dev. Live events don't pass through it, but a page that never loads has no session to send them to.
app.Mount("/dev/mail", devmail.New()) // devmail adds DevOnly itself
func Layout ¶
Layout is a group option setting the group's app layout: a component with an inner_block slot, rendered around each page inside the live tree. assigns is a func(c *loom.Ctx) A, where A is the layout's assigns type; it runs on every render of the page, so it may read values the page sets with c.Set.
loom.Layout(templates.App, func(c *loom.Ctx) templates.AppAssigns {
return templates.AppAssigns{User: currentUser(c.Request)}
})
func LiveLayout ¶
LiveLayout is a group option setting the group's app layout, like Layout, but with state and events of its own: state is a pointer to a struct with the methods of a page, Mount, Assigns (returning the layout's assigns type; its inner_block is filled in) and, as needed, Events and HandleInfo. Each page load gets its own copy, mounted before the page and kept for as long as the page is open. Events the layout handles go to it, whichever markup sends them, so the pages in it handle only their own; an event both handle is RT434 at startup. A page reaches its layout's state with LayoutOf.
Use it for chrome with state shared by every page: a composer, toasts, a command palette, unread counts.
g := app.Group(loom.LiveLayout(templates.Shell, &Shell{store: store}))
func Middleware ¶
Middleware is a group option wrapping the group's HTTP requests (page loads and assets) in plain net/http middleware, outer groups' first. Live events don't pass through it: they are checked with Auth.
func RootLayout ¶
RootLayout is a group option overriding the app's root layout for the group's routes. See App.RootLayout.
func SignedIn ¶
SignedIn is a group option requiring a signed-in user: an auth check that sends anyone else to signInPath, with ?next= the page they asked for. Like every auth check it runs on the page load and before every event, so a page whose session was signed out ends at its next event. With loom.Users, a user the loader can't find counts as signed out. The manifest lists it as loom.SignedIn.
signedIn := app.Group(loom.SignedIn("/login"))
type Page ¶
type Page interface {
// Mount is called once per page load, before the first render, to
// load the page's state.
Mount(c *Ctx)
}
Page is a live page. Its fields hold the page's state. Besides Mount, a page has an Assigns method returning its template's assigns type, and usually Events and HandleInfo:
func (p *TodoPage) Assigns(c *loom.Ctx) templates.TodoListAssigns func (p *TodoPage) Events() loom.Events // see Events func (p *TodoPage) HandleInfo(c *loom.Ctx, msg loom.Message) // see InfoHandler
Assigns is checked against the template when the route is registered (RT406), so a page and its template always agree; a wrong field or type in Assigns is a go build error.
type PageError ¶
type PageError struct {
Status int // the HTTP status: 403, 404, 429, 500 or 503
Title string // a sentence for people, e.g. "This page doesn't exist."
Code string // the Loom error code, e.g. RT444; "" if there is none
}
PageError is what an error page shows: never the error itself, which may hold file paths, SQL or a panic value. The detail goes to the log and `loom errors`, where Code finds it.
type ParamsHandler ¶
type ParamsHandler interface {
// HandleParams runs after Mount, and again each time the URL's
// query changes: c.Patch, a link to the same path, or the back and
// forward buttons. query is the URL's query; c.Request.URL is the
// new URL. The path values stay as Mount saw them. It sets
// the page's state from the URL; the runtime then calls Assigns and
// renders. It runs on the page's goroutine like an event handler.
HandleParams(c *Ctx, query url.Values)
}
ParamsHandler is a page whose state is (partly) in its URL.
type PatchOption ¶
type PatchOption func(*patch)
PatchOption changes how Ctx.Patch changes the URL.
func Replace ¶
func Replace() PatchOption
Replace makes Ctx.Patch replace the current history entry instead of adding one, for changes the back button shouldn't step through one by one, such as each keystroke of a search.
type Payload ¶
Payload is an event's data: the val-* attributes of the element that fired it (without the prefix), plus "value" for input, select and textarea elements, "checked" ("true" or "false") for checkboxes and radio buttons, "key" for keyboard events, and the named fields of a submitted form. See docs/live.md.
type PreviewInfo ¶
type PreviewInfo struct {
Component string `json:"component"` // e.g. kit.Button
Variants []string `json:"variants"` // its preview samples' names; the first is "default"
Themes []string `json:"themes"` // light, dark, then its tokens' named themes
NoPreview string `json:"no_preview,omitempty"`
}
PreviewInfo describes a component's previews.
func Previews ¶
func Previews() []PreviewInfo
Previews lists the preview variants of every registered component except root layouts, sorted by name.
type QueriesResult ¶
type QueriesResult struct {
Started time.Time `json:"started"`
Queries []data.QueryStat `json:"queries"` // the busiest first, by total time
}
QueriesResult counts the statements the app's database ran since it started, for `loom queries`: Loom's own (the job store, sessions, rate limits) as well as the app's.
type RedirectError ¶
type RedirectError struct{ URL string }
RedirectError is returned by an auth check, made with Redirect, to send the browser to URL instead of the page.
func (*RedirectError) Error ¶
func (e *RedirectError) Error() string
type RenderResult ¶
RenderResult is the result of rendering a component from JSON.
type RouteInfo ¶
type RouteInfo struct {
Path string `json:"path"`
Page string `json:"page"` // Go type, e.g. *todo.TodoPage
Component string `json:"component"` // e.g. templates.TodoApp
RootLayout string `json:"root_layout"` // e.g. templates.Root, or loom.DefaultRoot
Layout string `json:"layout"` // the app layout, e.g. templates.App; "" for none
Prefix string `json:"prefix"` // the mount prefix, e.g. /admin/jobs; "" outside mounts
Mount string `json:"mount"` // the mounted sub-app's Go type, e.g. *jobsweb.App; "" outside mounts
Auth []string `json:"auth"` // the auth checks, outer first, e.g. todo.requireAdmin
Middleware int `json:"middleware"` // how many middleware wrap the route
Watch []string `json:"watch"` // tables whose changes re-render the page (loom.Watch)
RateLimit []string `json:"rate_limit"` // rate limits, outer first, e.g. "5 per minute by email on sign_in"
}
RouteInfo describes a live route.
type RouteOption ¶
type RouteOption interface {
// contains filtered or unexported methods
}
RouteOption is an option of one route, given to Live: Watch.
func Watch ¶
func Watch(tables ...string) RouteOption
Watch is a route option naming tracked tables the page reads. When one of them changes (a commit anywhere, on any node), every page on the route re-runs Assigns and the browser gets the diff, as after an event. Run checks that each table exists and is tracked (DB622).
app.Live("/", &TodoList{}, templates.TodoApp, loom.Watch("todos"))
A watch is per table: every page on the route re-renders on any change to it. For finer grain, publish on a topic in tx.AfterCommit and c.Subscribe to it.
type RuntimeInfo ¶
type RuntimeInfo struct {
Goroutines int `json:"goroutines"`
HeapBytes uint64 `json:"heap_bytes"` // allocated and still in use
HeapObjects uint64 `json:"heap_objects"`
SysBytes uint64 `json:"sys_bytes"` // obtained from the OS
GCRuns uint32 `json:"gc_runs"`
// GCPauseMS is the longest of the last 256 collections' pauses.
GCPauseMS float64 `json:"gc_pause_ms"`
Streams int `json:"streams"` // open SSE streams: browsers connected
// DB is the connection pool, absent without a database.
DB *DBPoolInfo `json:"db,omitempty"`
}
RuntimeInfo is the process as it is now: goroutines, memory and the garbage collector, the browsers connected, and the database pool. A number that only grows across `loom stats` calls is a leak; a pool whose waits grow is short of connections (Config.DBMaxConns).
type SentMail ¶
type SentMail struct {
ID int `json:"id"` // 1 for the first mail the app kept, counting up
Time time.Time `json:"time"`
Mail
}
SentMail is a mail the dev outbox kept, as `loom mail --json` shows it.
type SessionDetail ¶
type SessionDetail struct {
SessionInfo
Component string `json:"component"`
Assigns any `json:"assigns"` // with template field names
// LayoutAssigns are the live layout's assigns (see LiveLayout), or
// absent if the page has none.
LayoutAssigns any `json:"layout_assigns,omitempty"`
HTML string `json:"html"`
Timers []TimerInfo `json:"timers"` // pending c.SendAfter timers, soonest first
Topics []string `json:"topics"` // PubSub topics the page and its live layout subscribe to
Watch []string `json:"watch"` // tables whose changes re-render the page (loom.Watch)
}
SessionDetail is a session with its current state.
type SessionInfo ¶
type SessionInfo struct {
SID string `json:"sid"`
Path string `json:"path"`
URL string `json:"url"` // the page's current URL, as patched (Ctx.Patch)
Page string `json:"page"`
Version int `json:"version"`
Connected int `json:"connected"` // open streams
IdleSeconds float64 `json:"idle_seconds"` // since the last stream closed; 0 while connected
Restarts int `json:"restarts"` // in the last minute
BusySeconds float64 `json:"busy_seconds"` // how long the running handler has run; 0 when idle
StartedAt time.Time `json:"started_at"`
}
SessionInfo describes a running page session.
type StatsResult ¶
type StatsResult struct {
Started time.Time `json:"started"`
Sessions int `json:"sessions"` // live pages open now
Events []trace.Stat `json:"events"` // by name, and by route, job type or table
Runtime RuntimeInfo `json:"runtime"`
}
StatsResult counts the app's events since it started, for `loom stats`.
type StorageList ¶
type StorageList struct {
Bucket string `json:"bucket"` // where it is, e.g. "s3 https://host/bucket", "dir .loom/storage"
Objects []storage.Object `json:"objects"`
}
StorageList is `loom storage --json`: the bucket and a page of its objects.
type SubApp ¶
type SubApp interface {
// Routes registers the sub-app's pages in g, with paths relative to
// the mount prefix. It may nest groups in g to set its own layouts
// and embedded assets.
Routes(g *Group)
}
SubApp is a reusable set of live pages, mounted with Group.Mount.
type TimerInfo ¶
type TimerInfo struct {
Event string `json:"event"`
Payload Payload `json:"payload"`
InMS int64 `json:"in_ms"` // milliseconds until it runs
}
TimerInfo is a page's pending timer, set with Ctx.SendAfter.
type Tx ¶
Tx is a database transaction, given to the function passed to DB.Tx. App code and jobs.Enqueue take a Tx.
type ValidateResult ¶
type ValidateResult struct {
OK bool `json:"ok"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Fix string `json:"fix,omitempty"`
}
ValidateResult is whether app.Validate passes, for `loom doctor`.
type View ¶
type View struct {
// contains filtered or unexported fields
}
View is one live page instance: a Page, its template and app layout, and its latest render. The runtime gives each page load its own View, owned by one goroutine; loomtest drives Views directly. A View is not safe for concurrent use.
func (*View) Close ¶
func (v *View) Close()
Close cancels the page's context (Ctx.Context). The runtime calls it when the session ends or the page restarts.
func (*View) Document ¶
Document renders the page's whole HTML document, as a GET serves it: the root layout with the live root inside. Without layouts, it is the live root alone.
func (*View) Event ¶
Event runs the handler for the event, the page's or its live layout's, then Assigns, and re-renders. It returns the diff to send to the client, or nil if nothing changed. The page's title may have changed even when the diff is nil; see Title. The route's auth checks run first; if one fails, Event returns its error and the page is unchanged. An event the page has no handler for is error RT423, one no element on the page binds is RT438, and one over a rate limit is a *LimitError (RT436); none of them changes anything.
func (*View) Failures ¶
Failures returns the errors the page reported with Ctx.Fail since the last call.
func (*View) HandlesParams ¶
HandlesParams reports whether the page implements ParamsHandler.
func (*View) Info ¶
Info delivers a message published on a topic the page subscribed to, by running its HandleInfo method, then Assigns, and re-rendering. It returns the diff to send to the client, or nil if nothing changed. Like Event, it runs the route's auth checks first. A message on a topic the live layout subscribed to goes to the layout's HandleInfo, and also to the page's if the page subscribed to it too.
func (*View) Kept ¶
Kept returns the page's kept state as the browser holds it, sealed: "" if neither the page nor its live layout keeps anything, or what they keep is over MaxKept (RT454). The runtime sends it with each render that changes it.
func (*View) LayoutAssigns ¶
LayoutAssigns returns the live layout's assigns of the current render, with no inner_block, or nil if the page has no live layout.
func (*View) Navigation ¶
Navigation returns the URL the page asked to navigate to with Ctx.Navigate since the last call, or "".
func (*View) NewTopics ¶
NewTopics returns the topics the page subscribed to since the last call, for the runtime to subscribe to.
func (*View) NextTimer ¶
NextTimer returns when the page's soonest timer is due, and false if it has none.
func (*View) Patch ¶
Patch changes the page's URL as a browser does when it follows a link to another URL of the page's route, or goes back or forward to one: it runs the route's auth checks, then the page's HandleParams with the new URL, and re-renders. It returns the diff to send to the client, or nil if nothing changed. The URL must be of the page's route, and the page must implement ParamsHandler (RT427).
func (*View) Patched ¶
Patched returns the URL the page patched to with Ctx.Patch since the last call, and whether it replaces the current history entry, or "" if it hasn't.
func (*View) Refresh ¶
Refresh re-runs the Assigns of the page and its live layout, and re-renders, as the runtime does when a table the route watches changes. It returns the diff to send to the client, or nil if nothing changed. Like Event, it runs the route's auth checks first.
func (*View) Restored ¶
Restored says what the page's mount did with the state a remount brought it (Keep): "restored the layout's", "dropped: another browser's" and so on, or "" if it brought none.
func (*View) Route ¶
Route returns the path of the page's route, e.g. /todos/{id}, or "" for a View no app routes.
func (*View) RunTimers ¶
RunTimers runs the handlers of the timers that are due, soonest first, then Assigns, and re-renders once. It returns the diff to send to the client, or nil if nothing changed or nothing was due. Like Event, it runs the route's auth checks first. Timers the handlers set run later, even if they are already due.
func (*View) SetClock ¶
SetClock sets the clock the View's timers use, instead of time.Now. loomtest sets a fake clock before anything else, so Page.Advance can move it.
func (*View) Tally ¶
Tally counts the statements run under the page's context since the last ResetTally: the session resets it before each action and reports it on the action's page.* event. A goroutine a handler started that still uses c.Context() counts toward whichever action is current.
func (*View) Trace ¶
Trace returns the trace id of the page's latest action: its mount, or the event, message, timer or re-render it last handled. The events that action emitted carry it (docs/events.md).
func (*View) URL ¶
URL returns the page's current URL, path and query, as a browser shows it: the URL it was loaded at, or the last one patched to.
Source Files
¶
- browser.go
- compress.go
- config.go
- db.go
- default_root_hx.go
- dev.go
- dev_events.go
- dev_jobs.go
- errorpage.go
- errors.go
- events.go
- gallery.go
- headers.go
- health.go
- http.go
- jobs.go
- keep.go
- layout.go
- loom.go
- mail.go
- manifest.go
- migrate.go
- migrations.go
- mount.go
- navigate.go
- notify.go
- origin.go
- outbox.go
- page.go
- patch.go
- ratelimit.go
- scripts.go
- secret.go
- session.go
- storage.go
- styles.go
- timer.go
- users.go
- view.go
- watch.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package auth is a sign-in sub-app: email and password, sign-in links by email, password resets, Google and GitHub, two-factor sign-in with an authenticator app, and an account page, mounted with one line.
|
Package auth is a sign-in sub-app: email and password, sign-in links by email, password resets, Google and GitHub, two-factor sign-in with an authenticator app, and an account page, mounted with one line. |
|
ui
Package ui holds the auth pages' copies of starter-kit components (`loom components add --dir auth/ui`).
|
Package ui holds the auth pages' copies of starter-kit components (`loom components add --dir auth/ui`). |
|
Package client embeds loom.js, the browser runtime for live pages.
|
Package client embeds loom.js, the browser runtime for live pages. |
|
cmd
|
|
|
loom
command
Command loom is the Loom command-line tool.
|
Command loom is the Loom command-line tool. |
|
Package compiler ties the compiler stages together: it finds .hx templates, parses and type-checks them, and generates Go code.
|
Package compiler ties the compiler stages together: it finds .hx templates, parses and type-checks them, and generates Go code. |
|
ast
Package ast defines the syntax tree for .hx templates.
|
Package ast defines the syntax tree for .hx templates. |
|
check
Package check type-checks the templates of one package against their component signatures and the Go types they use.
|
Package check type-checks the templates of one package against their component signatures and the Go types they use. |
|
diag
Package diag defines Loom's diagnostics: compile errors with a stable code, a position, a code excerpt and a fix.
|
Package diag defines Loom's diagnostics: compile errors with a stable code, a position, a code excerpt and a fix. |
|
gogen
Package gogen generates Go code from checked templates.
|
Package gogen generates Go code from checked templates. |
|
goinfo
Package goinfo reads what templates need to know about the Go package they live in: its name, its struct types and its helper functions.
|
Package goinfo reads what templates need to know about the Go package they live in: its name, its struct types and its helper functions. |
|
lexer
Package lexer splits .hx source into tokens.
|
Package lexer splits .hx source into tokens. |
|
parser
Package parser turns .hx source into an ast.File.
|
Package parser turns .hx source into an ast.File. |
|
source
Package source holds template source files and positions within them.
|
Package source holds template source files and positions within them. |
|
style
Package style compiles a component's <style> block and the app's design tokens.
|
Package style compiles a component's <style> block and the app's design tokens. |
|
Package data is Loom's SQL-first persistence layer: a database handle, transactions with AfterCommit hooks, the Querier interface that read helpers and jobs accept, typed reads (Select, Get), mapped errors, migrations and change tracking.
|
Package data is Loom's SQL-first persistence layer: a database handle, transactions with AfterCommit hooks, the Querier interface that read helpers and jobs accept, typed reads (Select, Get), mapped errors, migrations and change tracking. |
|
Package devmail is a mailbox for development: it shows the mail the app sent while it ran under `loom dev` with no mailer, such as auth's sign-in links and password resets, so a person can follow them in the browser the way an agent does with `loom mail`.
|
Package devmail is a mailbox for development: it shows the mail the app sent while it ran under `loom dev` with no mailer, such as auth's sign-in links and password resets, so a person can follow them in the browser the way an agent does with `loom mail`. |
|
Package diff encodes render trees for the wire and computes sparse diffs between two renders of the same template.
|
Package diff encodes render trees for the wire and computes sparse diffs between two renders of the same template. |
|
Package docs holds Loom's documentation, embedded so that `loom docs` and the `docs` MCP tool can show it outside this repository: an agent working on an app has the loom command but not Loom's source.
|
Package docs holds Loom's documentation, embedded so that `loom docs` and the `docs` MCP tool can show it outside this repository: an agent working on an app has the loom command but not Loom's source. |
|
examples
|
|
|
issues
Package issues is a Linear-style issue tracker, the larger of Loom's example apps: teams with issue lists grouped by status, filters, search, an issue page with editable properties, comments and history, a new-issue composer, an inbox fed by a background job, and keyboard shortcuts.
|
Package issues is a Linear-style issue tracker, the larger of Loom's example apps: teams with issue lists grouped by status, filters, search, an issue page with editable properties, comments and history, a new-issue composer, an inbox fed by a background job, and keyboard shortcuts. |
|
issues/cmd/issues
command
Command issues runs the Loom issue tracker example on a Postgres database.
|
Command issues runs the Loom issue tracker example on a Postgres database. |
|
tally
Package tally is Tally, a shared-expenses app: groups of people who split bills, see who owes whom, settle up, and get reminded.
|
Package tally is Tally, a shared-expenses app: groups of people who split bills, see who owes whom, settle up, and get reminded. |
|
tally/cmd/tally
command
Command tally runs Tally, the shared-expenses example, on a Postgres database:
|
Command tally runs Tally, the shared-expenses example, on a Postgres database: |
|
todo
Package todo is the Loom example app: a todo list where adding, toggling, deleting and filtering all happen on the server, and the browser receives small diffs.
|
Package todo is the Loom example app: a todo list where adding, toggling, deleting and filtering all happen on the server, and the browser receives small diffs. |
|
todo/cmd/todo
command
Command todo runs the Loom todo example on a Postgres database:
|
Command todo runs the Loom todo example on a Postgres database: |
|
todo/templates
Package templates holds the todo example's templates and the types they render.
|
Package templates holds the todo example's templates and the types they render. |
|
Package form is the state of an HTML form on a live page, between the template that renders it and the code that saves it: what the person typed, field by field, the typed struct it decodes to, and each field's error.
|
Package form is the state of an HTML form on a live page, between the template that renders it and the code that saves it: what the person typed, field by field, the typed struct it decodes to, and each field's error. |
|
internal
|
|
|
difftest
Package difftest makes random render trees for property tests of the diff engine (package diff).
|
Package difftest makes random render trees for property tests of the diff engine (package diff). |
|
hxtest
Package hxtest holds templates that exercise every template feature.
|
Package hxtest holds templates that exercise every template feature. |
|
notestest
Package notestest is a sub-app that keeps data in the app's database, for testing sub-app migrations: `loom db install` and the check that they are applied (RT433).
|
Package notestest is a sub-app that keeps data in the app's database, for testing sub-app migrations: `loom db install` and the check that they are applied (RT433). |
|
signintest
Package signintest is a tiny app for testing sign-in: a sign-in page that checks a password and signs in, and pages that require a signed-in user and read it typed.
|
Package signintest is a tiny app for testing sign-in: a sign-in page that checks a password and signs in, and pages that require a signed-in user and read it typed. |
|
subapptest
Package subapptest is a tiny sub-app for testing loom's Mount: two pages that link to each other with path, a root layout of its own that links an embedded stylesheet with asset, and an optional auth requirement.
|
Package subapptest is a tiny sub-app for testing loom's Mount: two pages that link to each other with path, a root layout of its own that links an embedded stylesheet with asset, and an optional auth requirement. |
|
Package jobs runs durable background jobs: typed, retried with backoff, scheduled, deduplicated, run on cron, and visible to the loom CLI.
|
Package jobs runs durable background jobs: typed, retried with backoff, scheduled, deduplicated, run on cron, and visible to the loom CLI. |
|
jobstest
Package jobstest is the conformance suite every jobs.Store must pass, so that the in-memory and Postgres stores behave identically.
|
Package jobstest is the conformance suite every jobs.Store must pass, so that the in-memory and Postgres stores behave identically. |
|
Package jobsweb is a live dashboard for Loom's background jobs, mounted in an app with one line:
|
Package jobsweb is a live dashboard for Loom's background jobs, mounted in an app with one line: |
|
Package kit is Loom's starter kit: components an app copies into its own components/ directory with `loom components add`, and then owns.
|
Package kit is Loom's starter kit: components an app copies into its own components/ directory with `loom components add`, and then owns. |
|
manage
Package manage lists the starter kit's components and copies them into an app: `loom components` and `loom components add` (see docs/components.md).
|
Package manage lists the starter kit's components and copies them into an app: `loom components` and `loom components add` (see docs/components.md). |
|
Package loomtest tests Loom components and pages without a browser.
|
Package loomtest tests Loom components and pages without a browser. |
|
Package pgjobs is the Postgres store for Loom jobs.
|
Package pgjobs is the Postgres store for Loom jobs. |
|
Package preview renders components' preview samples (declared with the parameters' enum and bool values and @preview sample values) as standalone HTML pages, and screenshots them in headless Chrome.
|
Package preview renders components' preview samples (declared with the parameters' enum and bool values and @preview sample values) as standalone HTML pages, and screenshots them in headless Chrome. |
|
Package pubsub delivers messages on named topics, within one process.
|
Package pubsub delivers messages on named topics, within one process. |
|
Package render defines the output of a compiled template: a tree of static strings and dynamic values.
|
Package render defines the output of a compiled template: a tree of static strings and dynamic values. |
|
Package storage keeps an app's files in a bucket: uploads, generated files, anything too big for a database row.
|
Package storage keeps an app's files in a bucket: uploads, generated files, anything too big for a database row. |
|
storagetest
Package storagetest is the conformance suite every storage.Bucket must pass, so that buckets in memory, on disk and on S3 behave the same.
|
Package storagetest is the conformance suite every storage.Bucket must pass, so that buckets in memory, on disk and on S3 behave the same. |
|
Package trace is Loom's event stream: one record of what a running app did, from a page mount to a job's last attempt, with stable names documented in docs/events.md like error codes.
|
Package trace is Loom's event stream: one record of what a running app did, from a page mount to a job's last attempt, with stable names documented in docs/events.md like error codes. |