This repository has been archived by the owner on Oct 26, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
gocoro.go
executable file
·237 lines (188 loc) · 6.87 KB
/
gocoro.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package gocoro
import (
"errors"
"sync/atomic"
"time"
)
// Coroutine represents a coroutine that executes alternately with the main / calling
// thread.
type Coroutine struct {
routine func(*Execution)
running *atomic.Bool
yield chan bool
execute chan bool
execution *Execution
finished *atomic.Bool
Properties *Properties // A set of properties that allows you to interface with a running coroutine. Linked through the running Execution.
OnStart func() // OnStart is a callback to a function called before the coroutine starts.
OnFinish func() // OnFinish is a callback to a function called after the coroutine finishes.
}
// NewCoroutine creates and returns a new Coroutine instance.
func NewCoroutine() Coroutine {
co := Coroutine{
yield: make(chan bool),
execute: make(chan bool),
running: &atomic.Bool{},
finished: &atomic.Bool{},
}
co.execution = &Execution{coroutine: &co, Properties: newProperties()}
co.Properties = co.execution.Properties
return co
}
// Run runs the given coroutine function.
// Any arguments passed along will be available to the script through the Execution object.
// Run will return an error if the coroutine is already running.
func (co *Coroutine) Run(coroutineFunc func(exe *Execution)) error {
co.finished.Store(false)
if co.running.CompareAndSwap(false, true) {
if co.OnStart != nil {
co.OnStart()
}
co.running.Store(true)
co.routine = coroutineFunc
go func() {
// Send something on execute first so the script doesn't update until we
// call Coroutine.Update() the first time.
co.execute <- true
co.routine(co.execution)
// If the coroutine wasn't running anymore, then we shouldn't push anything to yield to unblock the coroutine at the end
if co.running.CompareAndSwap(true, false) {
co.yield <- true
}
co.finished.Store(true)
}()
return nil
} else {
return errors.New("Coroutine is already running")
}
}
// Running returns whether the Coroutine is running or not.
func (co *Coroutine) Running() bool {
return co.running.Load()
}
// Update waits for the Coroutine to pause, either as a yield or when the Coroutine is finished. If the
// Coroutine isn't running anymore, Update doesn't do anything.
func (co *Coroutine) Update() {
if co.running.Load() {
<-co.execute // Wait to pull from the execute channel, indicating the coroutine can run
<-co.yield // Wait to pull from the yield channel, indicating the coroutine has paused / finished
}
if co.finished.CompareAndSwap(true, false) {
if co.OnFinish != nil {
co.OnFinish()
}
}
}
// Stop signals a running Coroutine to stop; the Execution object needs to pick up on this fact to end gracefully.
// Note that this does NOT kill the coroutine, as it internally runs in a goroutine - you'll need to detect this and
// end the coroutine from the coroutine function yourself.
func (co *Coroutine) Stop() {
wasRunning := co.running.Load()
co.running.Store(false)
if wasRunning {
<-co.execute // Pull from the execute channel so the coroutine can get out of the yield and realize it's borked
}
}
var ErrorCoroutineStopped = errors.New("Coroutine requested to be stopped")
// Execution represents a means to easily and simply manipulate coroutine execution from your running coroutine function.
type Execution struct {
coroutine *Coroutine
Properties *Properties // A set of properties that allows you to interface with the running coroutine object.
}
// Stopped returns true if the coroutine was requested to be stopped through Coroutine.Stop(). You can check this in your
// coroutine to exit early and clean up the coroutine as desired.
func (exe *Execution) Stopped() bool {
return !exe.coroutine.Running()
}
// Yield yields execution in the coroutine function, allowing the main / calling thread to continue.
// The coroutine will pick up from this point when Coroutine.Update() is called again.
// If the Coroutine has exited already, then this will immediately return with ErrorCoroutineStopped.
func (exe *Execution) Yield() error {
if !exe.coroutine.Running() {
return ErrorCoroutineStopped
}
exe.coroutine.yield <- true // Yield; we're done
exe.coroutine.execute <- true // Put something in the execute channel when we're ready to pick back up if we're not done
return nil
}
// YieldTime yields execution of the Coroutine for the specified duration time.
// Note that this function only checks the time in increments of however long the calling thread takes between calling Coroutine.Update().
// So, for example, if Coroutine.Update() is run, say, once every 20 milliseconds, then that's the fidelity of your waiting duration.
// If the Coroutine has stopped prematurely, then this will immediately return with ErrorCoroutineStopped.
func (exe *Execution) YieldTime(duration time.Duration) error {
start := time.Now()
for {
if time.Since(start) >= duration {
return nil
} else {
if err := exe.Yield(); err != nil {
return err
}
}
}
}
// YieldTicks yields execution of the Coroutine for the specified number of ticks.
// A tick is defined by one instance of Coroutine.Update() being called.
// If the Coroutine has stopped prematurely, then this will immediately return with ErrorCoroutineStopped.
func (exe *Execution) YieldTicks(tickCount int) error {
for {
if tickCount == 0 {
return nil
} else {
tickCount--
if err := exe.Yield(); err != nil {
return err
}
}
}
}
// YieldCompleter pauses the Coroutine until the provided Completer's Done() function returns true.
// If the Coroutine has stopped prematurely, then this will immediately return with ErrorCoroutineStopped.
func (exe *Execution) YieldCompleter(completer Completer) error {
for {
if completer.Done() {
return nil
} else {
if err := exe.Yield(); err != nil {
return err
}
}
}
}
// YieldFunc yields the running Coroutine until the provided function returns true.
// If the Coroutine has stopped prematurely, then this will immediately return with ErrorCoroutineStopped.
func (exe *Execution) YieldFunc(doFunc func() bool) error {
for {
if doFunc() {
return nil
} else {
if err := exe.Yield(); err != nil {
return err
}
}
}
}
// Completer provides an interface of an object that can be used to pause a Coroutine until it is completed.
// If the Completer's Done() function returns true, then the Coroutine will advance.
type Completer interface {
Done() bool
}
// Properties represents a simple map of strings to values. Properties can be used to interface between
// a Coroutine object and its running coroutine function through the Execution object.
type Properties map[string]any
func newProperties() *Properties {
return &Properties{}
}
func (p *Properties) Set(name string, value any) {
(*p)[name] = value
}
func (p Properties) Get(name string) any {
value := p[name]
return value
}
func (p *Properties) Delete(name string) {
delete(*p, name)
}
func (p *Properties) Clear() {
*p = map[string]any{}
}