### Development Setup Commands Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Commands to set up the development environment, including starting a Redis server and running tests. Ensure Redis is installed and accessible. ```sh # First we should start a redis-server redis-server # Then run tests npm run test ``` -------------------------------- ### Install bot-state-machine Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Install the package via npm. ```sh $ npm i bot-state-machine ``` -------------------------------- ### Complex State Machine Configuration Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md A comprehensive example demonstrating the initialization of a StateMachine with complex options, including default values for options that depend on flags and custom setters for option values. ```javascript const sm = new StateMachine(options) const root = sm.rootState() .flag('default-stock', '') const BuyCommand = root.command('buy') .option('stock', { default (key, flags) { const defaultStock = flags['default-stock'] if (!defaultStock) { throw new Error('stock is required') } return defaultStock } }) .option('position', { default: 'all-in', set (value) { if (value === 'all-in') { return 1 } if (Number.isNaN(value)) { throw TypeError(`${value} is not a number`) } return Number(value) } }) .action(function ({options}) { this.say(`buy ${options.stock}, position: ${options.position}`) }) const SetDefaultStock = root.command('set-default-stock') .option('stock') .action(function ({options}) { this.setFlag('default-stock', 'TSLA') }) const output = await sm.chat().input(input) ``` -------------------------------- ### Command Action Example with State Transition Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Demonstrates a command action that performs an asynchronous operation, provides feedback using 'this.say', and conditionally returns a target state based on success or failure. ```javascript someCommand.action(async function ({options, flags, state}) { try { await doSomethingWith(options) this.say('success') // If succeeded, back to the parent state return state.parent } catch (e) { this.say('fail, reason: %s', e.message) // Just stay on the current state return state } }) ``` -------------------------------- ### Initialize RedisSyncer with StateMachine Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Instantiate RedisSyncer with an ioredis client and pass it to the StateMachine constructor. Ensure ioredis is installed and a Redis server is accessible. ```javascript const Redis = require('ioredis') const {RedisSyncer} = require('bot-state-machine') const sm = new StateMachine({ syncer: new RedisSyncer( new Redis(6379, '127.0.0.1') ) }) ``` -------------------------------- ### Initialize StateMachine Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Import the core classes required to set up the state machine and syncers. ```js const { StateMachine, SimpleMemorySyncer, RedisSyncer } = require('bot-state-machine') ``` -------------------------------- ### Initialize StateMachine with Options Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Creates a new StateMachine instance with configurable options for timeouts, custom formatters, and distributed synchronization. Use 'nonExactMatch' for fuzzy command matching and 'actionTimeout' to set command execution limits. ```javascript const { StateMachine, RedisSyncer } = require('bot-state-machine') // Basic configuration const sm = new StateMachine({ nonExactMatch: true, // Enable fuzzy command matching actionTimeout: 5000, // Timeout for actions in milliseconds lockRefreshInterval: 1000, // Lock refresh interval in milliseconds format: (tpl, ...values) => { return util.format(tpl, ...values) }, joiner: (messages) => { return messages.join('\n') } }) ``` -------------------------------- ### Define Command Options with Defaults Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Illustrates defining command options, including those with default values. Avoid defining non-default options after options with defaults to prevent errors. ```javascript BuyCommand .option('position', { default: '100%' }) .option('stock') // ❌ This will cause an 'NON_DEFAULT_OPTION_FOLLOWS_DEFAULT' error ``` -------------------------------- ### StateMachine Initialization Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Initialize a new StateMachine instance with optional configuration. ```APIDOC ## new StateMachine(options) ### Description Initializes a new StateMachine instance. ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **options** (object) - Optional configuration object. - **nonExactMatch** (boolean) - Defaults to `false`. If true, commands will not require an exact match. - **format** (function) - Defaults to `util.format`. A function to format template strings. - **joiner** (function) - A function to join multiple messages. - **actionTimeout** (number) - Defaults to `5000` milliseconds. Timeout for `action` and `catch` execution. - **lockRefreshInterval** (number) - Defaults to `1000` milliseconds. Advanced option to prevent lock expiration. - **lockKey** (function) - A function to generate a unique lock key for each `distinctId`. - **storeKey** (function) - A function to generate a key for storing user state. - **syncer** (Syncer) - Defaults to `new SimpleMemorySyncer()`. The synchronization mechanism for state. ### Request Example ```javascript const { StateMachine } = require('bot-state-machine'); const sm = new StateMachine({ actionTimeout: 10000, lockKey: (distinctId) => `lock:${distinctId}` }); ``` ### Response #### Success Response (200) An instance of StateMachine. #### Response Example ```json { "message": "StateMachine initialized successfully" } ``` ``` -------------------------------- ### RedisSyncer Initialization Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md How to initialize the RedisSyncer using an ioredis instance. ```APIDOC ## new RedisSyncer(redis, options) ### Description Initializes a new Redis-based synchronizer for the state machine. ### Parameters - **redis** (ioredis) - Required - An instance of ioredis or an object with compatible interfaces. - **options** (Object) - Optional - Configuration options. - **lockExpire** (int) - Optional - Number of milliseconds until the lock expires. ### Request Example const Redis = require('ioredis'); const {RedisSyncer} = require('bot-state-machine'); const sm = new StateMachine({ syncer: new RedisSyncer( new Redis(6379, '127.0.0.1') ) }); ``` -------------------------------- ### StateMachine Constructor Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Initializes a new StateMachine instance with various configuration options. ```APIDOC ## StateMachine Constructor Creates a new state machine instance with configurable options for timeouts, custom formatters, and distributed synchronization. ### Method `new StateMachine(options)` ### Parameters #### Request Body - **nonExactMatch** (boolean) - Optional - Enable fuzzy command matching. - **actionTimeout** (number) - Optional - Timeout for actions in milliseconds. - **lockRefreshInterval** (number) - Optional - Lock refresh interval in milliseconds. - **format** (function) - Optional - Custom message formatter for i18n. - **joiner** (function) - Optional - Custom message joiner. ### Request Example ```javascript const { StateMachine, RedisSyncer } = require('bot-state-machine') const sm = new StateMachine({ nonExactMatch: true, actionTimeout: 5000, lockRefreshInterval: 1000, format: (tpl, ...values) => { return util.format(tpl, ...values) }, joiner: (messages) => { return messages.join('\n') } }) ``` ### Response #### Success Response (200) - **StateMachine instance** - The newly created state machine instance. ``` -------------------------------- ### sm.rootState() Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Creates and returns the root state of the state machine, serving as the initial entry point. ```APIDOC ## sm.rootState() Creates and returns the root state of the state machine. The root state is the initial entry point and serves as the default state when commands complete without specifying a target state. ### Method `sm.rootState()` ### Parameters None ### Request Example ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.flag('userLoggedIn', false, function(newValue, oldValue) { this.say(newValue ? 'Welcome back!' : 'Goodbye!') }) root.command('login') .action(function() { this.setFlag('userLoggedIn', true) }) ``` ### Response #### Success Response (200) - **State instance** - A state instance that can have flags and commands defined. ``` -------------------------------- ### Define Default Command Finder Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Shows how to define a default command finder function for a state, which is executed when no other command matches the input. ```javascript const Hello = state .command('hello') .option('name') .action(function ({options}) { this.say(`hello ${options.name}`) }) state.default(() => Hello) ``` -------------------------------- ### Command Definition and Options Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Defines how commands are created and how options with default values and custom setters are configured. ```APIDOC ## Command Definition and Options ### Options Principle & Example Options should be defined in a specific order. A non-default option cannot follow an option with a default value. ```js BuyCommand .option('position', { default: '100%' }) .option('stock') // ❌ This will cause an 'NON_DEFAULT_OPTION_FOLLOWS_DEFAULT' error ``` Here is a complex example demonstrating option definition with default values and setters: ```js const sm = new StateMachine(options) const root = sm.rootState() .flag('default-stock', '') const BuyCommand = root.command('buy') .option('stock', { default (key, flags) { const defaultStock = flags['default-stock'] if (!defaultStock) { throw new Error('stock is required') } return defaultStock } }) .option('position', { default: 'all-in', set (value) { if (value === 'all-in') { return 1 } if (Number.isNaN(value)) { throw TypeError(`${value} is not a number`) } return Number(value) } }) .action(function ({options}) { this.say(`buy ${options.stock}, position: ${options.position}`) }) const SetDefaultStock = root.command('set-default-stock') .option('stock') .action(function ({options}) { this.setFlag('default-stock', 'TSLA') }) const output = await sm.chat().input(input) ``` **Sequence Example:** sequence | input | error | output ---- | ---- | ---- | ---- 1 | `buy TSLA` | | `'buy TSLA, position: 1'` 2 | `buy` | `OPTIONS_NOT_FULFILLED` | 3 | `set-default-stock TSLA` | | `''` 4 | `buy` | | `'buy TSLA, position: 1'` 5 | `buy position=0.2` | | `'buy TSLA, position: 0.2'` ``` -------------------------------- ### Basic Usage of StateMachine Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Initialize a state machine, define a command with options, and process user input. ```js const {StateMachine} = require('bot-state-machine') // Configurations ////////////////////////////////////////////////////// const sm = new StateMachine() const rootState = sm.rootState() const Buy = rootState.command('buy') // bot-state-machine provides a Python-like argument parser, // so, `buy TSLA` is equivalent to `buy stock=TSLA` .option('stock') .action(async function ({options}) { await buyStock(options.stock) this.say('success') // If the action of a command returns `undefined`, then the // state machine will return to the root state after the command executed }) // If the action function rejects, then it will go into the catch function if exists. .catch(function (err) { this.say('failed') }) // Chat ////////////////////////////////////////////////////// // We could create as many chat tasks as we want, // so that we could handle arbitrary numbers of requests const chat = sm.chat() const output = await chat.input('buy TSLA') // or 'buy stock=TSLA' console.log(output) // success ``` -------------------------------- ### Define command actions and state transitions Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Uses action() to execute logic and return a state instance or parent to manage conversation flow. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() const OrderCommand = root.command('order') .action(function({ options, flags, state, distinctId }) { this.say('Starting order flow for user: %s', distinctId) // Return sub-state to continue conversation return StateConfirm }) const StateConfirm = OrderCommand.state('confirm') StateConfirm.command('yes') .action(function({ state }) { this.say('Order confirmed!') // Return undefined to go back to root state return undefined }) StateConfirm.command('no') .action(function({ state }) { this.say('Order cancelled') // Return parent to go back one level return state.parent }) StateConfirm.command('stay') .action(function({ state }) { this.say('Staying in confirm state') // Return current state to remain return state }) const chat = sm.chat('user1') await chat.input('order') // Output: "Starting order flow for user: user1" // State: now in StateConfirm await chat.input('yes') // Output: "Order confirmed!" // State: back to root ``` -------------------------------- ### Context Methods Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Utility methods available within the command execution context, such as `say` and `setFlag`. ```APIDOC ## Context Methods ### this.say(template, ...values): void - **template** `string` - **values** `Array` Say something to the user. The argument of the method is the same as Node.js `util.format()`, and will be formatted by `options.format`. ```js this.say('Hello %s!', 'world') ``` `options.format` is designed to provide better support for i18n. Could be used in: - command condition - command action - command catch - onchange method of state flag ### this.setFlag(name, value): void Could be used in: - command action - command catch ``` -------------------------------- ### Define Root State and Flags/Commands Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Creates the root state of the state machine, serving as the initial entry point. Flags can be defined with callback functions to react to value changes, and commands can be attached to this state. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() // Define flags on root state root.flag('userLoggedIn', false, function(newValue, oldValue) { this.say(newValue ? 'Welcome back!' : 'Goodbye!') }) // Define commands on root state root.command('login') .action(function() { this.setFlag('userLoggedIn', true) }) ``` -------------------------------- ### Advanced Configuration: Synchronization Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Details on configuring the state machine for clustered deployments or alternative storage solutions using syncers like Redis. ```APIDOC # Advanced Section The default configuration of `StateMachine` only works for single instance chat bot, and saves store data just in memory. If you want to deploy a chat bot cluster with many instances or to use some storage other than memory, you could use other syncers, such as the built-in `RedisSyncer` to use redis as the storage. ``` -------------------------------- ### Use 'this.say' for User Feedback Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Demonstrates the usage of the 'this.say' method to output formatted messages to the user, similar to Node.js util.format. ```javascript this.say('Hello %s!', 'world') ``` -------------------------------- ### Define Global Commands Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Define commands that are accessible from any state. ```js // A global command to return back to the parent state sm.command('back') .action(({state}) => state.parent) ``` ```js // A global command to cancel everything and return to root state sm.command('cancel') ``` -------------------------------- ### Implement a Custom MongoDB Syncer Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Create a custom storage backend by implementing read, lock, refreshLock, and unlock methods for distributed state persistence. ```javascript const { StateMachine } = require('bot-state-machine') // Custom MongoDB syncer example class MongoSyncer { constructor(collection, options = {}) { this.collection = collection this.lockExpire = options.lockExpire || 3000 } async read({ chatId, lockKey, storeKey }) { const lock = await this.collection.findOne({ _id: lockKey }) // Check if we can read (no lock or we own it) if (lock && lock.chatId !== chatId) { return { success: false } } const store = await this.collection.findOne({ _id: storeKey }) return { success: true, store: store?.data || {} } } async lock({ chatId, store, lockKey, storeKey }) { try { await this.collection.insertOne({ _id: lockKey, chatId, expireAt: new Date(Date.now() + this.lockExpire) }) await this.collection.updateOne( { _id: storeKey }, { $set: { data: store } }, { upsert: true } ) return { success: true } } catch (e) { return { success: false } } } async refreshLock({ lockKey }) { await this.collection.updateOne( { _id: lockKey }, { $set: { expireAt: new Date(Date.now() + this.lockExpire) } } ) } async unlock({ chatId, store, lockKey, storeKey }) { const result = await this.collection.deleteOne({ _id: lockKey, chatId }) if (result.deletedCount > 0) { await this.collection.updateOne( { _id: storeKey }, { $set: { data: store } }, { upsert: true } ) return { success: true } } return { success: false } } } // Usage const sm = new StateMachine({ syncer: new MongoSyncer(db.collection('bot_state'), { lockExpire: 5000 }) }) ``` -------------------------------- ### Create multi-step conversation flows with .state() Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Use .state() to create sub-states within a command. Only non-global commands support sub-states, which are useful for managing sequential conversation steps. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() // Create multi-step checkout flow const CheckoutCommand = root.command('checkout') .action(() => StateSelectPayment) const StateSelectPayment = CheckoutCommand.state('payment') .flag('paymentMethod', null) StateSelectPayment.command('card') .action(function() { this.setFlag('paymentMethod', 'credit_card') this.say('Credit card selected') return StateEnterAddress }) StateSelectPayment.command('paypal') .action(function() { this.setFlag('paymentMethod', 'paypal') this.say('PayPal selected') return StateEnterAddress }) const StateEnterAddress = CheckoutCommand.state('address') StateEnterAddress.command('address') .option('line1') .action(function({ options }) { this.say('Address: %s', options.line1) this.say('Order complete!') return undefined // Back to root }) const chat = sm.chat('shopper1') await chat.input('checkout') // Enters payment state await chat.input('card') // Output: "Credit card selected" await chat.input('address "123 Main St"') // Output: "Address: 123 Main St\nOrder complete!" ``` -------------------------------- ### Create Global Commands Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Defines global commands accessible from any state. These commands cannot have options, conditions, or sub-states. Use for universal actions like 'cancel' or 'help'. A command without an explicit action automatically returns to the root state. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() // Global command to return to parent state sm.command('back') .action(({ state }) => state.parent) // Global command to cancel and return to root (with alias) sm.command('cancel', '取消') .action(() => {}) // Returns undefined = go to root state // Global command with no action returns to root state automatically sm.command('help') ``` -------------------------------- ### Set default command finder in JavaScript Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Configures a fallback function to route unrecognized input to specific commands based on input patterns. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() const EchoCommand = root.command('echo') .option('message') .action(function({ options }) { this.say('You said: %s', options.message) }) const CalcCommand = root.command('calc') .option('expression') .action(function({ options }) { try { // Simple evaluation (in production, use a safe parser) const result = eval(options.expression) this.say('Result: %s', result) } catch (e) { this.say('Invalid expression') } }) // Default finder - route unrecognized input root.default((input, flags) => { // If input looks like math, use calc if (/^[\d\s+\-*/().]+$/.test(input)) { return CalcCommand } // Otherwise echo it back return EchoCommand }) const chat = sm.chat('user1') await chat.input('hello world') // Output: "You said: hello world" await chat.input('2 + 3 * 4') // Output: "Result: 14" await chat.input('echo testing') // Output: "You said: testing" ``` -------------------------------- ### sm.command(...names) Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Defines a global command that can be invoked from any state. ```APIDOC ## sm.command(...names) Creates a global command that can be invoked from any state. Global commands cannot have options, conditions, or sub-states. Useful for commands like "cancel" or "help" that should always be available. ### Method `sm.command(name1, [name2, ...])` ### Parameters - **names** (string) - One or more names for the global command. ### Request Example ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() sm.command('back') .action(({ state }) => state.parent) sm.command('cancel', '取消') .action(() => {}) // Returns undefined = go to root state sm.command('help') ``` ### Response #### Success Response (200) - **StateMachine instance** - The state machine instance with the global command added. ``` -------------------------------- ### command.action(executor) Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Defines the action to be executed when a command is triggered, including the structure of the arguments passed to the executor. ```APIDOC ### command.action(executor): this - **executor** `function(arg: CommandArgument): TargetState` Either async or sync function to do real things for the command Executes the command and transitions to the target state. ```ts interface CommandArgument { // The options for the command options: object // The shadow copy of the flags of the current state flags: object // The runtime state which the state machine is currently at. state: RuntimeState } ``` ```ts interface RuntimeState { // The id of the current state get id: string // The parent state of the current state get parent: RuntimeState } ``` ```ts type TargetState = State // So that we can go back to a parent state | RuntimeState // If the command action returns undefined, // then the state machine will go the root state | undefined ``` Here is an example to show how to use `CommandArgument`: ```js someCommand.action(async function ({options, flags, state}) { try { await doSomethingWith(options) this.say('success') // If succeeded, back to the parent state return state.parent } catch (e) { this.say('fail, reason: %s', e.message) // Just stay on the current state return state } }) ``` ``` -------------------------------- ### Command API Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Define and manage commands within the state machine. ```APIDOC ## sm.command(...names) ### Description Creates a global command that can be invoked from any state. Global commands cannot define conditions, options, or sub-states directly within their definition. ### Method `POST` (conceptual, as it registers a command) ### Endpoint `/api/commands` (conceptual) ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **names** (Array) - Required - The name of the command and its aliases. ### Request Example ```javascript const helpCommand = sm.command('help', 'h', '?'); ``` ### Response #### Success Response (200) Returns the created Command object. #### Response Example ```json { "commandName": "help", "aliases": ["h", "?"], "isGlobal": true } ``` ``` ```APIDOC ## command.state(stateName) ### Description Defines a sub-state for a command. The `stateName` must be unique among the sub-states of the command. ### Method `POST` (conceptual, as it adds a state to a command) ### Endpoint `/api/commands/{commandName}/states` (conceptual) ### Parameters #### Path Parameters - **commandName** (string) - Required - The name of the command to add the state to. #### Query Parameters None #### Request Body - **stateName** (string) - Required - The name of the sub-state. ### Request Example ```javascript const someCommand = sm.command('someCommand'); const subState = someCommand.state('subStateName'); ``` ### Response #### Success Response (200) Returns the created State object. #### Response Example ```json { "stateName": "subStateName", "commandName": "someCommand" } ``` ``` ```APIDOC ## command.condition(condition) ### Description Adds a condition to a command. The command will only execute its `action` or `catch` if this condition function returns `true`. Supports both async and sync functions. ### Method `POST` (conceptual, as it adds a condition to a command) ### Endpoint `/api/commands/{commandName}/condition` (conceptual) ### Parameters #### Path Parameters - **commandName** (string) - Required - The name of the command to add the condition to. #### Query Parameters None #### Request Body - **condition** (function) - Required - A function that takes `flags` as an argument and returns a boolean. Can use `this.say()` to provide feedback. - **flags** (object) - A shadow copy of the key-value pairs of all flags defined in the current state. ### Request Example ```javascript someCommand.condition(function ({ enabled }) { if (!enabled) { this.say('This command is not enabled.'); } return enabled; }); ``` ### Response #### Success Response (200) Returns the Command object for chaining. #### Response Example ```json { "commandName": "someCommand", "conditionAdded": true } ``` ``` ```APIDOC ## command.option(name, config) ### Description Defines an option (argument) for a command, including its aliases, default value, and a setter function for validation and coercion. ### Method `POST` (conceptual, as it adds an option to a command) ### Endpoint `/api/commands/{commandName}/options` (conceptual) ### Parameters #### Path Parameters - **commandName** (string) - Required - The name of the command to add the option to. #### Query Parameters None #### Request Body - **name** (string) - Required - The name of the option. - **config** (object) - Optional - Configuration for the option. - **alias** (string | Array) - Aliases for the option. - **default** (function | any) - The default value or a function to compute it. - **set** (function) - A setter function to validate and coerce the option's value. Can throw errors for validation failures. ### Request Example ```javascript someCommand.option('userId', { alias: 'u', default: (key, flags) => flags.defaultUserId, set: (value, key, flags) => { if (value.length < 5) { throw new Error('User ID must be at least 5 characters long.'); } return value; } }); ``` ### Response #### Success Response (200) Returns the Command object for chaining. #### Response Example ```json { "commandName": "someCommand", "optionAdded": { "name": "userId", "alias": "u" } } ``` ``` -------------------------------- ### Configure SimpleMemorySyncer for single-instance Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Provides in-memory locking for single-server deployments without external dependencies. ```javascript const { StateMachine, SimpleMemorySyncer } = require('bot-state-machine') // Explicit memory syncer configuration const sm = new StateMachine({ syncer: new SimpleMemorySyncer({ lockExpire: 3000 // Lock expires after 3 seconds }) }) const root = sm.rootState() root.command('process') .action(async function() { this.say('Processing...') await new Promise(resolve => setTimeout(resolve, 1000)) this.say('Done!') }) // Both chats for same user share locking const chat1 = sm.chat('user-bob') const chat2 = sm.chat('user-bob') // First request acquires lock const promise1 = chat1.input('process') // Second request fails - lock already held setTimeout(async () => { try { await chat2.input('process') } catch (err) { console.log(err.code) // Output: "LOCK_FAIL" } }, 100) await promise1 // Output: "Processing...\nDone!" ``` -------------------------------- ### Process User Input with chat.input() Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Receives user messages and processes them through the state machine. Returns a Promise resolving to the bot's output. Supports various input formats for commands and options, and includes error handling for unknown commands or lock failures. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.command('buy') .option('stock') .option('quantity', { default: 1 }) .action(function({ options }) { this.say('Buying %d shares of %s', options.quantity, options.stock) }) const chat = sm.chat('trader-alice') // Various input formats supported const output1 = await chat.input('buy TSLA') console.log(output1) // Output: "Buying 1 shares of TSLA" const output2 = await chat.input('buy stock=AAPL quantity=10') console.log(output2) // Output: "Buying 10 shares of AAPL" // Error handling try { await chat.input('unknown-command') } catch (err) { console.log(err.code) // Output: "UNKNOWN_COMMAND" console.log(err.output) // Any partial output before error } ``` -------------------------------- ### command.option(name, config) Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Defines a positional or named option for a command, supporting aliases, default values, and custom validation/coercion logic. ```APIDOC ## command.option(name, config) ### Description Defines a positional or named option for a command. Supports aliases, default values, and setter functions for validation and type coercion. ### Parameters - **name** (string) - Required - The name of the option. - **config** (object) - Required - Configuration object containing: - **alias** (array) - Optional - Alternative names for the option. - **default** (any) - Optional - Default value if not provided. - **set** (function) - Optional - Setter function for validation and type coercion. ``` -------------------------------- ### Create Chat Session with Context Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Initiates a new conversation session for a specific user, identified by distinctId. This ensures isolated state and locking per user. Custom context and command restrictions can be applied. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.command('greet') .option('name') .action(function({ options }) { this.say('Hello, %s!', options.name) // Access custom context if (this.context && this.context.language === 'es') { this.say('Hola, %s!', options.name) } }) // Create chat session with context const chat = sm.chat('user-bob', { context: { language: 'es', userId: 'bob' }, commands: ['greet'] // Restrict available commands }) const output = await chat.input('greet John') console.log(output) // Output: "Hello, John!\nHola, John!" ``` -------------------------------- ### Define command execution conditions Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Uses condition() to guard command execution based on state flags, returning false to block or true to proceed. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.flag('tradingEnabled', false) root.flag('balance', 1000) root.command('enable-trading') .action(function() { this.setFlag('tradingEnabled', true) this.say('Trading enabled') }) root.command('buy') .option('stock') .option('amount', { default: 100 }) .condition(function({ tradingEnabled, balance }) { if (!tradingEnabled) { this.say('Error: Trading is disabled. Run "enable-trading" first.') return false } if (balance < 100) { this.say('Error: Insufficient balance') return false } return true }) .action(function({ options }) { this.say('Purchased %s for $%d', options.stock, options.amount) }) const chat = sm.chat('user1') console.log(await chat.input('buy TSLA')) // Output: "Error: Trading is disabled. Run "enable-trading" first." await chat.input('enable-trading') console.log(await chat.input('buy TSLA')) // Output: "Purchased TSLA for $100" ``` -------------------------------- ### Define Command Condition Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Use a standard function instead of an arrow function when access to 'this.say' is required within a condition check. ```js // Pay attention that we could not use an arrow function here if we need to use `this.say` someCommand.condition(function ({enabled}) { if (!enable) { this.say('not enabled') } return enabled }) ``` -------------------------------- ### command.condition(conditionFn) Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Defines a condition that must be satisfied before the command action executes, allowing for state-based access control. ```APIDOC ## command.condition(conditionFn) ### Description Defines a condition that must be satisfied before the command action executes. Returns false to skip execution, true to proceed. Has access to state flags and can provide feedback via this.say(). ### Parameters - **conditionFn** (function) - Required - A function that receives state flags and returns a boolean. ``` -------------------------------- ### Define command error handler with .catch() Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Use .catch() to define an error handler for a command's action. The handler receives the error and can return a state to retry or throw to propagate a COMMAND_ERROR. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() async function placeOrder(stock) { // Simulate API call that might fail if (stock === 'INVALID') { throw new Error('Stock not found') } return { orderId: '12345', stock } } const TradeCommand = root.command('trade') .option('stock') .action(async function({ options }) { const result = await placeOrder(options.stock) this.say('Order placed: %s', result.orderId) return StateOrderDetails }) .catch(function(err, { options, state }) { this.say('Trade failed: %s', err.message) this.say('Stock: %s', options.stock) // Return to current state to retry return state }) const StateOrderDetails = TradeCommand.state('order-details') StateOrderDetails.command('done') .action(() => undefined) // Return to root const chat = sm.chat('user1') console.log(await chat.input('trade INVALID')) // Output: "Trade failed: Stock not found\nStock: INVALID" console.log(await chat.input('trade TSLA')) // Output: "Order placed: 12345" ``` -------------------------------- ### State Definitions Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Methods for defining states, including flags, commands, and default command finders. ```APIDOC ## State ### state.flag(key, defaultValue, onchange): this - **key** `string` the name of the key - **defaultValue** `any` the default value of the flag - **onchange** `function(newValue, oldValue)` invokes if the value of the flag is changed. Defines a flag. ### state.command(...names): Command Defines a command which is only available at the current state. ### state.default(defaultFinder): this - **defaultFinder** `function(input: str, flags: object): Command | undefined` async or sync function which will be executed if there is no matched command for the given input, and whose return value will be the command to use Defines a finder function to find the default command. ```js const Hello = state .command('hello') .option('name') .action(function ({options}) { this.say(`hello ${options.name}`) }) state.default(() => Hello) ``` input | output | comments ---- | ---- | ---- `hello world` | `'hello world'` `world` | `'hello world'` | `Hello` is the default command ``` -------------------------------- ### Define command options with validation Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Configures positional or named options with aliases, default values, and custom setter functions for type coercion. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.command('trade') .option('stock', { alias: ['ticker', 's'] // Alternative names for the option }) .option('quantity', { default: 100, set(value, key, flags) { const num = Number(value) if (Number.isNaN(num) || num <= 0) { throw new TypeError(`Invalid quantity: ${value}`) } return num } }) .option('side', { default: 'buy', set(value) { if (!['buy', 'sell'].includes(value)) { throw new Error('Side must be buy or sell') } return value } }) .action(function({ options }) { this.say('%s %d shares of %s', options.side, options.quantity, options.stock) }) const chat = sm.chat('user1') await chat.input('trade TSLA') // Output: "buy 100 shares of TSLA" await chat.input('trade ticker=AAPL quantity=50 side=sell') // Output: "sell 50 shares of AAPL" await chat.input('trade s=GOOG 25') // Output: "buy 25 shares of GOOG" ``` -------------------------------- ### sm.chat(distinctId, options) Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Creates a new conversation session for a specific user, ensuring isolated state and single-threaded execution per user. ```APIDOC ## sm.chat(distinctId, options) Creates a new conversation session for a specific user. Each user identified by distinctId has isolated state and locking, allowing the bot to serve multiple users simultaneously while ensuring single-threaded execution per user. ### Method `sm.chat(distinctId, options)` ### Parameters #### Path Parameters - **distinctId** (string) - Required - A unique identifier for the user. #### Query Parameters - **context** (object) - Optional - Custom context object to be available within the chat session. - **commands** (array of strings) - Optional - An array of command names to restrict available commands for this chat session. ``` -------------------------------- ### Custom Syncer Interface Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md The interface definition required to implement a custom synchronizer. ```APIDOC ## Syncer Interface ### Description To implement a custom syncer, the class must implement four specific methods: read, lock, refreshLock, and unlock. ### Methods - **read(arg: ReaderArg)**: Reads the store from storage while verifying lock ownership. - **lock(arg: SyncerArg)**: Acquires the lock and updates the storage. - **refreshLock(arg: RefresherArg)**: Refreshes the expiration time of the lock. - **unlock(arg: SyncerArg)**: Releases the lock and updates the store. ``` -------------------------------- ### State API Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Manage states within the state machine. ```APIDOC ## sm.rootState() ### Description Creates a root state for the state machine. ### Method `POST` (conceptual, as it creates a root state) ### Endpoint `/api/states/root` (conceptual) ### Parameters None ### Request Example ```javascript const root = sm.rootState(); ``` ### Response #### Success Response (200) Returns the created root State object. #### Response Example ```json { "stateName": "root", "isRoot": true } ``` ``` -------------------------------- ### Implement Redis distributed locking Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Uses Redis to synchronize task execution across multiple bot instances, preventing concurrent command execution for the same user. ```javascript const Redis = require('ioredis') const { StateMachine, RedisSyncer } = require('bot-state-machine') const redis = new Redis({ host: '127.0.0.1', port: 6379, password: 'your-password' }) const sm = new StateMachine({ syncer: new RedisSyncer(redis, { lockExpire: 5000 // Lock expires after 5 seconds }), lockRefreshInterval: 1000, // Refresh lock every second actionTimeout: 10000 // Action timeout 10 seconds }) const root = sm.rootState() root.command('long-task') .action(async function({ distinctId }) { this.say('Starting long task for %s...', distinctId) // Simulate long-running operation await new Promise(resolve => setTimeout(resolve, 3000)) this.say('Task completed!') }) // Instance 1 const chat1 = sm.chat('user-alice') // Instance 2 (different server) const chat2 = sm.chat('user-alice') // First request gets the lock chat1.input('long-task') .then(output => console.log('Chat1:', output)) .catch(err => console.log('Chat1 error:', err.code)) // Second request fails because lock is held setTimeout(() => { chat2.input('long-task') .then(output => console.log('Chat2:', output)) .catch(err => console.log('Chat2 error:', err.code)) // Output: "Chat2 error: NOT_OWN_LOCK" }, 100) ``` -------------------------------- ### Define persistent state flags with .flag() Source: https://context7.com/kaelzhang/bot-state-machine/llms.txt Use .flag() to define state-specific variables that persist during a conversation. Flags support default values and optional change listeners. ```javascript const { StateMachine } = require('bot-state-machine') const sm = new StateMachine() const root = sm.rootState() root.flag('volume', 50, function(newValue, oldValue) { this.say('Volume changed from %d to %d', oldValue, newValue) }) root.flag('muted', false, function(muted) { if (muted) { this.say('Audio muted') } else { this.say('Audio unmuted') } }) root.command('volume-up') .condition(function({ muted }) { if (muted) { this.say('Cannot change volume while muted') return false } return true }) .action(function({ flags }) { const newVolume = Math.min(100, flags.volume + 10) this.setFlag('volume', newVolume) }) root.command('mute') .action(function() { this.setFlag('muted', true) }) const chat = sm.chat('user1') await chat.input('volume-up') // Output: "Volume changed from 50 to 60" await chat.input('mute') // Output: "Audio muted" await chat.input('volume-up') // Output: "Cannot change volume while muted" ``` -------------------------------- ### command.catch(onError) Source: https://github.com/kaelzhang/bot-state-machine/blob/master/README.md Handles errors thrown by the command's action executor, providing a mechanism for error recovery or logging. ```APIDOC ### command.catch(onError): this - **onError** `function(err: Error, arg: CommandArgument): TargetState` - **err** `Error` the error thrown by command action - **arg** the same as the argument of the action executor If the command `action` throws an error, then `onError` will be invoked. If `onError` throws an error, it will result in a `COMMAND_ERROR` error, and stay on the current state. ```