Skip to content
This repository has been archived by the owner on Jan 12, 2022. It is now read-only.

feat: provide plugin dependencies sorting #281

Merged
merged 19 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/plugins/writing-a-new-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,35 @@ One would need to initialize a Wallet with the option `allowSensitiveOperations`

You can see the list of thoses [sensitive functions and properties](https://github.com/dashevo/wallet-lib/blob/master/src/CONSTANTS.js#L67), anything under `UNSAFE_*` will require this option to be set to true in order to be use from within a plugin.

## Injection order

While system plugins will by default be first injected in the system, in the case of a need for specific injection order.
Plugin can be sorted in such a way that in got injected before or after another set of plugins.
For this, use injectionOrder properties before and/or after.


In below example, this worker will be dependent on the methods getUTXOS to be internally available, and will be expected to be injected before TransactionSyncStreamWorker and after ChainPlugin.

```js
class WithInjectBeforeDependenciesWorker extends Worker {
constructor() {
super({
name: 'withInjectBeforeDependenciesWorker',
dependencies: [
'getUTXOS',
],
injectionOrder: {
after: [
'ChainPlugin'
],
before: [
'TransactionSyncStreamWorker'
]
}
});
}
}
```

## Accessing events

Expand Down
1 change: 1 addition & 0 deletions src/plugins/Plugins/ChainPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ChainPlugin extends StandardPlugin {
name: 'ChainPlugin',
executeOnStart: defaultOpts.executeOnStart,
firstExecutionRequired: defaultOpts.firstExecutionRequired,
awaitOnInjection: true,
dependencies: [
'storage',
'transport',
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/StandardPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class StandardPlugin extends EventEmitter {
this.pluginType = _.has(opts, 'type') ? opts.type : 'Standard';
this.name = _.has(opts, 'name') ? opts.name : 'UnnamedPlugin';
this.dependencies = _.has(opts, 'dependencies') ? opts.dependencies : [];

this.injectionOrder = _.has(opts, 'injectionOrder') ? opts.injectionOrder : { before: [], after: [] };
this.awaitOnInjection = _.has(opts, 'awaitOnInjection') ? opts.awaitOnInjection : false;
this.executeOnStart = _.has(opts, 'executeOnStart')
? opts.executeOnStart
: defaultOpts.executeOnStart;
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/Worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Worker extends StandardPlugin {
this.workerPass = 0;
this.isWorkerRunning = false;

this.awaitOnInjection = _.has(opts, 'awaitOnInjection')
? opts.awaitOnInjection
: false;

this.firstExecutionRequired = _.has(opts, 'firstExecutionRequired')
? opts.firstExecutionRequired
: defaultOpts.firstExecutionRequired;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/Workers/IdentitySyncWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class IdentitySyncWorker extends Worker {
executeOnStart: true,
firstExecutionRequired: true,
workerIntervalTime: 60 * 1000,
awaitOnInjection: true,
gapLimit: 10,
dependencies: [
'storage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class TransactionSyncStreamWorker extends Worker {
name: 'TransactionSyncStreamWorker',
executeOnStart: true,
firstExecutionRequired: true,
awaitOnInjection: true,
workerIntervalTime: 0,
gapLimit: 10,
dependencies: [
Expand Down
28 changes: 3 additions & 25 deletions src/types/Account/_initializeAccount.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
const _ = require('lodash');
const logger = require('../../logger');
const TransactionSyncStreamWorker = require('../../plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker');
const ChainPlugin = require('../../plugins/Plugins/ChainPlugin');
const IdentitySyncWorker = require('../../plugins/Workers/IdentitySyncWorker');
const EVENTS = require('../../EVENTS');
const { WALLET_TYPES } = require('../../CONSTANTS');

const preparePlugins = require('./_preparePlugins');
shumkov marked this conversation as resolved.
Show resolved Hide resolved
const ensureAddressesToGapLimit = require('../../utils/bip44/ensureAddressesToGapLimit');

// eslint-disable-next-line no-underscore-dangle
Expand All @@ -18,12 +14,6 @@ async function _initializeAccount(account, userUnsafePlugins) {
return new Promise(async (resolve, reject) => {
try {
if (account.injectDefaultPlugins) {
// TODO: Should check in other accounts if a similar is setup already
// TODO: We want to sort them by dependencies and deal with the await this way
// await parent if child has it in dependency
// if not, then is it marked as requiring a first exec
// if yes add to watcher list.

if ([WALLET_TYPES.HDWALLET, WALLET_TYPES.HDPUBLIC].includes(account.walletType)) {
ensureAddressesToGapLimit(
account.store.wallets[account.walletId],
Expand All @@ -34,22 +24,10 @@ async function _initializeAccount(account, userUnsafePlugins) {
} else {
await account.getAddress('0'); // We force what is usually done by the BIP44Worker.
}

if (!account.offlineMode) {
await account.injectPlugin(ChainPlugin, true);

// Transaction sync worker
await account.injectPlugin(TransactionSyncStreamWorker, true);

if (account.walletType === WALLET_TYPES.HDWALLET) {
await account.injectPlugin(IdentitySyncWorker, true);
}
}
}

_.each(userUnsafePlugins, (UnsafePlugin) => {
account.injectPlugin(UnsafePlugin, account.allowSensitiveOperations);
});
// Will sort and inject plugins.
await preparePlugins(account, userUnsafePlugins);

self.emit(EVENTS.STARTED, { type: EVENTS.STARTED, payload: null });

Expand Down
22 changes: 22 additions & 0 deletions src/types/Account/_preparePlugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const sortPlugins = require('./_sortPlugins');

const preparePlugins = function preparePlugins(account, userUnsafePlugins) {
function reducer(accumulatorPromise, [plugin, allowSensitiveOperation, awaitOnInjection]) {
return accumulatorPromise
.then(() => account.injectPlugin(plugin, allowSensitiveOperation, awaitOnInjection));
}

return new Promise((resolve, reject) => {
try {
const sortedPlugins = sortPlugins(account, userUnsafePlugins);
// It is important that all plugin got successfully injected in a sequential maneer
sortedPlugins.reduce(reducer, Promise.resolve()).then(() => resolve(sortedPlugins));

resolve(sortedPlugins);
} catch (e) {
reject(e);
}
});
};

module.exports = preparePlugins;
140 changes: 140 additions & 0 deletions src/types/Account/_sortPlugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const { each, findIndex } = require('lodash');
const TransactionSyncStreamWorker = require('../../plugins/Workers/TransactionSyncStreamWorker/TransactionSyncStreamWorker');
const ChainPlugin = require('../../plugins/Plugins/ChainPlugin');
const IdentitySyncWorker = require('../../plugins/Workers/IdentitySyncWorker');
const { WALLET_TYPES } = require('../../CONSTANTS');

const initPlugin = (UnsafePlugin) => {
const isInit = !(typeof UnsafePlugin === 'function');
return (isInit) ? UnsafePlugin : new UnsafePlugin();
};

/**
* Sort user defined plugins using the injectionOrder properties before or after when specified.
*
* Except if specified using before property, all system plugins (TxSyncStream, IdentitySync...)
* will be sorted on top.
*
* @param defaultSortedPlugins
* @param userUnsafePlugins
* @returns {*[]}
*/
const sortUserPlugins = (defaultSortedPlugins, userUnsafePlugins, allowSensitiveOperations) => {
const sortedPlugins = [];
const initializedSortedPlugins = [];

// We start by ensuring all default plugins get loaded and initialized on top
defaultSortedPlugins.forEach((defaultPluginParams) => {
sortedPlugins.push(defaultPluginParams);

// We also need to initialize them so we actually as we gonna need to read some properties.
const plugin = initPlugin(defaultPluginParams[0]);
initializedSortedPlugins.push(plugin);
});

// Iterate accross all user defined plugins
each(userUnsafePlugins, (UnsafePlugin) => {
const plugin = initPlugin(UnsafePlugin);

const {
awaitOnInjection,
shumkov marked this conversation as resolved.
Show resolved Hide resolved
injectionOrder: {
before: injectBefore,
after: injectAfter,
},
} = plugin;

const hasAfterDependencies = !!(injectAfter && injectAfter.length);
const hasBeforeDependencies = !!(injectBefore && injectBefore.length);
const hasPluginDependencies = (hasAfterDependencies || hasBeforeDependencies);

let injectionIndex = initializedSortedPlugins.length;

if (hasPluginDependencies) {
let injectionBeforeIndex = -1;
let injectionAfterIndex = -1;

if (hasBeforeDependencies) {
each(injectBefore, (pluginDependencyName) => {
const beforePluginIndex = findIndex(initializedSortedPlugins, ['name', pluginDependencyName]);
// TODO: we could have an handling that would postpone trying to insert the dependencies
// ensuring the case where we try to rely and sort based on user specified dependencies
// For now, require user to sort them when specifying the plugins.
if (beforePluginIndex === -1) throw new Error(`Dependency ${pluginDependencyName} not found`);
if (injectionBeforeIndex === -1 || injectionIndex > beforePluginIndex) {
injectionBeforeIndex = (injectionBeforeIndex === -1 || injectBefore > beforePluginIndex)
? beforePluginIndex
: injectionBeforeIndex;
}
});
}

if (hasAfterDependencies) {
each(injectAfter, (pluginDependencyName) => {
const afterPluginIndex = findIndex(initializedSortedPlugins, ['name', pluginDependencyName]);
if (afterPluginIndex === -1) throw new Error(`Dependency ${pluginDependencyName} not found`);
if (injectionAfterIndex === -1 || injectionAfterIndex < afterPluginIndex + 1) {
injectionAfterIndex = afterPluginIndex + 1;
}
});
}

if (
injectionBeforeIndex !== -1
&& injectionAfterIndex !== -1
&& injectionAfterIndex > injectionBeforeIndex
) {
throw new Error(`Conflicting dependency order for ${plugin.name}`);
}

if (
injectionBeforeIndex !== -1
|| injectionAfterIndex !== -1
) {
injectionIndex = (injectionBeforeIndex !== -1)
? injectionBeforeIndex
: injectionAfterIndex;
}
}

// We insert both initialized and uninitialized plugins as we gonna need to read property.
initializedSortedPlugins.splice(
injectionIndex,
0,
plugin,
);
sortedPlugins.splice(
injectionIndex,
0,
[UnsafePlugin, allowSensitiveOperations, awaitOnInjection],
);
});
initializedSortedPlugins.forEach((initializedSortedPlugin, i) => {
delete initializedSortedPlugins[i];
});
return sortedPlugins;
};

/**
* Sort plugins defined by users based on the before and after properties
* @param account
* @param userUnsafePlugins
* @returns {*[]}
*/
const sortPlugins = (account, userUnsafePlugins) => {
const plugins = [];

// eslint-disable-next-line no-async-promise-executor
if (account.injectDefaultPlugins) {
if (!account.offlineMode) {
plugins.push([ChainPlugin, true, true]);
plugins.push([TransactionSyncStreamWorker, true, true]);

if (account.walletType === WALLET_TYPES.HDWALLET) {
plugins.push([IdentitySyncWorker, true, true]);
}
}
}
return sortUserPlugins(plugins, userUnsafePlugins, account.allowSensitiveOperations);
};
module.exports = sortPlugins;
Loading