Situs ini dikemudikan dua tombol, seperti mesin pemiliknya. Membaca: j dan k menggulir, d dan u setengah layar, gg ke atas, G ke bawah, H dan L pindah jendela, ? membuka panduan. Spasi adalah kunci leader Neovim dan mengurus isi: Spasi lalu h beranda, r riset, p proyek, g gear, a tentang, / tag, atau Spasi lalu angka untuk melompat ke jendela bernomor itu. Home adalah prefix tmux dan mengurus jendela: Home lalu c membuka terminal, Home lalu & menutup jendela, Home lalu spasi ke jendela berikutnya. Saat fokus ada di papan ketik, h j k l memindahkan pilihan antar tombol dan Enter membukanya.

RisetR TentangA Gear: G untuk gear, papan ketik, terminal, editor, homelab.G
    ProyekP Tag: / seperti di vim: cari./
    ×
    Menu

    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. v0.5.0 membetulkan cara region paralel keluar dan membuat event yang diabaikan bisa dilihat.

    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"

    Rilis terbarunya v0.5.1, diberi tag pada 2026-09-17, sehari setelah v0.5.0.

    Dua aturan yang menentukan bentuknya

    Pertama: nol dependensi. go.mod di modul root tidak mewajibkan apa pun, dan CI menegakkannya: job bernama “engine has zero external dependencies” menggagalkan build kalau go list -m all mengembalikan apa pun selain modul itu sendiri. Aturan itu yang membuat go get github.com/arisros/fate tetap ringan, alasan integrasi Temporal dipisah jadi modul sendiri (fate/temporal), dan alasan fate-studio tinggal di repositori terpisah sama sekali.

    Aturan itu juga yang memaksa pemecahan package di v0.5.0: renderer pindah ke fate/render (render.ASCII, render.Mermaid, render.GraphJSON) dan pembanding snapshot ke fate/diff. Ini perubahan yang memutus API, ditandai begitu di changelog lengkap dengan tabel migrasi dari 0.4.0, tapi 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, dan RejectInvocation.

    Bug yang jadi alasan v0.5.0

    Perbaikan terbesar di v0.5.0 ada di region paralel, dan jenis bug-nya tidak pernah kelihatan dari nilai state saja.

    computeExitSet menelusur naik dari leaf aktif yang namanya paling awal secara alfabet, bukan dari domain transisinya. Akibatnya, transisi yang terjadi di satu region bisa menjalankan aksi Exit milik region lain dan mematikan timer serta invocation-nya, sementara snapshot masih melaporkan region itu aktif. Hasilnya state yang tampak siap dengan transisi tertunda yang tidak akan pernah berbunyi. Sisi masuknya punya masalah cerminan: masuk ke node paralel hanya memasuki node-nya, tanpa satu pun region-nya, jadi aksi Entry mereka tidak pernah berjalan.

    Exit set yang lama justru menyembunyikan bug di sisi masuk, karena ia juga tidak pernah mematikan efek-efek itu saat keluar. Membetulkan yang satu membuka yang lain. Sekarang exit set berisi setiap node aktif di bawah domain transisi, entry set memasuki setiap region di jalurnya, dan tidak ada hasil yang bergantung pada nama region mana yang urut duluan. History dangkal punya asumsi leaf-pertama yang sama dan dibetulkan sekalian.

    v0.5.1 adalah lanjutannya, dan isinya hanya perbaikan itu: transisi internal yang menargetkan state-nya sendiri sekarang kembali memasuki state awalnya, dan history dalam ke node paralel memulihkan state yang tersimpan, bukan state bawaan.

    Changelog-nya juga menyimpan bagian “known limitations”, yang menurut saya sama pentingnya dengan perbaikannya: transisi lintas region hanya memindahkan region target, OnDone untuk node paralel belum ada, dan urutan keluar ditentukan kedalaman, bukan urutan dokumen terbalik. Semua itu lebih sempit dari SCXML, dan menyebutkannya lebih murah daripada seseorang menemukannya sendiri.

    ”Mesinnya mengabaikan itu” sekarang terlihat

    Di statechart, event yang tidak ditangani state mana pun bukan galat, jadi Send membuangnya diam-diam. Itu benar, dan justru itu yang membuat “tidak terjadi apa-apa” susah dilacak. v0.5.0 menambahkan Can, yang mengevaluasi guard tanpa mengubah actor:

    if !actor.Can(evt) {
        // mesin akan mengabaikan event ini di state-nya sekarang
    }
    _ = actor.Send(ctx, evt)

    Karena guard menurut kontraknya murni, jawabannya pasti, bukan tebakan. Dengan semangat yang sama, FireTimer, ResolveInvocation, dan RejectInvocation sekarang mengembalikan bool yang menyatakan apakah panggilannya diterima mesin. Itu memutus method value dan pemenuhan interface, karena itu ia tercatat di bagian perubahan yang memutus.

    Metadata untuk alat, bukan untuk mesinnya

    Separuh lain v0.5.0 untuk perkakas seperti fate-studio. Guard adalah fungsi Go, jadi diagram tidak bisa tahu apa yang diperiksanya. CondMeta membiarkan kamu menjelaskannya:

    fate.TransitionConfig[Ctx, Evt]{
        Guard: approved,
        CondMeta: fate.Gates(
            fate.Field("$.score").Gte(60),
            fate.Field("$.status").Eq("approved"),
        ).Sample(`{"score": 65, "status": "approved"}`),
    }

    Ia tidak pernah memengaruhi apakah transisi terjadi; yang memutuskan tetap guard-nya. Ia hanya ikut di descriptor mesin supaya penampil bisa menampilkannya. UIStateOf melakukan hal yang sama untuk view model per state: fungsi dari konteks ke sebuah struct, yang JSON Schema-nya diturunkan lewat refleksi dan diterbitkan bersama state-nya. fate.Named dan TransitionConfig.GuardName memberi aksi dan guard nama yang terbaca. Percobaan sebelumnya menebak nama guard dari pointer fungsi dan salah memberi label, karena itu namanya sekarang ditulis eksplisit.

    Yang ada di dalam

    Konsep statechartDi fate
    State atomic / compound / parallel / finalNodeAtomic / NodeCompound / NodeParallel / NodeFinal
    History dangkal & dalamNodeHistory dengan HistoryShallow / HistoryDeep
    GuardTransitionConfig.Guard plus kombinator And / Or / Not / StateIn
    Aksi masuk, keluar, dan transisiAssign, Raise, Log, EnqueueActions, Named
    Transisi tertundaStateNodeConfig.After
    Child actor yang di-invoke atau di-spawnStateNodeConfig.Invoke
    PersistensiActor.Persist / NewActorFromSnapshot
    Metadata untuk perkakasTransitionConfig.CondMeta, StateNodeConfig.UIState
    Render dan difffate/render, fate/diff

    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. Bug paralel di atas adalah wujud salah urut itu.

    Di sekitar mesinnya

    • fate/httphandler membungkus sebuah mesin menjadi simulator HTTP: aliran SSE plus endpoint untuk mengirim event, membunyikan timer, menyelesaikan invocation, undo, dan ekspor. Format kabelnya sama dengan simulator fate-studio, jadi studio bisa mem-proxy ke mesin yang berjalan di dalam layananmu sendiri. Tidak ada autentikasi dan hanya dimaksudkan untuk pengembangan; SECURITY.md menyatakannya.
    • fate/snapshot menulis descriptor mesin ke <dir>/<nama>.json. Arahkan ke .fate/, direktori yang dimuat fate-studio secara bawaan, dan studio bisa menggambar mesinnya tanpa menjalankan apa pun.
    • cmd/fate adalah CLI kecil (go install github.com/arisros/fate/cmd/fate@latest) untuk merender descriptor ke ASCII, Mermaid, atau JSON graph, dan membandingkan dua snapshot.

    Status

    Masih v0.x: API bisa berubah antar rilis minor sampai v1.0.0, dan perubahan yang memutus selalu didaftar eksplisit di CHANGELOG.md.

    Proses rilisnya berubah di versi ini. Rilis sekarang dibuat oleh release-please, yang menjaga satu PR rilis tetap terbuka dan, begitu PR itu digabung, memberi tag versinya lalu membiarkan GoReleaser melampirkan biner CLI-nya. Setiap GitHub Action dipaku ke SHA commit, Dependabot mengawasi action dan modul Temporal, dan rangkaian ujinya (197 uji di 33 berkas, dijalankan dengan -race di Go 1.24 dan 1.25) punya batas cakupan 85%.

    id en
    rss gh in