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