Code Monkey home page Code Monkey logo

zenaton-node's Introduction

⚠️ This repository is abandoned.


Build and run event-driven processes within the product journey in days instead of months.
ie. payment, booking, personalized communication sequences, ETL processes and more. Explore the docs »
Website · Examples in Node · Tutorial in Node

NPM Version CircleCI License

Note: This library can be used both as:

  • a sdk to interact with workflows from your Node.js app
  • a sdk to build workflows.

Zenaton library for Node.js

Zenaton helps developers to easily build asynchronous workflows.

Build workflows using Zenaton functions to build control flows around your business logic and tasks - managing time, events and external services within one class. Functions include 'wait for a specific time or event', 'react to external events', 'run parallel tasks, 'create schedules' and more all by writing one line of code. More about Zenaton Functions

Key capabilities:

  • Standalone Tasks - dispatch or schedule an asynchronous business job with just one line of code.
  • Workflows as code - Combine Zenaton functions and Node.js to create infinite possibilities of logic.
  • Real-time Monitoring - Get a real-time view of workers and tasks - scheduled, processing and executed.
  • Scheduler - Schedule recurrent tasks and workflows and automatically retry tasks that fail or get alerts when there are errors or timeouts.
  • Error Handling: - Alerts for errors and timeouts and retry, resume or kill processes. React to errors by writing logic into workflow code to trigger retries or other actions.

You can sign up for an account on Zenaton and go through the tutorial in Node.

Node Documentation

You can find all the details on Zenaton's website.

Requirements

Node 8 and later.

Table of contents

Getting started

Installation

Install the Zenaton Agent

To install the Zenaton agent, run the following command:

curl https://install.zenaton.com/ | sh

Install the library

To add the latest version of the library to your project, run the following command:

npm install zenaton --save

TypeScript typings

For Typescript developers:

npm install @types/zenaton --save-dev

Quick start

Client Initialization

To start, you need to initialize the client. To do this, you need your Application ID and API Token. You can find both on your Zenaton account.

Then, initialize your Zenaton client:

/* client.js */

const { Client } = require("zenaton");

module.exports = new Client(
  "<YourApplicationId>",
  "<YourApiToken>",
  "<YourApplicationEnv>", // Use "dev" as default
);

Boot file

The next step is to have your Zenaton Agent listen to your application.

The Agent needs to be pointed to a boot file which will allow it to figure out where tasks or workflows are located when the time comes to run them.

/* boot.js */

const { task, workflow } = require("zenaton");

// define tasks (example)
task("hello_world_task", require("./tasks/HelloWorldTask"));

// define workflows (example)
workflow("my_first_workflow", require("./workflows/MyFirstWorkflow"));

To run the listen command:

zenaton listen --app_id=<YourApplicationId> --api_token=<YourApiToken> --app_env=<YourApplicationEnv> --boot=boot.js

Executing a background job

A job in Zenaton is created through the task function.

Let's start by implementing a first task printing something, and returning a value:

/* tasks/HelloWorldTask.js */
module.exports.handle = async function(name = "World") {
  return `Hello ${name}`!;
};

Now, when you want to run this task as a background job, you need to do the following:

/* launchHelloWorldTask.js */
const { run } = require("./client.js");

run.task("hello_world_task", "Me");

That's all you need to get started. With this, you can run many background jobs. However, the real power of Zenaton is to be able to orchestrate these jobs. The next section will introduce you to job orchestration.

Orchestrating background jobs

Job orchestration is what allows you to write complex business workflows simply. You can execute jobs sequentially, in parallel, conditionally based on the result of a previous job, and you can even use loops to repeat some tasks.

We wrote about some use-cases of job orchestration, you can take a look at these articles to see how people use job orchestration.

Using workflows

A workflow in Zenaton is created through the workflow function.

We will implement a very simple workflow that will execute sequentially the hello_world_task 3 times.

One important thing to remember is that your workflow implementation must be idempotent. You can read more about that in our documentation.

The implementation looks like this:

/* workflows/MyFirstWorkflow.js */
module.exports.handle = function*(name) {
  yield this.run.task("hello_world_task", name);
  yield this.run.task("hello_world_task", "Me");
  yield this.run.task("hello_world_task", "All");
};

Now that your workflow is implemented, you can ask for its processing like this:

/* launchMyFirstWorkflow.js */
const { run } = require("./client.js");

run.workflow("my_first_workflow", "Gilles");

There are many more features usable in workflows to get the orchestration done right. You can learn more in our documentation.

Getting help

Need help? Feel free to contact us by chat on Zenaton.

Found a bug? You can open a GitHub issue.

zenaton-node's People

Contributors

antoinereyt avatar ciboulette avatar dependabot[bot] avatar geomagilles avatar jalric avatar louisgraffeuil avatar mryawe avatar pylebecq avatar strato avatar yjouffrault avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

zenaton-node's Issues

Error when defining custom methods in Tasks and Workflows

the following code

const { Task } = require("zenaton");

module.exports = Task("TaskA", {
  handle: async function() {
    console.log("Task A starts");

    await this.test(0);

    console.log("Task A ends");
    return 0;
  },
  test: async function(a) {
    if (0 <= a) {
      console.log("1");
    } else {
      console.log("2");
    }
  }
});

returns an error Error: "test" is defined more than once in "TaskA" task. Same with workflows.

It should work.

ModifiedDeciderException in Wait following upgrade to 0.5.7

Since version 0.5.7, probably because of #44 , workflows containing wait are now throwing a ModifiedDeciderError, even when the workflow was not modified.

To reproduce:

  1. Start a workflow using zenaton node lib 0.5.6, with a Wait of 5 minutes
  2. When the wait start unlisten
  3. Upgrade your zenaton node lib to 0.5.7
  4. Listen again
  5. When the Wait completes, the next decision executed will result in a ModifiedDeciderError.

You cannot wait and event without a timeout

This works:

yield this.wait.event("UserActivatedEvent").for(30);

BUT, If I do this:

yield this.wait.event("UserActivatedEvent");

I run into this:
Screen Shot 2019-10-25 at 16 42 38

The full stacktrace:

Error: "Wait {
	eventName: 'UserActivatedEvent',
	timestamp: null,
	duration: null
}" behind "yield" is not valid instruction - check Zenaton syntax
    at Branch._checkYieldValue (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Branch.js:114:23)
    at Branch.run (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Branch.js:79:12)
    at _callee$ (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Decider.js:40:33)
    at tryCatch (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:45:40)
    at Generator.invoke [as _invoke] (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:271:22)
    at Generator.prototype.(anonymous function) [as next] (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:97:21)
    at asyncGeneratorStep (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
    at _next (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7)

Workflow and Task with the same name does not work

When I define a workflow and a task having the same name, the execution crashes at some point. It seems like we're trying to run the task as if we are running a workflow so we don't provide the done() callback function to the task.

For example, I defined a Workflow named HelloZenaton:

var { Workflow } = require("zenaton");

var Task = require("../Tasks/LaunchHelloZenaton");

module.exports = Workflow("HelloZenaton", function() {
  console.log('HelloZenaton workflow starts');
  new Task().execute();
  console.log('HelloZenaton workflow ends');
});

I define the task LaunchHelloZenaton, that will in turn execute a task having the same name as the workflow:

var {Task} = require("zenaton");
var HelloZenaton = require("./HelloZenaton");

module.exports = Task("LaunchHelloZenaton", function(done) {
  console.log("LaunchHelloZenaton task starts");
  new HelloZenaton().execute();
  console.log("LaunchHelloZenaton task ends");
  done(null, "LaunchHelloZenaton");
});

And the last task definition, having the same name as the workflow:

var {Task} = require("zenaton");

module.exports = Task("HelloZenaton", function(done) {
  console.log("HelloZenaton task starts");
  console.log("HelloZenaton task ends")
  done(null, "HelloZenaton");
});

Now if you try running this workflow, for example using the following code:

require("./client");

var AsynchronousWorkflow = require("./Workflows/HelloZenaton");
new AsynchronousWorkflow().dispatch();

Then it crashes at some point. This is the content found in zenaton.err file:

/Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Worker/v1/Worker.js:29
throw e;
^

TypeError: done is not a function
at /Users/pierre-yves/dev/examples-node/Tasks/HelloZenaton.js:6:3
at /Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Worker/v1/Processor.js:45:28
at Array.forEach (<anonymous>)
at module.exports.processFromTask (/Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Worker/v1/Processor.js:42:11)
at module.exports.process (/Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Worker/v1/Processor.js:28:19)
at Engine.execute (/Users/pierre-yves/dev/examples-node/node_modules/zenaton/lib/v1/Engine/Engine.js:50:27)
at TaskClass.execute (/Users/pierre-yves/dev/examples-node/node_modules/zenaton/lib/v1/Tasks/AbstractTask.js:16:25)
at /Users/pierre-yves/dev/examples-node/Tasks/LaunchHelloZenaton.js:7:22
at Worker.process (/Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Worker/v1/Worker.js:19:17)
at Slave.processJob (/Users/pierre-yves/dev/core/worker/libraries/javascript/lib/Loader/Slave.js:59:51)

Correct the behaviour of Event Subclasses in the Node Library

As known for some times, the when subclassing and Event class, the child Event is not handled properly by Zenaton changes has to be done, in coordination between the engine, agent and client libraries.

For more details please check:

zenaton/rfcs#19

Ability to load ES6 imports

The library can't comprehend ES6 imports, which is a problem for users who have already switched to this kind of imports on their stack (with Meteor for example).

No Init check library side: provided variable or connection

No errors are triggered when I init Zenaton client like this

Zenaton.init(emptyVariable, null, '')

Checks can be implemented code client side like in zenaton/exemples-node but is more the concern of the library. If I omit to implement checks and provide empty environment variables, no errors are displayed until I launch a workflow.

Improvement proposal:
Some library like elasticsearch client provide method to check connection.
Can be useful when deploy in production.

var elasticsearch = require('elasticsearch');
var client = new elasticsearch.Client({
  host: 'localhost:9200',
  log: 'trace'
});

client.ping({
  requestTimeout: 30000,
}, function (error) {
  if (error) {
    console.error('elasticsearch cluster is down!');
  } else {
    console.log('All is well');
  }
});

You cannot wait and event without a timeout

This works:

yield this.wait.event("UserActivatedEvent").for(30);

BUT, If I do this:

yield this.wait.event("UserActivatedEvent");

I run into this:
Screen Shot 2019-10-25 at 16 42 38

The full stacktrace:

Error: "Wait {
	eventName: 'UserActivatedEvent',
	timestamp: null,
	duration: null
}" behind "yield" is not valid instruction - check Zenaton syntax
    at Branch._checkYieldValue (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Branch.js:114:23)
    at Branch.run (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Branch.js:79:12)
    at _callee$ (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/lib/Code/yield/Decider/Decider.js:40:33)
    at tryCatch (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:45:40)
    at Generator.invoke [as _invoke] (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:271:22)
    at Generator.prototype.(anonymous function) [as next] (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/regenerator-runtime/runtime.js:97:21)
    at asyncGeneratorStep (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
    at _next (/Users/antoine/.zenaton/lib/worker-0.8.3/priv/javascript/node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7)

Weekday and DayOfMonth methods should wait when the day provided is today

Expected Behavior:

per the discussion with @geomagilles:

if we are on Monday, 11am on the 23rd of the month. Then:

.Monday(1) should wait until next Monday at 11am
.Monday(1).At("9") should wait until next Monday at 9am
.Monday(1).At("13") should wait 2 hours only
.DayOfMonth(23) should wait for one month
.DayOfMonth(23).At("13") should wait for only 2 hours

Problem:

Currently, if today is a Monday, when calling new Wait().monday() you don't actually wait until the next Monday, and the wait terminates immediately.

We have the same issue with the dayOfMonth method. If it's the 23rd and you call new Wait().dayOfMonth(23) you won't wait until the next month, but instead the wait will terminate immediately.

Specifically, I believe the two problematic lines are these:

if (now.isAfter(then)) {

then = (d > day) ? then.add(n, 'weeks') : then.add(n - 1, 'weeks')

Fix:

See zenaton/zenaton-go#7 for the fix in the go library.

Use of `execute` method outside a workflow should thrown an error

Expected behavior of execute method does make sense only within a workflow.

To be clear, the test should be that the method is used within a workflow - NOT that it is processed by an agent. So locally executing a workflow is still perfectly fine.

A specific Zenaton error should be thrown and documented.

Missing binding of first argument of task defined as functions

The documentation states the following:

Note: when executing the provided function, this is binded to the first argument provided. Eg. in new SimpleTask({id: 4}), this is binded to {id: 4} when executing the task.

Considering the following example:

require("./client");

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const { Task, Workflow } = require("zenaton");

const saveInDb = Task('saveInDb', async (done) => {
    console.log(this);
    done(null, 0);
});

const workflow1 = Workflow("workflow1", {
    init(initData) {
        this.id = initData.id
        this.name = initData.name
        this.sandbox = initData.sandbox
        this.temporary = initData.temporary
    },
    handle() {
        new saveInDb(this).dispatch();
    }
});

if (process.env.LAUNCH) {
    new workflow1({
        id: 1,
        name: "Test",
        sandbox: true,
        temporary: true
    }).dispatch();
}

We would expect it to print

{id: 1, name: "Test", sandbox: true, temporary: true}

Actually, it prints

{}

To run this example easily, save it to a file issue-bind.js in the examples-node repository.
Then, you can run zenaton listen --boot issue-bind.js and to start the workflow, run LAUNCH=1 node issue-bind.js

Either the documentation is wrong, or the binding is not done correctly.

Note: using the init() method to define the task work as expected:

require("./client");

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const { Task, Workflow } = require("zenaton");

const saveInDb = Task('saveInDb', {
    init(data) {
        this.data = data
    },
    handle(done) {
        console.log(this);
        done(null, 0);
    }
});

const workflow1 = Workflow("workflow1", {
    init(initData) {
        this.id = initData.id
        this.name = initData.name
        this.sandbox = initData.sandbox
        this.temporary = initData.temporary
    },
    handle() {
        new saveInDb(this).dispatch();
    }
});

if (process.env.LAUNCH) {
    new workflow1({
        id: 1,
        name: "Test",
        sandbox: true,
        temporary: true
    }).dispatch();
}

This prints the following:

{ data: { id: 1, name: 'Test', sandbox: true, temporary: true } }

Event triggered 2 times

Play with this example : https://zenaton.com/tutorial/node/event
I've modified EventWorkflow to display the internal propery of my event

const { Workflow } = require("zenaton");
const TaskA = require("../Tasks/TaskA");
const TaskB = require("../Tasks/TaskB");
const TaskC = require("../Tasks/TaskC");
const TaskD = require("../Tasks/TaskD");

module.exports = Workflow("EventWorkflow", {
  init(id) {
    this.id = id;
    this.state = true;
  },
  async handle() {
    await new TaskA().execute();

    if (this.state) {
      await new TaskB().execute();
    } else {
      await new TaskC().execute();
    }
  },
  async onEvent(eventName, eventData) {
    if (eventName === "MyEvent") {
      this.state = false;
      console.log(new Date(), "What is foo : ", eventData.foo ? eventData.foo : "?")
      await new TaskD().execute();
    }
  },
  id() {
    return this.id;
  }
});

with the following launch_event.js

require("./client");

const uniqid = require("uniqid");
const EventWorkflow = require("./Workflows/EventWorkflow");

const id = uniqid();
new EventWorkflow(id).dispatch().catch(err => {
  console.error(err);
});
console.log("Event will be sent in 2s");
setTimeout(function() {
  EventWorkflow.whereId(id)
    .send("MyEvent", { foo: "bar"})
    .catch(err => {
      console.error(err);
    });
  console.log("Event sent");
}, 2000);

The output in the agent is :

2019-03-24T08:38:20.142Z 'Task A starts'
2019-03-24T08:38:21.706Z 'What is foo : ' 'bar'
2019-03-24T08:38:22.096Z 'Task D starts'
2019-03-24T08:38:24.101Z 'Task D ends'                      // 2000 correct
2019-03-24T08:38:24.148Z 'Task A ends'                     // 4000 correct
2019-03-24T08:38:24.600Z 'What is foo : ' 'bar'
2019-03-24T08:38:25.412Z 'Task C starts'
2019-03-24T08:38:27.418Z 'Task C ends'                     // 2000 correct

As you can see the console log within the event is called 2 times ....

node launch_event.js 
2019-03-24T08:38:19.297Z 'Event will be sent in 2s'
2019-03-24T08:38:21.307Z 'Event sent'

Useless dependency

The dotenv dependency seems not used by the library.
Environment variable must comes from client side, it's probably just code legacy to delete.

"dependencies": {
  "@babel/runtime": "7.3.1",
  "axios": "0.18.0",
  "dotenv": "6.2.0",
  "moment-timezone": "0.5.23"
}

Generate ID when scheduling things

When tasks and workflows are scheduled, we want to add a generated ID from the library.
This ID will be a canonical ID representing the user intent.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.