Help us shape the future of Go! Take the 2026 Go Developer Survey

vero

package module
v0.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 19 Imported by: 0

README

vero

A Go backend with native macOS, Windows and Linux frontends. All built on macOS.

Go reference

Quickstart on macOS · The go worker · macOS · Windows · Linux · Run the examples

Quickstart on macOS

See all three running before building your own:

git clone https://github.com/calmdocs/vero && cd vero
./scripts/setup.sh                              # installs toolchains, builds everything
./scripts/run.sh --iso ~/Downloads/win11.iso    # opens all three

--iso is a Windows 11 ARM64 ISO, downloaded first from Microsoft. The third line installs it into a VM at ~/vm/vero-windows, which may take a bit of time and happens once: after that ./scripts/run.sh on its own opens all three, and the ISO can be deleted. Without the --iso ~/Downloads/win11.iso flag, macOS and Linux still open and Windows is skipped.

The go worker

All three apps below run this same Go program. Each section builds it for its own platform.

brew install go
mkdir -p ~/vero-example/worker && cd ~/vero-example/worker
go mod init worker
go get github.com/calmdocs/vero

main.go:

package main

import (
	"fmt"
	"time"

	"github.com/calmdocs/vero"
)

// The contract with the interface.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

func main() {
	w := vero.NewWorker(vero.WorkerOptions{})
	jobs := w.NewState(Status{Jobs: []Job{
		{ID: 1, Name: "Photos"},
		{ID: 2, Name: "Documents"},
	}})

	// The actual work.  Yours goes here.
	jobs.Every(200*time.Millisecond, func(s *Status) {
		for i := range s.Jobs {
			if !s.Jobs[i].Paused && s.Jobs[i].Progress < 100 {
				s.Jobs[i].Progress++
			}
		}
	})

	jobs.Update("addJob", func(s *Status) error {
		n := len(s.Jobs) + 1
		s.Jobs = append(s.Jobs, Job{ID: n, Name: fmt.Sprintf("Job %d", n)})
		return nil
	})

	jobs.UpdateItem("restartJob", func(j *Job) error {
		j.Progress = 0
		return nil
	})

	jobs.UpdateItem("pauseJob", func(j *Job) error {
		j.Paused = !j.Paused
		return nil
	})

	w.Serve()
}

macOS: Add vero to your own macOS app

Install Xcode from the App Store. Steps 2 and 3 happen in it, and it brings the lipo that step 1 uses.

1. Build the worker
cd ~/vero-example/worker
GOOS=darwin GOARCH=amd64 go build -o worker-amd64 && \
GOOS=darwin GOARCH=arm64 go build -o worker-arm64 && \
lipo -create worker-amd64 worker-arm64 -output worker
2. The macOS SwiftUI app

In Xcode, File -> New -> Project -> macOS -> App, with Interface set to SwiftUI. Then:

  • File -> Add Package Dependencies... -> https://github.com/calmdocs/vero
  • drag ~/vero-example/worker/worker into the project, ticking your app under Add to targets

Replace ContentView.swift with this:

import SwiftUI
import Vero

// The same two types, and the same request names, as the worker.
struct Job: Decodable, Identifiable {
    let id: Int
    let name: String
    let progress: Int
    let paused: Bool
}

struct Status: Decodable {
    let jobs: [Job]
}

struct AddJob: NamedRequest {
    static let name = "addJob"
    typealias Reply = Status
}

struct RestartJob: NamedRequest {
    static let name = "restartJob"
    typealias Reply = Status
    let id: Int
}

struct PauseJob: NamedRequest {
    static let name = "pauseJob"
    typealias Reply = Status
    let id: Int
}

struct ContentView: View {
    // Copies the worker out of the app bundle, launches it, restarts it if it
    // dies, stops it when the app exits, and republishes this view whenever
    // the worker pushes a new Status.
    @StateObject private var vero = VeroModel<Status>(
        bundledWorker: "worker", directoryName: "Example/bin")

    var body: some View {
        VStack(spacing: 0) {
            HStack {
                Button("Add job") { vero.call(AddJob()) }
                    .disabled(vero.isBusy)
                Spacer()
                if let problem = vero.problem {
                    Text(problem).foregroundStyle(.orange)
                }
            }
            .padding(8)

            List(vero.state?.jobs ?? []) { job in
                HStack {
                    Button { vero.call(RestartJob(id: job.id)) } label: {
                        Image(systemName: "arrow.clockwise")
                    }
                    .disabled(vero.isBusy)

                    Button { vero.call(PauseJob(id: job.id)) } label: {
                        Image(systemName: job.paused ? "play.fill" : "pause.fill")
                    }
                    .disabled(vero.isBusy)

                    Text(job.name)
                    ProgressView(value: Double(job.progress) / 100)
                }
            }
        }
        .frame(minWidth: 320, minHeight: 240)
    }
}
3. Run it

Press Cmd-R. Two jobs appear and their progress climbs. Add job puts a third in the list. Beside each job, the refresh button sets its progress back to zero and the pause button stops and starts it.

Windows: Add the same worker to a Windows app

Every step below runs on your Mac. Only step 4 needs a Windows on ARM machine, and step 4 can boot one for you on macOS.

Install the .NET SDK and a Windows ARM64 C compiler, once:

curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
export PATH="$HOME/.dotnet:$PATH"

mkdir -p ~/toolchains && cd ~/toolchains
curl -L "$(curl -s https://api.github.com/repos/mstorsjo/llvm-mingw/releases/latest \
    | grep -o 'https://[^"]*ucrt-macos-universal.tar.xz')" | tar -xJ
mv llvm-mingw-*-ucrt-macos-universal llvm-mingw
1. Build the worker and the library
cd ~/vero-example/worker
CGO_ENABLED=1 GOOS=windows GOARCH=arm64 \
    CC=$HOME/toolchains/llvm-mingw/bin/aarch64-w64-mingw32-clang \
    go build -buildmode=c-shared -o vero.dll github.com/calmdocs/vero/cshim
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o worker.exe .
2. The Windows WPF app

Run these:

mkdir ../wpf-app && cd ../wpf-app
curl -O https://raw.githubusercontent.com/calmdocs/vero/main/bindings/csharp/Vero.cs

Add these four files to the same directory:

VeroExample.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <Nullable>enable</Nullable>
    <RootNamespace>VeroExample</RootNamespace>
  </PropertyGroup>
</Project>

App.xaml:

<Application x:Class="VeroExample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"/>

MainWindow.xaml:

<Window x:Class="VeroExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="vero" Width="360" Height="260">
    <DockPanel Margin="8">
        <Button x:Name="AddJob" Content="Add job" Click="AddJob_Click"
                DockPanel.Dock="Top" HorizontalAlignment="Left" Padding="8,2"/>
        <ItemsControl x:Name="Jobs" Margin="0,8,0,0">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <DockPanel Margin="0,4">
                        <Button Content="&#x21bb;" Tag="{Binding Id}" Click="Restart_Click"/>
                        <Button Content="&#x23f8;" Tag="{Binding Id}" Click="Pause_Click" Margin="4,0,0,0"/>
                        <TextBlock Text="{Binding Name}" Width="90" Margin="8,0"/>
                        <ProgressBar Value="{Binding Progress}" Maximum="100" Height="12"/>
                    </DockPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </DockPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using Vero;

namespace VeroExample;

// The same types, and the same request names, as the worker.
public record Job(
    [property: JsonPropertyName("id")]       int Id,
    [property: JsonPropertyName("name")]     string Name,
    [property: JsonPropertyName("progress")] int Progress,
    [property: JsonPropertyName("paused")]   bool Paused);

public record Status(
    [property: JsonPropertyName("jobs")] Job[] Jobs);

public partial class MainWindow : Window
{
    private readonly VeroClient _vero;
    private readonly ObservableCollection<Job> _jobs = new();

    public MainWindow()
    {
        InitializeComponent();
        Jobs.ItemsSource = _jobs;

        // Launches the worker beside the executable, restarts it if it dies,
        // and stops it when this process exits.
        _vero = new VeroClient(Path.Combine(
            AppDomain.CurrentDomain.BaseDirectory, "worker.exe"));

        _ = ReadEvents();
    }

    // Pushed the instant the worker's state moves, on its own thread.
    private async System.Threading.Tasks.Task ReadEvents()
    {
        await foreach (var element in _vero.Events())
        {
            var status = element.Deserialize<Status>();
            if (status is not null)
                Dispatcher.Invoke(() => Apply(status));
        }
    }

    private void Apply(Status status)
    {
        _jobs.Clear();
        foreach (var job in status.Jobs) _jobs.Add(job);
    }

    private async void AddJob_Click(object sender, RoutedEventArgs e) =>
        await _vero.CallAsync("addJob", new { });

    private async void Restart_Click(object sender, RoutedEventArgs e) =>
        await _vero.CallAsync("restartJob", new { id = (int)((Button)sender).Tag });

    private async void Pause_Click(object sender, RoutedEventArgs e) =>
        await _vero.CallAsync("pauseJob", new { id = (int)((Button)sender).Tag });
}
3. Build the app
dotnet publish -c Release -r win-arm64 --self-contained \
    -p:EnableWindowsTargeting=true -o out
cp ../worker/vero.dll ../worker/worker.exe out/
4. Run it

On a Windows on ARM machine, run VeroExample.exe from out/.

No Windows machine? Boot one on your Mac. The first time, install Windows into the VM from a Microsoft ISO. The install is unattended, and happens once:

brew install qemu
git clone https://github.com/calmdocs/vero
vero/scripts/run-windows.sh --iso ~/Downloads/win11.iso --install --payload out

After that, this boots the same VM with your latest build on a disc:

vero/scripts/run-windows.sh --payload out

Windows opens in a window on your Mac, which you use like any other. In it, copy the vero folder from the CD drive to C:\, and run VeroExample.exe inside it. Two jobs appear and their progress climbs. Add job puts a third in the list. Beside each job, the refresh button sets its progress back to zero and the pause button stops and starts it.

Linux: Add the same worker to a Linux app

libvero.so has to be compiled on Linux, so step 1 builds it in a container. Everything else runs on your Mac.

Install colima and Docker once:

brew install colima docker && colima start
1. Build the worker and the library
cd ~/vero-example/worker
docker run --rm -v "$PWD":/src -w /src \
    -e GOCACHE=/tmp/gocache -e GOPATH=/tmp/go -e GOTOOLCHAIN=auto \
    golang:1.24-bookworm \
    sh -c 'CGO_ENABLED=1 go build -buildmode=c-shared -o libvero.so github.com/calmdocs/vero/cshim'

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o worker-linux .
2. The Linux GTK4 app

Run these:

mkdir ../gtk-app && cd ../gtk-app
curl -O https://raw.githubusercontent.com/calmdocs/vero/main/bindings/python/vero.py
cp ../worker/libvero.so .
cp ../worker/worker-linux worker

Then add main.py:

#!/usr/bin/env python3
import os
import gi

gi.require_version("Gtk", "4.0")
from gi.repository import GLib, Gtk

from vero import Vero, run_in_thread

HERE = os.path.dirname(os.path.abspath(__file__))


class Window(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title="vero", default_width=360)
        self.bars = {}

        # Launches the worker beside this file, restarts it if it dies, and
        # stops it when this process exits.
        self.vero = Vero(os.path.join(HERE, "libvero.so"),
                         os.path.join(HERE, "worker"))

        add = Gtk.Button(label="Add job")
        add.connect("clicked", lambda _: self.vero.call("addJob", {}))

        self.rows = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8,
                      margin_top=8, margin_bottom=8, margin_start=8, margin_end=8)
        box.append(add)
        box.append(self.rows)
        self.set_child(box)

        # latest draws a window that has just opened; events keep it current.
        if status := self.vero.latest():
            self.apply(status)
        run_in_thread(self.vero, lambda s: GLib.idle_add(self.apply, s))

    def apply(self, status):
        for job in status["jobs"]:
            if job["id"] not in self.bars:
                self.bars[job["id"]] = self.add_row(job)
            bar, pause = self.bars[job["id"]]
            bar.set_fraction(job["progress"] / 100)
            pause.set_icon_name("media-playback-start-symbolic" if job["paused"]
                                else "media-playback-pause-symbolic")
        return False  # GLib.idle_add: run once

    def add_row(self, job):
        bar = Gtk.ProgressBar(hexpand=True, valign=Gtk.Align.CENTER)
        restart = Gtk.Button(icon_name="view-refresh-symbolic")
        restart.connect(
            "clicked", lambda _, i=job["id"]: self.vero.call("restartJob", {"id": i}))
        pause = Gtk.Button(icon_name="media-playback-pause-symbolic")
        pause.connect(
            "clicked", lambda _, i=job["id"]: self.vero.call("pauseJob", {"id": i}))

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row.append(restart)
        row.append(pause)
        row.append(Gtk.Label(label=job["name"], width_chars=10, xalign=0))
        row.append(bar)
        self.rows.append(row)
        return bar, pause


app = Gtk.Application(application_id="com.example.vero")
app.connect("activate", lambda a: Window(a).present())
app.run(None)
3. Run it

On a Linux machine:

sudo apt-get install -y python3-gi gir1.2-gtk-4.0
chmod +x main.py && ./main.py

Or run it from your Mac, on a virtual display in a container, and watch it in Screen Sharing:

docker build -t vero-gtk - <<'EOF'
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      python3 python3-gi gir1.2-gtk-4.0 libgtk-4-1 xvfb x11vnc xauth \
    && rm -rf /var/lib/apt/lists/*
EOF

docker run --rm -p 5901:5900 -v "$PWD":/app -w /app vero-gtk sh -c '
    Xvfb :99 -screen 0 480x440x24 &
    sleep 2
    export DISPLAY=:99
    python3 main.py &
    sleep 4
    x11vnc -display :99 -forever -nopw -listen 0.0.0.0'

open vnc://localhost:5901

Two jobs appear and their progress climbs. Add job puts a third in the list. Beside each job, the refresh button sets its progress back to zero and the pause button stops and starts it.

Run the examples

example/menubar-app macOS, SwiftUI
example/wpf-app Windows, WPF
example/gtk-app Linux, GTK4

The Go API, with runnable examples: pkg.go.dev/github.com/calmdocs/vero.

The repository's own tests:

go test -race ./...
cd bindings/python && python3 -m unittest
swift build

Licence

MIT

Documentation

Overview

Package vero is a Go backend with native macOS, Windows and Linux frontends, all built on macOS.

The interface is the platform's own toolkit - SwiftUI, WPF, GTK - and Go supplies everything behind it. No webview, and no widget set of its own.

The go worker

All three apps run this same Go program. It is an ordinary binary: run it yourself and it is a daemon, launch it from an interface and it answers that interface, and it does not know which it is doing.

package main

import (
	"fmt"
	"time"

	"github.com/calmdocs/vero"
)

// The contract with the interface.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

func main() {
	w := vero.NewWorker(vero.WorkerOptions{})
	jobs := w.NewState(Status{Jobs: []Job{
		{ID: 1, Name: "Photos"},
		{ID: 2, Name: "Documents"},
	}})

	// The actual work.  Yours goes here.
	jobs.Every(200*time.Millisecond, func(s *Status) {
		for i := range s.Jobs {
			if !s.Jobs[i].Paused && s.Jobs[i].Progress < 100 {
				s.Jobs[i].Progress++
			}
		}
	})

	jobs.Update("addJob", func(s *Status) error {
		n := len(s.Jobs) + 1
		s.Jobs = append(s.Jobs, Job{ID: n, Name: fmt.Sprintf("Job %d", n)})
		return nil
	})

	jobs.UpdateItem("restartJob", func(j *Job) error {
		j.Progress = 0
		return nil
	})

	jobs.UpdateItem("pauseJob", func(j *Job) error {
		j.Paused = !j.Paused
		return nil
	})

	w.Serve()
}

The interface

A Supervisor launches the worker, restarts it if it dies, and carries the messages. It is Go, embedded in the application as a C archive, so the connection handling is written once rather than once per platform. Native code drives it through nine C functions, and a binding over those ships with vero: Sources/Vero for Swift, bindings/csharp for C#, bindings/python for Python. An interface declares the same types the worker does, and calls handlers by the same names.

The channel

The two talk over the pipes the operating system created when the supervisor launched the worker. That is not a smaller socket, it is a different guarantee: there is no filesystem object, so nothing else on the machine can connect even in principle, and the channel dies with the process. It also means a worker cannot outlive the interface that started it - when the application quits, or crashes, or is force quit, the worker's standard input closes and it exits, with no PID file and no heartbeat to get wrong.

Crashes

The worker is a separate process, so a panic in it - or one of Go's unrecoverable runtime failures, like a concurrent map write, which recover cannot catch - takes down the worker and not the interface. The supervisor notices, reports it, and starts a new one.

The wire format

Newline delimited JSON, in both directions. Your own message travels in "p", untouched:

to the worker     {"id":7,"n":"addJob","p":<your request>}
from the worker   {"t":"reply","id":7,"p":<your reply>}
                  {"t":"reply","id":7,"e":"what went wrong"}
                  {"t":"event","p":<your event>}

Events are unsolicited: the worker sends one whenever its state moves, so an interface never polls and never waits on a timer. Replies are matched to requests by id, so a slow request cannot hold up the events behind it.

Standard output carries this stream and nothing else. Logs go to standard error, always. A worker that no one is watching writes neither.

Example

A whole worker: your types, your logic, and four calls into vero.

Serve blocks until the interface goes away, then returns so this process can leave with it.

package main

import (
	"fmt"
	"time"

	"github.com/calmdocs/vero"
)

// The contract with the interface: the same fields Swift, C# and Python
// declare on their side.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

func main() {
	w := vero.NewWorker(vero.WorkerOptions{})

	// Everything the interface draws.  vero pushes it whenever it changes, and
	// every reply below is it.
	jobs := w.NewState(Status{Jobs: []Job{
		{WithID: vero.WithID[int]{ID: 1}, Name: "Photos"},
		{WithID: vero.WithID[int]{ID: 2}, Name: "Documents"},
	}})

	// The work, under the lock, until the interface goes away.
	jobs.Every(200*time.Millisecond, func(s *Status) {
		for i := range s.Jobs {
			if !s.Jobs[i].Paused && s.Jobs[i].Progress < 100 {
				s.Jobs[i].Progress++
			}
		}
	})

	// A button with no payload.
	jobs.Update("addJob", func(s *Status) error {
		n := len(s.Jobs) + 1
		s.Jobs = append(s.Jobs, Job{
			WithID: vero.WithID[int]{ID: n},
			Name:   fmt.Sprintf("Job %d", n),
		})
		return nil
	})

	// A button on one row: vero decodes the id, finds the job, and replies
	// with the new state.
	jobs.UpdateItem("pauseJob", func(j *Job) error {
		j.Paused = !j.Paused
		return nil
	})

	w.Serve()
}

Index

Examples

Constants

View Source
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.

View Source
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

View Source
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.

View Source
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 an interface: it means wait, not that anything you did was wrong.

View Source
var ShutdownGrace = 5 * time.Second

ShutdownGrace is how long Run waits for handlers to notice that the interface has gone before exiting regardless.

Functions

func Edit added in v0.5.0

func Edit[T Keyed[K], K comparable](items []T, key K, change func(*T)) error

Edit finds the item the interface 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.

Example

Edit finds an item by the id an interface sent, and changes it. The pointer is into the slice, so the change sticks.

package main

import (
	"fmt"

	"github.com/calmdocs/vero"
)

// The contract with the interface: the same fields Swift, C# and Python
// declare on their side.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

func main() {
	jobs := []Job{
		{WithID: vero.WithID[int]{ID: 1}, Name: "Photos", Progress: 50},
		{WithID: vero.WithID[int]{ID: 2}, Name: "Documents", Progress: 80},
	}

	err := vero.Edit(jobs, 1, func(j *Job) { j.Progress = 0 })
	fmt.Println(jobs[0].Progress, err)

	// A request naming something that is gone is refused, and the worker
	// carries on.
	fmt.Println(vero.Edit(jobs, 99, func(j *Job) { j.Progress = 0 }))

}
Output:
0 <nil>
no item with id 99

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"`
}

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

type Handler func(ctx context.Context, request json.RawMessage) (reply any, err error)

Handler answers one request.

ctx is cancelled when the interface goes away, so a handler 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 interface 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 added in v0.5.0

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 added in v0.5.0

type Keyed[K comparable] interface {
	Key() K
}

Keyed is anything the interface can name: a job with an id, a row with a primary key. Key is what the ID in a request matches.

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 added in v0.5.0

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.

const (
	// Starting means a worker is being launched, including a restart.
	Starting RunState = iota
	// Running means a worker is up and answering.
	Running
	// Restarting means one died and another is on the way.
	Restarting
	// Stopped means Stop was called; nothing further will be launched.
	Stopped
)

func (RunState) String added in v0.5.0

func (s RunState) String() string

String is what the bindings report: "starting", "running", "restarting" or "stopped".

type State

type State[T any] struct {
	// contains filtered or unexported fields
}

State is everything the interface 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 an interface 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 (*State[T]) Do added in v0.5.0

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.

Example

Do is a write from the worker's own code, rather than an answer to a request: the lock is taken for the moment of the change, and the new state is pushed to the interface like any other.

package main

import (
	"github.com/calmdocs/vero"
)

// The contract with the interface: the same fields Swift, C# and Python
// declare on their side.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

func main() {
	w := vero.NewWorker(vero.WorkerOptions{})
	jobs := w.NewState(Status{})

	go func() {
		// …work happens…
		jobs.Do(func(s *Status) {
			s.Jobs = append(s.Jobs, Job{WithID: vero.WithID[int]{ID: 7}, Name: "Found"})
		})
	}()

	w.Serve()
}

func (*State[T]) Every added in v0.5.0

func (s *State[T]) Every(interval time.Duration, fn func(*T))

Every runs fn with the state, under the lock, every interval, until the interface 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 added in v0.5.0

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.

func (*State[T]) Update added in v0.5.0

func (s *State[T]) Update(name string, fn func(*T) error)

Update answers a request that carries nothing - a button with no payload - changes the state, and replies with it.

func (s *Status) Add() error { … }

jobs.Update("addJob", (*Status).Add)

UpdateWith is the same for a request that carries something, and UpdateItem for one that names an item.

func (*State[T]) UpdateItem added in v0.6.0

func (s *State[T]) UpdateItem[J Keyed[K], K comparable](name string, change func(*J) error)

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 }

jobs.UpdateItem("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 (*State[T]) UpdateWith added in v0.6.0

func (s *State[T]) UpdateWith[Req any](name string, fn func(state *T, request Req) error)

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 an interface 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 interface 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.

Example

A request that carries something of its own needs a type for it, and UpdateWith rather than Update.

package main

import (
	"github.com/calmdocs/vero"
)

// The contract with the interface: the same fields Swift, C# and Python
// declare on their side.
type Job struct {
	vero.WithID[int]
	Name     string `json:"name"`
	Progress int    `json:"progress"`
	Paused   bool   `json:"paused"`
}

type Status struct {
	Jobs []Job `json:"jobs"`
}

func main() {
	type Rename struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}

	w := vero.NewWorker(vero.WorkerOptions{})
	jobs := w.NewState(Status{})

	jobs.UpdateWith("renameJob", func(s *Status, req Rename) error {
		return vero.Edit(s.Jobs, req.ID, func(j *Job) { j.Name = req.Name })
	})

	w.Serve()
}

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 an interface 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.

An interface 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 after dying.

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

	// 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

	// 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 interface's main thread. Whatever you do in them, hop to the main thread before touching the interface.

type WithID added in v0.5.0

type WithID[K comparable] struct {
	ID K `json:"id"`
}

WithID gives an item the id the interface names it by, and the Key that finds it. Embed it and the field is still yours to set:

type Job struct {
    vero.WithID[int]
    Name string `json:"name"`
}

Job{ID: 1, Name: "Photos"}

func (WithID[K]) Key added in v0.5.0

func (w WithID[K]) Key() K

Key is the id, so anything embedding WithID can be found by Find and Edit.

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

func (w *Worker) EmitOnChange(ctx context.Context, interval time.Duration, snapshot func() any)

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 interface 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: an interface 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 added in v0.4.0

func (w *Worker) Fallback(h Handler)

Fallback answers requests whose name matches nothing registered, including requests that carry no name at all.

func (*Worker) FallbackCalls added in v0.4.0

func (w *Worker) FallbackCalls() uint64

FallbackCalls counts the requests that have reached the fallback, so an application can tell when the old shape has stopped being used.

func (*Worker) Handle added in v0.5.0

func (w *Worker) Handle[Req any, Rep any](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.

Example

Handle is for a reply that is not the state: its own request and reply types, and a context that is cancelled when the interface goes away.

package main

import (
	"context"

	"github.com/calmdocs/vero"
)

func main() {
	type Query struct {
		Path string `json:"path"`
	}
	type Size struct {
		Bytes int64 `json:"bytes"`
	}

	w := vero.NewWorker(vero.WorkerOptions{})

	w.Handle("sizeOf", func(ctx context.Context, req Query) (Size, error) {
		// ctx ends when the interface does, so a slow answer can give up.
		return Size{Bytes: 1024}, nil
	})

	w.Serve()
}

func (*Worker) Log

func (w *Worker) Log(format string, a ...any)

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) NewState added in v0.5.0

func (w *Worker) NewState[T any](initial T) *State[T]

NewState gives this worker its state, and returns it for the application to change with Do.

jobs := w.NewState(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 an interface would have no way to tell two kinds of event apart.

func (*Worker) Serve

func (w *Worker) Serve() error

Serve answers the requests registered with Handle and Update.

Otherwise identical to Run: it blocks, it returns when the interface goes away, and on its own it never returns.

func (*Worker) Supervised

func (w *Worker) Supervised() bool

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.  An interface 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

	// StateInterval is how often the state is sampled for a change, for a
	// worker that has one.  Zero means DefaultStateInterval.
	StateInterval time.Duration

	// 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: an interface 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.

Directories

Path Synopsis
Command cshim builds vero as a C archive, so a native interface 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 interface can embed it and drive a worker without knowing anything about pipes, JSON framing or process supervision.
example
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL