Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mikesparr/redis-workflow
Simple Promise based multi-channel workflow rules engine with Redis backing using EventEmitter
https://github.com/mikesparr/redis-workflow
flow process-manager pubsub redis rules workflow
Last synced: about 1 month ago
JSON representation
Simple Promise based multi-channel workflow rules engine with Redis backing using EventEmitter
- Host: GitHub
- URL: https://github.com/mikesparr/redis-workflow
- Owner: mikesparr
- License: mit
- Created: 2018-05-15T16:28:38.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2018-11-29T21:54:56.000Z (about 6 years ago)
- Last Synced: 2024-12-21T19:02:31.823Z (about 2 months ago)
- Topics: flow, process-manager, pubsub, redis, rules, workflow
- Language: TypeScript
- Homepage:
- Size: 133 KB
- Stars: 5
- Watchers: 2
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Redis Workflow
Dynamic rules engine to allow configurable workflow in app without requiring code changes.
Using Redis as backing service, you can design workflows and whenever you `start` your workflow,
it will load your stored workflow(s) then attach a `pubsub` listener to Redis. Any time your
`pubsub` channel message appears, it will parse the event object, perform conditional logic, and if true
emit one or more defined actions.This app is loosely-based on popular enterprise systems workflow rules or business process features.
* Trigger (or event) - something happened
* Conditions (or filters) - criteria to take action
* Actions (or tasks) - what to do# Requirements
You must have Redis server running. This app treats redis like a persistent backup. It uses `pubsub` as a listener
for trigger events. It uses it to save and load workflows to avoid loss. The `set` and `get` workflows methods
just interact with memory and not database. See the `IWorkflowManager` interface source for more.# Extensibility
Most non-public properties and methods are `protected` so you may sub-class and override. `RedisWorkflowManager` is my
preferred implementation but you could also write a new manager class that implements `IWorkflowManager` interface.# Installation
```bash
npm install redis-workflow
yarn add redis-workflow
```# Test
The test script in `package.json` preprocesses the `.ts` file and then executes.`npm test` (also can run `npm run coverage`)
# Usage
The source was written in Typescript, yet it compiles to Javascript (`npm run build`). You can use in ES5 or later supported environments. The following code snippets are implemented in the `__tests__` folder.## Quick start (Node)
```javascript
const redis = require("redis");
const flow = require("redis-workflow");// instantiate
const config = flow.RedisConfig("localhost", 6379, null, null);
const client = redis.createClient(); // optionally pass into manager 2nd param to share
const manager = new flow.RedisWorkflowManager(config);// create first workflow
const trigger = new flow.Trigger("myTrigger");
const rule = new flow.Rule("myRule", "foo == bar");
const action = new flow.ImmediateAction("myAction");
const workflow = new flow.Workflow("myWorkflow", trigger, [rule], [action]);// add workflow to manager
manager.setWorkflows({"myChannel": [workflow]});// add listener for action
manager.on("myAction", (action) => {
// perform something here
console.log({action}); // should see after pubsub event below
});// add error handler
manager.on("error", (error) => {
// handle errors here
console.error(`An error occurred: `, error);
})// start workflow engine
manager.start("myChannel");// publish a test object to the pubsub channel
setTimeout(() => {
const event = {event: "myTrigger", context: {foo: "bar"}};
client.publish("myChannel", JSON.stringify(event), (err, reply) => {
console.log({err, reply});
});
}, 2500);// sometime later ...
setTimeout(() => {
manager.stop("myChannel"); // unsubscribe from pubsub and stop emitting actions
}, 5000);// note, if you publish an event and conditions do not == true, no action taken
```## Typescript
See more detailed examples in the respective `src/__tests__` and `src/lib/__tests__` folders.### Example with multiple rules and actions for a workflow
```typescript
import * as redis from "redis";
import * as flow from "redis-workflow"; // optionally import each class (see __tests__)const config: flow.RedisConfig = new flow.RedisConfig("localhost", 6379, null, null);
const manager: flow.IWorkflowManager = new flow.RedisWorkflowManager(config);
const publisher: redis.RedisClient = new redis.createClient(); // just for our example to pubsub message// build test workflow
const trigger: flow.ITrigger = new flow.Trigger("test.trigger101");
const rule1: flow.IRule = new flow.Rule("Foo should equal bar", `foo == "bar"`);
const rule2: flow.IRule = new flow.Rule("Should be in stock", "inStock > 0");
const action1: flow.IAction = new flow.DelayedAction("shipProduct").delay(5, "days"); // context added later when triggered
const action2: flow.IAction = new flow.ImmediateAction("adjustInventory");
const workflow: flow.IWorkflow = new flow.Workflow("test.workflow1", trigger, [rule1, rule2], [action1, action2]);// add first workflow to manager
manager.setWorkflows({"myChannel": [workflow]});// errors
manager.on(WorkflowEvents.Error, (error) => {
console.error(`Something bad happened: `, error);
})// delayed actions
manager.on(WorkflowEvents.Schedule, (action) => {
// handle delayed actions how you like (use your own scheduler)
console.log(`Delayed action received!`);switch (action.getName()) {
case "shipProduct":
// schedule with fulfillment
break;
default:
// add to cron
break;
}
});// immediate actions
manager.on(WorkflowEvents.Immediate, (action) => {
// handle immediate actions
console.log(`Immediate action received!`);switch (action.getName()) {
case "adjustInventory":
// task inventory management
break;
default:
// global handler or notification
break;
}
});// optionally handle actions by name
manager.on("adjustInventory", (action) => {
// do something here
const item: string = action.getContext().foo;
const inStock: number = action.getContext().inStock;
console.log(`Adjusting inventory for '${item}' from ${inStock} to ${inStock - 1}`);
});// optionally handle global actions (all types)
manager.on(WorkflowEvents.Audit, (action) => {
// publish action to stream pipeline
});// optionally handle actions that didn't meet rules
manager.on(WorkflowEvents.Invalid, (message) => {
// do something
switch (message.event) {
case "newOrder":
// notify cust svc to try and complete order
break;
default:
// log somewhere
break;
}
});// start manager (subscribes to pubsub channel)
manager.start("babyDivision")
.then(() => {
// do something if you like
});// build and publish trigger events to Redis pubsub channel
const message: {[key: string]: any} = {
context: {
foo: "bar",
inStock: 3,
},
event: "test.trigger101",
};// simulate time after manager starts before triggers appear
setTimeout(() => {
publisher.publish("babyDivision", JSON.stringify(message), (err: Error, reply: any) => {
// simulate time later shutting down workflow channel
setTimeout(() => {
manager.stop("babyDivision");
}, 3000);
});
}, 3000);
```## Triggers
Uses Redis `pubsub` listening for events to start workflow.```typescript
const trigger: ITrigger = new Trigger("test");
```### Payload
The `message` published to the topic will include a stringified JSON object as follows.```javascript
{
"event": "test", // must equal Trigger name
"context": {"myField": "myValue", "anotherField": "anotherValue"}
}
```## Conditions
Uses `mozjexl` Javascript expression language to evaluate string expressions, evaluating to `true` or `false`.```typescript
const rule: IRule = new Rule("Field must be valid", `myField == "myValue"`);
```## Actions
You define actions when building workflows. The action name will become an `EventEmitter` event you handle.```typescript
// optionally named
manager.on("eventName", (action) => {
// handle action here
});manager.on(flow.WorkflowEvents.Schedule, (action) => {
// handle action here
});manager.on(flow.WorkflowEvents.Immediate, (action) => {
// handle action here
});
```For each `Action` you add to your workflow, you one or more listeners like above. You can decide what functionality
your application performs if conditions are met, and actions are emitted.### Suggested action types
* Create record(s)
* Update record(s)
* Trigger another workflow
* Send message(s) or notification(s)# Scaling
This implementation using `pubsub` can scale by leveraging different channels per instance, with a fanout. If you
want workers, I would override the `start` method to use Redis `blrpoplpush` (blocking pop of list) and publish
trigger events to it instead. This way you can spin up unlimited workers and once one pops from list, others ignore.# Resilience
If you instantiate the manager and pass in the third optional argument `channels: string[]`, the app will attempt
to load workflows from the database.# Contributing
I haven't thought that far ahead yet. I needed this for my project and wanted to give back. ;-)# License
MIT (if you enhance it, fork and PR so the community benefits)