{"id":"busycityguy/stateq","name":"stateq","scope":"busycityguy","platform":"roblox","description":"An intuitive fully-typed Finite State Machine in Luau that supports async transitions by queueing events","version":"0.0.7","latest":"0.0.7","versions":["0.0.5","0.0.6","0.0.7"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"osyrisrblx/t":{"version":"^3.1.1","alias":"t"}},"integrity":"e158c88bc5419c523e0694f2105c0712f69dfd9b3bf3443d42a6a2b571e31d2b","likes":0,"downloads":0,"install":"forest install busycityguy/stateq","url":"https://forest.dev/p/roblox/busycityguy/stateq","files":"https://api.forest.dev/ai/package/roblox/busycityguy/stateq/files","readme":"# StateQ: A Finite State Machine (FSM) in Luau\n\nAn intuitive fully-typed Finite State Machine in [Luau](https://luau-lang.org/) that supports async transitions by queueing events, developed for use in Roblox experiences.\n\nThis project is licensed under the terms of the MIT license. See [LICENSE.md](https://github.com/busycityguy/finite-state-machine-luau/blob/main/LICENSE.md) for details.\n\n## This project is a work in progress\n\nTests need to be written and the API may receive small changes while this project is being finalized for a first release.\n\n# What's a finite state machine?\n\nA Finite State Machine (FSM) provides a way to enforce specific logical flow among a set of States. Given an Event, the FSM responds by looking up the corresponding Transition for that Event in its current State. A Transition is a callback function that is invoked when an Event is given to the FSM, and it returns the next State for the FSM to move to.\n\nThe FSM enforces that Events can only be called when a Transition is defined for that Event in its current State\nby erroring if an Event is called in an invalid state (a state with no Transition defined for that Event).\n\n# Included features\n\nThe FSM provides 6 signals. Five fire during normal handling of an event, and `eventErrored` fires when queued event processing fails.\nThese signals, transition callbacks, and state changes are processed in the following order:\n\n![SequenceDiagram](https://github.com/BusyCityGuy/finite-state-machine-luau/assets/55513323/9ace09e3-a16e-474b-83ca-aac91cd69492)\n\n1. Fire `beforeEvent` signal\n    - with arguments `eventName`, `beforeState`\n1. Call `transition.beforeAsync()` (required, returns next state)\n    - with the VarArgs from `:handle(eventName, transitionArgs...)`\n1. Fire `leavingState` signal\n    - with arguments `beforeState, afterState`\n1. Update `_currentState` to next state\n1. Fire `stateEntered` signal\n    - with arguments `afterState, beforeState`\n1. Call `transition.afterAsync()` (if specified)\n    - with the VarArgs from `:handle(eventName, transitionArgs...)`\n1. Fire `afterEvent` signal\n    - with arguments `eventName, afterState, beforeState`\n1. Fire `finished` signal if next state from `beforeAsync()` was `nil`\n    - with argument `beforeState`\n\nTransitions can be asynchronous, which is supported by queuing each Event submitted via :handle() and processing them in First-In-First-Out (FIFO) order. The next Event starts processing immediately after the previous Event's handler fires `afterEvent`.\n\nThe FSM can Finish if a Transition does not return a \"next state\" during an event marked as \"canBeFinal\".\nIn such a case, the FSM will fire a `finished` event and will error if any further Events are handled.\nA `nil` state means the FSM has Finished.\n\n# Example usage\n\nA simple state machine diagram for a light switch may look like this, where\n\n- States are represented as rectangles, and squares indicate the State can be Final\n- Events are represented as capsules\n\n![ExampleUsage](https://github.com/BusyCityGuy/finite-state-machine-luau/assets/55513323/3d5b2118-91ea-4427-ac2d-688fb0094d1f)\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal StateQ = require(ReplicatedStorage.Packages.StateQ)\n\nlocal LightState = {\n\tOn = \"On\",\n\tOff = \"Off\",\n}\n\nlocal Event = {\n\tSwitchOn = \"SwitchOn\",\n\tSwitchOff = \"SwitchOff\",\n}\n\nlocal light = StateQ.new(LightState.On, {\n\t[Event.SwitchOn] = {\n\t\tcanBeFinal = true,\n\t\tfrom = {\n\t\t\t[LightState.Off] = { -- From state\n\t\t\t\tbeforeAsync = function() -- Transition\n\t\t\t\t\tprint(\"Light is transitioning to On\")\n\t\t\t\t\tfor i = 1, 100 do\n\t\t\t\t\t\t-- do some action to increase brightness of a light here\n\t\t\t\t\t\ttask.wait()\n\t\t\t\t\tend\n\t\t\t\t\treturn LightState.On -- To next state\n\t\t\t\tend,\n\t\t\t\tafterAsync = function()\n\t\t\t\t\tprint(\"Light is now On\")\n\t\t\t\tend,\n\t\t\t}\n\t\t},\n\t},\n\t[Event.SwitchOff] = {\n\t\tcanBeFinal = true,\n\t\tfrom = {\n\t\t\t[LightState.On] = {\n\t\t\t\tbeforeAsync = function()\n\t\t\t\t\tprint(\"Light is transitioning to Off\")\n\t\t\t\t\treturn LightState.Off\n\t\t\t\tend,\n\t\t\t}\n\t\t},\n\t},\n})\n\nlight:handle(Event.SwitchOff) -- prints \"Light is transitioning to Off\"\nlight:handle(Event.SwitchOn) -- prints \"Light is transitioning to On\", increases brightness over time, and then prints \"Light is now On\"\nlight:handle(Event.SwitchOn) -- errors asynchronously via `eventErrored` (illegal event for the current state); see Error handling below\n```\n\n# Error handling\n\nEvery call to `:handle()` enqueues the event for processing on a background thread and returns immediately. Failures fall into two categories depending on when they occur.\n\n## Synchronous errors\n\nThese throw on the thread that called `:handle()` before the event is enqueued:\n\n- `eventName` is not a string (type check)\n- The machine was `destroy()`ed\n- `eventName` is not defined on the machine\n\nIn normal usage with known event names, these are programmer errors and should be left to throw during development.\n\nIf you are passing dynamic or untrusted event names, you can catch them with `pcall`:\n\n```luau\nlocal success, errorMessage = pcall(function()\n\tmachine:handle(someEventName)\nend)\nif not success then\n\twarn(errorMessage)\nend\n```\n\n## Asynchronous errors\n\nThese occur later, on the queue thread, while the event is actually being processed. They cannot propagate back to the `:handle()` caller, so they are surfaced through the `eventErrored` signal instead:\n\n- A transition callback (`beforeAsync` or `afterAsync`) throws\n- The event is illegal for the machine's **current** state at processing time (even if `:handle()` already returned)\n- An event is processed after the machine has finished\n- A non-final event's transition returns `nil`\n- A transition returns a value that fails the state type check\n\nListen for them when you want to log, recover, or assert in tests:\n\n```luau\nmachine.eventErrored:Connect(function(errorInfo: StateQ.EventError)\n\twarn(\n\t\terrorInfo.eventName,\n\t\terrorInfo.beforeState,\n\t\terrorInfo.phase,\n\t\terrorInfo.message\n\t)\nend)\n```\n\n`eventErrored` fires an `EventError` table:\n\n| Field | Description |\n|---|---|\n| `machineName` | Name passed to `StateQ.new`, or an auto-generated default |\n| `eventName` | The event being processed |\n| `beforeState` | State when processing started, if known (same as `beforeEvent`'s second argument) |\n| `afterState` | State at failure time when it differs from `beforeState` (e.g. `afterAsync` after a transition) |\n| `phase` | `\"queue\"`, `\"validation\"`, `\"beforeAsync\"`, or `\"afterAsync\"` |\n| `message` | The error message |\n| `traceback` | Failure stack and the `:handle()` call site (`Queued from:`) |\n\nUse `StateQ.formatEventError(errorInfo)` for a single log string. Compare `errorInfo.phase` against `StateQ.EventErrorPhase.beforeAsync`, etc.\n\nIf nothing is connected to `eventErrored`, the error is re-raised on a new thread so it still appears in the output rather than being silently dropped.\n\n### Swallowing asynchronous errors\n\nThe default re-raise is suppressed if at least one connection to `eventErrored` exists.\n\nSo if you intentionally want to swallow processing errors, you could connect an empty function:\n\n```luau\nmachine.eventErrored:Connect(function(_errorInfo) end)\n```\n\n\n# Detailed system flowchart\n\n![Flowchart](https://github.com/BusyCityGuy/finite-state-machine-luau/assets/55513323/5b3a5c8f-fd42-4021-b3a8-6da1256644d8)\n\n# Installation\n\n## Rojo users\n\nIf your project is set up to build with Rojo, the preferred installation method is using [Wally](https://wally.run/). Add this to your `wally.toml` file:\n\n```bash\n> StateQ = \"busycityguy/stateq@0.0.7\"\n```\n\nIf you're not using Wally, you can add this repository as a submodule of your project by running the following command:\n\n```bash\n> git submodule add <https://github.com/BusyCityGuy/finite-state-machine-luau> path/to/your/dependencies\n```\n\nIf you want to avoid submodules too, you can download the `.zip` file from the [latest release](https://github.com/BusyCityGuy/finite-state-machine-luau/releases/latest) page.\n\n## Non-Rojo users\n\nIf you aren't using Rojo, you can download the `.rbxm` file from the [latest release](https://github.com/BusyCityGuy/finite-state-machine-luau/releases/latest) page and drag it into Roblox Studio, placing the `Packages` folder in `ReplicatedStorage`.\n\n# Feedback\n\nIf you have other questions, bugs, feature requests, or feedback, please [open an issue](https://github.com/BusyCityGuy/finite-state-machine-luau/issues)!\n\n# Contributing\n\nSee the [Contributing readme](CONTRIBUTING.md).\n","readmeTruncated":false}