ooo

package module
v0.0.0-...-8c38013 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 35 Imported by: 6

README

ooo logo

ooo

ooo UI demo

Test

State management with real-time network access.

ooo provides a fast, zero-configuration In-memory layer for storing and synchronizing application state or settings. It uses an embedded storage engine with optional persistence via ko, and delivers changes to subscribers using JSON Patch for efficient updates.

When to use ooo
  • Application state/settings that need real-time sync across clients
  • Prototyping real-time features quickly
  • Small to medium datasets where speed matters more than scale
When NOT to use ooo

For large-scale data storage (millions of records, complex queries), use a dedicated database like nopog. You can combine both: ooo for real-time state, nopog for bulk data.

Ecosystem

Package Description
ooo Core server - in-memory state with WebSocket/REST API
ko Persistent storage adapter (LevelDB)
ooo-client JavaScript client with reconnecting WebSocket
auth JWT authentication middleware
mono Full-stack boilerplate (Go + React)
nopog PostgreSQL adapter for large-scale storage
pivot Multi-instance synchronization (AP distributed)

Features

  • Dynamic routing with glob patterns for collections
  • Real-time subscriptions via WebSocket
  • JSON Patch updates for efficient sync
  • Version checking (no data sent on version match while reconnecting)
  • RESTful CRUD reflected to subscribers
  • Filtering and Router.Use() middleware
  • Auto-managed timestamps (created, updated) with a monotonic clock for consistency on ntp/ptp synchronizations
  • Built-in web UI for data management

quickstart

client

There's a js client.

server

with go installed get the library

go get github.com/benitogf/ooo

create a file main.go

package main

import "github.com/benitogf/ooo"

func main() {
  server := ooo.Server{}
  server.Start("0.0.0.0:8800")
  server.WaitClose()
}

run the service:

go run main.go

API Reference

UI & Management
Method Description URL
GET Web interface http://{host}:{port}/
GET List all keys (paginated) http://{host}:{port}/?api=keys
GET Server info http://{host}:{port}/?api=info
GET Filter paths http://{host}:{port}/?api=filters
GET Connection state http://{host}:{port}/?api=state
Keys API Query Parameters
Parameter Description Default
page Page number (1-indexed) 1
limit Items per page (max 500) 50
filter Filter by prefix or glob pattern (none)
Data Operations
Method Description URL
POST Create/Update http://{host}:{port}/{key}
GET Read http://{host}:{port}/{key}
PATCH Partial update (JSON Patch) http://{host}:{port}/{key}
DELETE Delete http://{host}:{port}/{key}
WebSocket
Method Description URL
WS Server clock ws://{host}:{port}
WS Subscribe to path ws://{host}:{port}/{key}

control

static routes

Activating this flag will limit the server to process requests defined by filters

server := ooo.Server{}
server.Static = true
Filters

Filters control access and transform data. When Static mode is enabled, only filtered routes are available.

Paths support glob patterns (*) and multi-level globs like users/*/posts/*.

Filter Description
OpenFilter Enable route (required in static mode)
WriteFilter Transform/validate before write
AfterWriteFilter Callback after write completes
ReadObjectFilter Transform single object on read
ReadListFilter Transform list items on read
DeleteFilter Control delete operations
LimitFilter Maintain max entries in a list (auto-cleanup)
OpenFilter
// Enable a route (required when Static=true)
server.OpenFilter("books/*")
WriteFilter
// Validate/transform before write
server.WriteFilter("books/*", func(index string, data json.RawMessage) (json.RawMessage, error) {
    // return error to deny, or modified data
    return data, nil
})
AfterWriteFilter
// Callback after write completes
server.AfterWriteFilter("books/*", func(index string) {
    log.Println("wrote:", index)
})
ReadObjectFilter
// Transform single object on read
server.ReadObjectFilter("books/special", func(index string, data meta.Object) (meta.Object, error) {
    return data, nil
})
ReadListFilter
// Transform list items on read
server.ReadListFilter("books/*", func(index string, items []meta.Object) ([]meta.Object, error) {
    return items, nil
})
DeleteFilter
// Control delete (return error to prevent)
server.DeleteFilter("books/protected", func(key string) error {
    return errors.New("cannot delete")
})
LimitFilter

LimitFilter is implemented using a ReadListFilter (to limit visible items), a noop WriteFilter (to allow writes), a DeleteFilter (to allow deletes), and an AfterWriteFilter (to trigger cleanup). This means it includes open read and write access.

Supports count-based limits, time-based retention, or both combined. At least one constraint must be provided.

// Count-only: keep N most recent entries (auto-deletes oldest)
server.LimitFilter("logs/*", ooo.LimitFilterConfig{Limit: 100})

// Time-only: keep entries younger than MaxAge (retention policy)
server.LimitFilter("events/*", ooo.LimitFilterConfig{
    MaxAge: 24 * time.Hour,
})

// Combined: both count and time constraints (stricter wins)
server.LimitFilter("metrics/*", ooo.LimitFilterConfig{
    Limit:  1000,
    MaxAge: 7 * 24 * time.Hour,
})

// Dynamic limit based on runtime state
server.LimitFilter("games/*", ooo.LimitFilterConfig{
    LimitFunc: func() int { return getDeviceCap() },
    Order:     ooo.OrderAsc,
})

// Dynamic max age from external config
server.LimitFilter("audit/*", ooo.LimitFilterConfig{
    MaxAgeFunc: func() time.Duration { return getRetentionPolicy() },
})

// With periodic background cleanup
server.LimitFilter("telemetry/*", ooo.LimitFilterConfig{
    MaxAge: 30 * 24 * time.Hour,
    Cleanup: ooo.CleanupConfig{
        Enabled:  true,
        Interval: 10 * time.Minute, // default: 10min, minimum: 1min
    },
})

LimitFilterConfig options:

  • Limit - Maximum number of entries
  • LimitFunc - Dynamic limit function (func() int)
  • MaxAge - Maximum age of entries (time.Duration)
  • MaxAgeFunc - Dynamic max age function (func() time.Duration)
  • Order - Sort order: OrderDesc (default, most recent first) or OrderAsc (oldest first)
  • Cleanup - Periodic background cleanup config (CleanupConfig{Enabled, Interval})
  • Description - Human-readable description for the explorer UI
  • Schema - JSON schema struct for UI display
Middleware

Gate requests by registering standard http.Handler middleware via server.Router.Use(...). The chain is built into the matched handler by gorilla/mux, so it runs for every matched request — REST handlers, WebSocket upgrades, custom endpoints, proxy routes, and the explorer UI all dispatch through the chain. Unmatched paths (404/405) skip the chain, matching gorilla/mux's own behavior. Subrouters work as expected for per-prefix chains.

import "github.com/gorilla/mux"

server.Router = mux.NewRouter()
server.Router.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("X-API-Key") != "secret" {
            w.WriteHeader(http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
})
Custom Endpoints

Register custom HTTP endpoints with typed schemas visible in the UI.

server.Endpoint(ooo.EndpointConfig{
    Path:        "/policies/{id}",
    Description: "Manage access control policies",
    // Vars are route variables (mandatory) - auto-extracted from {id} in path
    Vars: ooo.Vars{"id": "Policy ID"},
    Methods: ooo.Methods{
        "GET": ooo.MethodSpec{
            Response: PolicyResponse{},
            // Params are query parameters (optional) - per method
            Params: ooo.Params{"filter": "Optional filter value"},
        },
        "PUT": ooo.MethodSpec{
            Request:  Policy{},
            Response: PolicyResponse{},
        },
    },
    Handler: func(w http.ResponseWriter, r *http.Request) {
        id := mux.Vars(r)["id"]           // Route variable (mandatory)
        filter := r.URL.Query().Get("filter") // Query param (optional)
        // ... handle request
    },
})
Proxies

Forward filters from remote ooo servers with path remapping.

// Proxy /settings/{deviceID} → /settings on remote
proxy.Route(server, "settings/*", proxy.Config{
    Resolve: func(localPath string) (address, remotePath string, err error) {
        return "localhost:8800", "settings", nil
    },
})

// Proxy list routes: /items/{deviceID}/* → /items/* on remote
proxy.RouteList(server, "items/*/*", proxy.Config{
    Resolve: func(localPath string) (address, remotePath string, err error) {
        parts := strings.SplitN(localPath, "/", 3)
        if len(parts) == 3 {
            return "localhost:8800", "items/" + parts[2], nil
        }
        return "localhost:8800", "items/*", nil
    },
})

I/O Operations

These functions handle JSON serialization/deserialization and provide a more convenient way to work with your data structures than using storage api directly.

Basic Operations
Get a Single Item
// Get retrieves a single item from the specified path
item, err := ooo.Get[YourType](server, "path/to/item")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Item: %+v\n", item.Data)
Get a List of Items
// GetList retrieves all items from a list path (ends with "/*")
items, err := ooo.GetList[YourType](server, "path/to/items/*")
if err != nil {
    log.Fatal(err)
}
for _, item := range items {
    fmt.Printf("Item: %+v (created: %v)\n", item.Data, item.Created)
}
Set an Item
// Set creates or updates an item at the specified path
err := ooo.Set(server, "path/to/item", YourType{
    Field1: "value1",
    Field2: "value2",
})
if err != nil {
    log.Fatal(err)
}
Add to a List
// Push adds an item to a list (path must end with "/*")
index, err := ooo.Push(server, "path/to/items/*", YourType{
    Field1: "new item",
    Field2: "another value",
})
if err != nil {
    log.Fatal(err)
}
fmt.Println("Created at:", index)
Delete Item(s)
// Delete removes item(s) at the specified path
// For single item: ooo.Delete(server, "path/to/item")
// For glob pattern: ooo.Delete(server, "items/*") removes all matching items
err := ooo.Delete(server, "path/to/item")
if err != nil {
    log.Fatal(err)
}
Remote Operations

Perform operations on remote ooo servers using the io package.

RemoteConfig
cfg := io.RemoteConfig{
    Client: &http.Client{Timeout: 10 * time.Second},
    Host:   "localhost:8800",
    SSL:    false, // set to true for HTTPS
}
RemoteGet
item, err := io.RemoteGet[YourType](cfg, "path/to/item")
RemoteSet
err := io.RemoteSet(cfg, "path/to/item", YourType{Field1: "value"})
RemotePush
err := io.RemotePush(cfg, "path/to/items/*", YourType{Field1: "new item"})
RemoteGetList
items, err := io.RemoteGetList[YourType](cfg, "path/to/items/*")
RemoteDelete
err := io.RemoteDelete(cfg, "path/to/item")
Subscribe Clients

Use the Go WebSocket client to subscribe to real-time updates.

SubscribeList
go client.SubscribeList(client.SubscribeConfig{
    Ctx:  ctx,
    Server: client.Server{Protocol: "ws", Host: "localhost:8800"},
}, "items/*", client.SubscribeListEvents[Item]{
    OnMessage: func(items []client.Meta[Item]) { /* handle updates */ },
    OnError:   func(err error) { /* handle error */ },
})
Subscribe
go client.Subscribe(client.SubscribeConfig{
    Ctx:  ctx,
    Server: client.Server{Protocol: "ws", Host: "localhost:8800"},
}, "config", client.SubscribeEvents[Config]{
    OnMessage: func(item client.Meta[Config]) { /* handle updates */ },
    OnError:   func(err error) { /* handle error */ },
})

For JavaScript, use ooo-client.

UI

ooo includes a built-in web-based ui to manage and monitor your data. The ui is automatically available at the root path (/) when the server starts.

Features
  • Storage Browser - Browse all registered filters and their data
  • Live Mode - Real-time WebSocket subscriptions with automatic updates
  • Static Mode - Traditional CRUD operations with JSON editor
  • State Monitor - View active WebSocket connections and subscriptions
  • Filter Management - Visual representation of filter types (open, read-only, write-only, custom, limit)

Shutdown & durability

server.Close(sig) runs a bounded graceful shutdown: it drains in-flight HTTP requests up to Server.Deadline, closes WebSocket connections, closes the storage backend, and runs any user-supplied teardown callbacks. server.WaitClose() is the convenience wrapper that blocks on SIGINT / SIGTERM / SIGHUP and then calls Close with the received signal.

SIGKILL is uncatchable

POSIX requires SIGKILL (and SIGSTOP) to be delivered by the kernel with no opportunity for user code to run. When SIGKILL arrives:

  • No deferred functions execute. Close does not run. OnClose does not run.
  • Any queued broadcasts, watcher events, and writes that the storage backend has not yet flushed to durable media are lost.
  • Connected subscribers receive a TCP RST and re-fetch state on reconnect.

Durability across hard kill is the responsibility of the storage backend, not of Server.Close. Pick a backend whose contract you trust and budget your shutdown window so SIGKILL never has to fire under normal operation.

Orchestrator configuration

The orchestrator should send SIGTERM first and grant a grace window at least as long as Server.Deadline plus the worst-case runtime of every user-supplied callback (preClose, proxy, OnClose). SIGKILL should be the orchestrator's stuck-teardown fallback, not the primary shutdown path.

On Kubernetes, set terminationGracePeriodSeconds accordingly. The default of 30s is enough for most configurations; raise it if you've increased Server.Deadline or if your callbacks flush sizable state.

Teardown hooks

Server.RegisterCloseHook(phase, fn) registers teardown code to run at one of three points in Close. Multiple hooks at the same phase run in registration order; phases themselves run in declaration order.

Phase When it runs Storage / stream / HTTP state Typical use
ooo.PreShutdown First, before any tear-down All up Flush in-memory state to storage, broadcast a "shutting down" message
ooo.ProxyTeardown After PreShutdown, before the stream closes Stream still up Unsubscribe from upstream proxy servers
ooo.PostShutdown Last, after everything else is closed All torn down Close user-owned resources (DB pools, log handles)

The earlier per-phase registrars (RegisterPreClose, RegisterProxyCleanup, and the OnClose field) remain available as deprecated thin shims over RegisterCloseHook, so existing code keeps working unchanged. New code should prefer the phased API.

Hooks run synchronously and Close blocks until each returns. By default there is no aggregate timeout on user callbacks — a callback that hangs hangs Close, which can push the orchestrator past terminationGracePeriodSeconds and trigger SIGKILL. Keep callbacks short, or make them respect their own timeouts.

To opt into a hard bound on the combined runtime of every registered hook, set Server.CloseCallbackBudget to a positive time.Duration. Only the time spent inside hooks counts toward this budget — the wall clock that Close spends in its own internal teardown (the Deadline-bounded HTTP drain, Storage.Close, waitgroup joins) is not charged against it, so a slow drain will not silently consume the budget that callbacks were meant to use. Once the cumulative callback-runtime exceeds the budget, remaining callbacks are skipped and a one-line warning is logged via Server.Console. A callback that is already running is not interrupted — Go cannot cancel arbitrary user code — only callbacks that have not yet started are skipped. The default (zero) preserves the existing unbounded contract.

Custom Endpoint handlers

HTTP handlers registered via server.Endpoint are not wrapped in a request timeout. During shutdown they receive a cancellation through r.Context().Done() once Server.Deadline elapses, but a handler that ignores its context will keep running and Go cannot kill it. Always check r.Context().Done() in long-running custom handlers if you want them to exit cleanly during shutdown.

Documentation

Index

Examples

Constants

View Source
const (
	OrderDesc = filters.OrderDesc // Most recent first (default)
	OrderAsc  = filters.OrderAsc  // Oldest first
)

Order constants for LimitFilterConfig

View Source
const DefaultMaxRequestBodyBytes = 10 * 1024 * 1024 // 10 MiB

DefaultMaxRequestBodyBytes is the default cap on REST request body size (POST / PATCH). Override via Server.MaxRequestBodyBytes. Set the field to a negative value to disable the cap.

Variables

View Source
var (
	ErrInvalidPath        = errors.New("ooo: invalid path")
	ErrNotFound           = errors.New("ooo: not found")
	ErrNoop               = errors.New("ooo: noop")
	ErrGlobNotAllowed     = errors.New("ooo: glob pattern not allowed for this operation")
	ErrGlobRequired       = errors.New("ooo: glob pattern required for this operation")
	ErrInvalidStorageData = errors.New("ooo: invalid storage data (empty)")
	ErrInvalidPattern     = errors.New("ooo: invalid pattern")
	ErrInvalidRange       = errors.New("ooo: invalid range")
	ErrInvalidLimit       = errors.New("ooo: invalid limit")
	ErrLockNotFound       = errors.New("ooo: lock not found can't unlock")
	ErrCantLockGlob       = errors.New("ooo: can't lock a glob pattern path")
)

Storage errors

View Source
var (
	ErrServerAlreadyActive = errors.New("ooo: server already active")
	ErrServerStartFailed   = errors.New("ooo: server start failed")
	ErrForcePatchConflict  = errors.New("ooo: ForcePatch and NoPatch cannot both be enabled")
	ErrNegativeWorkers     = errors.New("ooo: Workers cannot be negative")
	ErrNegativeDeadline    = errors.New("ooo: Deadline cannot be negative")
)

Server errors

View Source
var (
	ErrNotAuthorized = errors.New("ooo: request is not authorized")
	ErrInvalidKey    = errors.New("ooo: key is not valid")
	ErrEmptyKey      = errors.New("ooo: empty key")
)

REST/HTTP errors

View Source
var (
	ErrRouteNotDefined     = errors.New("ooo: route not defined, static mode")
	ErrInvalidFilterResult = errors.New("ooo: invalid filter result")
	ErrReservedPath        = errors.New("ooo: filter path conflicts with reserved UI paths")
)

Filter errors

View Source
var (
	NoopHook         = filters.NoopHook
	NoopNotify       = filters.NoopNotify
	NoopFilter       = filters.NoopFilter
	NoopObjectFilter = filters.NoopObjectFilter
	NoopListFilter   = filters.NoopListFilter
)

Re-export filter functions from filters package

View Source
var (
	ErrPathGlobRequired   = errors.New("io: path glob required")
	ErrPathGlobNotAllowed = errors.New("io: path glob not allowed")
)

Functions

func Delete

func Delete(server *Server, path string) error

Delete removes an item at the specified path from storage. The path must not contain glob patterns. The configured DeleteFilter (if any) is consulted before the delete and may reject it.

func Get

func Get[T any](server *Server, path string) (client.Meta[T], error)

Get retrieves a single item at the specified path. The configured ReadObjectFilter (with ReadListFilter fallback) is consulted before the value is returned, matching the REST read handler.

func GetList

func GetList[T any](server *Server, path string) ([]client.Meta[T], error)

GetList retrieves all items at the supplied glob path. The configured ReadListFilter (if any) is consulted with the same static-mode flag the REST handler uses; a filter rejection is propagated to the caller.

func IsRequestBodyTooLargeErr

func IsRequestBodyTooLargeErr(err error) bool

IsRequestBodyTooLargeErr reports whether err originated in a MaxBytesReader exhausting its quota. It matches both modern *http.MaxBytesError and the legacy "http: request body too large" sentinel that older stdlib paths still return.

func LoadCertPool

func LoadCertPool(caPaths ...string) (*x509.CertPool, error)

LoadCertPool builds an x509 certificate pool seeded with the host's system roots and extended with the PEM certificate files at caPaths. Use it to trust a private certificate authority in addition to the public roots. If the system pool is unavailable (some minimal containers) an empty pool is used so only caPaths are trusted.

func NewClient

func NewClient(timeout time.Duration, caPaths ...string) (*http.Client, error)

NewClient returns an http.Client tuned like the server's default outbound client, but whose TLS configuration trusts the certificate authorities at caPaths (in addition to the system roots). Assign the result to Server.Client before Start so inter-service HTTPS calls validate certificates signed by a private authority.

A timeout of zero applies the default 10s request timeout.

func Patch

func Patch[T any](server *Server, path string, item T) error

Patch applies a partial update to an existing item at the specified path. The path must not contain glob patterns and the item must already exist. The patch is merged with the existing data using JSON merge semantics; the configured WriteFilter (if any) sees the merged result and may reject or transform it. The AfterWriteFilter fires on success.

func Push

func Push[T any](server *Server, path string, item T) (string, error)

Push stores a value at a fresh key under the supplied glob path. The configured WriteFilter (if any) is consulted on the resolved path before the write; the AfterWriteFilter fires on success.

func Set

func Set[T any](server *Server, path string, item T) error

Set stores a value at the specified path. The path must not contain glob patterns. The configured WriteFilter (if any) is consulted before the write and may reject or transform the value; the AfterWriteFilter fires on success.

func Time

func Time() string

Time returns a string timestamp using the monotonic clock

Types

type Apply

type Apply = filters.Apply

Re-export filter types from filters package

type ApplyList

type ApplyList = filters.ApplyList

Re-export filter types from filters package

type ApplyObject

type ApplyObject = filters.ApplyObject

Re-export filter types from filters package

type Block

type Block = filters.Block

Re-export filter types from filters package

type CleanupConfig

type CleanupConfig = filters.CleanupConfig

CleanupConfig is an alias for filters.CleanupConfig for convenience. Use this with LimitFilter to configure periodic background cleanup.

type CloseHookPhase

type CloseHookPhase int

CloseHookPhase identifies when a teardown hook runs during Server.Close. Phases run in declaration order; multiple hooks at the same phase run in registration order. See RegisterCloseHook.

const (
	// PreShutdown runs first, before any internal teardown. Storage,
	// stream, and HTTP are still up — hooks may broadcast, read, or
	// write. Use this to flush in-memory state or notify subscribers
	// that the server is shutting down.
	PreShutdown CloseHookPhase = iota
	// ProxyTeardown runs after PreShutdown but before stream
	// connections are closed. Use this to unsubscribe from upstream
	// proxy servers while the stream layer is still available.
	ProxyTeardown
	// PostShutdown runs last, after every internal teardown step.
	// Storage, stream, and HTTP are all torn down — use this for
	// closing user-owned resources (DB pools, log handles). The
	// deprecated Server.OnClose field, if set, runs after every
	// PostShutdown hook.
	PostShutdown
)

type EndpointConfig

type EndpointConfig struct {
	Path        string
	Methods     Methods
	Description string
	Vars        Vars // Route variables like {id} - mandatory, auto-extracted from path if nil
	Handler     http.HandlerFunc
}

EndpointConfig configures a custom endpoint.

Handler contract: long-running handlers must respect r.Context().Done(). Server.Close gives handlers a graceful window of server.Deadline to exit, then force-closes connections to cancel their request contexts. A handler that ignores the context will leak its goroutine — Go offers no way to preempt it.

type FetchResult

type FetchResult struct {
	Data []byte
}

FetchResult holds the encoded body of a REST read (fetchREST). The WS subscribe path no longer produces a FetchResult — its snapshot is taken from the pool cache inside attachConn — so only the REST reader consumes this.

type FilterConfig

type FilterConfig = filters.Config

Re-export filter types from filters package

type LimitFilterConfig

type LimitFilterConfig = filters.LimitFilterConfig

LimitFilterConfig is an alias for filters.LimitFilterConfig for convenience. Use this with LimitFilter to configure limit and sort order.

type LimitFunc

type LimitFunc = filters.LimitFunc

LimitFunc is an alias for filters.LimitFunc for convenience. Use this to provide a dynamic limit function that is called each time the limit is needed.

type MaxAgeFunc

type MaxAgeFunc = filters.MaxAgeFunc

MaxAgeFunc is an alias for filters.MaxAgeFunc for convenience. Use this to provide a dynamic max age function that is called each time the max age is needed.

type MethodSpec

type MethodSpec struct {
	Request  any    // Go type for request body, nil for GET/DELETE
	Response any    // Go type for response body, nil if status-only
	Params   Params // Query parameters like ?category=x - optional
}

MethodSpec defines the specification for an HTTP method

type Methods

type Methods map[string]MethodSpec

Methods maps HTTP method to its specification

type Notify

type Notify = filters.Notify

Re-export filter types from filters package

type Params

type Params map[string]string

Params maps query parameter name to its description (e.g., ?category=x)

type ReadErrorProbe

type ReadErrorProbe struct {
	// contains filtered or unexported fields
}

ReadErrorProbe wraps an io.Reader and remembers the most recent non-nil error returned by Read, so callers can recover the underlying cause even when an upstream decoder or transport turns it into a less-specific error like EOF.

func (*ReadErrorProbe) Last

func (p *ReadErrorProbe) Last() error

Last returns the most recent error observed during Read. Returns nil if Read never failed.

func (*ReadErrorProbe) Read

func (p *ReadErrorProbe) Read(b []byte) (int, error)

Read forwards to the wrapped reader and records any non-nil error so callers can later disambiguate the cause via Last().

type Server

type Server struct {
	Name   string
	Router *mux.Router
	Stream stream.Stream

	NoBroadcastKeys []string
	Workers         int
	ForcePatch      bool
	NoPatch         bool
	// LosslessWatch makes storage→broadcast event delivery block instead of
	// dropping after SEND_TIMEOUT when a consumer stalls (see
	// storage.Options.LosslessWatch). Default false keeps production drop
	// resilience; set true for deterministic tests where a dropped broadcast
	// would desync a subscriber.
	LosslessWatch bool
	OnSubscribe   stream.Subscribe
	OnUnsubscribe stream.Unsubscribe
	OnStart       func()
	// OnClose runs as the final teardown step, after every PostShutdown
	// hook. Deprecated: use RegisterCloseHook(PostShutdown, fn) instead;
	// the field is kept for backwards compatibility and runs last so
	// existing callers see no behaviour change.
	OnClose func()
	// CloseCallbackBudget caps the aggregate runtime of user-supplied
	// teardown hooks registered via RegisterCloseHook (and the
	// deprecated RegisterPreClose / RegisterProxyCleanup / OnClose
	// surface) during Close. Only the time spent INSIDE hooks
	// counts against this budget — the wall clock spent in ooo's own
	// internal teardown (Deadline-bounded HTTP drain, Storage.Close,
	// waitgroup joins) is not charged. A callback already running is
	// not interrupted, but once the cumulative callback-runtime
	// exceeds this budget, subsequent callbacks are skipped and a
	// one-line warning is logged via the server's Console. Zero (the
	// default) means no bound — the pre-existing contract where every
	// callback runs to completion regardless of duration. Opt in by
	// setting a positive value, for example to keep the
	// SIGTERM-to-SIGKILL window from being exhausted by a single
	// misbehaving callback.
	CloseCallbackBudget time.Duration
	Deadline            time.Duration
	AllowedOrigins      []string
	AllowedMethods      []string
	AllowedHeaders      []string
	ExposedHeaders      []string
	Storage             storage.Database
	Address             string

	Silence bool
	Static  bool
	Tick    time.Duration
	Console *coat.Console
	Signal  chan os.Signal
	Client  *http.Client
	// CertFile and KeyFile, when both non-empty, switch the listener
	// from plaintext HTTP to HTTPS. The keypair is loaded once at Start;
	// a load failure aborts startup with an error rather than serving
	// plaintext. Leave both empty to serve plain HTTP (the default).
	CertFile string
	KeyFile  string
	// TLSConfig, when non-nil, is the base TLS configuration for the
	// HTTPS listener (min version, cipher suites, client-auth, ...). The
	// keypair loaded from CertFile/KeyFile is appended to its
	// Certificates. When nil a minimal modern default (TLS 1.2 floor) is
	// used. Ignored when CertFile/KeyFile are empty.
	TLSConfig           *tls.Config
	ReadTimeout         time.Duration
	WriteTimeout        time.Duration
	ReadHeaderTimeout   time.Duration
	IdleTimeout         time.Duration
	MaxRequestBodyBytes int64 // cap on REST request body size; defaults to DefaultMaxRequestBodyBytes (10 MiB). Set to a negative value to disable.
	OnStorageEvent      storage.EventCallback
	OnWatchPanic        func(ev storage.Event, r any) // optional: invoked on each recovered watch-goroutine panic with the offending event
	OnDroppedEvent      func(ev storage.Event)        // optional: invoked when the sharded watcher channel drops an event after timing out
	BeforeRead          func(key string)
	AfterWrite          func(key string)
	AfterWriteOp        func(key string, op string) // optional: operation-aware companion to AfterWrite ("set"/"del")
	GetPivotInfo        func() *ui.PivotInfo        // Optional: returns pivot status for UI
	NoCompress          bool                        // Disable gzip compression (useful for tests)
	WatchPanics         int64                       // Atomic counter of panics recovered in watch goroutines
	DroppedEvents       int64                       // Atomic counter of events dropped by the sharded watcher on send timeout
	// contains filtered or unexported fields
}

Server is the main application struct for the ooo server.

Name: display name for the server, shown in the storage explorer title

Router: can be predefined with routes and passed to be extended

Stream: manages WebSocket connections and broadcasts

NoBroadcastKeys: array of keys that should not broadcast on changes

Workers: number of workers to use as readers of the storage->broadcast channel

ForcePatch: flag to force patch operations even if the patch is bigger than the snapshot

NoPatch: flag to disable patch operations entirely, always send full snapshots

OnSubscribe: function to monitor subscribe events, can return error to deny subscription

OnUnsubscribe: function to monitor unsubscribe events

OnStart: function that triggers after the server has started successfully

OnClose: function that triggers after closing the application

Deadline: time duration of a request before timing out

AllowedOrigins: list of allowed origins for cross domain access, defaults to ["*"]

AllowedMethods: list of allowed methods for cross domain access, defaults to ["GET", "POST", "DELETE", "PUT", "PATCH"]

AllowedHeaders: list of allowed headers for cross domain access, defaults to ["Authorization", "Content-Type"]

ExposedHeaders: list of exposed headers for cross domain access, defaults to nil

Storage: database interface implementation

Address: the address the server is listening on (populated after Start)

Silence: output silence flag, suppresses console output when true

Static: static routing flag, when true only filtered routes are allowed

Tick: time interval between ticks on the clock websocket

Console: logging console for the server

Signal: os signal channel for graceful shutdown

Client: http client to make requests

ReadTimeout: maximum duration for reading the entire request

WriteTimeout: maximum duration before timing out writes of the response

ReadHeaderTimeout: amount of time allowed to read request headers

IdleTimeout: maximum amount of time to wait for the next request

OnStorageEvent: callback function triggered on storage events

BeforeRead: callback function triggered before read operations

AfterWrite: callback function triggered after a successful write (Set / Push / Patch / Del). Wired once at Start; further changes have no effect.

AfterWriteOp: operation-aware companion to AfterWrite, triggered with the operation ("set" / "del") and, unlike AfterWrite, BEFORE the change is broadcast to watchers — so a consumer can update derived per-key state in place before any event-driven reactor observes the write. Also wired once at Start; further changes have no effect.

Example
package main

import (
	"github.com/benitogf/ooo"
)

func main() {
	app := ooo.Server{}
	app.Start("localhost:8800")
	app.WaitClose()
}

func (*Server) Active

func (server *Server) Active() bool

Active reports whether the server is fully running — listener bound, not yet shutting down. Returns false during the listen-bind window (Start has been called but the listener has not yet been accepted by waitListen) and false once Close has been called.

func (*Server) AfterWriteFilter

func (server *Server) AfterWriteFilter(path string, apply Notify, cfg ...FilterConfig)

AfterWriteFilter add a filter that triggers after a successful write

func (*Server) Close

func (server *Server) Close(sig os.Signal)

Close runs the graceful shutdown sequence: drains in-flight HTTP requests up to Server.Deadline, closes all WebSocket connections, closes the storage backend, and invokes user-supplied callbacks at well-defined points.

Teardown sequence (in order):

  1. Mark the server as closing (atomic, idempotent — second concurrent call returns immediately).
  2. Run PreShutdown hooks (registered via RegisterCloseHook; RegisterPreClose is the deprecated equivalent). Storage, stream, and HTTP are still up — hooks may broadcast, read, or write.
  3. Stop the internal clock goroutine (bounded — clock selects on a stop channel).
  4. Run ProxyTeardown hooks (registered via RegisterCloseHook; RegisterProxyCleanup is the deprecated equivalent).
  5. Close all WebSocket connections (bounded — per-connection TCP close).
  6. Stop the HTTP server: graceful shutdown with a context bounded by Server.Deadline (default 10s), then force-close to cancel any still-running request contexts. Handlers that honour r.Context().Done() exit then; handlers that ignore the context leak (Go cannot kill them).
  7. Wait for HTTP handlers and the listen goroutine to exit.
  8. Close the storage backend.
  9. Wait for storage-watcher goroutines (bounded — they exit when storage closes their channels).
  10. Run PostShutdown hooks, then the deprecated Server.OnClose field if set. Storage, stream, and HTTP are all torn down — use these for closing user-owned resources only.

Bound:

  • HTTP request drain: bounded by Server.Deadline.
  • Internal waitgroups (clock, listen, watch, handler): bounded. Clock selects on clockStop, watch goroutines exit when storage closes their channels, the listen goroutine exits when the HTTP server's listener is force-closed at step 6, and the handler waitgroup only tracks the long-lived clock long-poll and WebSocket handlers — both of which exit when Stream.CloseAll closes their connections at step 5. REST publish / read / patch / unpublish and custom Endpoint handlers are not tracked by the handler waitgroup; see the next bullet for how they're bounded.
  • Storage.Close: bounded for in-tree layers; depends on the bottom-layer contract for embedded storages (e.g. ko, nopog).
  • preClose cleanups, proxy cleanups, and OnClose: bounded only when Server.CloseCallbackBudget is set to a positive value, which caps the aggregate runtime across all three batches. A callback already in flight is not interrupted, but once the budget is exhausted subsequent callbacks are skipped with a one-line warning. With the default (zero) budget, every callback runs to completion regardless of duration — keep them short or the orchestrator's SIGTERM-to-SIGKILL window has to cover the worst case across all of them in addition to Deadline.
  • HTTP handlers that ignore r.Context().Done(): NOT bounded. They can outlive Close. Custom Endpoint handlers in particular must check the context.

Hard-kill: SIGKILL is uncatchable; nothing in this sequence runs. See WaitClose for the SIGTERM/SIGKILL contract operators should configure for.

Safe to call from multiple goroutines: an atomic CompareAndSwap on `closing` lets only the first caller through; concurrent callers observe the in-progress shutdown and return immediately. The CAS is load-bearing — a plain Load + Store guard would let two callers both pass and both `close(server.clockStop)`, panicking the second.

func (*Server) DeleteFilter

func (server *Server) DeleteFilter(path string, apply Block, cfg ...FilterConfig)

DeleteFilter add a filter that runs before delete

func (*Server) Endpoint

func (server *Server) Endpoint(cfg EndpointConfig)

Endpoint registers a custom HTTP endpoint with metadata for UI visibility

func (*Server) LimitBody

func (server *Server) LimitBody(w http.ResponseWriter, r *http.Request) (io.Reader, *ReadErrorProbe)

LimitBody wraps r.Body with http.MaxBytesReader so a runaway POST/PATCH cannot buffer arbitrary bytes into memory. A non-positive MaxRequestBodyBytes disables the cap and returns r.Body unchanged so operators can opt out (test harnesses, trusted internal callers).

IMPORTANT: must be called before any write to w. http.MaxBytesReader signals the response writer side-channel to suppress connection keep-alive once the cap trips, and that side-channel is a no-op after the header has been committed. Today both REST callers (publish, patch) short-circuit on key checks without writing to w first, so the invariant holds — refactor that path with care. Same constraint applies to any other caller (proxy handlers, custom Endpoints). Router.Use() middleware that denies a request must short-circuit (not call next) — once next runs, the handler may invoke LimitBody, and any subsequent write to w from a deferred middleware closure would arrive too late.

The returned ReadErrorProbe (non-nil only when the cap is active) records the most recent error returned by Read. benitogf/go-json is known to swallow underlying reader errors as a plain io.EOF when it sees the truncated payload, so REST callers must consult the probe to distinguish "client sent too many bytes" (413) from "client sent invalid JSON" (400). Other consumers (notably net/http transports surfacing the body to an upstream) may surface a less-specific cause; the probe is the defensive fallback there as well.

func (*Server) LimitFilter

func (server *Server) LimitFilter(path string, cfg filters.LimitFilterConfig)

LimitFilter creates a limit filter for a glob pattern path that maintains count and/or time-based constraints. Uses a ReadListFilter (meta-based) to limit the view and AfterWrite to delete old entries. Supports optional periodic background cleanup. Also adds write and delete filters to allow creating and deleting items.

func (*Server) OpenFilter

func (server *Server) OpenFilter(name string, cfg ...FilterConfig)

OpenFilter open noop read and write filters For glob paths like "things/*", this also enables reading individual items like "things/123"

func (*Server) ReadListFilter

func (server *Server) ReadListFilter(path string, apply ApplyList, cfg ...FilterConfig)

ReadListFilter add a filter for []meta.Object reads. For glob paths like "things/*", individual item reads (e.g., "things/123") will also be allowed if no explicit ReadObjectFilter is registered for that path.

func (*Server) ReadObjectFilter

func (server *Server) ReadObjectFilter(path string, apply ApplyObject, cfg ...FilterConfig)

ReadObjectFilter add a filter for single meta.Object reads

func (*Server) RegisterCloseHook

func (server *Server) RegisterCloseHook(phase CloseHookPhase, fn func())

RegisterCloseHook registers fn to run during Server.Close at the given teardown phase. Multiple hooks at the same phase run in registration order. Phases run in CloseHookPhase declaration order (PreShutdown → ProxyTeardown → PostShutdown). The aggregate callback runtime is bounded by Server.CloseCallbackBudget when set. Panics if phase is out of range.

func (*Server) RegisterLimitFilter

func (server *Server) RegisterLimitFilter(lf *filters.LimitFilter, description string, schema map[string]any)

RegisterLimitFilter registers a limit filter and tracks it for the ui. The LimitFilter should already be created and its filters added to the server. This method stores a reference to the filter for lazy evaluation of dynamic limits.

func (*Server) RegisterOracleRoute

func (server *Server) RegisterOracleRoute(path string, methods []string)

RegisterOracleRoute mirrors a path registration onto the oracle router so the data wildcard can defer to it. Method-restricted routes pass methods; pass nil for any-method routes (proxies). Endpoint() and the proxy package call this after registering on Server.Router.

func (*Server) RegisterPreClose deprecated

func (server *Server) RegisterPreClose(cleanup func())

RegisterPreClose registers a cleanup function to be called at the very start of Close(), before stream and storage cleanup. This is useful for stopping background goroutines that depend on the stream being active. Multiple functions can be registered and will be called in registration order.

Deprecated: use RegisterCloseHook(PreShutdown, cleanup) instead.

func (*Server) RegisterProxy

func (server *Server) RegisterProxy(info ui.ProxyInfo)

RegisterProxy registers a proxy route for UI visibility. The append serializes with getProxies via the shared registry write lock.

func (*Server) RegisterProxyCleanup deprecated

func (server *Server) RegisterProxyCleanup(cleanup func())

RegisterProxyCleanup registers a cleanup function to be called when the server closes. This is used by proxy routes to clean up their remote subscriptions.

Deprecated: use RegisterCloseHook(ProxyTeardown, cleanup) instead.

func (*Server) RouterMutate

func (server *Server) RouterMutate(fn func())

RouterMutate runs fn while holding the routerMu write lock so Server.Router mutations (HandleFunc, Handle, MatcherFunc chaining) serialize with the syncRouter wrapper's Match. Used by Endpoint, setupRoutes, and the proxy package's Route/RouteList/RouteWithVars registrars.

func (*Server) Start

func (server *Server) Start(address string)

Start initializes and starts the http server and database connection. Panics if startup fails. Use StartWithError for error handling. If the server is already active, this is a no-op (does not panic).

func (*Server) StartWithError

func (server *Server) StartWithError(address string) error

StartWithError initializes and starts the http server and database connection. Returns an error if startup fails instead of calling log.Fatal.

Safe to call from multiple goroutines: an atomic CompareAndSwap claims the startup slot via the `serverStarting` sentinel so only the first caller proceeds; concurrent callers return ErrServerAlreadyActive immediately. The sentinel keeps `Active()` returning false through the listen-bind window — the field flips to `serverActive` only once waitListen has bound, and rolls back to `serverInactive` if the bind fails. Mirrors the Close-side CAS fix (PR #89) without shifting Active() semantics.

func (*Server) Validate

func (server *Server) Validate() error

Validate checks the server configuration for common issues. Call this before Start() to catch configuration errors early.

func (*Server) WaitClose

func (server *Server) WaitClose()

WaitClose blocks until the process receives SIGINT, SIGTERM, or SIGHUP and then runs the graceful Close sequence with the received signal.

SIGKILL is intentionally not listed. POSIX requires SIGKILL (and SIGSTOP) to be uncatchable: the kernel terminates the process with no opportunity for user code to run, so no defers, no Close, no OnClose, and no user callbacks execute. Anything sitting in in-process buffers (queued broadcasts, watcher events, writes that the storage backend has not yet flushed to durable media) is lost when SIGKILL is delivered. Durability across hard kill is the responsibility of the storage backend, not Server.Close.

Operators should configure their orchestrator to send SIGTERM first and grant a grace window at least as long as Server.Deadline plus the worst-case runtime of any user-supplied preClose / proxy / OnClose callback. On Kubernetes this is `terminationGracePeriodSeconds`. SIGKILL should be the orchestrator's fallback for a stuck SIGTERM teardown, not the primary shutdown path.

func (*Server) WriteFilter

func (server *Server) WriteFilter(path string, apply Apply, cfg ...FilterConfig)

WriteFilter add a filter that triggers on write

type Vars

type Vars map[string]string

Vars maps route variable name to its description (e.g., {id} in path)

Directories

Path Synopsis
samples
basic_server command
Package main demonstrates a basic ooo server setup.
Package main demonstrates a basic ooo server setup.
custom_endpoints command
Package main demonstrates custom HTTP endpoints with server.Endpoint().
Package main demonstrates custom HTTP endpoints with server.Endpoint().
io_operations command
Package main demonstrates I/O operations with typed helpers.
Package main demonstrates I/O operations with typed helpers.
limit_filter command
Package main demonstrates the LimitFilter for capped collections.
Package main demonstrates the LimitFilter for capped collections.
limit_filter_with_validation command
Package main demonstrates LimitFilter with custom write validation.
Package main demonstrates LimitFilter with custom write validation.
remote_io_operations command
Package main demonstrates remote I/O operations.
Package main demonstrates remote I/O operations.
static_routes_filters_audit command
Package main demonstrates static routes, filters, and request gating via Router.Use() middleware.
Package main demonstrates static routes, filters, and request gating via Router.Use() middleware.
Package schema generates JSON-shaped schemas and default-value skeletons from Go types via reflection.
Package schema generates JSON-shaped schemas and default-value skeletons from Go types via reflection.
Build script for ooo website Generates the samples section from the samples directory
Build script for ooo website Generates the samples section from the samples directory
preview command

Jump to

Keyboard shortcuts

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