Documentation
¶
Overview ¶
Package vero is a Go backend with native macOS, Windows and Linux frontends, all built on macOS. See the README for how to build one.
Index ¶
- Constants
- Variables
- func Edit[T Keyed[K], K comparable](items []T, key K, change func(*T)) error
- func Handle[Req any, Rep any](w *Worker, name string, fn func(ctx context.Context, request Req) (Rep, error))
- func Hosting() bool
- func InProcessWorker() func(in io.Reader, out io.Writer) error
- func ServeInProcess(serve func(in io.Reader, out io.Writer) error)
- func Update[T any](s *State[T], name string, fn func(*T) error)
- func UpdateItem[T any, J Keyed[K], K comparable](s *State[T], name string, change func(*J) error)
- func UpdateWith[T any, Req any](s *State[T], name string, fn func(state *T, request Req) error)
- type Envelope
- type Handler
- type ID
- type Keyed
- type RemoteError
- type RunState
- type State
- type Supervisor
- func (s *Supervisor) Call(ctx context.Context, name string, request any) (json.RawMessage, error)
- func (s *Supervisor) Err() error
- func (s *Supervisor) Latest() json.RawMessage
- func (s *Supervisor) Request(ctx context.Context, request any) (json.RawMessage, error)
- func (s *Supervisor) Restarts() int
- func (s *Supervisor) State() RunState
- func (s *Supervisor) Stop() error
- type SupervisorOptions
- type Worker
- type WorkerOptions
Constants ¶
const DefaultStateInterval = 100 * time.Millisecond
DefaultStateInterval is how often WorkerOptions.State is sampled for a change when StateInterval is not set. Fast enough that a progress bar moves smoothly, slow enough that nothing is sent while nothing is happening.
const MaxLineSize = 16 << 20 // 16 MiB
MaxLineSize caps one protocol line, so a corrupt or hostile stream cannot ask for an unbounded allocation. Payloads larger than this belong in a file with the path sent through the pipe.
Variables ¶
var ErrAlreadyRunning = errors.New("a worker is already running for this application")
ErrAlreadyRunning is returned when another process is already supervising this worker.
Two copies of an application - the one in /Applications and the one still in ~/Downloads after an update - are two processes, and without this they would each start a worker and both would work on the same state. One window too many is a nuisance; two workers is corruption.
var ErrCancelled = errors.New("vero: the request was cancelled")
ErrCancelled is what a request comes back with when its caller cancelled it. Bindings do not show it as a failure: nobody is waiting to see it.
var ErrWorkerNotRunning = errors.New("vero: the worker is not running")
ErrWorkerNotRunning is returned by Request when the worker has died and has not been restarted yet. It is worth distinguishing in a frontend: it means wait, not that anything you did was wrong.
var ShutdownGrace = 5 * time.Second
ShutdownGrace is how long Run waits for handlers to notice that the frontend has gone before exiting regardless.
Functions ¶
func Edit ¶
func Edit[T Keyed[K], K comparable](items []T, key K, change func(*T)) error
Edit finds the item the frontend means and changes it, which is most of what a handler does.
func (s *Status) Pause(req vero.ID[int]) error {
return vero.Edit(s.Jobs, req.ID, func(j *Job) { j.Paused = !j.Paused })
}
A request naming something that is gone is refused, and the worker carries on.
func Handle ¶
func Handle[Req any, Rep any](w *Worker, name string, fn func(ctx context.Context, request Req) (Rep, error))
Handle answers one request name.
Req is what the request decodes into and Rep is what the reply is encoded from, so neither the handler nor the caller has to guess. Use struct{} for a request that carries nothing.
func Hosting ¶ added in v0.12.0
func Hosting() bool
Hosting reports whether this process was started to supervise a worker rather than be one. NewWorker calls it, so an application only needs this if it builds its worker without NewWorker.
func InProcessWorker ¶ added in v0.12.0
InProcessWorker returns what ServeInProcess was given, or nil. The C shim uses it; an application has no reason to.
func ServeInProcess ¶ added in v0.12.0
ServeInProcess hands vero a worker to run inside this process, for the frontends that cannot start one beside it.
iOS is the case that needs it: an application there may not start another program, but it may compile Go in. The application calls this before its frontend asks for a worker, with the same function it would have given SupervisorOptions.Serve:
vero.ServeInProcess(func(in io.Reader, out io.Writer) error {
w := vero.NewWorker(vero.WorkerOptions{In: in, Out: out})
jobs := vero.NewState(w, Status{})
vero.Update(jobs, "addJob", func(s *Status) error { ... })
return w.Serve()
})
Everywhere else, leave it alone: a worker in a process of its own survives a panic that would take the frontend with it, which is the whole reason the two are separate when they can be.
func Update ¶
Update answers a request that carries nothing - a button with no payload - changes the state, and replies with it.
func (s *Status) Add() error { … }
vero.Update(jobs, "addJob", (*Status).Add)
UpdateWith is the same for a request that carries something, and UpdateItem for one that names an item.
func UpdateItem ¶
UpdateItem answers a request that names one item and changes it, which is what a button on a row sends.
func (j *Job) Pause() error { j.Paused = !j.Paused; return nil }
vero.UpdateItem(jobs, "pauseJob", (*Job).Pause)
The request is an ID, the item is found by Key, change runs under the lock with a pointer into the state, and the reply is the new state. A request naming something that is gone is refused and the worker carries on.
Which collection is worked out from the item type: the state's one []J field, or the state itself when it is a []J. A state with two fields of the same item type cannot say which, and says so at registration rather than guessing.
func UpdateWith ¶
Update answers a request by changing the state, and replies with it.
jobs.Update("restartJob", func(s *Status, req RestartJob) error {
s.Jobs[req.ID].Progress = 0
return nil
})
fn runs under the lock, with the state and the decoded request, and the reply is the state as it leaves. So a frontend cannot draw the state from before its own change, and there is nothing to copy or forget to return.
Returning an error sends that to the frontend and changes nothing else.
The lock is held for the duration, so this is for changing state, not for work: anything slow belongs in a goroutine the handler starts, writing back through Do. A handler that replies with something other than the state is Worker.Handle.
Types ¶
type Envelope ¶
type Envelope struct {
// Kind is "event" or "reply" on the way out of a worker, and empty on the
// way in, where every line is a request.
Kind string `json:"t,omitempty"`
// ID matches a reply to the request that caused it. Events have none.
ID uint64 `json:"id,omitempty"`
// Name routes a request to a handler registered with Handle or Update.
// Empty when
// the worker has a single handler that dispatches for itself.
//
// It lives here rather than inside the payload so that vero can route
// without knowing anything about the message, and so an application is
// not obliged to carry a type field in every request it defines.
Name string `json:"n,omitempty"`
// Payload is your own message, carried through untouched.
Payload json.RawMessage `json:"p,omitempty"`
// Error is set instead of Payload when a handler returned an error.
Error string `json:"e,omitempty"`
// Code classifies an error for a frontend that cannot match on English:
// "refused", "not_running", "already_running" or "failed". A host sets
// it; a worker does not, because the supervisor on the other side of it
// has the Go errors themselves.
Code string `json:"c,omitempty"`
}
Envelope is one line of the protocol. Everything is optional except what the message kind requires, so the same struct decodes in both directions.
type Handler ¶
Handler answers one request.
ctx is cancelled when the frontend goes away, and when the frontend stops waiting for this request's reply, as a window does when what asked has scrolled out of sight. A handler that does slow work stops early by watching ctx.Done(), and one that waits - a long poll, most obviously - must select on ctx.Done() as well. Without that the worker cannot exit when its parent does: it has stopped reading requests, but it is still waiting for the answer to one nobody will read. Returning an error sends the message to the caller rather than closing anything: the frontend can then tell "the worker refused this" from "the worker is not there", which want opposite responses.
Returning a nil reply and a nil error answers nothing at all, which suits a request that only causes an action.
type ID ¶
type ID[T any] struct { ID T `json:"id"` }
ID is a request that carries nothing but an identifier, which is most of them: restart this, cancel that, open the other. T is whatever your ids are.
jobs.Update("restartJob", func(s *Status, req vero.ID[int]) error {
…
})
type Keyed ¶
type Keyed[K comparable] interface { Key() K }
Keyed is anything the frontend can name: a job with an id, a row with a primary key. Key is what the ID in a request matches.
func (j Job) Key() int { return j.ID }
type RemoteError ¶
type RemoteError struct{ Message string }
RemoteError is what a Handler's error becomes on the other side of the pipe.
func (*RemoteError) Error ¶
func (e *RemoteError) Error() string
Error is the worker's own message, not vero's.
type RunState ¶
type RunState int
RunState is what the supervisor last saw the worker doing. It is the worker's lifecycle, not the state it publishes: that is Worker.NewState on the other side of the pipe.
type State ¶
type State[T any] struct { // contains filtered or unexported fields }
State is everything the frontend draws, and the lock that guards it.
Create one with Worker.NewState. The worker emits it whenever it changes and a handler registered with Worker.Update replies with it, so a frontend never polls and never draws the state from before its own change.
The worker reads it as JSON, under the same lock that Do takes, so there is no copy to get wrong: a snapshot cannot be encoded halfway through a change, and nothing can hand out a value that keeps changing underneath the comparison that decides whether to emit.
func NewState ¶
NewState gives this worker its state, and returns it for the application to change with Do.
jobs := vero.NewState(w, Status{Jobs: []Job{{ID: 1, Name: "Photos"}}})
jobs.Do(func(s *Status) { s.Jobs[0].Progress++ })
One worker has one state: calling this twice replaces the first, because the event channel carries no name and a frontend would have no way to tell two kinds of event apart.
func (*State[T]) Do ¶
func (s *State[T]) Do(f func(*T))
Do runs f with the state, under the lock. Every change goes through it.
jobs.Do(func(s *Status) { s.Jobs = append(s.Jobs, Job{}) })
Do not keep the pointer, start a goroutine with it, or block in f: the worker cannot encode the state while f holds the lock.
func (*State[T]) Every ¶
Every runs fn with the state, under the lock, every interval, until the frontend goes away. It returns straight away: the loop is a goroutine of its own.
jobs.Every(200*time.Millisecond, func(s *Status) {
for i := range s.Jobs { s.Jobs[i].Progress++ }
})
This is where a worker's own work goes when it is periodic. Writing the loop by hand costs a goroutine, a ticker that time.Tick never reclaims, and a nested Do.
func (*State[T]) JSON ¶
func (s *State[T]) JSON() (json.RawMessage, error)
JSON is the state, encoded under the lock.
This is the only way the state leaves: a Go value handed out would share the slices and maps inside it, and whatever received it would be reading them while Do writes. Use it to reply with the state by hand - Update does it for you - and pass the result straight back as the reply.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor runs a worker, keeps it running, and talks to it.
func Supervise ¶
func Supervise(opts SupervisorOptions) *Supervisor
Supervise launches the worker and keeps it alive until Stop.
It returns immediately; the worker comes up on its own goroutine. Requests made before it is ready return ErrWorkerNotRunning rather than blocking, so a frontend can draw itself at once and show that it is waiting.
func (*Supervisor) Call ¶
func (s *Supervisor) Call(ctx context.Context, name string, request any) (json.RawMessage, error)
Call sends a request to the handler registered under name, for a worker registered by name.
The name travels beside the payload rather than inside it, so an application's own message types stay exactly as it defined them.
func (*Supervisor) Err ¶
func (s *Supervisor) Err() error
Err reports why the worker never started, or nil.
The one that matters is ErrAlreadyRunning: another process holds the lock, so this Supervisor will not run a worker at all.
func (*Supervisor) Latest ¶
func (s *Supervisor) Latest() json.RawMessage
Latest returns the most recent event, or nil before the first one.
A frontend that has just drawn a window needs the current state without waiting for the next change, and this is that. Events keep it up to date afterwards.
func (*Supervisor) Request ¶
func (s *Supervisor) Request(ctx context.Context, request any) (json.RawMessage, error)
Request sends a request and waits for its reply.
A handler's error comes back as *RemoteError, which is worth telling apart from ErrWorkerNotRunning: one means the worker refused what you asked, the other that it is not there to ask.
func (*Supervisor) Restarts ¶
func (s *Supervisor) Restarts() int
Restarts counts how many times the worker has been relaunched without one of them lasting: a worker that runs for Healthy and then exits, whatever its reason, puts this back to zero.
So it answers "is this worker failing to start", which is the question worth asking, and not "how many times has it been started", which counts every deliberate exit as evidence of a fault.
func (*Supervisor) State ¶
func (s *Supervisor) State() RunState
State reports what the worker is doing now.
func (*Supervisor) Stop ¶
func (s *Supervisor) Stop() error
Stop shuts the worker down and waits for it to go.
Closing its standard input is the whole shutdown protocol: Run returns, the process exits, and anything it was holding is released the way it would be on any other exit. A worker that ignores that is killed after a grace period.
type SupervisorOptions ¶
type SupervisorOptions struct {
// Path is the worker binary. Args are passed to it verbatim, so the
// application keeps its own command line.
Path string
Args []string
// Dir and Env, if set, are the worker's working directory and
// environment. VERO_SERVE is added to Env either way.
Dir string
Env []string
// Serve runs a worker in this process, reading requests from in and
// writing events and replies to out, instead of Path being started as a
// program. Everything else is unchanged: the same requests, the same
// events, the same replies, over an io.Pipe rather than a pipe to a
// child.
//
// iOS is why this exists. An application there may not start another
// program, but it may compile Go in, so the worker runs on a goroutine
// of its own. Restarts lose their meaning when it does: a goroutine
// dies with the process it is in, and there is nothing left to restart
// it into.
Serve func(in io.Reader, out io.Writer) error
// OnEvent receives every event the worker emits, in order.
OnEvent func(event json.RawMessage)
// OnLog receives the worker's standard error, a line at a time. Workers
// log there precisely so this does not have to guess at framing.
OnLog func(line string)
// OnStateChange is told when the worker starts, dies, or is given up on.
OnStateChange func(RunState)
// Backoff is the wait before the first restart, doubling up to
// MaxBackoff. A worker that crashes on startup would otherwise be
// relaunched as fast as the machine can fork. Zero means 200ms and 30s.
Backoff time.Duration
MaxBackoff time.Duration
// Healthy is how long a worker has to run before the restarts behind it
// are forgotten: the backoff returns to Backoff and Restarts returns to
// zero. Zero means 30 seconds.
//
// Workers exit on purpose - to pick up a change to their configuration,
// or to run a version they have just downloaded - and one that served for
// an hour before doing so has told us nothing except that it is working.
// Without this the backoff climbs to MaxBackoff over a day of ordinary
// use, so an intentional exit costs half a minute of nothing, and any
// frontend watching the count concludes the worker is broken.
Healthy time.Duration
// Lock keys the single-worker lock: one process at a time supervises a
// worker under a given key. Empty means the worker's own path, which is
// what you want unless two applications share a binary and should still
// be allowed to run at once - then give them a key each.
Lock string
// NoLock turns the lock off, for a program that genuinely wants several
// workers at once.
NoLock bool
}
SupervisorOptions describes the worker to run and who to tell about it.
The callbacks are called from the supervisor's own goroutines, never on the frontend's main thread. Whatever you do in them, hop to the main thread before touching the frontend.
type Worker ¶
type Worker struct {
// contains filtered or unexported fields
}
Worker is the logic half of a vero application.
func NewWorker ¶
func NewWorker(opts WorkerOptions) *Worker
NewWorker prepares a worker. Whether a supervisor launched it is read from the environment, so the same binary and the same code run both ways.
Call this before starting any goroutine ¶
In serve mode standard output belongs to the protocol, so a stray Println anywhere in the program would corrupt a frame. Rather than ask every author of every line in the program to remember that, this takes the real standard output for itself and points the package-level os.Stdout at standard error.
fmt.Println and friends read os.Stdout at the moment they are called, so every existing print in the program - and every one anybody adds later, in any package, including ones you depend on and cannot edit - lands harmlessly on stderr from here on. There is nothing to audit and nothing to keep auditing.
It does mean reassigning a package-level variable, so call NewWorker before anything else could be printing: another goroutine writing while this reassigns is a data race, and one the race detector will find.
func (*Worker) EmitOnChange ¶
EmitOnChange emits what snapshot returns, whenever it differs from what was emitted last. It runs until ctx is done, so start it with go.
go w.EmitOnChange(ctx, 250*time.Millisecond, func() any { return state() })
Most workers do not call this: set WorkerOptions.State instead and Serve runs it for you, on a context that ends when the frontend does. This is for the ones whose events are not one state snapshot.
Emitting on a timer instead is the obvious thing to write and it defeats the point: a frontend is sent the same thing several times a second, and the quiet-when-nothing-happens property - the reason events are pushed rather than polled - is gone.
Comparison is by reflect.DeepEqual, so snapshot should return a value rather than something holding a pointer to state that keeps changing underneath it.
func (*Worker) Fallback ¶
Fallback answers requests whose name matches nothing registered, including requests that carry no name at all.
func (*Worker) FallbackCalls ¶
FallbackCalls counts the requests that have reached the fallback, so an application can tell when the old shape has stopped being used.
func (*Worker) Log ¶
Log writes a line to standard error, where logs belong. Standard output is reserved for the protocol, and a stray write there corrupts the stream.
func (*Worker) Serve ¶
Serve answers the requests registered with Handle and Update.
Otherwise identical to Run: it blocks, it returns when the frontend goes away, and on its own it never returns.
func (*Worker) Supervised ¶
Supervised reports whether a Supervisor launched this worker. Useful when the standalone form wants to behave differently - printing a summary and exiting, say, rather than running forever.
type WorkerOptions ¶
type WorkerOptions struct {
// JSON emits the event stream on standard output. A Supervisor turns
// this on for you; set it yourself when a script is watching.
JSON bool
// Quiet suppresses the human readable log on standard error.
Quiet bool
// Version is what -version reports. A frontend that ships this worker
// in its bundle asks for it to decide whether the copy it has on disk is
// older than the one it was built with, so the two halves have to agree
// on the flag: vero registers it here so they cannot drift.
Version string
// In and Out replace standard input and output, which is how a worker
// runs inside the application rather than beside it. Setting them also
// says this worker is being supervised, since nothing else would.
//
// iOS is why they exist: an application there may not start another
// program, so the only way to run a worker is in the same process, on
// the other end of an io.Pipe.
In io.Reader
Out io.Writer
// StateInterval is how often the state is sampled for a change, for a
// worker that has one. Zero means DefaultStateInterval.
StateInterval time.Duration
// Lock, when set, is a name for this worker, such as the app's id, of
// which only one may run at a time for this user, however it was
// started: by an app, or on its own as a daemon, which then cannot both
// work on the same files. A second one started on its own says that
// another is running and exits; one started by an app answers every
// request with ErrAlreadyRunning, which the app shows, and runs none of
// the program's own code.
Lock string
// ShowVersion is set by RegisterFlags when -version was passed. Answer it
// and exit before anything else starts - it runs as its own short-lived
// process, several times per launch, and must not open a port, touch a
// configuration directory, or print anything else.
ShowVersion bool
}
WorkerOptions controls what a worker writes, and where.
The two settings are deliberately separate. Format and volume are different questions: a frontend wants every state change as JSON, and a person reading a terminal or a journal wants a handful of lines saying what actually happened. Emitting the first to satisfy the second is how logs become unreadable.
func (*WorkerOptions) PrintVersionAndExit ¶
func (o *WorkerOptions) PrintVersionAndExit()
PrintVersionAndExit answers -version if it was asked for, and does not return in that case.
Call it immediately after flag.Parse, before anything else starts.
func (*WorkerOptions) RegisterFlags ¶
func (o *WorkerOptions) RegisterFlags(fs *flag.FlagSet)
RegisterFlags adds -json and -quiet to fs. Call it before flag.Parse.
A worker does not need a flag for serve mode: a Supervisor sets it through the environment, so nothing on the command line has to describe an arrangement the user did not ask for.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
vero
module
|
|
|
Command cshim builds vero as a C archive, so a native frontend can embed it and drive a worker without knowing anything about pipes, JSON framing or process supervision.
|
Command cshim builds vero as a C archive, so a native frontend can embed it and drive a worker without knowing anything about pipes, JSON framing or process supervision. |
|
example
|
|
|
ios-app/archive
command
The worker, compiled into the application.
|
The worker, compiled into the application. |
|
wasi-app
command
Command wasi-app drives a worker that is not a program.
|
Command wasi-app drives a worker that is not a program. |
|
wasi-app/worker
command
Command worker is the WASI example's worker: the half that would be a program anywhere else, and is a single worker.wasm here.
|
Command worker is the WASI example's worker: the half that would be a program anywhere else, and is a single worker.wasm here. |
|
web-app
command
|
|
|
worker
command
Command worker is the logic half of the example: the part you would write.
|
Command worker is the logic half of the example: the part you would write. |
|
kit
module
|







