// Package tinyspawn implements the Moxie spawn run model for TinyGo wasm apps. // // A domain is a goroutine with its own state and a single event loop. Spawn // starts one, fixes its channel set at creation, and returns a lifecycle // channel that closes when the handler returns. Domains communicate only over // the channels passed at spawn. // // The model is deliberately narrower than Go's "go" statement: // // - the entry point is a named function, not a closure; // - data is passed by value and copied into the domain; // - every channel is bound at spawn and stays bound; // - each channel direction has one producer and one consumer; // - package-scoped variables are read-only; // - a domain reaches state only through its own channels, never through // another package's globals; // - a handler is an event loop, not a one-shot job. // // On TinyGo the scheduler is cooperative and single-threaded. On native Moxie // the same layout becomes a tree of parallel domains. package tinyspawn import "git.smesh.lol/tiny-actor" // Spawn starts fn(data, chans) in a new domain and returns its lifecycle // channel. The channel closes when fn returns. // // data must be a value type. It is copied into the domain; the caller must not // expect changes to be visible either way. Channels belong in chans, never in // data. fn should be a named function with no captured state. func Spawn[D, C any](fn func(D, C), data D, chans C) <-chan struct{} { life := actor.NewLifecycle() actor.Go(life, func() { fn(data, chans) }) return life.Stopped() } // Duplex is one end of a bidirectional channel pair. Tx sends to the peer and // Rx receives from the peer. Each direction is a separate channel, so the pair // has exactly one writer and one reader per direction. type Duplex[T any] struct { Tx chan<- T Rx <-chan T } // NewDuplex creates an unbuffered bidirectional channel pair. The two returned // ends connect to each other. func NewDuplex[T any]() (Duplex[T], Duplex[T]) { return NewBufferedDuplex[T](0) } // NewBufferedDuplex creates a bidirectional channel pair with the given buffer // size in each direction. A buffer of 0 creates a rendezvous pair. func NewBufferedDuplex[T any](buffer int) (Duplex[T], Duplex[T]) { if buffer < 0 { buffer = 0 } ab := make(chan T, buffer) ba := make(chan T, buffer) return Duplex[T]{Tx: ab, Rx: ba}, Duplex[T]{Tx: ba, Rx: ab} } // Send writes v to the peer. It blocks until the peer receives, the buffer has // room, or the channel is closed. func (d Duplex[T]) Send(v T) { d.Tx <- v } // TrySend attempts to write v without blocking. It reports false when a // buffered send would block. func (d Duplex[T]) TrySend(v T) bool { select { case d.Tx <- v: return true default: return false } } // Recv returns the receive side for use in select. func (d Duplex[T]) Recv() <-chan T { return d.Rx } // Breaker is a cancellation channel. Closing it wakes every domain holding it. // A nil Breaker is not closed and never fires. type Breaker chan struct{} // NewBreaker creates an open breaker. func NewBreaker() Breaker { return make(Breaker) } // Close cancels every domain holding the breaker. Close must be called at most // once per breaker. func (b Breaker) Close() { close(b) } // Done returns the receive side for use in select. func (b Breaker) Done() <-chan struct{} { return b } // Closed reports whether the breaker has been closed. func (b Breaker) Closed() bool { select { case <-b: return true default: return false } }