{"id":20516648,"url":"https://github.com/block/kfsm","last_synced_at":"2025-04-14T00:36:12.620Z","repository":{"id":193992925,"uuid":"689789038","full_name":"block/kfsm","owner":"block","description":"Finite state machinery in Kotlin","archived":false,"fork":false,"pushed_at":"2024-11-11T05:35:18.000Z","size":119,"stargazers_count":24,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-11T06:29:09.621Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://cashapp.github.io/kfsm/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/block.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-10T22:34:04.000Z","updated_at":"2024-11-03T11:17:12.000Z","dependencies_parsed_at":"2023-09-11T06:28:09.217Z","dependency_job_id":"f48c70b3-134c-4d03-b5dc-bc24613f7630","html_url":"https://github.com/block/kfsm","commit_stats":null,"previous_names":["cashapp/kfsm","block/kfsm"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/block%2Fkfsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/block%2Fkfsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/block%2Fkfsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/block%2Fkfsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/block","download_url":"https://codeload.github.com/block/kfsm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248802893,"owners_count":21163939,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-15T21:29:59.283Z","updated_at":"2025-04-14T00:36:12.603Z","avatar_url":"https://github.com/block.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"kFSM is Finite State Machinery for Kotlin.\n\n[\u003cimg src=\"https://img.shields.io/nexus/r/app.cash.kfsm/kfsm.svg?label=latest%20release\u0026server=https%3A%2F%2Foss.sonatype.org\"/\u003e](https://central.sonatype.com/namespace/app.cash.kfsm)\n\n## How to use\n\nThere are four key components to building your state machine.\n\n1. The nodes representing different states in the machine - `State`\n2. The type to be transitioned through the machine - `Value`\n3. The effects that are defined by transitioning from one state to the next - `Transition`\n4. A transitioner, which can be customised when you need to define pre and post transition hooks - `Transitioner`\n\nLet's build a state machine for a traffic light.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e Green\n    Amber --\u003e Red\n    Green --\u003e Amber\n    Red --\u003e Green\n```\n\n### State\n\nThe states are a collection of related classes that define a distinct state that the value can be in. They also define\nwhich states are valid next states.\n\n```kotlin\nsealed class Color(to: () -\u003e Set\u003cColor\u003e) : app.cash.kfsm.State\u003cColor\u003e(to)\ndata object Green : Color({ setOf(Amber) })\ndata object Amber : Color({ setOf(Red) })\ndata object Red : Color({ setOf(Green) })\n```\n\n\u003e [!IMPORTANT]\n\u003e Be sure to define your state constructor with _functions_ rather than literal values \n\u003e if you require cycles in your state machine. Otherwise, you are likely to encounter\n\u003e null pointer exceptions from the Kotlin runtime's inability to define the types.\n\n### Value\n\nThe value is responsible for knowing and updating its current state.\n\n```kotlin\ndata class Light(override val state: Color) : Value\u003cLight, Color\u003e {\n    override fun update(newState: Color): Light = this.copy(state = newState)\n}\n```\n\n### Transition\n\nTypes that provide the required side-effects that define a transition in the machine.\n\n```kotlin\nabstract class ColorChange(\n    from: States\u003cColor\u003e,\n    to: Color\n) : Transition\u003cLight, Color\u003e(from, to) {\n    // Convenience constructor for when the from set has only one value\n    constructor(from: Color, to: Color) : this(States(from), to)\n}\n\nclass Go(private val camera: Camera) : ColorChange(from = Red, to = Green) {\n    override suspend fun effect(value: Light) = camera.disable()\n}\n\nobject Slow : ColorChange(from = Green, to = Amber)\n\nclass Stop(private val camera: Camera) : ColorChange(from = Amber, to = Red) {\n    override suspend fun effect(value: Light) = camera.enable()\n}\n```\n\n### Transitioner\n\nMoving a value from one state to another is done by the transitioner. We provide it with a function that declares how to\npersist values.\n\n```kotlin\nclass LightTransitioner(\n    private val database: Database\n) : Transitioner\u003cColorChange, Light, Color\u003e() {\n \n    override suspend fun persist(value: Light, change: ColorChange): Result\u003cLight\u003e = database.update(value)\n}\n```\n\nEach time a transition is successful, the persist function will be called.\n\n#### Pre and Post Transition Hooks\n\nIt is sometimes necessary to execute effects before and after a transition. These can be defined on the transitioner.\n\n```kotlin\nclass LightTransitioner ...  {\n    \n    // ...\n    \n    override suspend fun preHook(value: V, via: T): Result\u003cUnit\u003e = runCatching {\n        globalLock.lock(value)\n    }\n\n    override suspend fun postHook(from: S, value: V, via: T): Result\u003cUnit\u003e = runCatching {\n        globalLock.unlock(value)\n        notificationService.send(via.successNotifications())\n    }\n}\n```\n\n### Transitioning\n\nWith the state machine and transitioner defined, we can progress any value through the machine by using the\ntransitioner.\n\n```kotlin\nval transitioner = LightTransitioner(database)\nval greenLight: Result\u003cLight\u003e = transitioner.transition(redLight, Go)\n```\n\n### Putting it all together\n\n```kotlin\n// The state\nsealed class Color(to: () -\u003e Set\u003cColor\u003e) : app.cash.kfsm.State\u003cColor\u003e(to)\ndata object Green : Color({ setOf(Amber) })\ndata object Amber : Color({ setOf(Red) })\ndata object Red : Color({ setOf(Green) })\n\n// The value\ndata class Light(override val state: Color) : Value\u003cLight, Color\u003e {\n    override fun update(newState: Color): Light = this.copy(state = newState)\n}\n\n// The transitions\nabstract class ColorChange(\n    from: States\u003cColor\u003e,\n    to: Color\n) : Transition\u003cLight, Color\u003e(from, to) {\n    // Convenience constructor for when the from set has only one value\n    constructor(from: Color, to: Color) : this(States(from), to)\n}\nclass Go(private val camera: Camera) : ColorChange(from = Red, to = Green) {\n    override suspend fun effect(value: Light) = camera.disable()\n}\nobject Slow : ColorChange(from = Green, to = Amber)\nclass Stop(private val camera: Camera) : ColorChange(from = Amber, to = Red) {\n    override suspend fun effect(value: Light) = camera.enable()\n}\n\n// The transitioner\nclass LightTransitioner(\n    private val database: Database\n) : Transitioner\u003cColorChange, Light, Color\u003e() {\n    override suspend fun persist(value: Light, change: ColorChange): Result\u003cLight\u003e = database.update(value)\n}\n\n// main ...\nval transitioner = LightTransitioner(database)\nval greenLight: Result\u003cLight\u003e = transitioner.transition(redLight, Go)\n```\n\n### More examples\n\nSee [lib/src/test/kotlin/app/cash/kfsm/exemplar](https://github.com/cashapp/kfsm/tree/main/lib/src/test/kotlin/app/cash/kfsm/exemplar)\nfor a different example of how to use this library.\n\n### Coroutine Support\n\nIf you are using coroutines and need suspending function support, you can extend `TransitionerAsync` instead of \n`Transitioner` and implement any suspending transition effects via the `Transition.effectAsync` method.\n\n\n## Safety\n\nHow does kFSM help validate the correctness of your state machine and your values?\n\n1. It is impossible to define a Transition that does not comply with the transitions defined in the States. For example,\n   a transition that attempts to define an arrow between `Red` and `Amber` will fail at construction.\n2. If a value has already transitioned to the target state, then a subsequent request will not execute the transition a\n   second time. The result will be success. I.e. it is a no-op.\n    1. (unless you have defined a circular/self-transition, in which case it will)\n3. If a value is in a state unrelated to the executed transition, then the result will be an error and no effect will be\n   executed.\n\n### Testing your state machine\n\nThe utility `StateMachine.verify` will assert that a defined state machine is valid - i.e. that all states are visited\nfrom a given starting state.\n\n```kotlin\nStateMachine.verify(Green) shouldBeRight true\n```\n\n### Document your state machine\n\nThe utility `StateMachine.mermaid` will generate a mermaid diagram of your state machine. This can be rendered in markdown.\nThe diagram of `Color` above was created using this utility.\n\n```kotlin\nStateMachine.mermaid(Green) shouldBeRight \"\"\"stateDiagram-v2\n    [*] --\u003e Green\n    Amber --\u003e Red\n    Green --\u003e Amber\n    Red --\u003e Green\n    \"\"\".trimMargin()\n```\n\n## Documentation\n\nThe API documentation is published with each release\nat [https://cashapp.github.io/kfsm](https://cashapp.github.io/kfsm)\n\nSee a list of changes in each release in the [CHANGELOG](CHANGELOG.md).\n\nSee [lib/src/test/kotlin/app/cash/kfsm/exemplar](https://github.com/cashapp/kfsm/tree/main/lib/src/test/kotlin/app/cash/kfsm/exemplar)\nfor a different example of how to use this library.\n\nFor details on contributing, see the [CONTRIBUTING](CONTRIBUTING.md) guide.\n\n### Building\n\n\u003e [!NOTE]\n\u003e kFSM uses [Hermit](https://cashapp.github.io/hermit/).\n\u003e\n\u003e Hermit ensures that your team, your contributors, and your CI have the same consistent tooling. Here are\n\u003e the [installation instructions](https://cashapp.github.io/hermit/usage/get-started/#installing-hermit).\n\u003e\n\u003e [Activate Hermit](https://cashapp.github.io/hermit/usage/get-started/#activating-an-environment) either\n\u003e by [enabling the shell hooks](https://cashapp.github.io/hermit/usage/shell/) (one-time only, recommended) or\n\u003e manually sourcing the env with `. ./bin/activate-hermit`.\n\nUse gradle to run all tests\n\n```shell\ngradle build\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblock%2Fkfsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblock%2Fkfsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblock%2Fkfsm/lists"}