fate: Mesin Statechart untuk Go
Statechart Harel di Go — hierarki, region paralel, history, guard, dan transisi tertunda. Nol dependensi, deterministik, dan snapshot-nya bisa dipulihkan persis, termasuk di dalam workflow Temporal.
fate adalah implementasi statechart Harel di Go: state bersarang, region paralel, history dangkal dan dalam. Bukan finite automaton datar. Semantiknya mengikuti SCXML dan XState v5, ditulis dengan generics supaya konteks dan event-nya bertipe kuat.
import "github.com/arisros/fate"
Dua aturan yang menentukan bentuknya
Pertama: nol dependensi. Package inti hanya mengimpor standard library. Ini bukan sekadar gaya — itu yang membuat go get github.com/arisros/fate tidak pernah menarik net/http, dan itu juga alasan integrasi Temporal dipisah jadi modul sendiri (fate/temporal), serta alasan fate-studio tinggal di repositori terpisah sama sekali.
Aturan itu juga yang memaksa satu keputusan besar di rilis v0.5.0: renderer dan diff dikeluarkan dari root package menjadi fate/render dan fate/diff. Ini perubahan yang memutus API — dan ditandai begitu di changelog — tapi hasilnya, orang yang cuma butuh mesinnya tidak lagi ikut membawa kode visualisasi.
Kedua: deterministik. Machine bersifat immutable dan boleh dipakai bersama; Actor yang menyimpan state, dan state itu diserialisasi ke JSON lalu dipulihkan byte-per-byte:
blob, _ := a.Persist()
b, _ := fate.NewActorFromSnapshot[Ctx, Evt](m, blob)
Waktu tidak ditentukan oleh mesinnya
Ini bagian yang paling banyak mengubah desain. Transisi tertunda (after) tidak memakai time.After di dalam mesin. Mesin hanya melaporkan timer apa yang tertunggak, dan pihak luar yang memutuskan kapan timer itu berbunyi:
for _, pt := range a.PendingTimers() {
a.FireTimer(pt.ID)
}
Terlihat berbelit sampai kita ingat siapa penggunanya. Di dalam workflow Temporal, memanggil jam sungguhan itu terlarang — replay harus menghasilkan hasil yang sama persis. Dengan timer yang ditarik, bukan didorong, inti mesinnya buta terhadap jam dan aman dijalankan di lingkungan deterministik mana pun. Pola yang sama dipakai untuk invocation: PendingInvocations / ResolveInvocation.
Yang ada di dalam
| Konsep statechart | Di fate |
|---|---|
| State atomic / compound / parallel / final | NodeAtomic / NodeCompound / NodeParallel / NodeFinal |
| History dangkal & dalam | NodeHistory dengan HistoryShallow / HistoryDeep |
| Guard | TransitionConfig.Guard plus kombinator And / Or / Not / StateIn |
| Aksi masuk, keluar, dan transisi | Assign, Raise, Log, EnqueueActions |
| Transisi tertunda | StateNodeConfig.After |
| Child actor yang di-invoke atau di-spawn | StateNodeConfig.Invoke |
| Persistensi | Actor.Persist / NewActorFromSnapshot |
Bagian tersulit ada di algorithm.go, yang mengadaptasi algoritma transisi SCXML: mencari LCCA (least common compound ancestor) dari state sumber dan target, lalu menyusun exit set dari yang terdalam dan entry set dari yang terluar. Urutan itu bukan detail kosmetik — dia yang menentukan urutan aksi exit dan entry berjalan, dan salah urut berarti aksi berjalan pada state yang sudah tidak aktif.
Ada juga parity_test.go, yang menguji perilaku mesin ini terhadap perilaku yang diharapkan dari semantik XState. Ketika ada perbedaan, yang salah hampir selalu asumsi saya, bukan spesifikasinya.
Status
Masih v0.x — API bisa berubah antar rilis minor sampai v1.0.0, dan perubahan yang memutus selalu ditandai eksplisit di CHANGELOG.md.
Ada juga CLI kecil (go install github.com/arisros/fate/cmd/fate@latest) untuk merender statechart ke ASCII, Mermaid, atau JSON graph, dan mem-diff dua snapshot.