goscript

package
v1.15.0 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package goscript is the Go side of Tern's <script lang="go">: component logic written in Go with Svelte-style runes, while the markup stays Svelte.

<script lang="go" live>
import "github.com/Unfield/Tern/goscript"

func Setup(c *goscript.Component) {
	count := goscript.State(c, "count", 0)
	goscript.Derived(c, "doubled", func() int { return count.Get() * 2 })
	c.Func("increment", func() { count.Set(count.Get() + 1) })
}
</script>

<button onclick={increment}>{count} × 2 = {doubled}</button>

Names given to State, Derived, Show and Func become variables in the component's markup.

With live, the component runs on the server and the page talks to it over one WebSocket: this is the supported, production mode. Without live, the same code is compiled to WebAssembly and runs in the browser; that mode is experimental and needs "experimental": {"goScript": true} in tern.json. See docs/live-components.md.

Index

Constants

This section is empty.

Variables

View Source
var ErrArguments = errors.New("goscript: invalid arguments")

ErrArguments is returned when a Func's arguments can't be decoded.

View Source
var ErrDestroyed = errors.New("goscript: the component was destroyed")

ErrDestroyed is returned by an Instance after it is destroyed.

View Source
var ErrPanic = errors.New("goscript: the component failed")

ErrPanic is returned by Call when the Func panicked.

Functions

func ComponentID added in v1.3.0

func ComponentID(path string) string

ComponentID returns the ID of the component in the .svelte file at path, relative to the app's root, such as "src/lib/Counter.svelte".

func Effect

func Effect(c *Component, fn func())

Effect runs fn now and again whenever a value it reads changes. It does not run while rendering.

func Fail added in v1.3.0

func Fail(format string, args ...any) error

Fail returns an error for a Func whose message is sent to the page, which can show it: the call's promise rejects with it, and the Func's error is set to it. Other errors are not shown, so internal details don't leak.

func InitialState

func InitialState(id string, props []byte, locals map[string]any) (map[string]json.RawMessage, error)

InitialState runs a component's Setup with props and returns what it published, for server rendering. Effects, Do, Go and Every don't run.

func Instances added in v1.3.0

func Instances() int64

Instances returns how many live components are mounted, for metrics.

func Publish added in v1.3.0

func Publish(ctx context.Context, topic string, v any) error

Publish sends v to every component subscribed to topic with Subscribe, in this process and, with a broker, every other. A common pattern: a Func saves a change to the database, then publishes that it happened, and every subscribed component reloads what it shows.

func Register

func Register(id string, setup func(*Component))

Register makes a component's Setup available; generated code calls it from an init function.

func Registered

func Registered(id string) bool

Registered reports whether a component ID is registered.

func Run

func Run()

Run mounts every component with empty props, outside the browser, and prints each one's initial state as JSON. tern uses it at build time so server rendering and the first paint show real values.

func SetBroker added in v1.3.0

func SetBroker(b Broker)

SetBroker connects this process's Shared values, Presence counts and topics to other processes through b. kit.WithLiveBroker calls it when the app starts; nil disconnects.

func Subscribe added in v1.3.0

func Subscribe[T any](c *Component, topic string, fn func(T))

Subscribe calls fn as c, like a Func, for each message published to topic with Publish, in this process or, with a broker, any other. It stops when c is destroyed, and does nothing while rendering.

Types

type Broker added in v1.3.0

type Broker interface {
	// Publish delivers msg to the topic's subscribers in every process.
	Publish(ctx context.Context, topic string, msg []byte) error
	// Subscribe calls fn for each message on topic until ctx is canceled.
	Subscribe(ctx context.Context, topic string, fn func(msg []byte)) error
}

Broker carries messages between server processes, so live components on every instance of an app see the same Shared values, Presence counts and published messages. Any realtime.Broker works, such as realtime.NewMemoryBroker or a Postgres broker from realtime/pgbroker; set it with kit.WithLiveBroker.

type Component

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

Component is one mounted instance of a component. Its methods, and the signals it owns, must be used from the component's own code: Setup, its Funcs, Derived values and Effects, and functions passed to Do. Other goroutines go through Do.

func (*Component) Context added in v1.3.0

func (c *Component) Context() context.Context

Context returns a context that is canceled when the component is destroyed. Use it for work a Func or Go starts.

func (*Component) Do added in v1.3.0

func (c *Component) Do(fn func())

Do runs fn as the component, safely from any goroutine: changes it makes to signals reach the page like changes made by a Func. It does nothing while rendering or after the component is destroyed.

func (*Component) Emit added in v1.3.0

func (c *Component) Emit(name string, v any)

Emit sends an event to the component's parent in the page: Emit("saved", v) calls the onsaved prop with v. It does nothing while rendering.

func (*Component) Every added in v1.3.0

func (c *Component) Every(d time.Duration, fn func())

Every runs fn as the component every d until it is destroyed, such as to refresh a value. It does nothing while rendering.

func (*Component) Func

func (c *Component) Func(name string, fn any)

Func makes fn callable from the markup under name, such as onclick={increment}. Its arguments are decoded from the JavaScript values passed to it; DOM events arrive as an Event. fn may take a context.Context first, canceled when the component goes away, and may return a value, an error, or both. The value goes back to the page; an error made with Fail is shown to it, other errors are logged and reported as a generic failure.

func (*Component) Go added in v1.3.0

func (c *Component) Go(fn func(ctx context.Context))

Go runs fn in a new goroutine with the component's context, which is canceled when the component is destroyed. fn changes signals through Do. It does nothing while rendering.

func (*Component) Locals

func (c *Component) Locals() map[string]any

Locals returns request values a live component was mounted with, such as the signed-in user from ctx.Locals; nil in the browser.

func (*Component) OnDestroy

func (c *Component) OnDestroy(fn func())

OnDestroy runs fn when the component goes away: when its page closes or navigates, or its live connection ends.

func (*Component) Props

func (c *Component) Props(dst any) error

Props decodes the component's props, as they were when it mounted, into dst. Use Props for props that change while the component is shown.

func (*Component) Reject added in v1.3.0

func (c *Component) Reject(format string, args ...any)

Reject refuses to mount the component: Setup stops, nothing reaches the page but the message, which live.error shows. Call it in Setup when the visitor may not see the component, after checking Locals: a live component is reachable by anyone who can open the page's WebSocket, like an endpoint.

func (*Component) Rendering

func (c *Component) Rendering() bool

Rendering reports whether Setup runs only to compute the initial state, for server rendering or the build. Skip side effects then, such as counting a visitor. Do, Go and Every do nothing while rendering.

type Doc added in v1.15.0

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

Doc is a component's handle on an open document.

func (*Doc[T]) Get added in v1.15.0

func (x *Doc[T]) Get() T

Get returns the document now.

func (*Doc[T]) Show added in v1.15.0

func (x *Doc[T]) Show(c *Component, name string) *Signal[T]

Show publishes the document in c's markup under name and returns the signal.

func (*Doc[T]) Update added in v1.15.0

func (x *Doc[T]) Update(fn func(T) T) error

Update changes the document to fn of it. Only the fields fn changed are sent to other processes, each with its own stamp, so changes to other fields made meanwhile elsewhere are kept.

func (*Doc[T]) Use added in v1.15.0

func (x *Doc[T]) Use(c *Component) *Signal[T]

Use returns a signal in c that follows the document.

type DocOptions added in v1.15.0

type DocOptions[T any] struct {
	// Load reads a document when a process opens it and no other process
	// has it open, such as from the database. Without it, a document starts
	// as T's zero value.
	Load func(ctx context.Context, key string) (T, error)
	// Save writes a document after it changes, in the process where it
	// changed, at most every SaveEvery. Errors are logged.
	Save func(ctx context.Context, key string, v T) error
	// SaveEvery is how long Save waits after a change, gathering the
	// changes meanwhile. Default: 1 second.
	SaveEvery time.Duration
}

DocOptions configure a kind of shared document.

type DocSet added in v1.15.0

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

DocSet is a kind of shared document, such as "note": one document per key, such as "note:42", that everyone who opens it sees and changes.

func Docs added in v1.15.0

func Docs[T any](name string, opts DocOptions[T]) *DocSet[T]

Docs declares a kind of shared document under name, which must be the same in every process. T must encode to a JSON object, such as a struct: its top-level fields merge separately, so two people changing different fields at once both keep their change; for the same field, the last change wins everywhere. Declare it as a package variable:

type Note struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

var notes = goscript.Docs("note", goscript.DocOptions[Note]{Load: loadNote, Save: saveNote})

func Setup(c *goscript.Component) {
	note := notes.Open(c, "note:"+id)
	note.Show(c, "note")
	c.Func("title", func(s string) { note.Update(func(n Note) Note { n.Title = s; return n }) })
}

func (*DocSet[T]) Open added in v1.15.0

func (d *DocSet[T]) Open(c *Component, key string) *Doc[T]

Open opens the document key for c until c is destroyed, loading it if no component in this process has it open, and asking other processes for their copy. It works while rendering, so the page arrives with the document. A failed Load rejects the component.

type Error added in v1.3.0

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

Error is an error whose message is shown to the page, made by Fail.

func (*Error) Error added in v1.3.0

func (e *Error) Error() string

type Event

type Event struct {
	Type    string `json:"type"`
	Value   string `json:"value"`   // the target's value, for inputs
	Checked bool   `json:"checked"` // for checkboxes
	Key     string `json:"key"`     // for keyboard events
}

Event is a DOM event passed to a Func.

type GlobalSignal

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

GlobalSignal is a value shared by every instance of every component in this process, such as a counter all visitors see. A component shows it with Show or reads it reactively through Use. It is safe for concurrent use. Apps running several server processes use Shared instead.

func Global

func Global[T any](initial T) *GlobalSignal[T]

Global declares a value shared within this process.

func (*GlobalSignal[T]) Get

func (g *GlobalSignal[T]) Get() T

Get returns the current value. It doesn't subscribe: inside a Derived or Effect, read the signal Use or Show returns.

func (*GlobalSignal[T]) Set

func (g *GlobalSignal[T]) Set(v T)

Set changes the value for everyone.

func (*GlobalSignal[T]) Show

func (g *GlobalSignal[T]) Show(c *Component, name string) *Signal[T]

Show publishes the shared value in c's markup under name, keeps it up to date there, and returns the signal that follows it.

func (*GlobalSignal[T]) Update

func (g *GlobalSignal[T]) Update(fn func(T) T)

Update sets the value to fn of the current one, atomically.

func (*GlobalSignal[T]) Use added in v1.3.0

func (g *GlobalSignal[T]) Use(c *Component) *Signal[T]

Use returns a signal in c that follows the shared value, for Derived values and Effects.

type Instance

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

Instance is a mounted live component. It is safe for concurrent use.

func MountLive

func MountLive(ctx context.Context, id string, o LiveOptions) (*Instance, error)

MountLive mounts a component and waits for its Setup.

func StartLive added in v1.3.0

func StartLive(id string, o LiveOptions, failed func(error)) (*Instance, error)

StartLive mounts a component for a live connection without waiting: Setup runs on the component's own goroutine, and calls made meanwhile wait for it. If Setup fails, failed receives the error and the instance refuses calls. Tern's live endpoint calls it.

func (*Instance) Call

func (i *Instance) Call(ctx context.Context, name string, args []byte) (out []byte, err error)

Call runs one of the component's Funcs with JSON arguments, and returns its JSON result once the changes it caused have been emitted.

func (*Instance) CallAsync added in v1.3.0

func (i *Instance) CallAsync(name string, args []byte, done func(out []byte, err error))

CallAsync queues a call to a Func and returns at once; done receives the result on the component's goroutine. Calls run in the order queued.

func (*Instance) Destroy

func (i *Instance) Destroy()

Destroy stops the component, runs its OnDestroy functions and ends its goroutine. It waits for the component's running work to finish.

func (*Instance) DestroyAsync added in v1.3.0

func (i *Instance) DestroyAsync() bool

DestroyAsync is Destroy without waiting. It reports whether the instance was still running.

func (*Instance) Set added in v1.3.0

func (i *Instance) Set(ctx context.Context, name string, value []byte) error

Set sets a State declared with Bind to a JSON value from the page.

func (*Instance) SetAsync added in v1.3.0

func (i *Instance) SetAsync(name string, value []byte, done func(error))

SetAsync is Set without waiting; done receives the error.

func (*Instance) SetProps added in v1.3.0

func (i *Instance) SetProps(ctx context.Context, props []byte) error

SetProps passes new props from the parent.

func (*Instance) SetPropsAsync added in v1.3.0

func (i *Instance) SetPropsAsync(props []byte)

SetPropsAsync is SetProps without waiting.

type LiveOptions added in v1.3.0

type LiveOptions struct {
	Props   json.RawMessage            // the props, as JSON
	Restore map[string]json.RawMessage // values the page shows, for State declared with Bind or Restore
	Locals  map[string]any             // request values, for Component.Locals

	// Emit receives each value that changed; Commit is called after the
	// changes of one batch, so they can be sent together.
	Emit   func(name string, value []byte)
	Commit func()
	// Event receives what the component passes to Emit.
	Event func(name string, value []byte)
	// Panic receives a panic recovered in the component's code.
	Panic func(v any, stack []byte)
}

LiveOptions configure MountLive.

type Member added in v1.15.0

type Member[M any] struct {
	// ID identifies the member: one per component that joined, so the same
	// user in two tabs is two members.
	ID    string    `json:"id"`
	State M         `json:"state"`
	Since time.Time `json:"since"`
}

Member is someone in a room: each component that joined, with the state it shares, such as a name, a color and where its cursor is.

type Option added in v1.3.0

type Option interface {
	// contains filtered or unexported methods
}

Option changes how a State behaves.

var (
	// Bind lets the page set the State, with bind:value={live.bind.name}
	// or live.set("name", v). Treat the value like any other user input.
	// A bindable State is also restored when the page reconnects.
	Bind Option = optBind{}
	// Restore keeps the State's value when the page reconnects to a new
	// server process, such as after a deploy: the page sends back the value
	// it shows. Treat it like user input.
	Restore Option = optRestore{}
)

type PresenceCounter added in v1.3.0

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

PresenceCounter counts the components that joined it, across every process of an app, such as the visitors on a page. Processes report their counts through the broker; a process that stops reporting, because it crashed, drops out of the total after half a minute.

func Presence added in v1.3.0

func Presence(name string) *PresenceCounter

Presence declares a counter under name, which must be the same in every process. Declare it as a package variable.

func (*PresenceCounter) Count added in v1.3.0

func (p *PresenceCounter) Count() int

Count returns the current total.

func (*PresenceCounter) Join added in v1.3.0

func (p *PresenceCounter) Join(c *Component)

Join counts c until it is destroyed. It does nothing while rendering.

func (*PresenceCounter) Show added in v1.3.0

func (p *PresenceCounter) Show(c *Component, name string) *Signal[int]

Show publishes the total in c's markup under name and returns the signal that follows it.

func (*PresenceCounter) Use added in v1.3.0

func (p *PresenceCounter) Use(c *Component) *Signal[int]

Use returns a signal in c that follows the total.

type RoomMember added in v1.15.0

type RoomMember[M any] struct {
	// contains filtered or unexported fields
}

RoomMember is the handle of a component that joined a room.

func (*RoomMember[M]) ID added in v1.15.0

func (h *RoomMember[M]) ID() string

ID is the member's ID, to tell its own entry in the room's members; "" while rendering.

func (*RoomMember[M]) Set added in v1.15.0

func (h *RoomMember[M]) Set(state M)

Set replaces the member's state.

func (*RoomMember[M]) Update added in v1.15.0

func (h *RoomMember[M]) Update(fn func(M) M)

Update changes the member's state. Updates reach other processes at most every 50 milliseconds, so a cursor can update on every move.

type RoomSet added in v1.15.0

type RoomSet[M any] struct {
	// contains filtered or unexported fields
}

RoomSet is a kind of room, such as "board": one room per key, such as "board:42", with the members in it.

func Room added in v1.15.0

func Room[M any](name string) *RoomSet[M]

Room declares a kind of room under name, which must be the same in every process. Declare it as a package variable:

type Here struct {
	Name  string `json:"name"`
	Field string `json:"field"` // the field they're editing
}

var boards = goscript.Room[Here]("board")

func Setup(c *goscript.Component) {
	key := "board:" + id
	me := boards.Join(c, key, Here{Name: user.Name})
	boards.Show(c, key, "people")
	goscript.State(c, "me", me.ID())
	c.Func("focus", func(field string) { me.Update(func(h Here) Here { h.Field = field; return h }) })
}

func (*RoomSet[M]) Join added in v1.15.0

func (r *RoomSet[M]) Join(c *Component, key string, state M) *RoomMember[M]

Join adds c to the room key with state, until c is destroyed. While rendering it does nothing: the handle's methods do nothing then either.

func (*RoomSet[M]) Members added in v1.15.0

func (r *RoomSet[M]) Members(key string) []Member[M]

Members returns who is in the room key now.

func (*RoomSet[M]) Show added in v1.15.0

func (r *RoomSet[M]) Show(c *Component, key, name string) *Signal[[]Member[M]]

Show publishes the members of the room key in c's markup under name, such as {#each people as p (p.id)}, and returns the signal.

func (*RoomSet[M]) Use added in v1.15.0

func (r *RoomSet[M]) Use(c *Component, key string) *Signal[[]Member[M]]

Use returns a signal in c that follows the members of the room key.

type SharedSignal added in v1.3.0

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

SharedSignal is a value shared by every component in every process of an app, through the broker. When two processes set it at the same moment, one value wins everywhere (last writer wins), so Update is atomic only within a process: keep counts that must not lose updates in a database. Without a broker it behaves like a Global.

func Shared added in v1.3.0

func Shared[T any](name string, initial T) *SharedSignal[T]

Shared declares a value shared across processes under name, which must be the same in every process and unique in the app. Declare it as a package variable.

func (*SharedSignal[T]) Get added in v1.3.0

func (s *SharedSignal[T]) Get() T

Get returns the current value without subscribing.

func (*SharedSignal[T]) Set added in v1.3.0

func (s *SharedSignal[T]) Set(v T)

Set changes the value in every process.

func (*SharedSignal[T]) Show added in v1.3.0

func (s *SharedSignal[T]) Show(c *Component, name string) *Signal[T]

Show publishes the shared value in c's markup under name and returns the signal that follows it.

func (*SharedSignal[T]) Update added in v1.3.0

func (s *SharedSignal[T]) Update(fn func(T) T)

Update sets the value to fn of the current one, atomically within this process.

func (*SharedSignal[T]) Use added in v1.3.0

func (s *SharedSignal[T]) Use(c *Component) *Signal[T]

Use returns a signal in c that follows the shared value.

type Signal

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

Signal is a reactive value; see State.

func Derived

func Derived[T any](c *Component, name string, fn func() T) *Signal[T]

Derived declares a value computed from others, recomputed when they change. With a name, it is shown in the markup too.

func Props added in v1.3.0

func Props[T any](c *Component) *Signal[T]

Props returns the component's props as a signal of T, decoded from what the parent passes. It changes when the parent passes new props, so Derived values and Effects that read it rerun.

func State

func State[T any](c *Component, name string, initial T, opts ...Option) *Signal[T]

State declares a reactive value, shown in the markup under name. Reading it with Get inside a Derived or Effect subscribes to it.

func (*Signal[T]) Get

func (s *Signal[T]) Get() T

Get returns the value.

func (*Signal[T]) Peek

func (s *Signal[T]) Peek() T

Peek returns the value without subscribing to it.

func (*Signal[T]) Set

func (s *Signal[T]) Set(v T)

Set changes the value, updating the markup and everything derived from it. Treat values as immutable: set a new slice or map rather than changing the one a signal holds.

func (*Signal[T]) Update

func (s *Signal[T]) Update(fn func(T) T)

Update sets the value to fn of the current one.

Directories

Path Synopsis
Package livetest runs live components in Go tests, without a browser:
Package livetest runs live components in Go tests, without a browser:

Jump to

Keyboard shortcuts

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