syncedstate

package module
v0.0.0-...-c2fe3e1 Latest Latest
Warning

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

Go to latest
Published: May 23, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

README

svelte-synced-state

Easily sync Go backend state with Svelte over WebSockets.

This package keeps typed state objects in a Go process and mirrors them into Svelte 5 $state objects. The backend exports a standard net/http WebSocket handler. The frontend subscribes to named state objects, applies server snapshots and updates, and can sync local changes back as full-value replacements.

This project is based on the same state-syncing model as tauri-svelte-synced-store, adapted from Tauri events to a standard Go WebSocket backend.

Go

package main

import (
	"context"
	"log"
	"net/http"

	syncedstate "github.com/synthlabs/svelte-synced-state"
)

type InternalState struct {
	Authenticated bool   `json:"authenticated"`
	Name          string `json:"name"`
}

func main() {
	manager := syncedstate.NewManager()
	internal, err := syncedstate.Define(manager, "InternalState", InternalState{})
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/synced-state", manager.Handler(
		syncedstate.WithOriginPatterns("http://localhost:*"),
	))

	http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		err := internal.Update(r.Context(), func(state *InternalState) {
			state.Authenticated = true
			state.Name = r.FormValue("name")
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Handler options can tune the websocket send buffer. By default, a client is closed when its send buffer fills. Use WithBlockOnFullBuffer to apply backpressure to the goroutine sending the update instead:

http.Handle("/synced-state", manager.Handler(
	syncedstate.WithSendBuffer(128),
	syncedstate.WithBlockOnFullBuffer(),
))

For longer critical sections, use the lower-level lock handle:

locked, err := internal.Lock(context.Background())
if err != nil {
	return err
}
defer locked.Unlock()

locked.Value().Authenticated = true
locked.Value().Name = "Jerod"
return locked.Sync(context.Background())

Svelte

<script lang="ts">
	import { SyncedState } from 'svelte-synced-state';

	type InternalState = {
		authenticated: boolean;
		name: string;
	};

	const internal = new SyncedState<InternalState>('InternalState', {
		authenticated: false,
		name: ''
	});

	async function login(event: Event) {
		event.preventDefault();
		await internal.sync();
		await fetch('/login', {
			method: 'POST',
			body: new URLSearchParams({ name: internal.obj.name })
		});
	}
</script>

{#if internal.obj.authenticated}
	<h1>Welcome {internal.obj.name}</h1>
{/if}

<form onsubmit={login}>
	<input bind:value={internal.obj.name} />
	<button type="submit">Log in</button>
</form>

Protocol

The WebSocket transport uses JSON envelopes:

{ "type": "subscribe", "id": "1", "name": "InternalState" }
{ "type": "update", "name": "InternalState", "version": 2, "value": { "authenticated": true, "name": "Jerod" } }
{ "type": "set", "id": "2", "name": "InternalState", "version": 3, "value": { "authenticated": false, "name": "" } }

Supported message types are subscribe, unsubscribe, snapshot, set, update, and error. Snapshots and updates carry the current server-assigned version. Frontend set messages carry the next expected version, and stale writes receive a snapshot with the latest value/version plus an error string. V1 syncs full JSON values, not partial patches.

Development

pnpm --dir ui install
sh scripts/check.sh

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAlreadyDefined  = errors.New("syncedstate: state name already defined")
	ErrClosed          = errors.New("syncedstate: locked state already closed")
	ErrInvalidScope    = errors.New("syncedstate: invalid state scope")
	ErrMissingValue    = errors.New("syncedstate: message value is required")
	ErrNotFound        = errors.New("syncedstate: state name not found")
	ErrTypeMismatch    = errors.New("syncedstate: state type mismatch")
	ErrVersionConflict = errors.New("syncedstate: state version conflict")
	ErrWildcardName    = errors.New("syncedstate: wildcard state name requires an exact indexed state name")
)

Functions

This section is empty.

Types

type HandlerOption

type HandlerOption func(*handlerConfig)

func WithBlockOnFullBuffer

func WithBlockOnFullBuffer() HandlerOption

func WithOriginPatterns

func WithOriginPatterns(patterns ...string) HandlerOption

func WithSendBuffer

func WithSendBuffer(size int) HandlerOption

func WithWriteTimeout

func WithWriteTimeout(timeout time.Duration) HandlerOption

type IndexedScope

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

func MustIndexedScope

func MustIndexedScope[T any](name string) IndexedScope[T]

func NewIndexedScope

func NewIndexedScope[T any](name string) (IndexedScope[T], error)

func (IndexedScope[T]) Address

func (s IndexedScope[T]) Address(id string) (string, error)

func (IndexedScope[T]) Define

func (s IndexedScope[T]) Define(manager *Manager, id string, initial T, opts ...KeyOption) (*Key[T], error)

func (IndexedScope[T]) Lookup

func (s IndexedScope[T]) Lookup(manager *Manager, id string) (*Key[T], error)

func (IndexedScope[T]) MustAddress

func (s IndexedScope[T]) MustAddress(id string) string

func (IndexedScope[T]) Name

func (s IndexedScope[T]) Name() string

func (IndexedScope[T]) Wildcard

func (s IndexedScope[T]) Wildcard() string

type Key

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

func Define

func Define[T any](manager *Manager, name string, initial T, opts ...KeyOption) (*Key[T], error)

func DefineIndexed

func DefineIndexed[T any](manager *Manager, scopeName, id string, initial T, opts ...KeyOption) (*Key[T], error)

func DefineSingleton

func DefineSingleton[T any](manager *Manager, name string, initial T, opts ...KeyOption) (*Key[T], error)

func Lookup

func Lookup[T any](manager *Manager, name string) (*Key[T], error)

func LookupIndexed

func LookupIndexed[T any](manager *Manager, scopeName, id string) (*Key[T], error)

func LookupSingleton

func LookupSingleton[T any](manager *Manager, name string) (*Key[T], error)

func (*Key[T]) Lock

func (k *Key[T]) Lock(ctx context.Context) (*Locked[T], error)

func (*Key[T]) Name

func (k *Key[T]) Name() string

func (*Key[T]) Set

func (k *Key[T]) Set(ctx context.Context, value T, opts ...WriteOption) error

func (*Key[T]) Snapshot

func (k *Key[T]) Snapshot(ctx context.Context) (T, Meta, error)

func (*Key[T]) Update

func (k *Key[T]) Update(ctx context.Context, update func(*T), opts ...WriteOption) error

type KeyOption

type KeyOption func(*keyConfig)

type Locked

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

func (*Locked[T]) Sync

func (l *Locked[T]) Sync(ctx context.Context, opts ...WriteOption) error

func (*Locked[T]) Unlock

func (l *Locked[T]) Unlock()

func (*Locked[T]) Value

func (l *Locked[T]) Value() *T

type Manager

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

func NewManager

func NewManager(opts ...Option) *Manager

func (*Manager) Handler

func (m *Manager) Handler(opts ...HandlerOption) http.Handler

type Message

type Message struct {
	Type    MessageType     `json:"type"`
	ID      string          `json:"id,omitempty"`
	Name    string          `json:"name,omitempty"`
	Version uint64          `json:"version,omitempty"`
	Value   json.RawMessage `json:"value,omitempty"`
	Error   string          `json:"error,omitempty"`
}

type MessageType

type MessageType string
const (
	MessageSubscribe   MessageType = "subscribe"
	MessageUnsubscribe MessageType = "unsubscribe"
	MessageSet         MessageType = "set"
	MessageSnapshot    MessageType = "snapshot"
	MessageUpdate      MessageType = "update"
	MessageError       MessageType = "error"
)

type Meta

type Meta struct {
	Name    string
	Version uint64
}

type Option

type Option func(*Manager)

type SingletonScope

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

func MustSingletonScope

func MustSingletonScope[T any](name string) SingletonScope[T]

func NewSingletonScope

func NewSingletonScope[T any](name string) (SingletonScope[T], error)

func (SingletonScope[T]) Address

func (s SingletonScope[T]) Address() string

func (SingletonScope[T]) Define

func (s SingletonScope[T]) Define(manager *Manager, initial T, opts ...KeyOption) (*Key[T], error)

func (SingletonScope[T]) Lookup

func (s SingletonScope[T]) Lookup(manager *Manager) (*Key[T], error)

func (SingletonScope[T]) Name

func (s SingletonScope[T]) Name() string

type WriteOption

type WriteOption func(*writeConfig)

func WithVersion

func WithVersion(version uint64) WriteOption

Directories

Path Synopsis
indexed command
singleton command

Jump to

Keyboard shortcuts

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