Skip to content

Commit

Permalink
docs: Add documentation for Xstate Machine Persistence plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Mar 7, 2024
1 parent 31c8772 commit 782e8d9
Show file tree
Hide file tree
Showing 11 changed files with 358 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'

/**
* @class MachineStateInfoEntity
* Represents a machine state. It allows to continue a machine at a later point in time at the point it was left of
*
* @param {string} instanceId - The instance ID of the machine state.
* @param {string} [sessionId] - The session ID of the machine state. (optional)
* @param {string} machineName - The name of the machine.
* @param {string} [latestStateName] - The name of the latest state. (optional)
* @param {string} latestEventType - The type of the latest event.
* @param {string} state - The current state of the machine.
* @param {Date} createdAt - The date and time when the machine state was created.
* @param {Date} updatedAt - The date and time when the machine state was last updated.
* @param {number} updatedCount - The number of times the machine state has been updated.
* @param {Date} [expiresAt] - The date and time when the machine state expires. (optional)
* @param {Date} [completedAt] - The date and time when the machine state was completed. (optional)
* @param {string} [tenantId] - The ID of the tenant associated with the machine state. (optional)
*/
@Entity('MachineStateInfoEntity')
export class MachineStateInfoEntity extends BaseEntity {
@PrimaryColumn({ name: 'instance_id', type: 'varchar', nullable: false })
Expand Down
47 changes: 47 additions & 0 deletions packages/data-store/src/machineState/IAbstractMachineStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,58 @@ import {
StoreMachineStateGetArgs,
} from '../types'

/**
* Represents an abstract class for storing machine states.
* This class provides methods for persisting, retrieving, and deleting machine states.
*
* @interface
*/
export abstract class IAbstractMachineStateStore {
/**
* Persists the machine state.
*
* @param {StoreMachineStatePersistArgs} state - The object containing the machine state to persist.
* @return {Promise<StoreMachineStateInfo>} - A Promise that resolves to the information about the persisted machine state.
*/
abstract persistMachineState(state: StoreMachineStatePersistArgs): Promise<StoreMachineStateInfo>

/**
* Finds active machine states based on the given arguments.
*
* @param {StoreMachineStatesFindActiveArgs} args - The arguments for finding active machine states.
* @return {Promise<Array<StoreMachineStateInfo>>} - A promise that resolves with an array of active machine states.
*/
abstract findActiveMachineStates(args: StoreMachineStatesFindActiveArgs): Promise<Array<StoreMachineStateInfo>>

/**
* Retrieves the state of a particular machine.
*
* @param {StoreMachineStateGetArgs} args - The arguments for retrieving the machine state.
* @returns {Promise<StoreMachineStateInfo>} - A promise that resolves to the machine state information.
*/
abstract getMachineState(args: StoreMachineStateGetArgs): Promise<StoreMachineStateInfo>

/**
* Finds the machine states based on the given arguments.
*
* @param {StoreFindMachineStatesArgs} [args] - The arguments to filter the machine states.
* @returns {Promise<Array<StoreMachineStateInfo>>} - A promise that resolves to an array of machine state information.
*/
abstract findMachineStates(args?: StoreFindMachineStatesArgs): Promise<Array<StoreMachineStateInfo>>

/**
* Deletes a machine state.
*
* @param {StoreMachineStateDeleteArgs} args - The arguments for deleting the machine state.
* @return {Promise<boolean>} - A promise that resolves to a boolean indicating if the machine state was successfully deleted or not.
*/
abstract deleteMachineState(args: StoreMachineStateDeleteArgs): Promise<boolean>

/**
* Deletes expired machine states from the database.
*
* @param {StoreMachineStateDeleteExpiredArgs} args - The arguments for deleting expired machine states.
* @return {Promise<number>} - A promise that resolves to the number of deleted machine states.
*/
abstract deleteExpiredMachineStates(args: StoreMachineStateDeleteExpiredArgs): Promise<number>
}
3 changes: 3 additions & 0 deletions packages/data-store/src/machineState/MachineStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { IAbstractMachineStateStore } from './IAbstractMachineStateStore'

const debug = Debug('sphereon:ssi-sdk:machine-state:store')

/**
* Represents a data store for managing machine states.
*/
export class MachineStateStore extends IAbstractMachineStateStore {
private readonly _dbConnection: OrPromise<DataSource>

Expand Down
168 changes: 127 additions & 41 deletions packages/xstate-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,180 @@
<h1 align="center">
<br>
<a href="https://www.sphereon.com"><img src="https://sphereon.com/content/themes/sphereon/assets/img/logo.svg" alt="Sphereon" width="400"></a>
<br>XState Persistence (Typescript)
<br>XState Machine Persistence
<br>Allows to continue xstate machine at a later point in time

<br>
</h1>

---

**Warning: This package still is in very early development. Breaking changes without notice will happen at this point!**

---

A Veramo contact manager plugin. This plugin manages xstate and identity configurations to third parties and persists them. These configurations can then be used to establish a connection.
The XState Persistence Plugin is designed to manage and persist XState machine states, allowing for durable, long-term
storage of state machines.
This enables applications to save, load, and delete state machine instances, facilitating seamless state management and
recovery across sessions.

The XState Persistence Plugin for Veramo is designed to manage and persist XState machine states, allowing for durable, long-term storage of state machine snapshots. This enables applications to save, load, and delete state machine instances, facilitating seamless state management and recovery across sessions.
Features:

- Load State: Retrieve the current state of an XState machine from persistent storage.
- Delete Expired States: Automatically remove state instances that have exceeded their lifespan, ensuring efficient use of storage.
- Persist Machine Snapshot: Save snapshots of XState machine states, allowing for recovery and resumption of stateful processes.
- Delete Expired States: Automatically remove state instances that have exceeded their lifespan, or are finished
ensuring efficient use of storage.
- Persist Machine Snapshot: Save snapshots of XState machine states, allowing for recovery and resumption of stateful
processes. This can be done manually or it can be automatically registered with any xstate interpreter instance

Installation

To add the XState Persistence Plugin to your project, run:

```shell
yarn add @sphereon/xstate-persistence-plugin
yarn add @sphereon/ssi-sdk.xstate-machine-persistence
```

Or if you prefer using npm:

```shell
npm install @sphereon/xstate-persistence-plugin
npm install @sphereon/ssi-sdk.xstate-machine-persistence
```

Usage
# Usage

Configuring the Plugin with Veramo

First, ensure you have Veramo set up in your project. Then, integrate the XState Persistence Plugin as follows:

```typescript
import { createAgent } from '@veramo/core'
import { XStatePersistencePlugin } from '@sphereon/xstate-persistence-plugin'
import { MachineStatePersistence, DataStoreMachineStateMigrations, DataStoreMachineStateEntities } from '@sphereon/ssi-sdk.xstate-machine-persistence'

const dbConnection = await new DataSource({
type: 'sqlite',
database: ':memory:',
logging: 'all',
migrationsRun: false,
migrations: DataStoreMachineStateMigrations, // Database migrations for the data store, specific for state machines
synchronize: false,
entities: DataStoreMachineStateEntities, // All the entities needed for the data store, related to state machines
}).initialize()

const agent = createAgent({
plugins: [
new XStatePersistencePlugin({
// Plugin options here
new MachineStatePersistence({
eventTypes: ['EVERY'], // Enables listening to 'EVERY' events to persist the state on every state change
store: new MachineStateStore(dbConnection),
}),
],
})
```

Persisting a Machine Snapshot
## Automatic registration of state change persistence

To save the current state of an XState machine:
You can use a simple method on an Xstate machine interpreter to automatically persist the latest state on every state
change of the machine, allowing for later continuation of the machine.

```typescript
await agent.persistMachineSnapshot({
stateId: 'your-state-instanceId',
type: 'YourmachineId',
eventName: 'YOUR_EVENT_NAME',
state: 'serialized-state-here', // Your XState machine state serialized as a string
expiresAt: new Date('2023-01-01'), // Optional expiration date
import { createMachine, interpret } from 'xstate'
import { machineStatePersistRegistration } from '@sphereon/ssi-sdk.xstate-machine-persistence'

const context = { ...agent.context, agent }
export const exampleMachine = createMachine({
predictableActionArguments: true,
id: 'example',
context: {},
initial: 'init',
states: {
init: {
id: 'init',
on: {
finalize: {
target: 'final',
},
},
},
final: {
id: 'final',
type: 'final',
},
},
})

const instance = interpret(exampleMachine).start()

/**
* - instance is the Xstate Machine interpreter instance
* - context is the agent context
* - machineName is optional. It will be deduced from the machine if not provided. If you use a different name, be sure to use that for any future methods as well
* - instanceId is optional. Allows you to provide your own unique Id. If not provided a random uuid will be generated
*/
const registration = await machineStatePersistRegistration({ instance, context, machineName: exampleMachine.id })
console.log(JSON.stringify(registration))
/**
* {
* "machineName": "example",
* "instanceId": "585b72e3-0655-4aee-a575-1234873ea7b0",
* "createdAt": "2024-03-07T22:47:45.445Z"
* }
*/

// That is all. From this point on the machine will persist the state on every state change. You can use the instanceId value if you want to do anything with the persisted object at a later point in time
```

Loading a State
## Retrieving machine state info.

You can retrieve machine state info in 2 ways. If you know the instanceId, you can directly get it. Otherwise you can query for the active, read not finalized or cleaned up, instances of machine states.

To load the latest snapshot of a specific machine type:
Getting a single machine state info object by instance id:

```typescript
const state = await agent.loadState({
type: 'YourmachineId',
})
const machineStateInfo = await agent.machineStateGet({ instanceId })
console.log(JSON.stringify(machineStateInfo, null, 2))
/**
* {
* "instanceId": "585b72e3-0655-4aee-a575-1234873ea7b0",
* "sessionId": "x:1", <=== The xtsate session id. Please note that this is only unique for a single xstate instance in memory and will be lost accross restarts
* "machineName": "example",
* "latestStateName": "init", <=== The latest state of the xstate machine for easy access
* "latestEventType": "increment", <=== The latest event of the xstate machine for easy access
* "state": { <=== This is the actual Xstate state
* "actions": [],
* "activities": {},
* "meta": {},
* ....
* },
* "createdAt": "2024-03-07T23:00:11.438Z",
* "updatedAt": "2024-03-07T23:00:11.543Z",
* "updatedCount": 1, <=== The amount of updates applied to the persisted state (the amount of events/statechanges)
* "expiresAt": null, <=== If this date-time is set, the machine state will not be used anymore
* "completedAt": null <=== The date-time the instance reached a final state
* }
*/
```

Deleting Expired States
Getting active machine state info objects by machine name:

To clean up expired states from the storage:
```typescript
const machineStateInfos = await agent.machineStatesFindActive({ machineName: 'example' })
console.log(JSON.stringify(machineStateInfos[0], null, 2))
// See console.log example in previous code block
```

````typescript
await agent.deleteExpiredMachineStates({
type: 'YourmachineId', // Optional: Specify the machine type to narrow down the deletion
});```
## Manual persistence and cleanup methods

Contributing
Persisting a Machine Snapshot

Contributions are welcome! Please open an issue or submit a pull request for any bugs, features, or improvements.
License
To save the current state of an XState machine:

This project is licensed under the MIT License.
````
```typescript
await agent.machineStatePersist({
instanceId: 'your-state-instanceId',
state: 'You Xstate here',
})
```

Deleting Expired States

To clean up expired states from the storage:

```typescript
await agent.machineStatesDeleteExpired({
deleteDoneStates: false, // Optional: If true, will delete any states that are completed. If false, will delete any expired states, no matter whether they are done or not
machineName: 'example', // Optional: Only delete istances for machines named 'example'
})
```
4 changes: 2 additions & 2 deletions packages/xstate-persistence/agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ dbConnection:
synchronize: false
migrationsRun: true
migrations:
$require: './packages/data-store?t=object#DataStoreMigrations'
$require: './packages/data-store?t=object#DataStoreMachineStateMigrations'
entities:
$require: './packages/data-store?t=object#DataStoreMachineStateInfoEntities'
$require: './packages/data-store?t=object#DataStoreMachineStateEntities'

server:
baseUrl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ export const counterMachine = createMachine({
on: {
increment: {
actions: assign({
count: (context) => {
console.log(context.count + 1)
return context.count + 1
},
count: (context) => context.count + 1,
}),
},
finalize: {
Expand Down Expand Up @@ -137,6 +134,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro

it('should automatically store xstate state changes', async (): Promise<void> => {
const init = await machineStatePersistRegistration({ context, instance, machineName: instance.machine.id })
console.log(JSON.stringify(init, null, 2))
if (!init) {
return Promise.reject(new Error('No init'))
}
Expand All @@ -156,6 +154,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
expect(activeStates[0].createdAt).toBeDefined()
expect(activeStates[0].state).toBeDefined()
expect(activeStates[0].state.context.count).toEqual(1)
console.log(JSON.stringify(activeStates[0], null, 2))

instance.send('increment')
// Wait some time since events are async
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ export class MachineStatePersistence implements IAgentPlugin {
}

private async machineStateInit(args: InitMachineStateArgs): Promise<MachineStateInit> {
debug(`machineStateInit for machine name ${args.machineName} and tenant ${args.tenantId}`)
const machineInit = {
...args,
const { tenantId, machineName, expiresAt } = args
debug(`machineStateInit for machine name ${machineName} and tenant ${tenantId}`)
const machineInit: MachineStateInit = {
machineName,
tenantId,
expiresAt,
instanceId: args.instanceId ?? uuidv4(),
createdAt: args.createdAt ?? new Date(),
}
Expand Down
Loading

0 comments on commit 782e8d9

Please sign in to comment.