tinyspawn.go raw

   1  // Package tinyspawn implements the Moxie spawn run model for TinyGo wasm apps.
   2  //
   3  // A domain is a goroutine with its own state and a single event loop. Spawn
   4  // starts one, fixes its channel set at creation, and returns a lifecycle
   5  // channel that closes when the handler returns. Domains communicate only over
   6  // the channels passed at spawn.
   7  //
   8  // The model is deliberately narrower than Go's "go" statement:
   9  //
  10  //   - the entry point is a named function, not a closure;
  11  //   - data is passed by value and copied into the domain;
  12  //   - every channel is bound at spawn and stays bound;
  13  //   - each channel direction has one producer and one consumer;
  14  //   - package-scoped variables are read-only;
  15  //   - a domain reaches state only through its own channels, never through
  16  //     another package's globals;
  17  //   - a handler is an event loop, not a one-shot job.
  18  //
  19  // On TinyGo the scheduler is cooperative and single-threaded. On native Moxie
  20  // the same layout becomes a tree of parallel domains.
  21  package tinyspawn
  22  
  23  import "git.smesh.lol/tiny-actor"
  24  
  25  // Spawn starts fn(data, chans) in a new domain and returns its lifecycle
  26  // channel. The channel closes when fn returns.
  27  //
  28  // data must be a value type. It is copied into the domain; the caller must not
  29  // expect changes to be visible either way. Channels belong in chans, never in
  30  // data. fn should be a named function with no captured state.
  31  func Spawn[D, C any](fn func(D, C), data D, chans C) <-chan struct{} {
  32  	life := actor.NewLifecycle()
  33  	actor.Go(life, func() {
  34  		fn(data, chans)
  35  	})
  36  	return life.Stopped()
  37  }
  38  
  39  // Duplex is one end of a bidirectional channel pair. Tx sends to the peer and
  40  // Rx receives from the peer. Each direction is a separate channel, so the pair
  41  // has exactly one writer and one reader per direction.
  42  type Duplex[T any] struct {
  43  	Tx chan<- T
  44  	Rx <-chan T
  45  }
  46  
  47  // NewDuplex creates an unbuffered bidirectional channel pair. The two returned
  48  // ends connect to each other.
  49  func NewDuplex[T any]() (Duplex[T], Duplex[T]) {
  50  	return NewBufferedDuplex[T](0)
  51  }
  52  
  53  // NewBufferedDuplex creates a bidirectional channel pair with the given buffer
  54  // size in each direction. A buffer of 0 creates a rendezvous pair.
  55  func NewBufferedDuplex[T any](buffer int) (Duplex[T], Duplex[T]) {
  56  	if buffer < 0 {
  57  		buffer = 0
  58  	}
  59  	ab := make(chan T, buffer)
  60  	ba := make(chan T, buffer)
  61  	return Duplex[T]{Tx: ab, Rx: ba}, Duplex[T]{Tx: ba, Rx: ab}
  62  }
  63  
  64  // Send writes v to the peer. It blocks until the peer receives, the buffer has
  65  // room, or the channel is closed.
  66  func (d Duplex[T]) Send(v T) {
  67  	d.Tx <- v
  68  }
  69  
  70  // TrySend attempts to write v without blocking. It reports false when a
  71  // buffered send would block.
  72  func (d Duplex[T]) TrySend(v T) bool {
  73  	select {
  74  	case d.Tx <- v:
  75  		return true
  76  	default:
  77  		return false
  78  	}
  79  }
  80  
  81  // Recv returns the receive side for use in select.
  82  func (d Duplex[T]) Recv() <-chan T {
  83  	return d.Rx
  84  }
  85  
  86  // Breaker is a cancellation channel. Closing it wakes every domain holding it.
  87  // A nil Breaker is not closed and never fires.
  88  type Breaker chan struct{}
  89  
  90  // NewBreaker creates an open breaker.
  91  func NewBreaker() Breaker {
  92  	return make(Breaker)
  93  }
  94  
  95  // Close cancels every domain holding the breaker. Close must be called at most
  96  // once per breaker.
  97  func (b Breaker) Close() {
  98  	close(b)
  99  }
 100  
 101  // Done returns the receive side for use in select.
 102  func (b Breaker) Done() <-chan struct{} {
 103  	return b
 104  }
 105  
 106  // Closed reports whether the breaker has been closed.
 107  func (b Breaker) Closed() bool {
 108  	select {
 109  	case <-b:
 110  		return true
 111  	default:
 112  		return false
 113  	}
 114  }
 115