Electron is a NodeJS based system to enforce atomic transactions for multiple asynchronous interactions. Electron works to ensure that each async (or sync) stage of multi step operation is successful and if any of the stages fails it rolls backs automatically to the previous state of the application.
Lets break this down in plain english! You are building an API which needs to call the Google Maps API, Store the data in your database, and then finally process a users payment for their transaction. This operation has multiple steps and if any one of them fails your will be in a sticky situation as a developer! Can you imagine if you processed a users payment but your code failed to update the database saying that the user paid you so you never actually deliver the product to a user!
With electron we aim to solve this problem by implementing a fully transactional system which processes each stage (calling Google, updating db, processing payment) sequentially and if any one of the stages fails electron will automatically rollback so you never have a half updated database!
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
In order to run this software, Node JS is needed. You can download Node JS which comes pre bundled with NPM by following this link.
You can install this software via npm by using
npm install electron-tx --save
or by including the source files in your project.
The entry point to the library is the index.js
file.
For example: const Electron = require('my/project/path/electron/index.js');
Try out electron with a simple example!
const Electron = require('electron-tx');
const transaction = new Electron();
transaction.addStage('My First Stage!', {
up: () => {
console.log('Trying to do some async action here!');
},
down: () => {
console.log('Dont worry I can undo the async action!');
}
});
transaction.execute()
.then(() => {
console.log('The transaction was successful!');
})
.catch(err => {
console.log('There was some issue in one of the stages :(', err);
});
An electron transaction consists of a series of stages. Each stage should be completely independent of other operations in the application and should have all the data necessary to make decisions and perform its action to completion.
Stages take two parameters a unique name which identifies the stage and the stage configuration object.
A stage is defined using the addStage()
function and when the stage executes it calls the up()
function that is defined in the stage configuration.
Stages are run one at a time and will only proceed to the next stage if the previous stage succeeds. If any of the stages fails electron
will automatically iterate through all the previous stages and roll back the changes that were applied by using the down()
method. You can think
of the down()
function as the command + Z
of the operation!
It is up to the developer to provide the implementation for both applying a change (with up()
) and reverting a change (with down()
).
You can return any type of data to the up/down methods (including async actions and promises) as they will be wrapped in promises under the hood.
For example:
const stripe = require('stripe');
const AWS = require('aws-sdk');
const request = require('request-promise-native');
const Electron = require('electron-tx');
const transaction = new Electron();
transaction.addStage('Get Customers', {
up: () => {
return stripe.customers.find('cus_1234')
},
down: () => {
...
},
});
transaction.addStage('Update Database', {
up: () => {
// We can return promises to these
return AWS.DynamoDB.put(...).promise()
},
down: () => {
return AWS.DynamoDB.delete(...).promise()
}
});
transaction.addStage('Call API', {
up: () => {
return request.get(...)
},
down: () => {
return true; // Nothing to undo
}
});
transaction.execute();
Running transaction.execute()
will start executing all the stages one by one in the order in which they were added.
You may be wondering about interdependencies between stages and passing data between them. What happens when my second stage depends on data generated by my first stage? For this you will need to shift your mindset from a synchronous way of writing code to that of callbacks and Promises. The following example won't work and is the incorrect way to pass data between stages.
const stripe = require('stripe');
const AWS = require('aws-sdk');
const Electron = require('electron-tx');
const transaction = new Electron();
let customerId = null;
transaction.addStage('get_customers', {
up: (data) => {
console.log(data); // null (this is the first stage there is no data)
const customer = stripe.customers.find('cus_1234');
// This is bad and will not work as expected
customerId = customer.id;
return true;
},
down: () => ...,
});
transaction.addStage('update_database', {
up: (data) => {
console.log(data); // { 'get_customers': true }
// Throws Error cannot update customerId of null expected String
return AWS.DynamoDB.update(customerId).promise()
// or
// Throws error Cannot update customerId of true expected String
return AWS.DynamoDB.update(data.get_customers).promise();
},
down: () => {
return AWS.DynamoDB.delete(...).promise()
}
});
transaction.execute();
Luckily for us Electron automatically passes returned values from stage to stage keyed by the stages name! Each stage will have the output of the resolved promise
returned to the up()
function of all the previous stages.
Check out the following (correct) example
const stripe = require('stripe');
const AWS = require('aws-sdk');
const Electron = require('electron-tx');
const transaction = new Electron();
transaction.addStage('get_customers', {
up: (data) => {
console.log(data); // null (this is the first stage there is no data)
return stripe.customers.find('cus_1234')
},
down: () => {
...
},
});
transaction.addStage('update_database', {
up: (data) => {
console.log(data); // { 'get_customers': { ... data returned from promise } }
return AWS.DynamoDB.update(data['get_customers'].customerId).promise()
},
down: () => {
return AWS.DynamoDB.delete(...).promise()
}
});
transaction.execute();
Note: The down()
method does not have access to all the previous stages data. It only has access to the data produced by
its respective up method.
transaction.addStage('update_database', {
up: () => {
return AWS.DynamoDB.update(...).promise()
},
down: (data) => {
console.log(data); // { Item: { ... } } only the result of this stages up() call
return AWS.DynamoDB.delete(...).promise()
}
});
Electron comes with event hooks on a per stage basis. You can trigger events using the before()
and after()
functions within
the stage configuration. See the following example of using event hooks. The before hook is always called but the after hook is only called
if the stage was successful.
transaction.addStage('update_database', {
before: (data) => {
console.log('Attempting to update database!');
console.log(data); // { prev_stage_1: {}, prev_stage_2: {}, etc...}
},
up: () => true,
down: () => true,
after: () => {
console.log('Database update successful!')
},
});
Automated tests for this system can be run using the command
npm test
Coming Soon
- NodeJS - The server side library used
- Javascript - Programming language used
- Lodash - A modern JavaScript utility library delivering modularity, performance & extras.
- NPM - Dependency Management
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
We use SemVer for versioning. For the versions available, see the tags on this repository.
- Christian Bartram - Creator & Developer - cbartram
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE.md file for details