diff --git a/docs/tutorials/event-checkpointer.md b/docs/tutorials/event-checkpointer.md
new file mode 100644
index 0000000000..53e3aa377e
--- /dev/null
+++ b/docs/tutorials/event-checkpointer.md
@@ -0,0 +1,75 @@
+This tutorial describes the approaches that can be selected by users of the fabric-network module for replaying missed events emitted by peers.
+
+### Overview
+
+Events are emitted by peers when blocks are committed. Two types of events support checkpointing:
+1. Contract events (also known as chaincode events) - Defined in transactions to be emitted. E.g. an event emitted when a commercial paper is sold
+2. Block Events - Emitted when a block is committed
+
+In the case of an application crashing and events being missed, applications may still want to execute the event callback for the event it missed. Peers in a Fabric network support event replay, and to support this, the fabric-network module supports checkpointing strategies that track the last block and transactions in that block, that have been seen by the client.
+
+### Checkpointers
+
+The `BaseCheckpoint` class is an interface that is to be used by all Checkpoint classes. fabric-network has one default class, `FileSystemCheckpointer` that is exported as a factory in the `CheckpointFactories`. The `FILE_SYSTEM_CHECKPOINTER` is the default checkpointer.
+
+A checkpoint factory is a function that returns an instance with `BaseCheckpointer` as a parent class. These classes implement the `async save(channelName, listenerName)` and `async load()` functions.
+
+A checkpointer is called each time the event callback is triggered.
+
+The checkpointer can be set when connecting to a gateway or when creating the event listener.
+```javascript
+const { Gateway, CheckpointFactories } = require('fabric-network');
+
+const connectOptions = {
+ checkpointer: {
+ factory: CheckpointFactories.FILE_SYSTEM_CHECKPOINTER,
+ options: {} // Options usable by the factory
+ }
+};
+
+const gateway = new Gateway()
+await gateway.connect(connectionProfile, connectOptions);
+```
+
+Configuring a listener to be checkpointed required two properties:
+1. `replay : boolean` - Tells the listener to record a checkpoint. Required if checkpointing is desired
+2. `checkpointer : BaseCheckpointer` - If a checkpointer is not specified in the gateway, it must be specified here
+```javascript
+const listener = await contract.addContractListener('saleEventListener', 'sale', (err, event, blockNumber, txId) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ // -- Do something
+}, {replay: true, checkpointer: {factory: MyCheckpointer});
+```
+
+### Custom Checkpointer
+
+Users can configure their own checkpointer. This requires two components to be created:
+1. The Checkpointer class
+2. The Factory
+
+```javascript
+class DbCheckpointer extends BaseCheckpointer {
+ constructor(channelName, listenerName, dbOptions) {
+ super(channelName, listenerName);
+ this.db = new Db(dbOptions);
+ }
+
+ async save(transactionId, blockNumber) { /* Your implementation using a database */ }
+
+ async load() { /* Your implementation using a database*/ }
+}
+
+function BD_CHECKPOINTER_FACTORY(channelName, listenerName, options) {
+ return new DbCheckpointer(channelName, listenerName, options);
+}
+
+const gateway = new Gateway();
+await gateway.connect({
+ checkpointer: {
+ factory: DB_CHECKPOINTER_FACTORY,
+ options: {host: 'http://localhost'}
+});
+```
diff --git a/docs/tutorials/event-hub-management.md b/docs/tutorials/event-hub-management.md
new file mode 100644
index 0000000000..c6071dd6dc
--- /dev/null
+++ b/docs/tutorials/event-hub-management.md
@@ -0,0 +1,43 @@
+This tutorial describes how to define the behavior of the event hub selection strategy used when event hubs disconnect or new event hubs are required.
+
+The `ChannelEventHub` is a fabric-client class that receives contract, commit and block events from the event hub within a peer. The `fabric-network` abstracts the event hub away, and instead uses an event hub selection strategy to create new event hub instances or reuse existing instances.
+
+The interface for an event hub selection strategy is as follows:
+
+```javascript
+class BaseEventHubSelectionStrategy {
+ /**
+ * Returns the next peer in the list per the strategy implementation
+ * @returns {ChannelPeer}
+ */
+ getNextPeer() {
+ // Peer selection logic. Called whenever an event hub is required
+ }
+
+ /**
+ * Updates the status of a peers event hub
+ * @param {ChannelPeer} deadPeer The peer that needs its status updating
+ */
+ updateEventHubAvailability(deadPeer) {
+ // Peer availability update logic. Called whenever the event hub disconnects.
+ }
+}
+```
+
+The event hub strategy exists at a gateway level, and is included in the `GatewayOptions` in the form of a factory function. The factory gives the event hub selection strategy instance a list of peers that it can select event hubs from.
+
+```javascript
+function EXAMPLE_EVENT_HUB_SELECTION_FACTORY(network) {
+ const orgPeers = getOrganizationPeers(network);
+ const eventEmittingPeers = filterEventEmittingPeers(orgPeers);
+ return new ExampleEventHubSelectionStrategy(eventEmittingPeers);
+}
+
+const gateway = new Gateway();
+await gateway.connect(connectionProfile, {
+ ...
+ eventHubSelectionOptions: {
+ strategy: EXAMPLE_EVENT_HUB_SELECTION_FACTORY
+ }
+})
+```
diff --git a/docs/tutorials/listening-to-events.md b/docs/tutorials/listening-to-events.md
new file mode 100644
index 0000000000..e01044e7b6
--- /dev/null
+++ b/docs/tutorials/listening-to-events.md
@@ -0,0 +1,111 @@
+This tutorial describes the different ways to listen to events emitted by a network using the fabric-network module.
+
+### Overview
+
+There are three event types that can be subscribed to:
+1. Contract events - Those emitted explicitly by the chaincode developer within a transaction
+2. Transaction (Commit) events - Those emitted automatically when a transaction is committed after an invoke
+3. Block events - Those emitted automatically when a block is committed
+
+Listening for these events allows the application to react without directly calling a transaction. This is ideal in use cases such as tracking network analytics.
+
+### Usage
+
+Each listener type takes at least one parameter, the event callback. This is the function that is called when an event is detected. This callback is overridden by the `fabric-network` in order to support `Checkpointing`.
+
+#### Contract events
+
+```javascript
+const gateway = new Gateway();
+await gateway.connect(connectionProfile, gatewayOptions);
+const network = await gateway.getNetwork('my-channel');
+const contract = network.getContract('my-contract');
+
+/**
+ * @param {String} listenerName the name of the event listener
+ * @param {String} eventName the name of the event being listened to
+ * @param {Function} callback the callback function with signature (error, event, blockNumber, transactionId, status)
+ * @param {Object} options
+**/
+const listener = await contract.addContractListener('my-contract-listener', 'sale', (error, event, blockNumber, transactionId, status) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ console.log(`Block Number: ${blockNumber} Transaction ID: ${transactionId} Status: ${status}`);
+})
+```
+Notice that there is no need to specify an event hub, as the `EventHubSelectionStrategy` will select it automatically.
+
+#### Block events
+
+```javascript
+const gateway = new Gateway();
+await gateway.connect(connectionProfile, gatewayOptions);
+const network = await gateway.getNetwork('my-channel');
+
+/**
+ * @param {String} listenerName the name of the event listener
+ * @param {Function} callback the callback function with signature (error, blockNumber, transactionId, status)
+ * @param {Object} options
+**/
+const listener = await network.addBlockListener('my-block-listener', (error, block) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ console.log(`Block: ${block}`);
+}, {filtered: true /*false*/})
+```
+When listening for block events, it is important to specify if you want a filtered or none filtered event, as this determines which event hub is compatible with the request.
+
+#### Commit events
+
+Option 1:
+```javascript
+const gateway = new Gateway();
+await gateway.connect(connectionProfile, gatewayOptions);
+const network = await gateway.getNetwork('my-channel');
+const contract = network.getContract('my-contract');
+
+const transaction = contract.newTransaction('sell');
+/**
+ * @param {String} transactionId the name of the event listener
+ * @param {Function} callback the callback function with signature (error, transactionId, status, blockNumber)
+ * @param {Object} options
+**/
+const listener = await network.addCommitListener(transaction.getTransactionID().getTransactionID(), (error, transactionId, status, blockNumber) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ console.log(`Transaction ID: ${transactionId} Status: ${status} Block number: ${blockNumber}`);
+}, {});
+```
+
+Option 2:
+```javascript
+const gateway = new Gateway();
+await gateway.connect(connectionProfile, gatewayOptions);
+const network = await gateway.getNetwork('my-channel');
+const contract = network.getContract('my-contract');
+
+const transaction = contract.newTransaction('sell');
+/**
+ * @param {String} transactionId the name of the event listener
+ * @param {Function} callback the callback function with signature (error, transactionId, status, blockNumber)
+ * @param {Object} options
+**/
+const listener = await transaction.addCommitListener((error, transactionId, status, blockNumber) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ console.log(`Transaction ID: ${transactionId} Status: ${status} Block number: ${blockNumber}`);
+});
+```
+
+
+
+
+
diff --git a/docs/tutorials/transaction-commit-events.md b/docs/tutorials/transaction-commit-events.md
index 721af0a9ac..2d5b9b504f 100644
--- a/docs/tutorials/transaction-commit-events.md
+++ b/docs/tutorials/transaction-commit-events.md
@@ -1,5 +1,5 @@
This tutorial describes the approaches that can be selected by users of the
-fabric-network module for ensuring that submitted transactions are commited
+fabric-network module for ensuring that submitted transactions are committed
on peers.
### Overview
@@ -61,7 +61,7 @@ strategies, it is possible to implement your own event handling. This is
achieved by specifying your own factory function as the event handling
strategy. The factory function should return a *transaction event handler*
object and take two parameters:
-1. Transaction ID: `String`
+1. Transaction: `Transaction`
2. Blockchain network: `Network`
The Network provides access to peers and event hubs from which events should
diff --git a/docs/tutorials/tutorials.json b/docs/tutorials/tutorials.json
index 455eacdf90..5d8ead971e 100644
--- a/docs/tutorials/tutorials.json
+++ b/docs/tutorials/tutorials.json
@@ -35,6 +35,15 @@
"query-peers": {
"title": "fabric-network: How to select peers for evaluating transactions (queries)"
},
+ "event-checkpointer": {
+ "title": "fabric-network: How to replay missed events"
+ },
+ "event-hub-management": {
+ "title": "fabric-network: How to automatically select and reconnect to event hubs"
+ },
+ "listening-to-events": {
+ "title": "fabric-network: How to listen to events"
+ },
"grpc-settings": {
"title": "fabric-client: How to set gRPC settings"
},
diff --git a/fabric-client/lib/ChannelEventHub.js b/fabric-client/lib/ChannelEventHub.js
index e7edd91502..12419f3bca 100644
--- a/fabric-client/lib/ChannelEventHub.js
+++ b/fabric-client/lib/ChannelEventHub.js
@@ -21,6 +21,7 @@ const logger = utils.getLogger('ChannelEventHub.js');
const {Identity} = require('./msp/identity');
const TransactionID = require('./TransactionID');
const util = require('util');
+const EventHubDisconnectError = require('./errors/EventHubDisconnectError');
const BlockDecoder = require('./BlockDecoder.js');
@@ -588,7 +589,7 @@ class ChannelEventHub {
/**
* Disconnects the ChannelEventHub from the peer event source.
- * Will close all event listeners and send an Error object
+ * Will close all event listeners and send an EventHubDisconnectError object
* with the message "ChannelEventHub has been shutdown" to
* all listeners that provided an "onError" callback.
*/
@@ -597,14 +598,14 @@ class ChannelEventHub {
logger.debug('disconnect - disconnect is running');
} else {
this._disconnect_running = true;
- this._disconnect(new Error('ChannelEventHub has been shutdown'));
+ this._disconnect(new EventHubDisconnectError('ChannelEventHub has been shutdown'));
this._disconnect_running = false;
}
}
/**
* Disconnects the ChannelEventHub from the fabric peer service.
- * Will close all event listeners and send an Error object
+ * Will close all event listeners and send an EventHubDisconnectError object
* with the message "ChannelEventHub has been shutdown" to
* all listeners that provided an "onError" callback.
*/
@@ -1387,6 +1388,10 @@ class ChannelEventHub {
}
}
+ isFiltered() {
+ return !!this._filtered_stream;
+ }
+
/*
* private internal method for processing block events
* @param {Object} block protobuf object
@@ -1462,7 +1467,7 @@ class ChannelEventHub {
}
if (trans_reg.disconnect) {
logger.debug('_callTransactionListener - automatically disconnect');
- this._disconnect(new Error('Shutdown due to disconnect on transaction id registration'));
+ this._disconnect(new EventHubDisconnectError('Shutdown due to disconnect on transaction id registration'));
}
}
@@ -1556,7 +1561,7 @@ class ChannelEventHub {
logger.debug('_callChaincodeListener - automatically unregister tx listener for %s', tx_id);
}
if (chaincode_reg.event_reg.disconnect) {
- this._disconnect(new Error('Shutdown due to disconnect on transaction id registration'));
+ this._disconnect(new EventHubDisconnectError('Shutdown due to disconnect on transaction id registration'));
}
} else {
logger.debug('_callChaincodeListener - NOT calling chaincode listener callback');
@@ -1577,7 +1582,7 @@ class ChannelEventHub {
this._start_stop_registration.unregister_action();
}
if (this._start_stop_registration.disconnect) {
- this._disconnect(new Error('Shutdown due to end block number has been seen'));
+ this._disconnect(new EventHubDisconnectError('Shutdown due to end block number has been seen'));
}
}
}
diff --git a/fabric-client/lib/errors/EventHubDisconnectError.js b/fabric-client/lib/errors/EventHubDisconnectError.js
new file mode 100644
index 0000000000..e1086a501b
--- /dev/null
+++ b/fabric-client/lib/errors/EventHubDisconnectError.js
@@ -0,0 +1,22 @@
+/*
+ Copyright 2019, 2018 IBM All Rights Reserved.
+
+ SPDX-License-Identifier: Apache-2.0
+
+*/
+
+'use strict';
+
+/**
+ * Error when an event hub is disconnected.
+ * @interface
+ * @memberof module:fabric-network
+ * @property {String} [message] The error message
+ */
+class EventHubDisconnectError extends Error {
+ constructor(message) {
+ super(message);
+ }
+}
+
+module.exports = EventHubDisconnectError;
diff --git a/fabric-network/index.js b/fabric-network/index.js
index 937e0c4b06..bcfa81b9c3 100644
--- a/fabric-network/index.js
+++ b/fabric-network/index.js
@@ -60,4 +60,6 @@ module.exports.FileSystemWallet = require('./lib/impl/wallet/filesystemwallet');
module.exports.CouchDBWallet = require('./lib/impl/wallet/couchdbwallet');
module.exports.DefaultEventHandlerStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies');
module.exports.DefaultQueryHandlerStrategies = require('fabric-network/lib/impl/query/defaultqueryhandlerstrategies');
+module.exports.CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories');
+module.exports.EventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies');
module.exports.TimeoutError = require('fabric-network/lib/errors/timeouterror');
diff --git a/fabric-network/lib/contract.js b/fabric-network/lib/contract.js
index 1bb62a0516..dd6c2449c6 100644
--- a/fabric-network/lib/contract.js
+++ b/fabric-network/lib/contract.js
@@ -7,6 +7,7 @@
'use strict';
const Transaction = require('fabric-network/lib/transaction');
+const ContractEventListener = require('./impl/event/contracteventlistener');
const logger = require('./logger').getLogger('Contract');
const util = require('util');
@@ -47,7 +48,7 @@ function verifyNamespace(namespace) {
* @hideconstructor
*/
class Contract {
- constructor(network, chaincodeId, gateway, namespace) {
+ constructor(network, chaincodeId, gateway, checkpointer, namespace) {
logger.debug('in Contract constructor');
verifyNamespace(namespace);
@@ -57,6 +58,7 @@ class Contract {
this.chaincodeId = chaincodeId;
this.gateway = gateway;
this.namespace = namespace;
+ this.checkpointer = checkpointer;
}
/**
@@ -86,6 +88,23 @@ class Contract {
return this.chaincodeId;
}
+ getCheckpointer(options) {
+ if (options) {
+ if (typeof options.checkpointer === 'undefined') {
+ return this.checkpointer;
+ } else if (typeof options.checkpointer === 'function') {
+ return options.checkpointer;
+ } else if (options.checkpointer === false) {
+ return null;
+ }
+ }
+ return this.checkpointer;
+ }
+
+ getEventHubSelectionStrategy() {
+ return this.network.eventHubSelectionStrategy;
+ }
+
/**
* Get event handler options specified by the user when creating the gateway.
* @private
@@ -152,6 +171,32 @@ class Contract {
async evaluateTransaction(name, ...args) {
return this.createTransaction(name).evaluate(...args);
}
+
+ /**
+ * Create a commit event listener for this transaction.
+ * @param {string} listenerName The name of the listener
+ * @param {Function} callback - This callback will be triggered when
+ * a transaction commit event is emitted. It takes parameters
+ * of error, event payload, block number, transaction ID and status
+ * @param {RegistrationOptions} [options] - Optional. Options on
+ * registrations allowing start and end block numbers.
+ * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection
+ * @returns {CommitEventListener}
+ * @async
+ */
+ async addContractListener(listenerName, eventName, callback, options) {
+ if (!options) {
+ options = {};
+ }
+ options.replay = options.replay ? true : false;
+ options.checkpointer = this.getCheckpointer(options);
+ const listener = new ContractEventListener(this, listenerName, eventName, callback, options);
+ const network = this.getNetwork();
+ network.saveListener(listenerName, listener);
+ await listener.register();
+ return listener;
+ }
+
}
module.exports = Contract;
diff --git a/fabric-network/lib/gateway.js b/fabric-network/lib/gateway.js
index 013e1e81da..f02f215549 100644
--- a/fabric-network/lib/gateway.js
+++ b/fabric-network/lib/gateway.js
@@ -11,6 +11,8 @@ const Client = require('fabric-client');
const Network = require('./network');
const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies');
const QueryStrategies = require('fabric-network/lib/impl/query/defaultqueryhandlerstrategies');
+const EventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies');
+const CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories');
const logger = require('./logger').getLogger('Gateway');
@@ -25,6 +27,8 @@ const logger = require('./logger').getLogger('Gateway');
* @property {module:fabric-network.Gateway~DefaultQueryHandlerOptions} [queryHandlerOptions] Options for the inbuilt
* default query handler capability.
* @property {module:fabric-network.Gateway~DiscoveryOptions} [discovery] Discovery options.
+ * @property {module:fabric-network.Gateway~DefaultEventHubSelectionOptions} [eventHubSelectionOptions] Event hub selection options.
+ * @property {module:fabric-network.Network~CheckpointerFactory} [checkpointer] Event hub selection options.
*/
/**
@@ -57,6 +61,38 @@ const logger = require('./logger').getLogger('Gateway');
* fails.
*/
+/**
+ * @typedef {Object} Gateway~CheckpointerFactory
+ * @memberof module:fabric-network
+ * @param {String} channelName the name of the channel the checkpoint exists in
+ * @param {String} listenerName the name of the listener being checkpointed
+ * @param {Object} [options] Optional. Options to configure behaviour of customer checkpointers i.e.
+ * Supplying database connection details
+ * @returns {BaseCheckpointer}
+ */
+
+/**
+ * @typedef {Object} Gateway~DefaultEventHubSelectionOptions
+ * @memberof module:fabric-network
+ * @property {?module:fabric-network.Gateway~DefaultEventHubSelectionFactory} [strategy=MSPID_SCOPE_ROUND_ROBIN] Selects the next
+ * event hub in the event of a new listener being created or an event hub disconnect
+ */
+
+/**
+ * @typedef {Object} Gateway~DefaultEventHubSelectionFactory
+ * @memberof module:fabric-network
+ * @param {module:fabric-network.Network} network The network the event hub is being selected for
+ * @returns {module:fabric-network.Gateway~BaseEventHubSelectionStrategy}
+ */
+
+/**
+ * @typedef {Object} Gateway~BaseEventHubSelectionStrategy
+ * @memberof module:fabric-network
+ * @property {Function} getNextPeer Function that returns the next peer in the list of available peers
+ * @property {Function} updateEventHubAvailability Function that updates the availability of an event hub
+ */
+
+
/**
* @typedef {Object} Gateway~DefaultQueryHandlerOptions
* @memberof module:fabric-network
@@ -129,6 +165,13 @@ class Gateway {
},
discovery: {
enabled: Client.getConfigSetting('initialize-with-discovery', true)
+ },
+ checkpointer: {
+ factory: CheckpointFactories.FILE_SYSTEM_CHECKPOINTER,
+ options: {}
+ },
+ eventHubSelectionOptions: {
+ strategy: EventHubSelectionStrategies.MSPID_SCOPE_ROUND_ROBIN,
}
};
}
diff --git a/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js b/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js
new file mode 100644
index 0000000000..cd8ff04935
--- /dev/null
+++ b/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+/**
+ * An abstract selection strategy that provides an interface for other selection
+ * strategies
+ *
+ * @memberof module:fabric-network
+ * @class
+ */
+class AbstractEventHubSelectionStrategy {
+ constructor(peers) {
+ this.peers = peers;
+ }
+ /**
+ * Gets the next peer
+ * @returns {Peer}
+ */
+ getNextPeer() {
+ throw new Error('Abstract method called.');
+ }
+
+ /**
+ * Updates the availability of the peer
+ * @param {Peer} deadPeer Disconnected peer
+ */
+ updateEventHubAvailability(deadPeer) {
+ return;
+ }
+
+ getPeers() {
+ return this.peers;
+ }
+}
+
+module.exports = AbstractEventHubSelectionStrategy;
diff --git a/fabric-network/lib/impl/event/abstracteventlistener.js b/fabric-network/lib/impl/event/abstracteventlistener.js
new file mode 100644
index 0000000000..1f4888b5fb
--- /dev/null
+++ b/fabric-network/lib/impl/event/abstracteventlistener.js
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+'use strict';
+
+const Long = require('long');
+
+const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError');
+const BaseCheckpointer = require('./basecheckpointer');
+const logger = require('fabric-client/lib/utils').getLogger('AbstractEventListener');
+
+/**
+ * @typedef {Object} module:fabric-network.Network~ListenerOptions
+ * @memberof module:fabric-network
+ * @property {Object} checkpointer The checkpointer factory and options
+ * @property {Gateway~CheckpointerFactory} checkpointer.factory The checkpointer factory
+ * @property {Object} checkpointer.options The checkpoint configuration options
+ * @property {boolean} replay event replay and checkpointing on listener
+ * @extends RegistrationOpts
+ */
+
+/**
+ * Event listener base class handles initializing common properties across contract, transaction
+ * and block event listeners.
+ *
+ * Instances of the event listener are stateful and must only be used for one listener
+ * @private
+ * @class
+ */
+class AbstractEventListener {
+ /**
+ * Constructor
+ * @param {module:fabric-network.Network} network The network
+ * @param {string} listenerName The name of the listener being created
+ * @param {function} eventCallback The function called when the event is triggered.
+ * It has signature (err, ...args) where args changes depending on the event type
+ * @param {module:fabric-network.Network~ListenerOptions} options Event handler options
+ */
+ constructor(network, listenerName, eventCallback, options) {
+ if (!options) {
+ options = {};
+ }
+ this.channel = network.getChannel();
+ this.network = network;
+ this.listenerName = listenerName;
+ this.eventCallback = eventCallback;
+ this.options = options;
+
+ this._registered = false;
+ this._firstCheckpoint = {};
+ this._registration = null;
+ this._filtered = typeof options.filtered === 'boolean' ? options.filtered : true;
+ }
+
+ /**
+ * Called by the super classes register function. Saves information needed to start
+ * listening, and disconnects an event hub if it is the incorrect type
+ */
+ async register(chaincodeId) {
+ logger.debug(`Register event listener: ${this.listenerName}`);
+ if (this.getEventHubManager().getPeers().length === 0) {
+ logger.error('No peers available to register a listener');
+ return;
+ }
+ if (this._registered) {
+ throw new Error('Listener already registered');
+ }
+ if (this.eventHub && this.eventHub.isconnected() && !!this.eventHub.isFiltered() !== this._filtered) {
+ this.eventHub.disconnect();
+ this.eventHub = null;
+ }
+
+ if (this.options.checkpointer) {
+ if (typeof this.options.checkpointer.factory === 'function') {
+ this.checkpointer = this.options.checkpointer.factory(
+ this.channel.getName(),
+ this.listenerName,
+ this.options.checkpointer.options
+ );
+ this.checkpointer.setChaincodeId(chaincodeId);
+ }
+ }
+ if (this.useEventReplay()) {
+ if (!this.getCheckpointer()) {
+ logger.error('Opted to use checkpointing without defining a checkpointer');
+ }
+ }
+
+ let checkpoint;
+ if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) {
+ this._firstCheckpoint = checkpoint = await this.checkpointer.load();
+ const blockchainInfo = await this.channel.queryInfo();
+ if (checkpoint && checkpoint.blockNumber && blockchainInfo.height - 1 <= Number(checkpoint.blockNumber)) {
+ logger.debug(`Requested Block Number: ${checkpoint.blockNumber} Latest Block Number: ${blockchainInfo.height - 1}`);
+ this.options.startBlock = Long.fromValue(checkpoint.blockNumber);
+ }
+ }
+ }
+
+ /**
+ * Called by the super classes unregister function. Removes state from the listener so it
+ * can be reregistered at a later time
+ */
+ unregister() {
+ logger.debug(`Unregister event listener: ${this.listenerName}`);
+ this._registered = false;
+ delete this.options.startBlock;
+ delete this.options.endBlock;
+ delete this.options.disconnect;
+ this._firstCheckpoint = {};
+ }
+
+ /**
+ * @returns {boolean} Listeners registration status
+ */
+ isregistered() {
+ return this._registered;
+ }
+
+ /**
+ * Returns the checkpoint instance created by the checkpoint factory
+ * @returns {BaseCheckpointer} Checkpointer instance specific to this listener
+ */
+ getCheckpointer() {
+ return this.checkpointer;
+ }
+
+ /**
+ * Returns the event hub manager from the network
+ * @returns {EventHubManager} Event hub manager
+ */
+ getEventHubManager() {
+ const network = this.network;
+ return network.getEventHubManager();
+ }
+
+ useEventReplay() {
+ return this.options.replay;
+ }
+
+ /**
+ * Check if the event hub error is a disconnect message
+ * @param {Error} error The error emitted by the event hub
+ * @returns {boolean} is shutdown message
+ * @private
+ */
+ _isShutdownMessage(error) {
+ if (error) {
+ logger.debug('Event hub shutdown.');
+ return error instanceof EventHubDisconnectError;
+ }
+ return false;
+ }
+}
+
+module.exports = AbstractEventListener;
diff --git a/fabric-network/lib/impl/event/basecheckpointer.js b/fabric-network/lib/impl/event/basecheckpointer.js
new file mode 100644
index 0000000000..c221cc5e28
--- /dev/null
+++ b/fabric-network/lib/impl/event/basecheckpointer.js
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+/**
+ * Base checkpointer providing an interface for checkpointers
+ * @class
+ */
+class BaseCheckpointer {
+ /**
+ * The constructor
+ * @param {Object} options The options to configure the checkpointer
+ */
+ constructor(options) {
+ this.options = options || {};
+ this._chaincodeId = null;
+ }
+
+ /**
+ * Updates the storage mechanism
+ * @param {String} transactionId the transaction ID
+ * @param {Number} blockNumber the block number
+ * @async
+ */
+ async save(transactionId, blockNumber) {
+ throw new Error('Method has not been implemented');
+ }
+
+ /**
+ * Loads the latest checkpoint
+ * @async
+ */
+ async load() {
+ throw new Error('Method has not been implemented');
+ }
+
+ /**
+ * Sets the chaincode ID to group together listeners
+ * @param {String} chaincodeId the chaincodeId
+ */
+ async setChaincodeId(chaincodeId) {
+ this._chaincodeId = chaincodeId;
+ }
+}
+
+module.exports = BaseCheckpointer;
diff --git a/fabric-network/lib/impl/event/blockeventlistener.js b/fabric-network/lib/impl/event/blockeventlistener.js
new file mode 100644
index 0000000000..b667d0595e
--- /dev/null
+++ b/fabric-network/lib/impl/event/blockeventlistener.js
@@ -0,0 +1,112 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const BaseCheckpointer = require('./basecheckpointer');
+const AbstractEventListener = require('./abstracteventlistener');
+
+const logger = require('fabric-network/lib/logger').getLogger('BlockEventListener');
+const util = require('util');
+
+/**
+ * The Block Event listener class handles block events from the channel.
+ *
+ *
+ * @private
+ * @class
+ */
+class BlockEventListener extends AbstractEventListener {
+ constructor(network, listenerName, eventCallback, options) {
+ super(network, listenerName, eventCallback, options);
+ }
+
+ /**
+ * Finds and connects to an event hub then creates the listener registration
+ */
+ async register() {
+ await super.register();
+ if (!this.eventHub) {
+ return this._registerWithNewEventHub();
+ }
+ this._registration = this.eventHub.registerBlockEvent(
+ this._onEvent.bind(this),
+ this._onError.bind(this),
+ this.options
+ );
+ this.eventHub.connect(!this._filtered);
+ this._registered = true;
+ }
+
+ /**
+ * Unregisters the registration from the event hub
+ */
+ unregister() {
+ super.unregister();
+ if (this.eventHub) {
+ this.eventHub.unregisterBlockEvent(this._registration);
+ }
+ }
+
+ /**
+ * The callback triggered when the event was successful. Checkpoints the last
+ * block and transaction seen once the callback has run and unregisters the
+ * listener if the unregister flag was provided
+ * @param {*} block Either a full or filtered block
+ * @private
+ */
+ _onEvent(block) {
+ const blockNumber = Number(block.number);
+
+ try {
+ this.eventCallback(null, block);
+ } catch (err) {
+ logger.error(util.format('Error executing callback: %s', err));
+ }
+ if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) {
+ this.checkpointer.save(null, blockNumber);
+ }
+ if (this._registration.unregister) {
+ this.unregister();
+ }
+ }
+
+ /**
+ * This callback is triggered when the event was unsuccessful. If the error indicates
+ * that the event hub shutdown and the listener is still registered, it updates the
+ * {@link EventHubSelectionStrategy} status of event hubs (if implemented) and finds a
+ * new event hub to connect to
+ * @param {Error} error The error emitted
+ * @private
+ */
+ async _onError(error) {
+ logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error));
+ if (error) {
+ if (this._isShutdownMessage(error) && this.isregistered()) {
+ this.getEventHubManager().updateEventHubAvailability(this.eventHub._peer);
+ await this._registerWithNewEventHub();
+ }
+ }
+ this.eventCallback(error);
+ }
+
+ /**
+ * Finds a new event hub for the listener in the event of one shutting down. Will
+ * create a new instance if checkpointer is being used, or reuse one if not
+ * @private
+ */
+ async _registerWithNewEventHub() {
+ this.unregister();
+ if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) {
+ this.eventHub = this.getEventHubManager().getReplayEventHub();
+ } else {
+ this.eventHub = this.getEventHubManager().getEventHub();
+ }
+ await this.register();
+ }
+}
+
+module.exports = BlockEventListener;
diff --git a/fabric-network/lib/impl/event/checkpointfactories.js b/fabric-network/lib/impl/event/checkpointfactories.js
new file mode 100644
index 0000000000..2769fae0d6
--- /dev/null
+++ b/fabric-network/lib/impl/event/checkpointfactories.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const FileSystemCheckpointer = require('./filesystemcheckpointer');
+
+function FILE_SYSTEM_CHECKPOINTER(channelName, listenerName, options) {
+ return new FileSystemCheckpointer(channelName, listenerName, options);
+}
+
+module.exports = {
+ FILE_SYSTEM_CHECKPOINTER
+};
diff --git a/fabric-network/lib/impl/event/commiteventlistener.js b/fabric-network/lib/impl/event/commiteventlistener.js
new file mode 100644
index 0000000000..39ca67bb4e
--- /dev/null
+++ b/fabric-network/lib/impl/event/commiteventlistener.js
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const AbstractEventListener = require('./abstracteventlistener');
+const logger = require('fabric-network/lib/logger').getLogger('CommitEventListener');
+const util = require('util');
+
+/**
+ * The Commit Event Listener handles transaction commit events
+ *
+ * @private
+ * @class
+ */
+class CommitEventListener extends AbstractEventListener {
+ /**
+ *
+ * @param {module:fabric-network.Network} network The fabric network
+ * @param {string} transactionId the transaction id being listened to
+ * @param {Function} eventCallback The event callback called when a transaction is commited.
+ * It has signature (err, transactionId, status, blockNumber)
+ * @param {*} options
+ */
+ constructor(network, transactionId, eventCallback, options) {
+ const listenerName = transactionId + Math.random();
+ super(network, listenerName, eventCallback, options);
+ this.transactionId = transactionId;
+ }
+
+ async register() {
+ await super.register();
+ if (!this.eventHub) {
+ logger.debug('No event hub. Retrieving new one.');
+ return await this._registerWithNewEventHub();
+ }
+ if (this._isAlreadyRegistered(this.eventHub)) { // Prevents a transaction being registered twice with the same event hub
+ logger.debug('Event hub already has registrations. Generating new event hub instance.');
+ if (!this.options.fixedEventHub) {
+ this.eventHub = this.getEventHubManager().getEventHub(this.eventHub._peer, true);
+ } else {
+ this.eventHub = this.getEventHubManager().getFixedEventHub(this.eventHub._peer);
+ }
+ }
+ const txid = this.eventHub.registerTxEvent(
+ this.transactionId,
+ this._onEvent.bind(this),
+ this._onError.bind(this),
+ Object.assign({unregister: true}, this.options)
+ );
+ this._registration = this.eventHub._transactionRegistrations[txid];
+ this.eventHub.connect(!this._filtered);
+ this._registered = true;
+ }
+
+ /**
+ * @param {module:fabric-client.ChannelEventHub} eventhub Event hub.
+ * @param {boolean} isFixed If true only this peers event hub will be used
+ */
+ setEventHub(eventHub, isFixed) {
+ this.eventHub = eventHub;
+ this.options.fixedEventHub = isFixed;
+ }
+
+ unregister() {
+ super.unregister();
+ if (this.eventHub) {
+ this.eventHub.unregisterTxEvent(this.transactionId);
+ }
+ }
+
+ _onEvent(txid, status, blockNumber) {
+ logger.debug('_onEvent:', util.format('success for transaction %s', txid));
+ blockNumber = Number(blockNumber);
+
+ try {
+ this.eventCallback(null, txid, status, blockNumber);
+ } catch (err) {
+ logger.debug(util.format('_onEvent error from callback: %s', err));
+ }
+ if (this._registration.unregister) {
+ this.unregister();
+ }
+ }
+
+ _onError(error) {
+ logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error));
+ this.eventCallback(error);
+ }
+
+
+ async _registerWithNewEventHub() {
+ if (this.isregistered()) {
+ this.unregister();
+ }
+ if (this.options.fixedEventHub && !this.eventHub) {
+ throw new Error(`Cannot use a fixed event hub without setting an event hub ${this.listenerName}`);
+ }
+ if (!this.options.fixedEventHub) {
+ this.eventHub = this.getEventHubManager().getReplayEventHub();
+ } else {
+ this.eventHub = this.getEventHubManager().getFixedEventHub(this.eventHub._peer);
+ }
+
+ this.options.disconnect = true;
+ await this.register();
+ }
+
+ _isAlreadyRegistered(eventHub) {
+ if (!eventHub) {
+ throw new Error('Event hub not given');
+ }
+ const registrations = eventHub._transactionRegistrations;
+ if (registrations[this.transactionId]) {
+ return true;
+ }
+ return false;
+ }
+}
+
+module.exports = CommitEventListener;
diff --git a/fabric-network/lib/impl/event/contracteventlistener.js b/fabric-network/lib/impl/event/contracteventlistener.js
new file mode 100644
index 0000000000..f6b9372090
--- /dev/null
+++ b/fabric-network/lib/impl/event/contracteventlistener.js
@@ -0,0 +1,129 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const AbstractEventListener = require('./abstracteventlistener');
+const BaseCheckpointer = require('./basecheckpointer');
+const logger = require('fabric-network/lib/logger').getLogger('ContractEventListener');
+const util = require('util');
+
+/**
+ * The Contract Event Listener handles contract events from the chaincode.
+ *
+ * @private
+ * @class
+ */
+class ContractEventListener extends AbstractEventListener {
+ /**
+ * Constructor.
+ * @param {Contract} contract The contract instance
+ * @param {string} listenerName The name of the listener
+ * @param {string} eventName The name of the contract event being listened for
+ * @param {function} eventCallback The event callback called when an event is recieved.
+ * It has signature (err, BlockEvent, blockNumber, transactionId)
+ * @param {*} options
+ */
+ constructor(contract, listenerName, eventName, eventCallback, options) {
+ super(contract.getNetwork(), listenerName, eventCallback, options);
+ this.contract = contract;
+ this.eventName = eventName;
+ }
+
+ /**
+ * Finds and connects to an event hub then creates the listener registration
+ */
+ async register() {
+ await super.register(this.contract.getChaincodeId());
+ if (!this.eventHub) {
+ return await this._registerWithNewEventHub();
+ }
+ this._registration = this.eventHub.registerChaincodeEvent(
+ this.contract.getChaincodeId(),
+ this.eventName,
+ this._onEvent.bind(this),
+ this._onError.bind(this),
+ this.options
+ );
+ this._registered = true;
+ this.eventHub.connect(!this._filtered);
+ }
+
+ /**
+ * Unregisters the registration from the event hub
+ */
+ unregister() {
+ super.unregister();
+ if (this.eventHub) {
+ this.eventHub.unregisterChaincodeEvent(this._registration);
+ }
+ }
+
+ /**
+ * The callback triggered when the event was successful. Checkpoints the last
+ * block and transaction seen once the callback has run and unregisters the
+ * listener if the unregister flag was provided
+ * @param {ChaincodeEvent} event the event emitted
+ * @param {number} blockNumber the block number this transaction was commited inside
+ * @param {string} transactionId the transaction ID of the transaction this event was emitted by
+ * @param {string} status the status of the the transaction
+ */
+ async _onEvent(event, blockNumber, transactionId, status) {
+ logger.debug(`_onEvent[${this.listenerName}]:`, util.format('success for transaction %s', transactionId));
+ blockNumber = Number(blockNumber);
+ if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) {
+ const checkpoint = await this.checkpointer.load();
+ if (checkpoint && checkpoint.transactionIds && checkpoint.transactionIds.includes(transactionId)) {
+ logger.debug(util.format('_onEvent skipped transaction: %s', transactionId));
+ return;
+ }
+ await this.checkpointer.save(transactionId, blockNumber);
+ }
+
+ try {
+ this.eventCallback(null, event, blockNumber, transactionId, status);
+ } catch (err) {
+ logger.debug(util.format('_onEvent error from callback: %s', err));
+ }
+ if (this._registration.unregister) {
+ this.unregister();
+ }
+ }
+
+ /**
+ * This callback is triggerend when the event was unsuccessful. If the error indicates
+ * that the event hub shutdown and the listener is still registered, it updates the
+ * {@link EventHubSelectionStrategy} status of event hubs (if implemented) and finds a
+ * new event hub to connect to
+ * @param {Error} error The error emitted
+ */
+ async _onError(error) {
+ logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error));
+ if (error) {
+ if (this._isShutdownMessage(error) && this.isregistered()) {
+ this.getEventHubManager().updateEventHubAvailability(this.eventHub._peer);
+ await this._registerWithNewEventHub();
+ }
+ }
+ this.eventCallback(error);
+ }
+
+ /**
+ * Finds a new event hub for the listener in the event of one shutting down. Will
+ * create a new instance if checkpointer is being used, or reuse one if not
+ */
+ async _registerWithNewEventHub() {
+ this.unregister();
+ if (this.checkpointer instanceof BaseCheckpointer && this.useEventReplay()) {
+ this.eventHub = this.getEventHubManager().getReplayEventHub();
+ } else {
+ this.eventHub = this.getEventHubManager().getEventHub();
+ }
+ await this.register(this.contract.getChaincodeId());
+ }
+}
+
+module.exports = ContractEventListener;
diff --git a/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js b/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js
index b3e16be2dd..9cee8533a9 100644
--- a/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js
+++ b/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js
@@ -12,32 +12,36 @@ const TransactionEventHandler = require('fabric-network/lib/impl/event/transacti
function getOrganizationEventHubs(network) {
const peers = network.getChannel().getPeersForOrg();
- return network.getEventHubFactory().getEventHubs(peers);
+ return network.getEventHubManager().getEventHubs(peers);
}
function getNetworkEventHubs(network) {
const peers = network.getChannel().getPeers();
- return network.getEventHubFactory().getEventHubs(peers);
+ return network.getEventHubManager().getEventHubs(peers);
}
-function MSPID_SCOPE_ALLFORTX(transactionId, network, options) {
+function MSPID_SCOPE_ALLFORTX(transaction, options) {
+ const network = transaction.getNetwork();
const eventStrategy = new AllForTxStrategy(getOrganizationEventHubs(network));
- return new TransactionEventHandler(transactionId, eventStrategy, options);
+ return new TransactionEventHandler(transaction, eventStrategy, options);
}
-function MSPID_SCOPE_ANYFORTX(transactionId, network, options) {
+function MSPID_SCOPE_ANYFORTX(transaction, options) {
+ const network = transaction.getNetwork();
const eventStrategy = new AnyForTxStrategy(getOrganizationEventHubs(network));
- return new TransactionEventHandler(transactionId, eventStrategy, options);
+ return new TransactionEventHandler(transaction, eventStrategy, options);
}
-function NETWORK_SCOPE_ALLFORTX(transactionId, network, options) {
+function NETWORK_SCOPE_ALLFORTX(transaction, options) {
+ const network = transaction.getNetwork();
const eventStrategy = new AllForTxStrategy(getNetworkEventHubs(network));
- return new TransactionEventHandler(transactionId, eventStrategy, options);
+ return new TransactionEventHandler(transaction, eventStrategy, options);
}
-function NETWORK_SCOPE_ANYFORTX(transactionId, network, options) {
+function NETWORK_SCOPE_ANYFORTX(transaction, options) {
+ const network = transaction.getNetwork();
const eventStrategy = new AnyForTxStrategy(getNetworkEventHubs(network));
- return new TransactionEventHandler(transactionId, eventStrategy, options);
+ return new TransactionEventHandler(transaction, eventStrategy, options);
}
/**
diff --git a/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js b/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js
new file mode 100644
index 0000000000..1cefec242d
--- /dev/null
+++ b/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const FabricConstants = require('fabric-client/lib/Constants');
+const RoundRobinEventHubSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy');
+
+function getOrganizationPeers(network) {
+ return network.getChannel().getPeersForOrg();
+}
+
+function filterEventEmittingPeers(peers) {
+ return peers.filter((peer) => peer.isInRole(FabricConstants.NetworkConfig.EVENT_SOURCE_ROLE));
+}
+
+function MSPID_SCOPE_ROUND_ROBIN(network) {
+ const orgPeers = getOrganizationPeers(network);
+ const eventEmittingPeers = filterEventEmittingPeers(orgPeers);
+ return new RoundRobinEventHubSelectionStrategy(eventEmittingPeers);
+}
+
+/**
+ * @typedef DefaultEventHubSelectionStrategies
+ * @memberof module:fabric-network
+ * @property {function} MSPID_SCOPE_ROUND_ROBIN Reconnect to any of the event emitting peers in the org after
+ * a disconnect occurs
+ */
+module.exports = {
+ MSPID_SCOPE_ROUND_ROBIN
+};
diff --git a/fabric-network/lib/impl/event/eventhubmanager.js b/fabric-network/lib/impl/event/eventhubmanager.js
new file mode 100644
index 0000000000..3a5f9bba13
--- /dev/null
+++ b/fabric-network/lib/impl/event/eventhubmanager.js
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const EventHubFactory = require('./eventhubfactory');
+
+/**
+ * The Event Hub Manager is responsible for creating and distributing event hubs.
+ * It uses the event hub factory to reuse event hubs that exists, and maintains
+ * its own list of new event hubs that are used for event replay
+ * @private
+ * @class
+ */
+class EventHubManager {
+ /**
+ * Constructor
+ * @param {module:fabric-network.Network} network The network
+ */
+ constructor(network) {
+ this.channel = network.getChannel();
+ this.eventHubFactory = new EventHubFactory(this.channel);
+ this.eventHubSelectionStrategy = network.getEventHubSelectionStrategy();
+ this.newEventHubs = [];
+ }
+ /**
+ * Gets an event hub. If given a peer, it will get that peers event hub, otherwise
+ * it will get the next peer defined by the {@link EventHubSelectionStrategy}
+ * @param {Peer} peer A peer instance
+ * @param {boolean} filtered Flag to decide between filtered and unfiltered events
+ * @returns {module:fabric-client.ChannelEventHub} The event hub
+ */
+ getEventHub(peer, filtered) {
+ if (!peer) {
+ peer = this.eventHubSelectionStrategy.getNextPeer();
+ }
+ peer = peer.getPeer ? peer.getPeer() : peer;
+ const eventHub = this.eventHubFactory.getEventHub(peer);
+ if (eventHub.isconnected() && eventHub.isFiltered() !== !!filtered) {
+ return this.getReplayEventHub(peer);
+ }
+ return eventHub;
+ }
+
+ /**
+ * Gets a list of event hubs from the {@link EventHubFactory} for a list of peers
+ * @param {module:fabric-client.Peer[]} peers A list of peer instances
+ */
+ getEventHubs(peers) {
+ return this.eventHubFactory.getEventHubs(peers);
+ }
+
+ /**
+ * Gets a new event hub instance for a give peer and updates the list of new event
+ * hubs that have been created
+ * @param {module:fabric-client.Peer} peer A peer instance
+ * @returns {module:fabric-client.ChannelEventHub} The event hub
+ */
+ getReplayEventHub(peer) {
+ for (const index in this.newEventHubs) {
+ const eventHub = this.newEventHubs[index];
+ if (this._isNewEventHub(eventHub) && (!peer || eventHub.getName() === peer.getName())) {
+ this.newEventHubs.splice(index, 1);
+ }
+ }
+ peer = this.eventHubSelectionStrategy.getNextPeer();
+ const eh = this.channel.newChannelEventHub(peer);
+ this.newEventHubs.push(eh);
+ return eh;
+ }
+
+ /**
+ * Will get a new event hub instance without the possibility of selecting a new peer
+ * @param {module:fabric-client.Peer} peer A peer instance
+ * @returns {module:fabric-client.ChannelEventHub} The event hub
+ */
+ getFixedEventHub(peer) {
+ const eventHub = this.channel.newChannelEventHub(peer);
+ this.newEventHubs.push(eventHub);
+ return eventHub;
+ }
+
+ /**
+ * When called with a peer, it updates the {@link EventHubSelectionStategy} with the
+ * new status of a peer to allow for intelligent strategies
+ * @param {module:fabric-client:Peer} deadPeer A peer instance
+ */
+ updateEventHubAvailability(deadPeer) {
+ return this.eventHubSelectionStrategy.updateEventHubAvailability(deadPeer);
+ }
+
+ /**
+ * Disconnect from and delete all event hubs
+ */
+ dispose() {
+ this.eventHubFactory.dispose();
+ this.newEventHubs.forEach((eh) => eh.disconnect());
+ }
+
+ getEventHubFactory() {
+ return this.eventHubFactory;
+ }
+
+ getPeers() {
+ return this.eventHubSelectionStrategy.getPeers();
+ }
+
+ /**
+ * Check if an event hub has any registrations
+ * @param {module:fabric-client.ChannelEventHub} eventHub An event hub instance
+ * @returns {boolean}
+ */
+ _isNewEventHub(eventHub) {
+ if (!eventHub) {
+ throw new Error('event hub not given');
+ }
+ const chaincodeRegistrations = Object.values(eventHub._chaincodeRegistrants).length;
+ const blockRegistrations = Object.values(eventHub._blockRegistrations).length;
+ const txRegistrations = Object.values(eventHub._transactionRegistrations).length;
+ return (chaincodeRegistrations + blockRegistrations + txRegistrations) === 0;
+ }
+}
+
+module.exports = EventHubManager;
diff --git a/fabric-network/lib/impl/event/filesystemcheckpointer.js b/fabric-network/lib/impl/event/filesystemcheckpointer.js
new file mode 100644
index 0000000000..299df23d97
--- /dev/null
+++ b/fabric-network/lib/impl/event/filesystemcheckpointer.js
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
+const BaseCheckpointer = require('./basecheckpointer');
+
+
+class FileSystemCheckpointer extends BaseCheckpointer {
+ constructor(channelName, listenerName, options = {}) {
+ super(options);
+ if (!options.basePath) {
+ options.basePath = path.join(os.homedir(), '/.hlf-checkpoint');
+ }
+ this._basePath = path.resolve(options.basePath); // Ensure that this path is correct
+ this._channelName = channelName;
+ this._listenerName = listenerName;
+ }
+
+ async _initialize() {
+ const checkpointPath = this._getCheckpointFileName();
+ await fs.ensureDir(path.join(this._basePath, this._channelName));
+ await fs.createFile(checkpointPath);
+ }
+
+ async save(transactionId, blockNumber) {
+ const checkpointPath = this._getCheckpointFileName(this._chaincodeId);
+ if (!(await fs.exists(checkpointPath))) {
+ await this._initialize();
+ }
+ const checkpoint = await this.load();
+ if (Number(checkpoint.blockNumber) === Number(blockNumber)) {
+ const transactionIds = checkpoint.transactionIds;
+ if (transactionId) {
+ transactionIds.push(transactionId);
+ }
+ checkpoint.transactionIds = transactionIds;
+ } else {
+ if (transactionId) {
+ checkpoint.transactionIds = [transactionId];
+ } else {
+ checkpoint.transactionIds = [];
+ }
+ checkpoint.blockNumber = blockNumber;
+ }
+ await fs.writeFile(checkpointPath, JSON.stringify(checkpoint));
+ }
+
+ async load() {
+ const checkpointPath = this._getCheckpointFileName(this._chaincodeId);
+ if (!(await fs.exists(checkpointPath))) {
+ await this._initialize();
+ }
+ const checkpointBuffer = (await fs.readFile(checkpointPath));
+ let checkpoint = checkpointBuffer.toString('utf8');
+ if (!checkpoint) {
+ checkpoint = {};
+ } else {
+ checkpoint = JSON.parse(checkpoint);
+ }
+ return checkpoint;
+ }
+
+ _getCheckpointFileName() {
+ let filePath = path.join(this._basePath, this._channelName);
+ if (this._chaincodeId) {
+ filePath = path.join(filePath, this._chaincodeId);
+ }
+ return path.join(filePath, this._listenerName);
+ }
+}
+
+module.exports = FileSystemCheckpointer;
diff --git a/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js b/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js
new file mode 100644
index 0000000000..2d91ff2cbd
--- /dev/null
+++ b/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const AbstractEventHubSelectionStrategy = require('./abstracteventhubselectionstrategy');
+
+/**
+ * The Round Robin Event Hub Strategy is a basic event hub selection strategy used to
+ * spread out load onto different event hubs and track event hub availability
+ *
+ * @memberof module:fabric-network
+ * @implements {module:fabric-network.AbstractEventHubSelectionStrategy}
+ * @class
+ */
+class RoundRobinEventHubSelectionStrategy extends AbstractEventHubSelectionStrategy {
+ /**
+ * Constructor.
+ * @param {Peer[]} peers The list of peers that the strategy can chose from
+ */
+ constructor(peers = []) {
+ super(peers);
+ this.lastPeerIdx = null;
+ }
+
+ /**
+ * Gets the next peer in the list of peers
+ * @returns {Peer}
+ */
+ getNextPeer() {
+ if (this.lastPeerIdx === null || this.lastPeerIdx === this.peers.length - 1) {
+ this.lastPeerIdx = 0;
+ } else {
+ this.lastPeerIdx++;
+ }
+ return this.peers[this.lastPeerIdx];
+ }
+}
+
+module.exports = RoundRobinEventHubSelectionStrategy;
diff --git a/fabric-network/lib/impl/event/transactioneventhandler.js b/fabric-network/lib/impl/event/transactioneventhandler.js
index cdeb827494..a04e42a87e 100644
--- a/fabric-network/lib/impl/event/transactioneventhandler.js
+++ b/fabric-network/lib/impl/event/transactioneventhandler.js
@@ -1,5 +1,5 @@
/**
- * Copyright 2018 IBM All Rights Reserved.
+ * Copyright 2019 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -29,12 +29,13 @@ class TransactionEventHandler {
/**
* Constructor.
* @private
- * @param {String} transactionId Transaction ID.
+ * @param {Transaction} transaction Traneaction object.
* @param {Object} strategy Event strategy implementation.
* @param {TransactionEventHandlerOptions} [options] Additional options.
*/
- constructor(transactionId, strategy, options) {
- this.transactionId = transactionId;
+ constructor(transaction, strategy, options) {
+ this.transaction = transaction;
+ this.transactionId = transaction.getTransactionID().getTransactionID();
this.strategy = strategy;
const defaultOptions = {
@@ -80,19 +81,17 @@ class TransactionEventHandler {
}
async _registerTxEventListeners() {
- const registrationOptions = {unregister: true};
+ const registrationOptions = {unregister: true, fixedEventHub: true};
const promises = this.eventHubs.map((eventHub) => {
- return new Promise((resolve) => {
+ return new Promise(async (resolve) => {
logger.debug('_registerTxEventListeners:', `registerTxEvent(${this.transactionId}) for event hub:`, eventHub.getName());
-
- eventHub.registerTxEvent(
- this.transactionId,
- (txId, code) => this._onEvent(eventHub, txId, code),
- (err) => this._onError(eventHub, err),
- registrationOptions
- );
- eventHub.connect();
+ await this.transaction.addCommitListener((err, txId, code) => {
+ if (err) {
+ return this._onError(eventHub, err);
+ }
+ return this._onEvent(eventHub, txId, code);
+ }, registrationOptions, eventHub);
resolve();
});
});
@@ -126,7 +125,7 @@ class TransactionEventHandler {
}
_onError(eventHub, err) {
- logger.info('_onError:', util.format('received error from peer %s: %s', eventHub.getPeerAddr(), err));
+ logger.debug('_onError:', util.format('received error from peer %s: %s', eventHub.getPeerAddr(), err));
this._receivedEventHubResponse(eventHub);
this.strategy.errorReceived(this._strategySuccess.bind(this), this._strategyFail.bind(this));
@@ -141,7 +140,7 @@ class TransactionEventHandler {
* @private
*/
_strategySuccess() {
- logger.info('_strategySuccess:', util.format('strategy success for transaction %j', this.transactionId));
+ logger.debug('_strategySuccess:', util.format('strategy success for transaction %j', this.transactionId));
this.cancelListening();
this._resolveNotificationPromise();
diff --git a/fabric-network/lib/network.js b/fabric-network/lib/network.js
index af1ce7372b..edb26bf64e 100644
--- a/fabric-network/lib/network.js
+++ b/fabric-network/lib/network.js
@@ -7,7 +7,9 @@
'use strict';
const FabricConstants = require('fabric-client/lib/Constants');
const Contract = require('./contract');
-const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener');
+const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener');
const logger = require('./logger').getLogger('Network');
const util = require('util');
@@ -31,8 +33,8 @@ class Network {
this.gateway = gateway;
this.channel = channel;
this.contracts = new Map();
- this.eventHubFactory = new EventHubFactory(channel);
this.initialized = false;
+ this.listeners = new Map();
}
/**
@@ -106,6 +108,12 @@ class Network {
// Must be created after channel initialization to ensure discovery has located peers
const queryHandlerOptions = this.gateway.getOptions().queryHandlerOptions;
this.queryHandler = queryHandlerOptions.strategy(this, queryHandlerOptions);
+
+ this.checkpointer = this.gateway.getOptions().checkpointer;
+
+ const eventHubSelectionOptions = this.gateway.getOptions().eventHubSelectionOptions;
+ this.eventHubSelectionStrategy = eventHubSelectionOptions.strategy(this);
+ this.eventHubManager = new EventHubManager(this);
}
/**
@@ -134,6 +142,7 @@ class Network {
this,
chaincodeId,
this.gateway,
+ this.getCheckpointer(),
name
);
this.contracts.set(key, contract);
@@ -144,33 +153,111 @@ class Network {
_dispose() {
logger.debug('in _dispose');
+ this.listeners.forEach(listener => listener.unregister());
// Danger as this cached in gateway, and also async so how would
// network._dispose() followed by network.initialize() be safe ?
// make this private is the safest option.
this.contracts.clear();
- this.eventHubFactory.dispose();
+ this.eventHubManager.dispose();
this.channel.close();
this.initialized = false;
}
/**
- * Get the event hub factory for this network.
+ * Get the query handler for this network.
* @private
- * @returns {EventHubFactory} An event hub factory.
+ * @returns {object} A query handler.
*/
- getEventHubFactory() {
- return this.eventHubFactory;
+ getQueryHandler() {
+ return this.queryHandler;
}
/**
- * Get the query handler for this network.
+ * Get the checkpoint factory
* @private
- * @returns {object} A query handler.
+ * @returns {Function} The checkpointer factory
*/
- getQueryHandler() {
- return this.queryHandler;
+ getCheckpointer() {
+ return this.checkpointer;
+ }
+
+ /**
+ * Get the event hub manager
+ * @private
+ * @returns {EventHubManager} An event hub manager
+ */
+ getEventHubManager() {
+ return this.eventHubManager;
+ }
+
+ /**
+ * Get the event hub selection strategy
+ * @private
+ * @returns {BaseEventHubSelectionStrategy}
+ */
+ getEventHubSelectionStrategy() {
+ return this.eventHubSelectionStrategy;
+ }
+
+ /**
+ * Save the listener to a map in Network
+ * @param {String} listenerName the name of the listener being saved
+ * @param {AbstractEventListener} listener the listener to be saved
+ * @private
+ */
+ saveListener(listenerName, listener) {
+ if (this.listeners.has(listenerName)) {
+ listener.unregister();
+ throw new Error(`Listener already exists with the name ${listenerName}`);
+ }
+ this.listeners.set(listenerName, listener);
+ }
+
+ /**
+ * Create a block event listener
+ * @param {String} listenerName the name of the listener
+ * @param {Function} callback the callback called when an event is triggered with signature (error, block)
+ * @param {Object} [options] Optional. Tjhe event listener options
+ */
+ async addBlockListener(listenerName, callback, options) {
+ if (!options) {
+ options = {};
+ }
+ options.replay = options.replay ? true : false;
+ options.checkpointer = this.getCheckpointer(options);
+ const listener = new BlockEventListener(this, listenerName, callback, options);
+ this.saveListener(listenerName, listener);
+ await listener.register();
+ return listener;
+ }
+
+ /**
+ * Create a commit event listener for this transaction.
+ * @param {string} transactionId The transactionId being watched
+ * @param {Function} callback - This callback will be triggered when
+ * a transaction commit event is emitted. It takes parameters
+ * of error, transactionId, transaction status and block number
+ * @param {RegistrationOptions} [options] - Optional. Options on
+ * registrations allowing start and end block numbers.
+ * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection
+ * @returns {CommitEventListener}
+ * @async
+ */
+ async addCommitListener(transactionId, callback, options, eventHub) {
+ if (!options) {
+ options = {};
+ }
+ options.replay = false;
+ options.checkpointer = null;
+ const listener = new CommitEventListener(this, transactionId, callback, options);
+ if (eventHub) {
+ listener.setEventHub(eventHub, options.fixedEventHub);
+ }
+ this.saveListener(listener.listenerName, listener);
+ await listener.register();
+ return listener;
}
}
diff --git a/fabric-network/lib/transaction.js b/fabric-network/lib/transaction.js
index 48b678565b..5f2ba04430 100644
--- a/fabric-network/lib/transaction.js
+++ b/fabric-network/lib/transaction.js
@@ -100,6 +100,14 @@ class Transaction {
return this;
}
+ /**
+ * Returns the network from the contract
+ * @returns {module:fabric-network.Network}
+ */
+ getNetwork() {
+ return this._contract.getNetwork();
+ }
+
/**
* Submit a transaction to the ledger. The transaction function name
* will be evaluated on the endorsing peers and then submitted to the ordering service
@@ -117,7 +125,7 @@ class Transaction {
const network = this._contract.getNetwork();
const channel = network.getChannel();
const txId = this._transactionId.getTransactionID();
- const eventHandler = this._createTxEventHandler(txId, network, this._contract.getEventHandlerOptions());
+ const eventHandler = this._createTxEventHandler(this, network, this._contract.getEventHandlerOptions());
const request = this._buildRequest(args);
@@ -237,6 +245,23 @@ class Transaction {
return this._queryHandler.evaluate(query);
}
+
+ /**
+ * Create a commit event listener for this transaction.
+ * @param {Function} callback - This callback will be triggered when
+ * a transaction commit event is emitted. It takes parameters
+ * of error, transactionId, transaction status and block number
+ * @param {RegistrationOptions} [options] - Optional. Options on
+ * registrations allowing start and end block numbers.
+ * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection
+ * @returns {CommitEventListener}
+ * @async
+ */
+ async addCommitListener(callback, options, eventHub) {
+ const txid = this.getTransactionID().getTransactionID();
+ const network = this._contract.getNetwork();
+ return network.addCommitListener(txid, callback, options, eventHub);
+ }
}
module.exports = Transaction;
diff --git a/fabric-network/test/contract.js b/fabric-network/test/contract.js
index 30a83e3f41..599868032a 100644
--- a/fabric-network/test/contract.js
+++ b/fabric-network/test/contract.js
@@ -8,10 +8,12 @@
const sinon = require('sinon');
const Channel = require('fabric-client/lib/Channel');
+const ChannelEventHub = require('fabric-client/lib/ChannelEventHub');
const Client = require('fabric-client');
const TransactionID = require('fabric-client/lib/TransactionID.js');
const chai = require('chai');
+const expect = chai.expect;
chai.use(require('chai-as-promised'));
chai.should();
@@ -20,6 +22,9 @@ const Gateway = require('../lib/gateway');
const Network = require('fabric-network/lib/network');
const Transaction = require('../lib/transaction');
const TransactionEventHandler = require('../lib/impl/event/transactioneventhandler');
+const BaseCheckpointer = require('./../lib/impl/event/basecheckpointer');
+const ContractEventListener = require('./../lib/impl/event/contracteventlistener');
+const EventHubManager = require('./../lib/impl/event/eventhubmanager');
describe('Contract', () => {
const chaincodeId = 'CHAINCODE_ID';
@@ -29,12 +34,18 @@ describe('Contract', () => {
let mockChannel, mockClient, mockGateway;
let contract;
let mockTransactionID;
+ let mockCheckpointer;
+ let mockEventHubManager;
+ let mockEventHub;
beforeEach(() => {
+ mockEventHub = sinon.createStubInstance(ChannelEventHub);
mockChannel = sinon.createStubInstance(Channel);
mockClient = sinon.createStubInstance(Client);
mockGateway = sinon.createStubInstance(Gateway);
+ mockCheckpointer = sinon.createStubInstance(BaseCheckpointer);
+ mockEventHubManager = sinon.createStubInstance(EventHubManager);
mockGateway.getClient.returns(mockClient);
mockGateway.getOptions.returns({
eventHandlerOptions: {
@@ -46,15 +57,18 @@ describe('Contract', () => {
}
}
});
-
network = new Network(mockGateway, mockChannel);
+ mockEventHubManager.getEventHub.returns(mockEventHub);
+ mockEventHubManager.getReplayEventHub.returns(mockEventHub);
+ network.eventHubManager = mockEventHubManager;
+ mockEventHubManager.getPeers.returns(['peer1']);
mockTransactionID = sinon.createStubInstance(TransactionID);
mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000');
mockClient.newTransactionID.returns(mockTransactionID);
mockChannel.getName.returns('testchainid');
- contract = new Contract(network, chaincodeId, mockGateway);
+ contract = new Contract(network, chaincodeId, mockGateway, mockCheckpointer);
});
afterEach(() => {
@@ -63,7 +77,7 @@ describe('Contract', () => {
describe('#constructor', () => {
it('throws if namespace is not a string', () => {
- (() => new Contract(network, chaincodeId, mockGateway, 123))
+ (() => new Contract(network, chaincodeId, mockGateway, mockCheckpointer, 123))
.should.throw(/namespace/i);
});
});
@@ -89,6 +103,43 @@ describe('Contract', () => {
});
});
+ describe('#getCheckpointer', () => {
+ it('should return the global checkpointer if it is undefined in options', () => {
+ const checkpointer = contract.getCheckpointer();
+ expect(checkpointer).to.equal(mockCheckpointer);
+ });
+
+ it('should return the global checkpointer if it is undefined in options object', () => {
+ const checkpointer = contract.getCheckpointer({});
+ expect(checkpointer).to.equal(mockCheckpointer);
+ });
+
+ it('should return the global checkpointer if it is true in options', () => {
+ const checkpointer = contract.getCheckpointer({checkpointer: 'LOL'});
+ expect(checkpointer).to.equal(mockCheckpointer);
+ });
+
+ it('should return the checkpointer passed as an option', () => {
+ const checkpointerFactory = () => {};
+ const checkpointer = contract.getCheckpointer({checkpointer: checkpointerFactory});
+ expect(checkpointer).to.equal(checkpointerFactory);
+ expect(checkpointer).to.not.equal(mockCheckpointer);
+ });
+
+ it('should return null if checkpointer is false', () => {
+ const checkpointer = contract.getCheckpointer({checkpointer: false});
+ expect(checkpointer).to.be.null;
+ });
+ });
+
+ describe('#getEventHubSelectionStrategy', () => {
+ it('should return the eventhub selection strategy', () => {
+ network.eventHubSelectionStrategy = 'selection-strategy';
+ const strategy = contract.getEventHubSelectionStrategy();
+ expect(strategy).to.equal('selection-strategy');
+ });
+ });
+
describe('#getEventHandlerOptions', () => {
it('returns event handler options from the gateway', () => {
const result = contract.getEventHandlerOptions();
@@ -109,7 +160,7 @@ describe('Contract', () => {
const name = 'name';
const expected = `${namespace}:${name}`;
- contract = new Contract(network, chaincodeId, mockGateway, namespace);
+ contract = new Contract(network, chaincodeId, mockGateway, mockCheckpointer, namespace);
const result = contract.createTransaction(name);
result.getName().should.equal(expected);
@@ -163,4 +214,39 @@ describe('Contract', () => {
result.should.equal(expected);
});
});
+
+
+ describe('#addContractListener', () => {
+ let listenerName;
+ let testEventName;
+ let callback;
+ beforeEach(() => {
+ listenerName = 'testContractListener';
+ testEventName = 'testEvent';
+ callback = () => {};
+ });
+ it('should create options if the options param is undefined', async () => {
+ const listener = await contract.addContractListener(listenerName, testEventName, callback);
+ expect(listener).to.be.instanceof(ContractEventListener);
+ expect(network.listeners.get(listenerName)).to.equal(listener);
+ });
+
+ it('should create an instance of ContractEventListener and add it to the list of listeners', async () => {
+ const listener = await contract.addContractListener(listenerName, testEventName, callback, {});
+ expect(listener).to.be.instanceof(ContractEventListener);
+ expect(network.listeners.get(listenerName)).to.equal(listener);
+ });
+
+ it('should change options.replay=undefined to options.replay=false', async () => {
+ sinon.spy(contract, 'getCheckpointer');
+ await contract.addContractListener(listenerName, testEventName, callback, {replay: undefined});
+ sinon.assert.calledWith(contract.getCheckpointer, {replay: false, checkpointer: sinon.match.instanceOf(BaseCheckpointer)});
+ });
+
+ it('should change options.replay=\'true\' to options.replay=true', async () => {
+ sinon.spy(contract, 'getCheckpointer');
+ await contract.addContractListener(listenerName, testEventName, callback, {replay: 'true'});
+ sinon.assert.calledWith(contract.getCheckpointer, {checkpointer: sinon.match.instanceOf(BaseCheckpointer), replay: true});
+ });
+ });
});
diff --git a/fabric-network/test/gateway.js b/fabric-network/test/gateway.js
index 3c534c19a5..b90b4b572d 100644
--- a/fabric-network/test/gateway.js
+++ b/fabric-network/test/gateway.js
@@ -397,6 +397,7 @@ describe('Gateway', () => {
mockClient.getChannel.withArgs('bar').returns(mockInternalChannel);
gateway.getCurrentIdentity = sinon.stub().returns({_mspId: 'MSP01'});
gateway.getOptions().queryHandlerOptions.strategy = () => {};
+ gateway.getOptions().eventHubSelectionOptions.strategy = () => {};
const network2 = await gateway.getNetwork('bar');
network2.should.be.instanceof(Network);
@@ -410,6 +411,7 @@ describe('Gateway', () => {
mockClient.newChannel.withArgs('bar').returns(mockInternalChannel);
gateway.getCurrentIdentity = sinon.stub().returns({_mspId: 'MSP01'});
gateway.getOptions().queryHandlerOptions.strategy = () => {};
+ gateway.getOptions().eventHubSelectionOptions.strategy = () => {};
const network2 = await gateway.getNetwork('bar');
network2.should.be.instanceof(Network);
diff --git a/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js b/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js
new file mode 100644
index 0000000000..a57004d3c1
--- /dev/null
+++ b/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2018 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy');
+
+describe('AbstractEventHubStrategy', () => {
+ let strategy;
+ let peers;
+
+ beforeEach(() => {
+ peers = ['peer1'];
+ strategy = new AbstractEventHubSelectionStrategy(peers);
+ });
+ describe('#getNextPeer', () => {
+ it('should throw', () => {
+ expect(() => strategy.getNextPeer()).to.throw(/Abstract/);
+ });
+ });
+ describe('#updateEventHubAvailability', () => {
+ it('should throw', () => {
+ expect(strategy.updateEventHubAvailability()).to.be.undefined;
+ });
+ });
+
+ describe('#getPeers', () => {
+ it('should return a list of peers', () => {
+ expect(strategy.getPeers()).to.equal(peers);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/abstracteventlistener.js b/fabric-network/test/impl/event/abstracteventlistener.js
new file mode 100644
index 0000000000..712a71633c
--- /dev/null
+++ b/fabric-network/test/impl/event/abstracteventlistener.js
@@ -0,0 +1,200 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const Channel = require('fabric-client/lib/Channel');
+const ChannelEventHub = require('fabric-client/lib/ChannelEventHub');
+const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError');
+const Contract = require('./../../../lib/contract');
+const Network = require('./../../../lib/network');
+const EventHubManager = require('./../../../lib/impl/event/eventhubmanager');
+const AbstractEventListener = require('./../../../lib/impl/event/abstracteventlistener');
+const FileSystemCheckpointer = require('./../../../lib/impl/event/filesystemcheckpointer');
+
+describe('AbstractEventListener', () => {
+ let sandbox;
+
+ let testListener;
+ let contractStub;
+ let networkStub;
+ let checkpointerStub;
+ let eventHubManagerStub;
+ let channelStub;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+
+ eventHubManagerStub = sandbox.createStubInstance(EventHubManager);
+ contractStub = sandbox.createStubInstance(Contract);
+ networkStub = sandbox.createStubInstance(Network);
+ networkStub.getEventHubManager.returns(eventHubManagerStub);
+ contractStub.getNetwork.returns(networkStub);
+ checkpointerStub = sandbox.createStubInstance(FileSystemCheckpointer);
+ checkpointerStub.setChaincodeId = sandbox.stub();
+ channelStub = sandbox.createStubInstance(Channel);
+ networkStub.getChannel.returns(channelStub);
+ channelStub.getName.returns('mychannel');
+ eventHubManagerStub.getPeers.returns(['peer1']);
+ channelStub.queryInfo.returns({height: 0});
+
+ contractStub.getChaincodeId.returns('ccid');
+ const callback = (err) => {};
+ testListener = new AbstractEventListener(networkStub, 'testListener', callback, {option: 'anoption', replay: true});
+
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ describe('#constructor', () => {
+ it('should set the correct properties on instantiation', () => {
+ const callback = (err) => {};
+ const listener = new AbstractEventListener(networkStub, 'testlistener', callback, {option: 'anoption'});
+ expect(listener.network).to.equal(networkStub);
+ expect(listener.listenerName).to.equal('testlistener');
+ expect(listener.eventCallback).to.equal(callback);
+ expect(listener.options).to.deep.equal({option: 'anoption'});
+ expect(listener.checkpointer).to.be.undefined;
+ expect(listener._registered).to.be.false;
+ expect(listener._firstCheckpoint).to.deep.equal({});
+ expect(listener._registration).to.be.null;
+ });
+
+ it('should set options if options is undefined', () => {
+ const callback = (err) => {};
+ const listener = new AbstractEventListener(networkStub, 'testlistener', callback);
+ expect(listener.options).to.deep.equal({});
+ });
+
+ it('should set this.filtered to be options.filtered', () => {
+ const listener = new AbstractEventListener(networkStub, 'testListener', () => {}, {filtered: false});
+ expect(listener._filtered).to.be.false;
+ });
+ });
+
+ describe('#register', () => {
+ it('should throw if the listener is already registered', () => {
+ testListener._registered = true;
+ expect(testListener.register()).to.be.rejectedWith('Listener already registered');
+ });
+
+ it('should not call checkpointer._initialize() or checkpointer.load()', async () => {
+ await testListener.register();
+ sinon.assert.notCalled(checkpointerStub.load);
+ });
+
+ it('should not call checkpointer.initialize()', async () => {
+ const checkpoint = {transactionId: 'txid', blockNumber: '10'};
+ checkpointerStub.load.returns(checkpoint);
+ testListener.checkpointer = checkpointerStub;
+ await testListener.register();
+ sinon.assert.called(checkpointerStub.load);
+ expect(testListener.options.startBlock.toNumber()).to.equal(10); // Start block is a Long
+ expect(testListener._firstCheckpoint).to.deep.equal(checkpoint);
+ });
+
+ it('should disconnect and reset the event hub if it emits the wrong type of events', async () => {
+ const eventHub = sinon.createStubInstance(ChannelEventHub);
+ eventHub.isFiltered.returns(true);
+ eventHub.isconnected.returns(true);
+ testListener.eventHub = eventHub;
+ testListener._filtered = false;
+ await testListener.register();
+ sinon.assert.called(eventHub.disconnect);
+ expect(testListener.eventHub).to.be.null;
+ });
+
+ it('should return undefined and log when no peers are available', async() => {
+ eventHubManagerStub.getPeers.returns([]);
+ await testListener.register();
+ });
+
+ it('should call the checkpointer factory if it is set', async () => {
+ const checkpointerFactoryStub = sinon.stub().returns(checkpointerStub);
+ const listener = new AbstractEventListener(networkStub, 'testlistener', () => {}, {replay: true, checkpointer: {factory: checkpointerFactoryStub}});
+ await listener.register();
+ sinon.assert.calledWith(checkpointerFactoryStub, 'mychannel', 'testlistener');
+ sinon.assert.called(checkpointerStub.setChaincodeId);
+ expect(listener.checkpointer).to.equal(checkpointerStub);
+ });
+
+ it('should log an error if replay is enabled and no checkpointer is given', async () => {
+ const listener = new AbstractEventListener(networkStub, 'testlistener', () => {}, {replay: true});
+ await listener.register();
+ });
+
+ });
+
+ describe('#unregister', () => {
+ beforeEach(async () => {
+ checkpointerStub.load.returns({transactionId: 'txid', blockNumber: '10'});
+ testListener.checkpointer = checkpointerStub;
+ await testListener.register();
+ });
+ it('should reset the correct variables', async () => {
+ await testListener.unregister();
+ expect(testListener._registered).to.be.false;
+ expect(testListener.startBlock).to.be.undefined;
+ expect(testListener.options.endBlock).to.be.undefined;
+ expect(testListener._firstCheckpoint).to.deep.equal({});
+ });
+ });
+
+ describe('#isRegistered', () => {
+ it('should return false if the listener has not been registered', () => {
+ expect(testListener.isregistered()).to.be.false;
+ });
+
+ // Abstract listener does not change the register status
+ it('should return false if the listener has been registered', async () => {
+ await testListener.register();
+ expect(testListener.isregistered()).to.be.false;
+ });
+
+ it('should return false if registered and unregistered', async () => {
+ await testListener.register();
+ testListener.unregister();
+ expect(testListener.isregistered()).to.be.false;
+ });
+ });
+
+ describe('#getCheckpointer', () => {
+ it('should return undefined if checkpointer has not been set', () => {
+ expect(testListener.getCheckpointer()).to.be.undefined;
+ });
+
+ it('should return the checkpointer if it has been set', () => {
+ testListener.checkpointer = checkpointerStub;
+ expect(testListener.getCheckpointer()).to.equal(checkpointerStub);
+ });
+ });
+
+ describe('#getEventHubManager', () => {
+ it('shouild return the event hub manager from the network', () => {
+ expect(testListener.getEventHubManager()).to.equal(eventHubManagerStub);
+ });
+ });
+
+ describe('#_isShutdownMessage', () => {
+ it('should return false if an error is not given', () => {
+ expect(testListener._isShutdownMessage()).to.be.false;
+ });
+
+ it('should return false if error message does not match', () => {
+ expect(testListener._isShutdownMessage(new Error('An error'))).to.be.false;
+ });
+
+ it('should return true if the error message does match', () => {
+ expect(testListener._isShutdownMessage(new EventHubDisconnectError())).to.be.true;
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/basecheckpointer.js b/fabric-network/test/impl/event/basecheckpointer.js
new file mode 100644
index 0000000000..2df881e6e2
--- /dev/null
+++ b/fabric-network/test/impl/event/basecheckpointer.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+chai.use(require('chai-as-promised'));
+
+const BaseCheckpointer = require('./../../../lib/impl/event/basecheckpointer');
+
+describe('BaseCheckpointer', () => {
+ describe('#constructor', () => {
+ it('options should be set', () => {
+ const checkpointer = new BaseCheckpointer();
+ expect(checkpointer.options).to.deep.equal({});
+ });
+
+ it('options should be set to an object', () => {
+ const checkpointer = new BaseCheckpointer({option: 'anoption'});
+ expect(checkpointer.options).to.deep.equal({option: 'anoption'});
+ });
+ });
+
+ describe('#save', () => {
+ it('should throw an exception', () => {
+ const checkpointer = new BaseCheckpointer();
+ expect(checkpointer.save()).to.be.rejectedWith('Method has not been implemented');
+ });
+ });
+
+ describe('#load', () => {
+ it('should throw an exception', () => {
+ const checkpointer = new BaseCheckpointer();
+ expect(checkpointer.load()).to.be.rejectedWith('Method has not been implemented');
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/blockeventlistener.js b/fabric-network/test/impl/event/blockeventlistener.js
new file mode 100644
index 0000000000..92a7a70432
--- /dev/null
+++ b/fabric-network/test/impl/event/blockeventlistener.js
@@ -0,0 +1,189 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+chai.use(require('chai-as-promised'));
+
+const Contract = require('fabric-network/lib/contract');
+const Network = require('fabric-network/lib/network');
+const ChannelEventHub = require('fabric-client/lib/ChannelEventHub');
+const Channel = require('fabric-client/lib/Channel');
+const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError');
+const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const Checkpointer = require('fabric-network/lib/impl/event/basecheckpointer');
+
+describe('BlockEventListener', () => {
+ let sandbox;
+ let channelStub;
+ let contractStub;
+ let networkStub;
+ let eventHubStub;
+ let checkpointerStub;
+ let eventHubManagerStub;
+ let blockEventListener;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ channelStub = sandbox.createStubInstance(Channel);
+ channelStub.getName.returns('mychannel');
+ contractStub = sandbox.createStubInstance(Contract);
+ contractStub.getChaincodeId.returns('chaincodeid');
+ networkStub = sandbox.createStubInstance(Network);
+ networkStub.getChannel.returns(channelStub);
+ contractStub.getNetwork.returns(networkStub);
+ eventHubManagerStub = sinon.createStubInstance(EventHubManager);
+ eventHubManagerStub.getPeers.returns(['peer1']);
+ networkStub.getEventHubManager.returns(eventHubManagerStub);
+ eventHubStub = sandbox.createStubInstance(ChannelEventHub);
+ checkpointerStub = sandbox.createStubInstance(Checkpointer);
+ blockEventListener = new BlockEventListener(networkStub, 'test', () => {}, {replay: true});
+ });
+ describe('#register', () => {
+ it('should register a block event, connect to the event hub and set the register flag', async () => {
+ blockEventListener.eventHub = eventHubStub;
+ sandbox.spy(blockEventListener._onEvent, 'bind');
+ sandbox.spy(blockEventListener._onError, 'bind');
+ await blockEventListener.register();
+ sinon.assert.calledWith(
+ eventHubStub.registerBlockEvent,
+ sinon.match.func,
+ sinon.match.func,
+ {replay: true}
+ );
+ sinon.assert.calledWith(blockEventListener._onEvent.bind, blockEventListener);
+ sinon.assert.calledWith(blockEventListener._onError.bind, blockEventListener);
+ sinon.assert.called(eventHubStub.connect);
+ expect(blockEventListener._registered).to.be.true;
+ });
+
+ it('should call _registerWithNewEventHub', async () => {
+ sandbox.stub(blockEventListener, '_registerWithNewEventHub');
+ await blockEventListener.register();
+ sinon.assert.called(blockEventListener._registerWithNewEventHub);
+ });
+ });
+
+ describe('#unregister', () => {
+ it('should not call ChannelEventHub.unregisterBlockEvent', async () => {
+ await blockEventListener.unregister();
+ sinon.assert.notCalled(eventHubStub.unregisterBlockEvent);
+ });
+
+ it('should call ChannelEventHub.unregisterBlockEvent', async () => {
+ eventHubStub.registerBlockEvent.returns('registration');
+ blockEventListener.eventHub = eventHubStub;
+ await blockEventListener.register();
+ blockEventListener.unregister();
+ sinon.assert.calledWith(eventHubStub.unregisterBlockEvent, 'registration');
+ });
+ });
+
+ describe('#_onEvent', () => {
+ beforeEach(() => {
+ blockEventListener._registration = {};
+ sandbox.spy(blockEventListener, 'unregister');
+ sandbox.stub(blockEventListener, 'eventCallback');
+ });
+
+ it('should call the event callback', () => {
+ const block = {number: '10'};
+ blockEventListener._onEvent(block);
+ sinon.assert.calledWith(blockEventListener.eventCallback, null, block);
+ sinon.assert.notCalled(checkpointerStub.save);
+ sinon.assert.notCalled(blockEventListener.unregister);
+ });
+
+ it('should save a checkpoint', () => {
+ const block = {number: '10'};
+ blockEventListener.checkpointer = checkpointerStub;
+ blockEventListener._onEvent(block);
+ sinon.assert.calledWith(checkpointerStub.save, null, 10);
+ });
+
+ it('should unregister if registration.unregister is set', () => {
+ const block = {number: '10'};
+ blockEventListener._registration.unregister = true;
+ blockEventListener._onEvent(block);
+ sinon.assert.calledWith(blockEventListener.eventCallback, null, block);
+ sinon.assert.called(blockEventListener.unregister);
+ });
+
+ it ('should not save a checkpoint if the callback fails', () => {
+ const block = {number: '10'};
+ blockEventListener.eventCallback.throws(new Error());
+ blockEventListener.checkpointer = checkpointerStub;
+ blockEventListener._onEvent(block);
+ sinon.assert.calledWith(blockEventListener.eventCallback, null, block);
+ sinon.assert.calledWith(checkpointerStub.save, null, 10);
+ });
+ });
+
+ describe('#_onError', () => {
+ beforeEach(() => {
+ blockEventListener.eventHub = eventHubStub;
+ eventHubStub._peer = 'peer';
+ blockEventListener._registration = {};
+ sandbox.spy(blockEventListener, 'unregister');
+ sandbox.stub(blockEventListener, 'eventCallback');
+ sandbox.stub(blockEventListener, '_registerWithNewEventHub');
+ });
+
+ it('should call the event callback with an error', async () => {
+ const error = new EventHubDisconnectError();
+ await blockEventListener._onError(error);
+ sinon.assert.calledWith(blockEventListener.eventCallback, error);
+ });
+
+ it('should update event hub availability and reregister if disconnected', async () => {
+ const error = new EventHubDisconnectError();
+ blockEventListener.eventHub = eventHubStub;
+ blockEventListener._registered = true;
+ await blockEventListener._onError(error);
+ sinon.assert.calledWith(eventHubManagerStub.updateEventHubAvailability, 'peer');
+ sinon.assert.called(blockEventListener._registerWithNewEventHub);
+ sinon.assert.calledWith(blockEventListener.eventCallback, error);
+ });
+
+ it('should call the error callback if the error is null', async () => {
+ const error = null;
+ blockEventListener.eventHub = eventHubStub;
+ blockEventListener._registered = true;
+ await blockEventListener._onError(error);
+ sinon.assert.calledWith(blockEventListener.eventCallback, error);
+ });
+ });
+
+ describe('#_registerWithNewEventHub', () => {
+ beforeEach(() => {
+ blockEventListener._registration = {};
+ sandbox.spy(blockEventListener, 'unregister');
+ sandbox.stub(blockEventListener, 'eventCallback');
+ eventHubManagerStub.getReplayEventHub.returns(eventHubStub);
+ eventHubManagerStub.getEventHub.returns(eventHubStub);
+ sinon.stub(blockEventListener, 'register');
+ });
+
+ it('should call unregister, get a new event hub and reregister', () => {
+ blockEventListener._registerWithNewEventHub();
+ sinon.assert.called(blockEventListener.unregister);
+ sinon.assert.called(eventHubManagerStub.getEventHub);
+ sinon.assert.called(blockEventListener.register);
+ });
+
+ it('should get a replay event hub if a checkpointer is present', () => {
+ blockEventListener.checkpointer = checkpointerStub;
+ blockEventListener._registerWithNewEventHub();
+ sinon.assert.called(blockEventListener.unregister);
+ sinon.assert.called(eventHubManagerStub.getReplayEventHub);
+ sinon.assert.called(blockEventListener.register);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/checkpointfactories.js b/fabric-network/test/impl/event/checkpointfactories.js
new file mode 100644
index 0000000000..8df6fc5a06
--- /dev/null
+++ b/fabric-network/test/impl/event/checkpointfactories.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories');
+const FileSystemCheckpointer = require('fabric-network/lib/impl/event/filesystemcheckpointer');
+
+describe('CheckpointFactories', () => {
+ describe('#FILE_SYSTEM_CHECKPOINTER', () => {
+ it('should return an instance of the file system checkpointer', () => {
+ const checkpointer = CheckpointFactories.FILE_SYSTEM_CHECKPOINTER('channelName', 'listnerName');
+ expect(checkpointer).to.be.instanceof(FileSystemCheckpointer);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/commiteventlistener.js b/fabric-network/test/impl/event/commiteventlistener.js
new file mode 100644
index 0000000000..143cd28a99
--- /dev/null
+++ b/fabric-network/test/impl/event/commiteventlistener.js
@@ -0,0 +1,228 @@
+/**
+ * Copyright 2019 Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+chai.use(require('chai-as-promised'));
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const Contract = require('fabric-network/lib/contract');
+const Network = require('fabric-network/lib/network');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const ChannelEventHub = require('fabric-client/lib/ChannelEventHub');
+const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener');
+
+describe('CommitEventListener', () => {
+ let sandbox;
+ let eventHubManagerStub;
+ let eventHubStub;
+ let contractStub;
+ let networkStub;
+ let listener;
+ let callback;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ eventHubStub = sandbox.createStubInstance(ChannelEventHub);
+ eventHubStub._transactionRegistrations = [];
+ contractStub = sandbox.createStubInstance(Contract);
+ networkStub = sandbox.createStubInstance(Network);
+ contractStub.getNetwork.returns(networkStub);
+ eventHubManagerStub = sinon.createStubInstance(EventHubManager);
+ eventHubManagerStub.getPeers.returns(['peer1']);
+ networkStub.getEventHubManager.returns(eventHubManagerStub);
+
+ callback = () => {};
+ listener = new CommitEventListener(networkStub, 'transactionId', callback, {});
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ describe('#constructor', () => {
+ it('should set the listener name and transactionId', () => {
+ expect(listener.transactionId).to.equal('transactionId');
+ expect(listener.listenerName).to.match(/^transactionId[.0-9]+$/);
+ });
+ });
+
+ describe('#register', () => {
+ beforeEach(() => {
+ sandbox.stub(listener, '_registerWithNewEventHub');
+ // sandbox.stub(eventHubManagerStub, 'getEventHub');
+ });
+
+ it('should grab a new event hub if one isnt given', async () => {
+ await listener.register();
+ sinon.assert.called(listener._registerWithNewEventHub);
+ });
+
+ it('should assign a new event hub if given on has registrations', async () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ newEventHub._transactionRegistrations = {};
+ eventHubManagerStub.getEventHub.returns(newEventHub);
+ listener.eventHub = eventHubStub;
+ eventHubStub._peer = 'peer';
+ eventHubStub._transactionRegistrations = {transactionId: 'registration'};
+ await listener.register();
+ sinon.assert.calledWith(eventHubManagerStub.getEventHub, 'peer');
+ });
+
+ it('should call registerTxEvent', async () => {
+ listener.eventHub = eventHubStub;
+ await listener.register();
+ sinon.assert.calledWith(
+ eventHubStub.registerTxEvent,
+ 'transactionId',
+ sinon.match.func,
+ sinon.match.func,
+ {unregister: true}
+ );
+ sinon.assert.called(eventHubStub.connect);
+ expect(listener._registered).to.be.true;
+ });
+
+ it('should assign an an event hub instance from the same peer if options.fixedEventHub is true', async () => {
+ eventHubManagerStub.getFixedEventHub.returns(eventHubStub);
+ eventHubStub._peer = 'peer';
+ listener.setEventHub(eventHubStub, true);
+ eventHubStub._transactionRegistrations = {transactionId: {}};
+ await listener.register();
+ sinon.assert.calledWith(eventHubManagerStub.getFixedEventHub, eventHubStub._peer);
+ });
+ });
+
+ describe('#unregister', () => {
+ it('should not call ChannelEventHub.unregisterTxEvent', () => {
+ listener.unregister();
+ sinon.assert.notCalled(eventHubStub.unregisterTxEvent);
+ });
+
+ it('should call ChannelEventHub.unregisterBlockEvent', () => {
+ listener.eventHub = eventHubStub;
+ listener.register();
+ listener.unregister();
+ sinon.assert.calledWith(eventHubStub.unregisterTxEvent, 'transactionId');
+ });
+ });
+
+ describe('#_onEvent', () => {
+ beforeEach(() => {
+ listener._registration = {};
+ sandbox.spy(listener, 'unregister');
+ sandbox.stub(listener, 'eventCallback');
+ });
+
+ it('should call the event callback', () => {
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ listener._onEvent(transactionId, status, blockNumber);
+ sinon.assert.calledWith(listener.eventCallback, null, transactionId, status, Number(blockNumber));
+ sinon.assert.notCalled(listener.unregister);
+ });
+
+ it('should unregister if registration.unregister is set', () => {
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ listener._registration.unregister = true;
+ listener._onEvent(transactionId, status, blockNumber);
+ sinon.assert.calledWith(listener.eventCallback, null, transactionId, status, 10);
+ sinon.assert.called(listener.unregister);
+ });
+
+ it('should not fail if eventCallback throws', () => {
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ listener.eventCallback.throws(new Error('forced error'));
+ listener._onEvent(transactionId, status, blockNumber);
+ });
+ });
+
+ describe('#_onError', () => {
+ beforeEach(() => {
+ eventHubStub._peer = 'peer';
+ listener._registration = {};
+ sandbox.spy(listener, 'unregister');
+ sandbox.stub(listener, 'eventCallback');
+ });
+ it('should call eventCallback', () => {
+ listener.eventHub = eventHubStub;
+ const error = new Error();
+ listener._onError(error);
+ sinon.assert.calledWith(listener.eventCallback, error);
+ });
+ });
+
+ describe('#setEventHub', () => {
+ it('should set the eventhub', () => {
+ listener.setEventHub('new event hub');
+ expect(listener.eventHub).to.equal('new event hub');
+ });
+
+ it('should set options.fixedEventHub', () => {
+ listener.setEventHub('new event hub', true);
+ expect(listener.options.fixedEventHub).to.be.true;
+ });
+ });
+
+ describe('#_registerWithNewEventHub', () => {
+ beforeEach(() => {
+ listener._registration = {};
+ sandbox.spy(listener, 'unregister');
+ sandbox.stub(listener, 'eventCallback');
+ eventHubManagerStub.getReplayEventHub.returns(eventHubStub);
+ sinon.stub(listener, 'register');
+ });
+
+ it('should call the correct methods', () => {
+ listener._registerWithNewEventHub();
+ sinon.assert.called(eventHubManagerStub.getReplayEventHub);
+ expect(listener.eventHub).to.equal(eventHubStub);
+ expect(listener.options.disconnect).to.be.true;
+ sinon.assert.called(listener.register);
+ });
+
+ it('should call EventHubManager.getFixedEventHub if options.fixedEventHub', () => {
+ eventHubStub._peer = 'peer';
+ listener.eventHub = eventHubStub;
+ listener.options.fixedEventHub = true;
+ listener._registerWithNewEventHub();
+ sinon.assert.calledWith(eventHubManagerStub.getFixedEventHub, eventHubStub._peer);
+ });
+
+ it('should unregister if the listener is already registered', () => {
+ listener._registered = true;
+ listener._registerWithNewEventHub();
+ sinon.assert.called(listener.unregister);
+ });
+
+ it('should throw if options.fixedEventHub is set and no event hub is given', () => {
+ listener.options.fixedEventHub = true;
+ expect(listener._registerWithNewEventHub()).to.be.rejectedWith();
+ });
+ });
+
+ describe('#_isAlreadyRegistered', () => {
+ it('should throw if no event hub is given', () => {
+ expect(() => listener._isAlreadyRegistered()).to.throw(/Event hub not given/);
+ });
+
+ it('should return false if no registration exists', () => {
+ expect(listener._isAlreadyRegistered(eventHubStub)).to.be.false;
+ });
+
+ it('should return true if registration exists', () => {
+ eventHubStub._transactionRegistrations = {transactionId: 'registration'};
+ expect(listener._isAlreadyRegistered(eventHubStub)).to.be.true;
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/contracteventlistener.js b/fabric-network/test/impl/event/contracteventlistener.js
new file mode 100644
index 0000000000..03dfa0d90e
--- /dev/null
+++ b/fabric-network/test/impl/event/contracteventlistener.js
@@ -0,0 +1,231 @@
+/*
+ Copyright 2019 IBM All Rights Reserved.
+
+ SPDX-License-Identifier: Apache-2.0
+*/
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const Contract = require('fabric-network/lib/contract');
+const Network = require('fabric-network/lib/network');
+const Channel = require('fabric-client/lib/Channel');
+const ChannelEventHub = require('fabric-client/lib/ChannelEventHub');
+const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError');
+const ContractEventListener = require('fabric-network/lib/impl/event/contracteventlistener');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const Checkpointer = require('fabric-network/lib/impl/event/basecheckpointer');
+
+describe('ContractEventListener', () => {
+ let sandbox;
+ let contractStub;
+ let networkStub;
+ let channelStub;
+ let eventHubStub;
+ let checkpointerStub;
+ let eventHubManagerStub;
+ let contractEventListener;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ contractStub = sandbox.createStubInstance(Contract);
+ contractStub.getChaincodeId.returns('chaincodeid');
+ networkStub = sandbox.createStubInstance(Network);
+ channelStub = sandbox.createStubInstance(Channel);
+ channelStub.getName.returns('channelName');
+ networkStub.getChannel.returns(channelStub);
+ contractStub.getNetwork.returns(networkStub);
+ eventHubManagerStub = sinon.createStubInstance(EventHubManager);
+ networkStub.getEventHubManager.returns(eventHubManagerStub);
+ eventHubManagerStub.getPeers.returns(['peer1']);
+ eventHubStub = sandbox.createStubInstance(ChannelEventHub);
+ checkpointerStub = sandbox.createStubInstance(Checkpointer);
+ contractEventListener = new ContractEventListener(contractStub, 'test', 'eventName', () => {}, {});
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ describe('#contructor', () => {
+ it('should set the listener name', () => {
+ expect(contractEventListener.eventName).to.equal('eventName');
+ });
+ });
+
+ describe('#register', () => {
+ it('should register a block event, connect to the event hub and set the register flag', async () => {
+ contractEventListener.eventHub = eventHubStub;
+ sandbox.spy(contractEventListener._onEvent, 'bind');
+ sandbox.spy(contractEventListener._onError, 'bind');
+ await contractEventListener.register();
+ sinon.assert.calledWith(
+ eventHubStub.registerChaincodeEvent,
+ 'chaincodeid',
+ 'eventName',
+ sinon.match.func,
+ sinon.match.func,
+ {}
+ );
+ sinon.assert.calledWith(contractEventListener._onEvent.bind, contractEventListener);
+ sinon.assert.calledWith(contractEventListener._onError.bind, contractEventListener);
+ sinon.assert.called(eventHubStub.connect);
+ expect(contractEventListener._registered).to.be.true;
+ });
+
+ it('should call _registerWithNewEventHub', async () => {
+ sandbox.stub(contractEventListener, '_registerWithNewEventHub');
+ await contractEventListener.register();
+ sinon.assert.called(contractEventListener._registerWithNewEventHub);
+ });
+ });
+
+ describe('#unregister', () => {
+ it('should not call ChannelEventHub.unregisterChaincodeEvent', () => {
+ contractEventListener.unregister();
+ sinon.assert.notCalled(eventHubStub.unregisterChaincodeEvent);
+ });
+
+ it('should call ChannelEventHub.unregisterBlockEvent', async () => {
+ eventHubStub.registerChaincodeEvent.returns('registration');
+ contractEventListener.eventHub = eventHubStub;
+ await contractEventListener.register();
+ contractEventListener.unregister();
+ sinon.assert.calledWith(eventHubStub.unregisterChaincodeEvent, 'registration');
+ });
+ });
+
+ describe('#_onEvent', () => {
+ beforeEach(() => {
+ contractEventListener._registration = {};
+ sandbox.spy(contractEventListener, 'unregister');
+ sandbox.stub(contractEventListener, 'eventCallback');
+ });
+
+ it('should call the event callback', () => {
+ const event = {name: 'eventName'};
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ contractEventListener._onEvent(event, blockNumber, transactionId, status);
+ sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status);
+ sinon.assert.notCalled(checkpointerStub.save);
+ sinon.assert.notCalled(contractEventListener.unregister);
+ });
+
+ it('should save a checkpoint', async () => {
+ const event = {name: 'eventName'};
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ contractEventListener.checkpointer = checkpointerStub;
+ contractEventListener.options.replay = true;
+ await contractEventListener._onEvent(event, blockNumber, transactionId, status);
+ sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status);
+ sinon.assert.calledWith(checkpointerStub.save, 'transactionId', 10);
+ sinon.assert.notCalled(contractEventListener.unregister);
+ });
+
+ it('should unregister if registration.unregister is set', async () => {
+ const event = {name: 'eventName'};
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ contractEventListener._registration.unregister = true;
+ await contractEventListener._onEvent(event, blockNumber, transactionId, status);
+ sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status);
+ sinon.assert.called(contractEventListener.unregister);
+ });
+
+ it ('should not save a checkpoint if the callback fails', async () => {
+ const event = {name: 'eventName'};
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ contractEventListener.eventCallback.throws(new Error());
+ contractEventListener.checkpointer = checkpointerStub;
+ contractEventListener.options.replay = true;
+ await contractEventListener._onEvent(event, blockNumber, transactionId, status);
+ sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status);
+ sinon.assert.calledWith(checkpointerStub.save, 'transactionId', 10);
+ });
+
+ it('should skip a transaction if it is in the checkpoint', async () => {
+ contractEventListener.checkpointer = checkpointerStub;
+ const checkpoint = {transactionIds: ['transactionId']};
+ contractEventListener._firstCheckpoint = checkpoint;
+ checkpointerStub.load.returns(checkpoint);
+ const event = {name: 'eventName'};
+ const blockNumber = '10';
+ const transactionId = 'transactionId';
+ const status = 'VALID';
+ contractEventListener.options.replay = true;
+ await contractEventListener._onEvent(event, blockNumber, transactionId, status);
+ sinon.assert.notCalled(contractEventListener.eventCallback);
+ });
+ });
+
+ describe('#_onError', () => {
+ beforeEach(() => {
+ eventHubStub._peer = 'peer';
+ contractEventListener._registration = {};
+ sandbox.spy(contractEventListener, 'unregister');
+ sandbox.stub(contractEventListener, 'eventCallback');
+ sandbox.stub(contractEventListener, '_registerWithNewEventHub');
+ });
+
+ it('should call the event callback with an error', () => {
+ contractEventListener.eventHub = eventHubStub;
+ const error = new Error();
+ contractEventListener._onError(error);
+ sinon.assert.calledWith(contractEventListener.eventCallback, error);
+ });
+
+ it('should update event hub availability and reregister if disconnected', async () => {
+ const error = new EventHubDisconnectError('ChannelEventHub has been shutdown');
+ contractEventListener.eventHub = eventHubStub;
+ contractEventListener._registered = true;
+ await contractEventListener._onError(error);
+ sinon.assert.calledWith(eventHubManagerStub.updateEventHubAvailability, 'peer');
+ sinon.assert.called(contractEventListener._registerWithNewEventHub);
+ sinon.assert.calledWith(contractEventListener.eventCallback, error);
+ });
+
+ it('should call the error callback if the error is null', async () => {
+ const error = null;
+ contractEventListener.eventHub = eventHubStub;
+ contractEventListener._registered = true;
+ await contractEventListener._onError(error);
+ sinon.assert.calledWith(contractEventListener.eventCallback, error);
+ });
+ });
+
+ describe('#_registerWithNewEventHub', () => {
+ beforeEach(() => {
+ contractEventListener._registration = {};
+ sandbox.spy(contractEventListener, 'unregister');
+ sandbox.stub(contractEventListener, 'eventCallback');
+ eventHubManagerStub.getReplayEventHub.returns(eventHubStub);
+ eventHubManagerStub.getEventHub.returns(eventHubStub);
+ sinon.stub(contractEventListener, 'register');
+ });
+
+ it('should call unregister, get a new event hub and reregister', () => {
+ contractEventListener._registerWithNewEventHub();
+ sinon.assert.called(contractEventListener.unregister);
+ sinon.assert.called(eventHubManagerStub.getEventHub);
+ sinon.assert.called(contractEventListener.register);
+ });
+
+ it('should get a replay event hub if a checkpointer is present', () => {
+ contractEventListener.checkpointer = checkpointerStub;
+ contractEventListener.options.replay = true;
+ contractEventListener._registerWithNewEventHub();
+ sinon.assert.called(contractEventListener.unregister);
+ sinon.assert.called(eventHubManagerStub.getReplayEventHub);
+ sinon.assert.called(contractEventListener.register);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/defaulteventhandlerstrategies.js b/fabric-network/test/impl/event/defaulteventhandlerstrategies.js
index 7cd9af0038..c87359a476 100644
--- a/fabric-network/test/impl/event/defaulteventhandlerstrategies.js
+++ b/fabric-network/test/impl/event/defaulteventhandlerstrategies.js
@@ -10,9 +10,11 @@ const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;
-const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
const ChannelEventHub = require('fabric-client').ChannelEventHub;
const Network = require('fabric-network/lib/network');
+const Transaction = require('fabric-network/lib/transaction');
+const TransactionID = require('fabric-client/lib/TransactionID');
const Channel = require('fabric-client').Channel;
const AllForTxStrategy = require('fabric-network/lib/impl/event/allfortxstrategy');
const AnyForTxStrategy = require('fabric-network/lib/impl/event/anyfortxstrategy');
@@ -32,6 +34,7 @@ describe('DefaultEventHandlerStrategies', () => {
let options;
let stubNetwork;
+ let stubTransaction;
beforeEach(() => {
options = {
@@ -49,8 +52,8 @@ describe('DefaultEventHandlerStrategies', () => {
const stubEventHub = sinon.createStubInstance(ChannelEventHub);
stubEventHub.isconnected.returns(true);
- const stubEventHubFactory = sinon.createStubInstance(EventHubFactory);
- stubEventHubFactory.getEventHubs.withArgs([stubPeer]).resolves([stubEventHub]);
+ const stubEventHubManager = sinon.createStubInstance(EventHubManager);
+ stubEventHubManager.getEventHubs.withArgs([stubPeer]).resolves([stubEventHub]);
const channel = sinon.createStubInstance(Channel);
channel.getPeers.returns([stubPeer]);
@@ -58,7 +61,13 @@ describe('DefaultEventHandlerStrategies', () => {
stubNetwork = sinon.createStubInstance(Network);
stubNetwork.getChannel.returns(channel);
- stubNetwork.getEventHubFactory.returns(stubEventHubFactory);
+ stubNetwork.getEventHubManager.returns(stubEventHubManager);
+
+ stubTransaction = sinon.createStubInstance(Transaction);
+ const stubTransactionId = sinon.createStubInstance(TransactionID);
+ stubTransactionId.getTransactionID.returns(transactionId);
+ stubTransaction.getTransactionID.returns(stubTransactionId);
+ stubTransaction.getNetwork.returns(stubNetwork);
});
afterEach(() => {
@@ -71,13 +80,17 @@ describe('DefaultEventHandlerStrategies', () => {
let eventHandler;
beforeEach(() => {
- eventHandler = createTxEventHandler(transactionId, stubNetwork, options);
+ eventHandler = createTxEventHandler(stubTransaction, options);
});
it('Returns a TransactionEventHandler', () => {
expect(eventHandler).to.be.an.instanceOf(TransactionEventHandler);
});
+ it('Sets transaction on event handler', () => {
+ expect(eventHandler.transaction).to.equal(stubTransaction);
+ });
+
it('Sets transaction ID on event handler', () => {
expect(eventHandler.transactionId).to.equal(transactionId);
});
diff --git a/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js b/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js
new file mode 100644
index 0000000000..113d80c6e1
--- /dev/null
+++ b/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const Channel = require('fabric-client/lib/Channel');
+const Network = require('fabric-network/lib/network');
+const RoundRobinEventSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy');
+const DefaultEventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies');
+
+describe('DefaultEventHubSelectionStrategies', () => {
+ describe('#MSPID_SCOPE_ROUND_ROBIN', () => {
+ let network;
+ let channel;
+ let peer1, peer2, peer3;
+ beforeEach(() => {
+ network = sinon.createStubInstance(Network);
+ channel = sinon.createStubInstance(Channel);
+ network.getChannel.returns(channel);
+ peer1 = sinon.stub({isInRole() {}});
+ peer2 = sinon.stub({isInRole() {}});
+ peer3 = sinon.stub({isInRole() {}});
+ channel.getPeersForOrg.returns([peer1, peer2, peer3]);
+ });
+
+ it('should get organization peers and filter by those that are in the correct roles', () => {
+ peer1.isInRole.returns(true);
+ peer2.isInRole.returns(false);
+ peer3.isInRole.returns(true);
+
+ const strategy = DefaultEventHubSelectionStrategies.MSPID_SCOPE_ROUND_ROBIN(network);
+ expect(strategy).to.be.instanceof(RoundRobinEventSelectionStrategy);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/eventhubmanager.js b/fabric-network/test/impl/event/eventhubmanager.js
new file mode 100644
index 0000000000..410d55b894
--- /dev/null
+++ b/fabric-network/test/impl/event/eventhubmanager.js
@@ -0,0 +1,267 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+chai.use(require('chai-as-promised'));
+
+const {Channel, ChannelEventHub, Peer} = require('fabric-client');
+const Network = require('fabric-network/lib/network');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory');
+const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy');
+
+
+function populateEventHub(eventHub) {
+ eventHub._chaincodeRegistrants = {};
+ eventHub._blockRegistrations = {};
+ eventHub._transactionRegistrations = {};
+ return eventHub;
+}
+
+describe('EventHubManager', () => {
+ let sandbox;
+ let networkStub;
+ let channelStub;
+ let eventHubFactoryStub;
+ let eventHubSelectionStrategyStub;
+ let eventHubManager;
+ let defaultNewEventHub;
+ let anotherEventHub;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ channelStub = sandbox.createStubInstance(Channel);
+ networkStub = sandbox.createStubInstance(Network);
+ networkStub.getChannel.returns(channelStub);
+ eventHubFactoryStub = sandbox.createStubInstance(EventHubFactory);
+ eventHubSelectionStrategyStub = sandbox.createStubInstance(AbstractEventHubSelectionStrategy);
+ networkStub.getEventHubSelectionStrategy.returns(eventHubSelectionStrategyStub);
+ eventHubManager = new EventHubManager(networkStub);
+ eventHubManager.eventHubFactory = eventHubFactoryStub;
+ defaultNewEventHub = populateEventHub(sandbox.createStubInstance(ChannelEventHub));
+ defaultNewEventHub.isFiltered.returns(true);
+ defaultNewEventHub.getName.returns('peer');
+ anotherEventHub = populateEventHub(sandbox.createStubInstance(ChannelEventHub));
+ anotherEventHub.isFiltered.returns(false);
+ anotherEventHub.getName.returns('peer');
+ channelStub.newChannelEventHub.returns(defaultNewEventHub);
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ describe('#constructor', () => {
+ it('should set the right parameters', () => {
+ const em = new EventHubManager(networkStub);
+ expect(em.channel).to.equal(channelStub);
+ expect(em.eventHubFactory).to.be.instanceOf(EventHubFactory);
+ expect(em.eventHubSelectionStrategy).to.equal(eventHubSelectionStrategyStub);
+ expect(em.newEventHubs).to.deep.equal([]);
+ });
+ });
+
+ describe('#getEventHub', () => {
+ it('should return an event hub from the event hub factory given a peer', () => {
+ const peer = {getPeer: () => 'peer'};
+ eventHubFactoryStub.getEventHub.returns(anotherEventHub);
+
+ const eventHub = eventHubManager.getEventHub(peer);
+ sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer');
+ expect(eventHub).to.equal(anotherEventHub);
+ });
+
+ it('should return an event hub from the event hub factory given a peer name', () => {
+ const peer = 'peer';
+ eventHubFactoryStub.getEventHub.returns(anotherEventHub);
+
+ const eventHub = eventHubManager.getEventHub(peer);
+ sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer');
+ expect(eventHub).to.equal(anotherEventHub);
+ });
+
+ it('should return an event hub from the event hub factory without a peer', () => {
+ const peer = {getPeer: () => 'peer'};
+ eventHubFactoryStub.getEventHub.returns(anotherEventHub);
+ eventHubSelectionStrategyStub.getNextPeer.returns(peer);
+ const eventHub = eventHubManager.getEventHub();
+ sinon.assert.called(eventHubSelectionStrategyStub.getNextPeer);
+ sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer');
+ expect(eventHub).to.equal(anotherEventHub);
+ });
+
+ it('should not return the unfiltered event hub and request a new isntance', () => {
+ eventHubFactoryStub.getEventHub.returns(anotherEventHub);
+ anotherEventHub.isconnected.returns(true);
+ eventHubManager.newEventHubs = [defaultNewEventHub];
+ anotherEventHub.isFiltered.returns(false);
+ expect(eventHubManager.getEventHub({getName: () => 'peer'}, true)).to.equal(defaultNewEventHub);
+ });
+ });
+
+ describe('#getEventHubs', () => {
+ it('should call the event hub factory', () => {
+ const peers = ['peer1', 'peer2', 'peer3'];
+ const ehs = ['eh1', 'eh2', 'eh3'];
+ eventHubFactoryStub.getEventHubs.returns(ehs);
+ const eventHubs = eventHubManager.getEventHubs(peers);
+ sinon.assert.calledWith(eventHubFactoryStub.getEventHubs, peers);
+ expect(eventHubs).to.deep.equal(eventHubs);
+ });
+ });
+
+ describe('#getReplayEventHub', () => {
+ it('should return an existing event hub with no registrations and no peer is given', () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ newEventHub._chaincodeRegistrants = {};
+ newEventHub._blockRegistrations = {};
+ newEventHub._transactionRegistrations = {};
+ eventHubManager.newEventHubs = [newEventHub];
+ const eventHub = eventHubManager.getReplayEventHub();
+ expect(eventHub).to.be.instanceof(ChannelEventHub);
+ expect(eventHubManager.newEventHubs).to.have.length(1);
+ });
+
+ it('should return an existing event hub with no registrations when the peer is given', () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ newEventHub._chaincodeRegistrants = {};
+ newEventHub._blockRegistrations = {};
+ newEventHub._transactionRegistrations = {};
+ newEventHub.getName.returns('peer1');
+ eventHubManager.newEventHubs = [newEventHub];
+ const peer = sandbox.createStubInstance(Peer);
+ peer.getName.returns('peer1');
+ const eventHub = eventHubManager.getReplayEventHub(peer);
+ expect(eventHub).to.instanceof(ChannelEventHub);
+ expect(eventHubManager.newEventHubs).to.have.length(1);
+ });
+
+ it('should return a new event hub if the peer and event hub name dont match', () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ newEventHub._chaincodeRegistrants = {};
+ newEventHub._blockRegistrations = {};
+ newEventHub._transactionRegistrations = {};
+ newEventHub.getName.returns('peer2');
+ eventHubManager.newEventHubs = [newEventHub];
+ const peer = sandbox.createStubInstance(Peer);
+ peer.getName.returns('peer1');
+ const eventHub = eventHubManager.getReplayEventHub(peer);
+ expect(eventHub).to.equal(defaultNewEventHub);
+ expect(eventHubManager.newEventHubs[eventHubManager.newEventHubs.length - 1]).to.equal(defaultNewEventHub);
+ });
+
+ it('should return a new event hub if the event hub isnt new', () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ newEventHub._chaincodeRegistrants = {'registration1': 'some registration'};
+ newEventHub._blockRegistrations = {};
+ newEventHub._transactionRegistrations = {};
+ newEventHub.getName.returns('peer1');
+ eventHubManager.newEventHubs = [newEventHub];
+ const peer = sandbox.createStubInstance(Peer);
+ peer.getName.returns('peer1');
+ const eventHub = eventHubManager.getReplayEventHub(peer);
+ expect(eventHub).to.equal(defaultNewEventHub);
+ expect(eventHubManager.newEventHubs[eventHubManager.newEventHubs.length - 1]).to.equal(defaultNewEventHub);
+ });
+ });
+
+ describe('#getFixedEventHub', () => {
+ it('should get a new eventhub', () => {
+ const newEventHub = sandbox.createStubInstance(ChannelEventHub);
+ channelStub.newChannelEventHub.returns(newEventHub);
+ const eventHub = eventHubManager.getFixedEventHub('peer');
+ sinon.assert.calledWith(channelStub.newChannelEventHub, 'peer');
+ expect(eventHub).to.equal(newEventHub);
+ });
+ });
+
+ describe('#updateEventHubAvailability', () => {
+ it('should call eventHubSelectionStratefy.updateEventHubAvailability', () => {
+ eventHubManager.updateEventHubAvailability('peer');
+ sinon.assert.calledWith(eventHubSelectionStrategyStub.updateEventHubAvailability, 'peer');
+ });
+ });
+
+ describe('#dispose', () => {
+ it('should call dispose on the eventHubFactory', () => {
+ eventHubManager.dispose();
+ sinon.assert.called(eventHubFactoryStub.dispose);
+ });
+
+ it('should call disconnect on the each new event hub', () => {
+ const eventHub1 = sandbox.createStubInstance(ChannelEventHub);
+ const eventHub2 = sandbox.createStubInstance(ChannelEventHub);
+ const eventHub3 = sandbox.createStubInstance(ChannelEventHub);
+ eventHubManager.newEventHubs = [eventHub1, eventHub2, eventHub3];
+ eventHubManager.dispose();
+ for (const eh of eventHubManager.newEventHubs) {
+ sinon.assert.called(eh.disconnect);
+ }
+ });
+ });
+
+ describe('#getEventHubFactory', () => {
+ it ('should return the event hub factory', () => {
+ const eventHubFactory = eventHubManager.getEventHubFactory();
+ expect(eventHubFactory).to.equal(eventHubFactoryStub);
+ });
+ });
+
+ describe('#_isNewEventHub', () => {
+ it('should throw if no event hub is given', () => {
+ expect(() => eventHubManager._isNewEventHub()).to.throw('event hub not given');
+ });
+
+ it('should return true if there are no registrations', () => {
+ const eventHub = sandbox.createStubInstance(ChannelEventHub);
+ eventHub._chaincodeRegistrants = {};
+ eventHub._blockRegistrations = {};
+ eventHub._transactionRegistrations = {};
+ const isNew = eventHubManager._isNewEventHub(eventHub);
+ expect(isNew).to.be.true;
+ });
+
+ it('should return false if there is one chaincode registration', () => {
+ const eventHub = sandbox.createStubInstance(ChannelEventHub);
+ eventHub._chaincodeRegistrants = {someregistration: 'registration'};
+ eventHub._blockRegistrations = {};
+ eventHub._transactionRegistrations = {};
+ const isNew = eventHubManager._isNewEventHub(eventHub);
+ expect(isNew).to.be.false;
+ });
+
+ it('should return false if there is one block registration', () => {
+ const eventHub = sandbox.createStubInstance(ChannelEventHub);
+ eventHub._chaincodeRegistrants = {};
+ eventHub._blockRegistrations = {someregistration: 'registration'};
+ eventHub._transactionRegistrations = {};
+ const isNew = eventHubManager._isNewEventHub(eventHub);
+ expect(isNew).to.be.false;
+ });
+
+ it('should return false if there is one transaction registration', () => {
+ const eventHub = sandbox.createStubInstance(ChannelEventHub);
+ eventHub._chaincodeRegistrants = {};
+ eventHub._blockRegistrations = {};
+ eventHub._transactionRegistrations = {someregistration: 'registration'};
+ const isNew = eventHubManager._isNewEventHub(eventHub);
+ expect(isNew).to.be.false;
+ });
+ });
+
+ describe('#getPeers', () => {
+ it('should call EventHubSelectionStrategy.getPeers', () => {
+ const peers = ['peer1'];
+ eventHubSelectionStrategyStub.getPeers.returns(peers);
+ expect(eventHubManager.getPeers()).to.equal(peers);
+ sinon.assert.called(eventHubSelectionStrategyStub.getPeers);
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/filesystemcheckpointer.js b/fabric-network/test/impl/event/filesystemcheckpointer.js
new file mode 100644
index 0000000000..b9177f8959
--- /dev/null
+++ b/fabric-network/test/impl/event/filesystemcheckpointer.js
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const fs = require('fs-extra');
+const os = require('os');
+const path = require('path');
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const rewire = require('rewire');
+const FileSystemCheckpointer = rewire('fabric-network/lib/impl/event/filesystemcheckpointer');
+
+
+describe('FileSystemCheckpointer', () => {
+ const revert = [];
+ let sandbox;
+ let checkpointer;
+ let channelName;
+ let listenerName;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ sandbox.stub(os, 'homedir').returns('home');
+ sandbox.spy(path, 'join');
+ sandbox.stub(fs, 'ensureDir');
+ sandbox.stub(fs, 'createFile');
+ sandbox.stub(fs, 'exists');
+ sandbox.stub(fs, 'writeFile');
+ sandbox.stub(fs, 'readFile');
+ sandbox.stub(path, 'resolve').callsFake(a => a);
+ revert.push(FileSystemCheckpointer.__set__('path', path));
+ revert.push(FileSystemCheckpointer.__set__('os', os));
+ revert.push(FileSystemCheckpointer.__set__('fs', fs));
+
+ channelName = 'mychannel';
+ listenerName = 'mylistener';
+ checkpointer = new FileSystemCheckpointer(channelName, listenerName);
+ });
+
+ afterEach(() => {
+ if (revert.length) {
+ revert.forEach(Function.prototype.call, Function.prototype.call);
+ }
+ sandbox.restore();
+ });
+
+ describe('#constructor', () => {
+ it('should set basePath without options', () => {
+ const check = new FileSystemCheckpointer(channelName, listenerName);
+ sinon.assert.called(os.homedir);
+ expect(check._basePath).to.equal('home/.hlf-checkpoint');
+ expect(check._channelName).to.equal(channelName);
+ expect(check._listenerName).to.equal(listenerName);
+ });
+
+ it('should set basePath with options', () => {
+ const check = new FileSystemCheckpointer(channelName, listenerName, {basePath: 'base-path'});
+ sinon.assert.called(os.homedir);
+ expect(check._basePath).to.equal('base-path');
+ });
+ });
+
+ describe('#_initialize', () => {
+ it('should initialize the checkpoint file', async () => {
+ await checkpointer._initialize();
+ sinon.assert.calledWith(fs.ensureDir, `home/.hlf-checkpoint/${channelName}`);
+ sinon.assert.calledWith(fs.createFile, checkpointer._getCheckpointFileName());
+ });
+ });
+
+ describe('#save', () => {
+
+ it('should initialize the checkpointer file doesnt exist', async () => {
+ fs.readFile.resolves(new Buffer(''));
+ fs.exists.resolves(false);
+ sinon.spy(checkpointer, '_initialize');
+ sinon.spy(checkpointer, 'load');
+ await checkpointer.save('transaction1', 0);
+ sinon.assert.calledWith(fs.exists, checkpointer._getCheckpointFileName());
+ sinon.assert.called(checkpointer._initialize);
+ });
+
+ it('should update an existing checkpoint', async () => {
+ fs.exists.resolves(true);
+ fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']}));
+ await checkpointer.save('transactionId1', 0);
+ sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':0, 'transactionIds':['transactionId', 'transactionId1']}));
+ });
+
+ it('should not update an existing checkpoint', async () => {
+ fs.exists.resolves(true);
+ fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']}));
+ await checkpointer.save(null, 0);
+ sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':0, 'transactionIds':['transactionId']}));
+ });
+
+ it('should update an existing checkpoint when blockNumber changes', async () => {
+ fs.exists.resolves(true);
+ fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']}));
+ await checkpointer.save('transactionId', 1);
+ sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':1, 'transactionIds':['transactionId']}));
+ });
+
+ it('should not add a list of transactions to a checkpoint', async () => {
+ fs.exists.resolves(true);
+ fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']}));
+ await checkpointer.save(null, 1);
+ sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':1, 'transactionIds':[]}));
+ });
+ });
+
+ describe('#load', () => {
+ it('should initialize the checkpointer if memory map doesnt exist', async () => {
+ fs.exists.resolves(false);
+ fs.readFile.resolves('{}');
+ const checkpoint = await checkpointer.load();
+ expect(checkpoint).to.deep.equal({});
+ });
+
+ it('should return the checkpoint', async () => {
+ fs.exists.resolves(true);
+ const checkpoint = {blockNumber: 0, transactionIds: []};
+ fs.readFile.resolves(JSON.stringify(checkpoint));
+ const loadedCheckpoint = await checkpointer.load();
+ expect(loadedCheckpoint).to.deep.equal(checkpoint);
+ });
+
+ it('should return an empty object if the checkpoint is empty', async () => {
+ fs.exists.resolves(false);
+ const checkpoint = '';
+ fs.readFile.resolves(checkpoint);
+ const loadedCheckpoint = await checkpointer.load();
+ expect(loadedCheckpoint).to.deep.equal({});
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js b/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js
new file mode 100644
index 0000000000..c3cdb09b42
--- /dev/null
+++ b/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2019 IBM All Rights Reserved.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+const sinon = require('sinon');
+
+const RoundRobinEventHubSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy');
+const Peer = require('fabric-client/lib/Peer');
+
+describe('RoundRobinEventHubSelectionStrategy', () => {
+ let sandbox;
+ let peer1, peer2;
+ let strategy;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ peer1 = sandbox.createStubInstance(Peer);
+ peer1.getName.returns('peer1');
+ peer2 = sandbox.createStubInstance(Peer);
+ peer2.getName.returns('peer2');
+ strategy = new RoundRobinEventHubSelectionStrategy([peer1, peer2]);
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ describe('#constructor', () => {
+ it('should create an of peers', () => {
+ expect(strategy.peers).to.be.instanceOf(Array);
+ expect(strategy.peers).to.deep.equal([peer1, peer2]);
+ });
+
+ it('should create an empty array of peers by default', () => {
+ const testStrategy = new RoundRobinEventHubSelectionStrategy();
+ expect(testStrategy.peers).to.deep.equal([]);
+ });
+ });
+
+ describe('#getNextPeer', () => {
+ let nextPeerStrategy;
+ before(() => {
+ nextPeerStrategy = new RoundRobinEventHubSelectionStrategy([peer1, peer2]);
+ });
+
+ it('should pick peer1 next', () => {
+ expect(nextPeerStrategy.getNextPeer()).to.deep.equal(peer1);
+ });
+ it('should pick peer2 next', () => {
+ expect(nextPeerStrategy.getNextPeer()).to.deep.equal(peer2);
+ });
+ });
+
+ describe('#updateEventHubAvailability', () => {
+ it('should not throw if a dead peer is not given', () => {
+ expect(() => strategy.updateEventHubAvailability()).not.to.throw();
+ });
+ });
+});
diff --git a/fabric-network/test/impl/event/transactioneventhandler.js b/fabric-network/test/impl/event/transactioneventhandler.js
index d235a1761e..328c63ca80 100644
--- a/fabric-network/test/impl/event/transactioneventhandler.js
+++ b/fabric-network/test/impl/event/transactioneventhandler.js
@@ -1,5 +1,5 @@
/**
- * Copyright 2018 IBM All Rights Reserved.
+ * Copyright 2019 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -13,25 +13,25 @@ const sinon = require('sinon');
const ChannelEventHub = require('fabric-client').ChannelEventHub;
+const Transaction = require('fabric-network/lib/transaction');
const TransactionEventHandler = require('fabric-network/lib/impl/event/transactioneventhandler');
const TimeoutError = require('fabric-network/lib/errors/timeouterror');
+const TransactionID = require('fabric-client/lib/TransactionID');
describe('TransactionEventHandler', () => {
let stubEventHub;
let stubStrategy;
+ let stubTransaction;
+ let stubTransactionID;
const transactionId = 'TRANSACTION_ID';
beforeEach(() => {
// Include _stubInfo property on stubs to enable easier equality comparison in tests
+ stubTransaction = sinon.createStubInstance(Transaction);
+ stubTransactionID = sinon.createStubInstance(TransactionID);
+ stubTransactionID.getTransactionID.returns(transactionId);
+ stubTransaction.getTransactionID.returns(stubTransactionID);
stubEventHub = sinon.createStubInstance(ChannelEventHub);
- stubEventHub._stubInfo = 'eventHub';
- stubEventHub.getName.returns('eventHub');
- stubEventHub.getPeerAddr.returns('eventHubAddress');
- stubEventHub.registerTxEvent.callsFake((transaction_Id, onEventFn, onErrorFn) => {
- stubEventHub._transactionId = transaction_Id;
- stubEventHub._onEventFn = onEventFn;
- stubEventHub._onErrorFn = onErrorFn;
- });
stubStrategy = {
getEventHubs: sinon.stub(),
@@ -47,22 +47,28 @@ describe('TransactionEventHandler', () => {
describe('#constructor', () => {
it('has a default timeout of zero if no options supplied', () => {
- const handler = new TransactionEventHandler(transactionId, stubStrategy);
+ const handler = new TransactionEventHandler(stubTransaction, stubStrategy);
expect(handler.options.commitTimeout).to.equal(0);
});
it('uses timeout from supplied options', () => {
const options = {commitTimeout: 1};
- const handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ const handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
expect(handler.options.commitTimeout).to.equal(options.commitTimeout);
});
+
+ it('should set transactionID and transaction', () => {
+ const handler = new TransactionEventHandler(stubTransaction, stubStrategy);
+ expect(handler.transactionId).to.equal(transactionId);
+ expect(handler.transaction).to.equal(stubTransaction);
+ });
});
describe('event handling:', () => {
let handler;
beforeEach(() => {
- handler = new TransactionEventHandler(transactionId, stubStrategy);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy);
});
afterEach(() => {
@@ -70,64 +76,86 @@ describe('TransactionEventHandler', () => {
});
describe('#startListening', () => {
- it('calls registerTxEvent() on event hub with transaction ID', async () => {
+ it('calls addCommitListener() on transaction with callback', async () => {
await handler.startListening();
- sinon.assert.calledWith(stubEventHub.registerTxEvent, transactionId);
+ sinon.assert.calledWith(stubTransaction.addCommitListener, sinon.match.func);
});
it('sets auto-unregister option when calling registerTxEvent() on event hub', async () => {
await handler.startListening();
sinon.assert.calledWith(
- stubEventHub.registerTxEvent,
- sinon.match.any,
- sinon.match.any,
- sinon.match.any,
- sinon.match.has('unregister', true)
+ stubTransaction.addCommitListener,
+ sinon.match.func,
+ sinon.match.has('unregister', true),
+ stubEventHub
);
});
- it('calls connect() on event hub', async () => {
- await handler.startListening();
- sinon.assert.called(stubEventHub.connect);
+ it('should call transaction.addCommitListener', () => {
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy);
+ handler.startListening();
+ sinon.assert.calledWith(
+ stubTransaction.addCommitListener,
+ sinon.match.func,
+ sinon.match.has('unregister', true),
+ stubEventHub
+ );
+ });
+
+ it('should call _onError if err is set', () => {
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy);
+ sinon.spy(handler, '_onError');
+ handler.startListening();
+ const err = new Error('an error');
+ stubTransaction.addCommitListener.callArgWith(0, err);
+ sinon.assert.calledWith(handler._onError, stubEventHub, err);
+ });
+
+ it('should call _onEvent if err is set', () => {
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy);
+ sinon.spy(handler, '_onEvent');
+ handler.startListening();
+ stubTransaction.addCommitListener.callArgWith(0, null, transactionId, 'VALID');
+ sinon.assert.calledWith(handler._onEvent, stubEventHub, transactionId, 'VALID');
});
});
it('calls eventReceived() on strategy when event hub sends valid event', async () => {
+ stubTransaction.addCommitListener.yields(null, transactionId, 'VALID');
await handler.startListening();
- stubEventHub._onEventFn(transactionId, 'VALID');
sinon.assert.calledWith(stubStrategy.eventReceived, sinon.match.func, sinon.match.func);
});
it('does not call errorReceived() on strategy when event hub sends valid event', async () => {
+ stubTransaction.addCommitListener.yields(null, transactionId, 'VALID');
await handler.startListening();
- stubEventHub._onEventFn(transactionId, 'VALID');
sinon.assert.notCalled(stubStrategy.errorReceived);
});
it('calls errorReceived() on strategy when event hub sends an error', async () => {
+ stubTransaction.addCommitListener.yields(new Error());
await handler.startListening();
- stubEventHub._onErrorFn(new Error('EVENT HUB ERROR'));
sinon.assert.calledWith(stubStrategy.errorReceived, sinon.match.func, sinon.match.func);
});
it('does not call eventReceived() on strategy when event hub sends an error', async () => {
+ stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR'));
await handler.startListening();
- stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR'));
sinon.assert.notCalled(stubStrategy.eventReceived);
});
it('fails when event hub sends an invalid event', async () => {
const code = 'ERROR_CODE';
+ stubTransaction.addCommitListener.yields(null, transactionId, code);
await handler.startListening();
- stubEventHub._onEventFn(transactionId, code);
return expect(handler.waitForEvents()).to.be.rejectedWith(code);
});
it('succeeds when strategy calls success function after event received', async () => {
stubStrategy.eventReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars
+ stubTransaction.addCommitListener.yields(null, transactionId, 'VALID');
await handler.startListening();
- stubEventHub._onEventFn(transactionId, 'VALID');
return expect(handler.waitForEvents()).to.be.fulfilled;
});
@@ -135,16 +163,16 @@ describe('TransactionEventHandler', () => {
const error = new Error('STRATEGY_FAIL');
stubStrategy.eventReceived = ((successFn, failFn) => failFn(error));
+ stubTransaction.addCommitListener.yields(null, transactionId, 'VALID');
await handler.startListening();
- stubEventHub._onEventFn(transactionId, 'VALID');
return expect(handler.waitForEvents()).to.be.rejectedWith(error);
});
it('succeeds when strategy calls success function after error received', async () => {
stubStrategy.errorReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars
+ stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR'));
await handler.startListening();
- stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR'));
return expect(handler.waitForEvents()).to.be.fulfilled;
});
@@ -152,14 +180,14 @@ describe('TransactionEventHandler', () => {
const error = new Error('STRATEGY_FAIL');
stubStrategy.errorReceived = ((successFn, failFn) => failFn(error));
+ stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR'));
await handler.startListening();
- stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR'));
return expect(handler.waitForEvents()).to.be.rejectedWith(error);
});
it('succeeds immediately with no event hubs', async () => {
stubStrategy.getEventHubs.returns([]);
- handler = new TransactionEventHandler(transactionId, stubStrategy);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy);
await handler.startListening();
return expect(handler.waitForEvents()).to.be.fulfilled;
});
@@ -180,7 +208,7 @@ describe('TransactionEventHandler', () => {
it('fails on timeout if timeout set', async () => {
const options = {commitTimeout: 418};
- handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
await handler.startListening();
const promise = handler.waitForEvents();
clock.runAll();
@@ -191,16 +219,16 @@ describe('TransactionEventHandler', () => {
stubStrategy.eventReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars
const options = {commitTimeout: 0};
- handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
+ stubTransaction.addCommitListener.yields(null, transactionId, 'VALID');
await handler.startListening();
clock.runAll();
- stubEventHub._onEventFn(transactionId, 'VALID');
return expect(handler.waitForEvents()).to.be.fulfilled;
});
it('timeout failure message includes event hubs that have not responded', async () => {
const options = {commitTimeout: 418};
- handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
await handler.startListening();
const promise = handler.waitForEvents();
clock.runAll();
@@ -211,7 +239,7 @@ describe('TransactionEventHandler', () => {
it('does not timeout if no event hubs', async () => {
stubStrategy.getEventHubs.returns([]);
const options = {commitTimeout: 418};
- handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
await handler.startListening();
clock.runAll();
return expect(handler.waitForEvents()).to.be.fulfilled;
@@ -219,7 +247,7 @@ describe('TransactionEventHandler', () => {
it('timeout failure error has transaction ID property', async () => {
const options = {commitTimeout: 418};
- handler = new TransactionEventHandler(transactionId, stubStrategy, options);
+ handler = new TransactionEventHandler(stubTransaction, stubStrategy, options);
await handler.startListening();
const promise = handler.waitForEvents();
clock.runAll();
diff --git a/fabric-network/test/impl/wallet/basewallet.js b/fabric-network/test/impl/wallet/basewallet.js
index b3ae653e5b..94f772ca60 100644
--- a/fabric-network/test/impl/wallet/basewallet.js
+++ b/fabric-network/test/impl/wallet/basewallet.js
@@ -27,7 +27,10 @@ describe('BaseWallet', () => {
});
describe('Unimplemented methods', () => {
- const wallet = new BaseWallet();
+ let wallet;
+ beforeEach(() => {
+ wallet = new BaseWallet();
+ });
it('throws exception calling import()', () => {
return wallet.import(null, null).should.be.rejectedWith('Not implemented');
@@ -55,5 +58,4 @@ describe('BaseWallet', () => {
});
});
-
});
diff --git a/fabric-network/test/impl/wallet/couchdbwallet.js b/fabric-network/test/impl/wallet/couchdbwallet.js
index d0878b5404..f0810be6fd 100644
--- a/fabric-network/test/impl/wallet/couchdbwallet.js
+++ b/fabric-network/test/impl/wallet/couchdbwallet.js
@@ -212,6 +212,18 @@ describe('CouchDBWalletKeyValueStore', () => {
const deleted = await kvs.delete('key');
deleted.should.be.false;
});
+
+ it('should return false if error occurs when finding the key', async () => {
+ destroyStub.yields(new Error());
+ const deleted = await kvs.delete('key');
+ deleted.should.be.false;
+ });
+
+ it('should return false if error occurs when finding the key', async () => {
+ destroyStub.yields(new Error());
+ const deleted = await kvs.delete('key');
+ deleted.should.be.false;
+ });
});
describe('#exists', () => {
diff --git a/fabric-network/test/impl/wallet/inmemorywallet.js b/fabric-network/test/impl/wallet/inmemorywallet.js
index f7633f1b70..423be3374f 100644
--- a/fabric-network/test/impl/wallet/inmemorywallet.js
+++ b/fabric-network/test/impl/wallet/inmemorywallet.js
@@ -85,6 +85,13 @@ describe('InMemoryWallet', () => {
});
});
+ describe('#getIdentityInfo', () => {
+ it('should not return null', async () => {
+ wallet.getAllLabels = () => Promise.resolve(['user3']);
+ const info = await wallet.list();
+ info.should.deep.equal([{label: 'user3', mspId: 'not provided', identifier: 'not provided'}]);
+ });
+ });
});
describe('label storage', () => {
@@ -206,13 +213,23 @@ mb3MM6J+V7kciO3hSyP5OJSBPWGlsjxQj2m55aFutmlleVfr6YiaLnYd
const list = await wallet.list();
list.length.should.equal(0);
});
- });
+ it('should return a list containing not provided', async () => {
+ wallet.walletMixin.getIdentityInfo = () => null;
+ const list = await wallet.list();
+ list.length.should.equal(2);
+ list[0].mspId.should.equal('not provided');
+ list[1].mspId.should.equal('not provided');
+ });
+ });
});
- describe('InMemoryKVS', async () => {
- const wallet = new InMemoryWallet();
- const store = await wallet.getStateStore('test');
+ describe('InMemoryKVS', () => {
+ let store;
+ beforeEach(async () => {
+ const wallet = new InMemoryWallet();
+ store = await wallet.getStateStore('test');
+ });
it('#getValue', async () => {
await store.setValue('user1', 'val1');
diff --git a/fabric-network/test/network.js b/fabric-network/test/network.js
index 25cd65705b..5796730344 100644
--- a/fabric-network/test/network.js
+++ b/fabric-network/test/network.js
@@ -12,7 +12,6 @@ const InternalChannel = rewire('fabric-client/lib/Channel');
const Peer = InternalChannel.__get__('ChannelPeer');
const Client = require('fabric-client');
const ChannelEventHub = Client.ChannelEventHub;
-const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory');
const TransactionID = require('fabric-client/lib/TransactionID.js');
const FABRIC_CONSTANTS = require('fabric-client/lib/Constants');
@@ -25,6 +24,10 @@ const Network = require('../lib/network');
const Gateway = require('../lib/gateway');
const Contract = require('../lib/contract');
const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies');
+const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy');
+const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager');
+const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener');
+const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener');
describe('Network', () => {
let mockChannel, mockClient;
@@ -33,9 +36,13 @@ describe('Network', () => {
let network;
let mockTransactionID, mockGateway;
let stubQueryHandler;
+ let stubEventHubSelectionStrategy;
+ let mockEventHubManager;
+ let mockEventHub;
beforeEach(() => {
mockChannel = sinon.createStubInstance(InternalChannel);
+ mockEventHub = sinon.createStubInstance(ChannelEventHub);
mockClient = sinon.createStubInstance(Client);
mockTransactionID = sinon.createStubInstance(TransactionID);
mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000');
@@ -73,6 +80,8 @@ describe('Network', () => {
stubQueryHandler = {};
+ stubEventHubSelectionStrategy = sinon.createStubInstance(AbstractEventHubSelectionStrategy);
+
mockGateway = sinon.createStubInstance(Gateway);
mockGateway.getOptions.returns({
useDiscovery: false,
@@ -85,14 +94,24 @@ describe('Network', () => {
stubQueryHandler.network = theNetwork;
return stubQueryHandler;
}
+ },
+ eventHubSelectionOptions: {
+ strategy: (theNetwork) => {
+ stubEventHubSelectionStrategy.network = theNetwork;
+ return stubEventHubSelectionStrategy;
+ }
}
});
mockGateway.getClient.returns(mockClient);
mockClient.getPeersForOrg.returns([mockPeer1, mockPeer2]);
- network = new Network(mockGateway, mockChannel);
+ mockEventHubManager = sinon.createStubInstance(EventHubManager);
+ mockEventHubManager.getPeers.returns([mockPeer1, mockPeer2]);
+ mockEventHubManager.getEventHub.returns(mockEventHub);
+ network = new Network(mockGateway, mockChannel);
+ network.eventHubManager = mockEventHubManager;
});
afterEach(() => {
@@ -231,16 +250,16 @@ describe('Network', () => {
});
it('calls dispose() on the event hub factory', () => {
- const spy = sinon.spy(network.getEventHubFactory(), 'dispose');
+ const spy = network.getEventHubManager().dispose;
network._dispose();
sinon.assert.called(spy);
});
- });
- describe('#getEventHubFactory', () => {
- it('Returns an EventHubFactory', () => {
- const result = network.getEventHubFactory();
- result.should.be.an.instanceOf(EventHubFactory);
+ it('calls unregister on its listeners', () => {
+ const mockListener = sinon.createStubInstance(BlockEventListener);
+ network.listeners.set('mockListener', mockListener);
+ network._dispose();
+ sinon.assert.calledOnce(mockListener.unregister);
});
});
@@ -259,4 +278,86 @@ describe('Network', () => {
result.network.should.equal(network);
});
});
+
+ describe('#addCommitListener', () => {
+ let listenerName;
+ let callback;
+ beforeEach(() => {
+ listenerName = '00000000-0000-0000-0000-000000000000';
+ callback = () => {};
+ mockEventHub._transactionRegistrations = {};
+ mockEventHub._transactionRegistrations[listenerName] = {}; // Preregister listener with eh
+ });
+
+ it('should create options if the options param is undefined', async () => {
+ const listener = await network.addCommitListener(listenerName, callback, null, mockEventHub);
+ listener.should.to.be.instanceof(CommitEventListener);
+ listener.eventHub.should.to.equal(mockEventHub);
+ network.listeners.get(listener.listenerName).should.to.equal(listener);
+ });
+
+ it('should create an instance of BlockEventListener and add it to the list of listeners', async () => {
+ const listener = await network.addCommitListener(listenerName, callback, {}, mockEventHub);
+ listener.should.to.be.instanceof(CommitEventListener);
+ listener.eventHub.should.to.equal(mockEventHub);
+ network.listeners.get(listener.listenerName).should.to.equal(listener);
+ });
+
+ it('should not set an event hub if an event hub is not given', async () => {
+ mockEventHubManager.getReplayEventHub.returns(mockEventHub);
+ const listener = await network.addCommitListener(listenerName, callback, null);
+ listener.eventHub.should.equal(mockEventHub);
+ });
+ });
+
+ describe('#addBlockListener', () => {
+ let listenerName;
+ let callback;
+ beforeEach(() => {
+ listenerName = 'testBlockListener';
+ callback = () => {};
+ });
+
+ it('should create options if the options param is undefined', async () => {
+ const listener = await network.addBlockListener(listenerName, callback);
+ listener.should.to.be.instanceof(BlockEventListener);
+ network.listeners.get(listenerName).should.to.equal(listener);
+ });
+
+ it('should create an instance of BlockEventListener and add it to the list of listeners', async () => {
+ const listener = await network.addBlockListener(listenerName, callback, {});
+ listener.should.to.be.instanceof(BlockEventListener);
+ network.listeners.get(listenerName).should.to.equal(listener);
+ });
+
+ it('should change options.replay=undefined to options.replay=false', async () => {
+ sinon.spy(network, 'getCheckpointer');
+ await network.addBlockListener(listenerName, callback, {replay: undefined});
+ sinon.assert.calledWith(network.getCheckpointer, {checkpointer: undefined, replay: false});
+ });
+
+ it('should change options.replay=\'true\' to options.replay=true', async () => {
+ sinon.spy(network, 'getCheckpointer');
+ await network.addBlockListener(listenerName, callback, {replay: 'true'});
+ sinon.assert.calledWith(network.getCheckpointer, {checkpointer: undefined, replay: true});
+ });
+ });
+
+ describe('#saveListener', () => {
+ it ('should register a new listener if the name isnt taken', () => {
+ const listener = {};
+ network.listeners.set('listener1', {});
+ network.saveListener('listener2', listener);
+ network.listeners.get('listener2').should.equal(listener);
+ });
+
+ it('should throw if the listener is registered with an existing name', () => {
+ const listener = sinon.createStubInstance(BlockEventListener);
+ network.listeners.set('listener1', listener);
+ (() => {
+ network.saveListener('listener1', listener);
+ }).should.throw('Listener already exists with the name listener1');
+ sinon.assert.called(listener.unregister);
+ });
+ });
});
diff --git a/fabric-network/test/transaction.js b/fabric-network/test/transaction.js
index 7a5511d348..0e6e062158 100644
--- a/fabric-network/test/transaction.js
+++ b/fabric-network/test/transaction.js
@@ -57,6 +57,7 @@ describe('Transaction', () => {
let stubContract;
let transaction;
let channel;
+ let network;
let stubQueryHandler;
beforeEach(() => {
@@ -70,7 +71,7 @@ describe('Transaction', () => {
evaluate: sinon.fake.resolves(expectedResult)
};
- const network = sinon.createStubInstance(Network);
+ network = sinon.createStubInstance(Network);
network.getQueryHandler.returns(stubQueryHandler);
stubContract.getNetwork.returns(network);
@@ -204,11 +205,9 @@ describe('Transaction', () => {
it('uses a supplied event handler strategy', async () => {
const stubEventHandler = sinon.createStubInstance(TransactionEventHandler);
- const txId = transaction.getTransactionID().getTransactionID();
- const network = stubContract.getNetwork();
const options = stubContract.getEventHandlerOptions();
const stubEventHandlerFactoryFn = sinon.stub();
- stubEventHandlerFactoryFn.withArgs(txId, network, options).returns(stubEventHandler);
+ stubEventHandlerFactoryFn.withArgs(transaction, stubContract.getNetwork(), options).returns(stubEventHandler);
await transaction.setEventHandlerStrategy(stubEventHandlerFactoryFn).submit();
@@ -316,4 +315,22 @@ describe('Transaction', () => {
return expect(promise).to.be.rejectedWith('Transaction has already been invoked');
});
});
+
+ describe('#addCommitListener', () => {
+ it('should call Network.addCommitlistner', async () => {
+ network.addCommitListener.resolves('listener');
+ const callback = (err, transationId, status, blockNumber) => {};
+ const listener = await transaction.addCommitListener(callback, {}, 'eventHub');
+ expect(listener).to.equal('listener');
+ sinon.assert.calledWith(network.addCommitListener, 'TRANSACTION_ID', callback, {}, 'eventHub');
+ });
+ });
+
+ describe('#getNetwork', () => {
+ it('should call Contract.getNetwork', () => {
+ stubContract.getNetwork.returns(network);
+ expect(transaction.getNetwork()).to.equal(network);
+ sinon.assert.called(stubContract.getNetwork);
+ });
+ });
});
diff --git a/fabric-network/types/index.d.ts b/fabric-network/types/index.d.ts
index 4993304462..d1132c404c 100644
--- a/fabric-network/types/index.d.ts
+++ b/fabric-network/types/index.d.ts
@@ -20,6 +20,12 @@ export interface GatewayOptions {
discovery?: DiscoveryOptions;
eventHandlerOptions?: DefaultEventHandlerOptions;
queryHandlerOptions?: DefaultQueryHandlerOptions;
+ checkpointer?: CheckpointerOptions;
+}
+
+export interface CheckpointerOptions {
+ factory: CheckpointerFactory;
+ options: object;
}
export interface DiscoveryOptions {
@@ -39,7 +45,7 @@ export class DefaultEventHandlerStrategies {
public static NETWORK_SCOPE_ANYFORTX: TxEventHandlerFactory;
}
-export type TxEventHandlerFactory = (transactionId: TransactionId, network: Network, options: object) => TxEventHandler;
+export type TxEventHandlerFactory = (transaction: Transaction, options: object) => TxEventHandler;
export interface TxEventHandler {
startListening(): Promise;
@@ -83,12 +89,15 @@ export class Gateway {
export interface Network {
getChannel(): Channel;
getContract(chaincodeId: string, name?: string): Contract;
+ addBlockListener(listenerName: string, callback: (block: Client.Block) => void, options?: object): BlockEventListener;
+ addCommitListener(listenerName: string, callback: (error: Error, transactionId: string, status: string, blockNumber: string) => void, options?: object): TransactionEventListener;
}
export interface Contract {
createTransaction(name: string): Transaction;
evaluateTransaction(name: string, ...args: string[]): Promise;
submitTransaction(name: string, ...args: string[]): Promise;
+ addContractListener(listenerName: string, eventName: string, callback: (error: Error, event: {[key: string]: any}, blockNumber: string, transactionId: string, status: string) => void, options?: object): ContractEventListener;
}
export interface TransientMap {
@@ -98,16 +107,18 @@ export interface Transaction {
evaluate(...args: string[]): Promise;
getName(): string;
getTransactionID(): TransactionId;
+ getNetwork(): Network;
setTransient(transientMap: TransientMap): this;
submit(...args: string[]): Promise;
+ addCommitListener(callback: (error: Error, transactionId: string, status: string, blockNumber: string) => void, options: object, eventHub?: Client.ChannelEventHub): void;
}
-export interface FabricError {
+export interface FabricError extends Error {
cause?: Error;
transactionId?: string;
}
-export class TimeoutError implements FabricError {}
+export interface TimeoutError extends FabricError {} // tslint:disable-line:no-empty-interface
//-------------------------------------------
// Wallet Management
@@ -172,3 +183,54 @@ export class HSMWalletMixin implements WalletMixin {
public static createIdentity(mspId: string, certificate: string): Identity;
constructor();
}
+
+export interface Checkpoint {
+ blockNumber: number;
+ transactionIds: string[];
+}
+
+export class BaseCheckpointer {
+ public setChaincodeId(chaincodeId: string): void;
+}
+
+export class FileSystemCheckpointer extends BaseCheckpointer {
+ constructor();
+ public initialize(channelName: string, listenerName: string): void;
+ public save(transactionId: string, blockNumber: string): void;
+ public load(): Checkpoint;
+}
+
+export type CheckpointerFactory = (channelName: string, listenerName: string, options: object) => BaseCheckpointer;
+
+export class EventHubManager {
+ constructor();
+ public getEventHub(peer: Client.Peer): Client.ChannelEventHub;
+ public getEventHubs(peers: Client.Peer[]): Client.ChannelEventHub[];
+ public getReplayEventHub(peer: Client.Peer): Client.ChannelEventHub;
+ public getReplayEventHubs(peers: Client.Peer[]): Client.ChannelEventHub[];
+}
+
+export class TransactionEventListener {
+ public register(): void;
+ public setEventHub(eventHub: Client.ChannelEventHub): void;
+ public unregister(): void;
+}
+
+export class ContractEventListener {
+ public register(): void;
+ public unregister(): void;
+}
+
+export class BlockEventListener {
+ public register(): void;
+ public unregister(): void;
+}
+
+export interface BaseEventHubSelectionStrategy {
+ getNextPeer(): Client.Peer;
+ updateEventHubAvailability(deadPeer: Client.Peer): void;
+}
+
+export class DefaultEventHubSelectionStrategies {
+ public static MSPID_SCOPE_ROUND_ROBIN: BaseEventHubSelectionStrategy;
+}
diff --git a/package.json b/package.json
index abb308e2d6..aec19055b1 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,5 @@
"type": "Apache-2.0",
"url": "https://github.com/hyperledger/fabric/blob/master/LICENSE"
}
- ],
- "dependencies": {}
+ ]
}
diff --git a/test/fixtures/src/node_cc/example_cc/chaincode.js b/test/fixtures/src/node_cc/example_cc/chaincode.js
index be0e972d75..bec03ff4fa 100644
--- a/test/fixtures/src/node_cc/example_cc/chaincode.js
+++ b/test/fixtures/src/node_cc/example_cc/chaincode.js
@@ -247,6 +247,7 @@ const Chaincode = class {
}
async echo(stub, args) {
+ stub.setEvent('echo', Buffer.from('content'));
if (args.length > 0) {
return shim.success(Buffer.from(args[0]));
} else {
diff --git a/test/integration/fabric-ca-services-tests.js b/test/integration/fabric-ca-services-tests.js
index a1b2a18202..387d575c08 100644
--- a/test/integration/fabric-ca-services-tests.js
+++ b/test/integration/fabric-ca-services-tests.js
@@ -102,6 +102,7 @@ test('\n\n ** FabricCAServices: Test enroll() With Dynamic CSR **\n\n', (t) => {
t.fail('Failed to import the public key from the enrollment certificate. ' + err.stack ? err.stack : err);
t.end();
}).then(() => {
+ caService.setConfigSetting('socket-operation-timeout', 100000);
return caService._fabricCAClient.register(enrollmentID, null, 'client', userOrg, 1, [], signingIdentity);
}).then((secret) => {
t.comment('secret: ' + JSON.stringify(secret));
diff --git a/test/integration/network-e2e/sample-transaction-event-handler.js b/test/integration/network-e2e/sample-transaction-event-handler.js
index 1e9535262a..96de3d5ee9 100644
--- a/test/integration/network-e2e/sample-transaction-event-handler.js
+++ b/test/integration/network-e2e/sample-transaction-event-handler.js
@@ -23,7 +23,10 @@ class SampleTransactionEventHandler {
* @param {Number} [options.commitTimeout] Time in seconds to wait for commit events to be reveived.
*/
constructor(transactionId, eventHubs, options) {
- this.transactionId = transactionId;
+ if (typeof transactionId === 'object') {
+ this.transaction = transactionId;
+ }
+ this.transactionId = this.transaction ? this.transaction.getTransactionID().getTransactionID() : transactionId;
this.eventHubs = eventHubs;
const defaultOptions = {
diff --git a/test/scenario/chaincode/events/node/index.js b/test/scenario/chaincode/events/node/index.js
new file mode 100644
index 0000000000..5fd5828f42
--- /dev/null
+++ b/test/scenario/chaincode/events/node/index.js
@@ -0,0 +1,10 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict';
+
+const Events = require('./lib/events');
+
+module.exports.Events = Events;
+module.exports.contracts = [Events];
diff --git a/test/scenario/chaincode/events/node/lib/events.js b/test/scenario/chaincode/events/node/lib/events.js
new file mode 100644
index 0000000000..ce6dc9147f
--- /dev/null
+++ b/test/scenario/chaincode/events/node/lib/events.js
@@ -0,0 +1,26 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/* eslint-disable no-console */
+
+'use strict';
+const {Contract} = require('fabric-contract-api');
+
+class Events extends Contract {
+ async initLedger(ctx) {
+ console.info('Instantiated events');
+ }
+
+ async createValue(ctx) {
+ const {stub} = ctx;
+ stub.setEvent('create', Buffer.from('content'));
+ }
+
+ async createValueDisconnect(ctx) {
+ const {stub} = ctx;
+ stub.setEvent('dc', Buffer.from('content'));
+ }
+}
+
+module.exports = Events;
diff --git a/test/scenario/chaincode/events/node/metadata/.noignore b/test/scenario/chaincode/events/node/metadata/.noignore
new file mode 100644
index 0000000000..0f60d0f7c2
--- /dev/null
+++ b/test/scenario/chaincode/events/node/metadata/.noignore
@@ -0,0 +1,5 @@
+#
+# Copyright 2019 IBM All Rights Reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
diff --git a/test/scenario/chaincode/events/node/package.json b/test/scenario/chaincode/events/node/package.json
new file mode 100644
index 0000000000..0143c1381f
--- /dev/null
+++ b/test/scenario/chaincode/events/node/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "events",
+ "version": "1.0.0",
+ "description": "events chaincode implemented in node.js",
+ "main": "index.js",
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ },
+ "scripts": {
+ "start": "fabric-chaincode-node start"
+ },
+ "engine-strict": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "fabric-contract-api": "unstable",
+ "fabric-shim": "unstable"
+ }
+}
diff --git a/test/scenario/chaincode/fabcar/node/package.json b/test/scenario/chaincode/fabcar/node/package.json
new file mode 100644
index 0000000000..79caa7076b
--- /dev/null
+++ b/test/scenario/chaincode/fabcar/node/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "fabcar",
+ "version": "1.0.0",
+ "description": "FabCar contract implemented in JavaScript",
+ "main": "index.js",
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ },
+ "scripts": {
+ "lint": "eslint .",
+ "pretest": "npm run lint",
+ "test": "nyc mocha --recursive",
+ "start": "fabric-chaincode-node start"
+ },
+ "engineStrict": true,
+ "author": "Hyperledger",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "fabric-contract-api": "unstable",
+ "fabric-shim": "unstable"
+ },
+ "devDependencies": {
+ "chai": "^4.1.2",
+ "eslint": "^4.19.1",
+ "mocha": "^5.2.0",
+ "nyc": "^12.0.2",
+ "sinon": "^6.0.0",
+ "sinon-chai": "^3.2.0"
+ },
+ "nyc": {
+ "exclude": [
+ "coverage/**",
+ "test/**"
+ ],
+ "reporter": [
+ "text-summary",
+ "html"
+ ],
+ "all": true,
+ "check-coverage": true,
+ "statements": 100,
+ "branches": 100,
+ "functions": 100,
+ "lines": 100
+ }
+}
diff --git a/test/scenario/features/event.feature b/test/scenario/features/event.feature
new file mode 100644
index 0000000000..469951d616
--- /dev/null
+++ b/test/scenario/features/event.feature
@@ -0,0 +1,34 @@
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+@networkAPI
+@clean-gateway
+Feature: Listen to events using a fabric-network
+ Background:
+ Given I have deployed a tls Fabric network
+ And I can create and join all channels from the tls common connection profile
+ And I can create a gateway named test_gateway as user User1 within Org1 using the tls common connection profile
+ Given I can install/instantiate node chaincode at version 1.0.0 named events to the tls Fabric network for all organizations on channel mychannel with endorsement policy 1AdminOr2Other and args [initLedger]
+
+ Scenario: Using a Contract I can listen to contract events emmited by instantiated chaincodes
+ When I use the gateway named test_gateway to listen for create events with listener createValueListener on chaincode events instantiated on channel mychannel
+ When I use the gateway named test_gateway to submit 5 transactions with args [createValue] for chaincode events instantiated on channel mychannel
+ Then I receive 5 events from the listener createValueListener
+ When I use the gateway named test_gateway to listen for dc events with listener ehDisconnectListener on chaincode events instantiated on channel mychannel
+ When I use the gateway named test_gateway to submit 10 transactions with args [createValueDisconnect] for chaincode events instantiated on fabric channel mychannel disconnecting the event hub on listener ehDisconnectListener every 5 transactions
+ Then I receive 10 events from the listener ehDisconnectListener
+
+ Scenario: Using a Contract I can listen to block events emmited by networks
+ When I use the gateway named test_gateway to listen for filtered_block_events with listener filteredBlockListener on chaincode events instantiated on channel mychannel
+ When I use the gateway named test_gateway to submit a transaction with args [createValue] for chaincode events instantiated on channel mychannel
+ Then I receive at least 1 events from the listener filteredBlockListener
+ When I use the gateway named test_gateway to listen for unfiltered_block_events with listener unfilteredBlockListener on chaincode events instantiated on channel mychannel
+ When I use the gateway named test_gateway to submit a transaction with args [createValue] for chaincode events instantiated on channel mychannel
+ Then I receive at least 1 events from the listener unfilteredBlockListener
+
+ Scenario: I can listen to a transaction commit event
+ When I use the gateway named test_gateway to create a transaction named transaction1 that calls createValue using chaincode events instantiated on channel mychannel
+ When I use the transaction named transaction1 to create a commit listener called transaction1Listener
+ When I use the transaction named transaction1 to submit a transaction with args []
+ Then I receive 1 events from the listener transaction1Listener
diff --git a/test/scenario/features/lib/chaincode.js b/test/scenario/features/lib/chaincode.js
index 066d876226..6fb48e24f3 100644
--- a/test/scenario/features/lib/chaincode.js
+++ b/test/scenario/features/lib/chaincode.js
@@ -147,7 +147,7 @@ async function instantiateChaincode(ccName, ccId, ccType, args, version, upgrade
Promise.reject('Unsupported test ccType: ' + ccType);
}
- Client.setConfigSetting('request-timeout', 120000);
+ Client.setConfigSetting('request-timeout', 300000);
const type = upgrade ? 'upgrade' : 'instantiate';
diff --git a/test/scenario/features/lib/network.js b/test/scenario/features/lib/network.js
index 0fa8d1cd58..5457b61e5b 100644
--- a/test/scenario/features/lib/network.js
+++ b/test/scenario/features/lib/network.js
@@ -7,10 +7,21 @@
const {Gateway, InMemoryWallet, X509WalletMixin} = require('fabric-network');
const testUtil = require('./utils.js');
const fs = require('fs');
+const chai = require('chai');
+const expect = chai.expect;
// Internal Map of connected gateways
const gateways = new Map();
+// Internal Map of event listenerData
+// {calls, payloads}
+const listeners = new Map();
+// {listener, payloads}
+const transactions = new Map();
+
+// transaction types
+const types = ['evaluate', 'error', 'submit'];
+
/**
* Perform an in memeory ID setup
* @param {InMemoryWallet} inMemoryWallet the in memory wallet to use
@@ -70,7 +81,7 @@ async function connectGateway(ccp, tls, userName, orgName, gatewayName) {
}
await gateway.connect(ccp.profile, opts);
- gateways.set(gatewayName, gateway);
+ gateways.set(gatewayName, {gateway});
// Ensure that all connections have had time to process in the background
await testUtil.sleep(testUtil.TIMEOUTS.SHORT_INC);
@@ -84,7 +95,7 @@ async function connectGateway(ccp, tls, userName, orgName, gatewayName) {
*/
async function disconnectGateway(gatewayName) {
try {
- const gateway = gateways.get(gatewayName);
+ const gateway = gateways.get(gatewayName).gateway;
await gateway.disconnect();
gateways.delete(gatewayName);
} catch (err) {
@@ -100,10 +111,12 @@ async function disconnectAllGateways() {
try {
for (const key of gateways.keys()) {
testUtil.logMsg('disconnecting from Gateway ', key);
- const gateway = gateways.get(key);
+ const gateway = gateways.get(key).gateway;
await gateway.disconnect();
}
gateways.clear();
+ listeners.clear();
+ transactions.clear();
} catch (err) {
testUtil.logError('disconnectAllGateways() failed with error ', err);
throw err;
@@ -138,7 +151,7 @@ async function retrieveContractFromGateway(gateway, channelName, chaincodeId) {
*/
async function performGatewayTransaction(gatewayName, ccName, channelName, args, submit) {
// Get contract from Gateway
- const gateway = gateways.get(gatewayName);
+ const gateway = gateways.get(gatewayName).gateway;
const contract = await retrieveContractFromGateway(gateway, channelName, ccName);
// Split args
@@ -162,7 +175,201 @@ async function performGatewayTransaction(gatewayName, ccName, channelName, args,
}
}
-module.exports.connectGateway = connectGateway;
-module.exports.performGatewayTransaction = performGatewayTransaction;
-module.exports.disconnectGateway = disconnectGateway;
-module.exports.disconnectAllGateways = disconnectAllGateways;
+async function performGatewayTransactionWithListener(gatewayName, ccName, channelName, args) {
+ // Get contract from Gateway
+ const gatewayObj = gateways.get(gatewayName);
+ const gateway = gatewayObj.gateway;
+ const contract = await retrieveContractFromGateway(gateway, channelName, ccName);
+
+ // Split args
+ const argArray = args.slice(1, -1).split(',');
+ const func = argArray[0];
+ const funcArgs = argArray.slice(1);
+ try {
+ testUtil.logMsg('Submitting transaction [' + func + '] with arguments ' + args);
+ // const result = await contract.submitTransaction(func, ...funcArgs);
+ const transaction = contract.createTransaction(func);
+ await transaction.addCommitListener((err, ...cbArgs) => {
+
+ });
+ const result = await transaction.submit(...funcArgs);
+ gatewayObj.result = {type: 'submit', response: result.toString()};
+ testUtil.logMsg('Successfully submitted transaction [' + func + ']');
+ return Promise.resolve();
+ } catch (err) {
+ gatewayObj.result = {type: 'error', result: err.toString()};
+ testUtil.logError(err);
+ throw err;
+ }
+}
+
+/**
+ * Compare the last gateway transaction response with a passed value
+ * @param {String} type type of resposne
+ * @param {*} msg the message to compare against
+ */
+function lastResponseCompare(gatewayName, msg) {
+ const gatewayObj = gateways.get(gatewayName);
+ return (gatewayObj.result.response.localeCompare(msg) === 0);
+}
+
+/**
+ * Retrieve the last gateway transaction result
+ * @param {String} type type of resposne
+ */
+function lastResult(gatewayName) {
+ const gatewayObj = gateways.get(gatewayName);
+ return gatewayObj.result;
+}
+
+/**
+ * Compare the last gateway transaction type with a passed value
+ * @param {String} gatewayName gateway name
+ * @param {String} type type of resposne
+ */
+function lastTypeCompare(gatewayName, type) {
+ const gatewayObj = gateways.get(gatewayName);
+
+ if (!gatewayObj) {
+ throw new Error('Unknown gateway with name ' + gatewayName);
+ }
+
+ if (!gatewayObj.result) {
+ throw new Error('No existing response on gateway ' + gatewayName);
+ }
+
+ if (types.indexOf(type) === -1) {
+ throw new Error('Unknown type transaction type ' + type + ', must be one of [evaluate, error, submit]');
+ }
+
+ return gatewayObj.result.type.localeCompare(type) === 0;
+}
+
+function getGateway(gatewayName) {
+ if (gateways.get(gatewayName)) {
+ return gateways.get(gatewayName).gateway;
+ } else {
+ return undefined;
+ }
+}
+
+async function createContractListener(gatewayName, channelName, ccName, eventName, listenerName) {
+ const gateway = gateways.get(gatewayName).gateway;
+ const contract = await retrieveContractFromGateway(gateway, channelName, ccName);
+ if (!listeners.has(listenerName)) {
+ listeners.set(listenerName, {calls: 0, payloads: []});
+ }
+ const listener = await contract.addContractListener(listenerName, eventName, (err, ...args) => {
+ if (err) {
+ testUtil.logMsg('Contract event error', err);
+ return err;
+ }
+ testUtil.logMsg('Received a contract event', listenerName);
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.payloads.push(args);
+ listenerInfo.calls = listenerInfo.payloads.length;
+ }, {replay: true});
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.listener = listener;
+ listeners.set(listenerName, listenerInfo);
+}
+
+async function createBlockListener(gatewayName, channelName, ccName, listenerName, filtered) {
+ const gateway = gateways.get(gatewayName).gateway;
+ const contract = await retrieveContractFromGateway(gateway, channelName, ccName);
+ const network = contract.getNetwork();
+ if (!listeners.has(listenerName)) {
+ listeners.set(listenerName, {calls: 0, payloads: []});
+ }
+ const listener = await network.addBlockListener(listenerName, (err, block) => {
+ if (err) {
+ testUtil.logMsg('Block event error', err);
+ return err;
+ }
+ testUtil.logMsg('Received a block event', listenerName);
+ if (filtered) {
+ expect(block).to.have.property('channel_id');
+ expect(block).to.have.property('number');
+ expect(block).to.have.property('filtered_transactions');
+ } else {
+ expect(block).to.have.property('header');
+ expect(block).to.have.property('data');
+ expect(block).to.have.property('metadata');
+ }
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.payloads.push(block);
+ listenerInfo.calls = listenerInfo.payloads.length;
+ }, {filtered, replay: true});
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.listener = listener;
+ listeners.set(listenerName, listenerInfo);
+}
+
+function getListenerInfo(listenerName) {
+ if (listeners.has(listenerName)) {
+ return listeners.get(listenerName);
+ }
+ return {};
+}
+
+function resetListenerCalls(listenerName) {
+ if (listeners.has(listenerName)) {
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.payloads = [];
+ listenerInfo.calls = 0;
+ }
+}
+
+async function createTransaction(gatewayName, transactionName, fcnName, chaincodeId, channelName) {
+ const gateway = getGateway(gatewayName);
+ const contract = await retrieveContractFromGateway(gateway, channelName, chaincodeId);
+ const transaction = contract.createTransaction(fcnName);
+ transactions.set(transactionName, transaction);
+}
+
+async function createCommitListener(transactionName, listenerName) {
+ const transaction = transactions.get(transactionName);
+ if (!transaction) {
+ throw new Error(`Transaction with name ${transactionName} does not exist`);
+ }
+ const listener = await transaction.addCommitListener((err, ...args) => {
+ if (err) {
+ testUtil.logMsg('Commit event error', err);
+ return err;
+ }
+ testUtil.logMsg('Received a commit event', listenerName);
+ const listenerInfo = listeners.get(listenerName);
+ listenerInfo.payloads.push(args);
+ listenerInfo.calls = listenerInfo.payloads.length;
+ });
+ listeners.set(listenerName, {listener, payloads: []});
+}
+
+async function submitExistingTransaction(transactionName, args) {
+ const transaction = transactions.get(transactionName);
+ if (!transaction) {
+ throw new Error(`Transaction with name ${transactionName} does not exist`);
+ }
+ const argsSplit = args.slice(1, -1).split(', ');
+ return await transaction.submit(...argsSplit);
+}
+
+
+module.exports = {
+ connectGateway,
+ performGatewayTransaction,
+ performGatewayTransactionWithListener,
+ disconnectGateway,
+ disconnectAllGateways,
+ lastResponseCompare,
+ lastResult,
+ lastTypeCompare,
+ getGateway,
+ createContractListener,
+ createBlockListener,
+ getListenerInfo,
+ resetListenerCalls,
+ createTransaction,
+ createCommitListener,
+ submitExistingTransaction
+};
diff --git a/test/scenario/features/steps/admin_steps.js b/test/scenario/features/steps/admin_steps.js
index 5822a6ba74..fa57557d6a 100644
--- a/test/scenario/features/steps/admin_steps.js
+++ b/test/scenario/features/steps/admin_steps.js
@@ -18,6 +18,9 @@ const ccpPath = configRoot + '/ccp.json';
const tlsCcpPath = configRoot + '/ccp-tls.json';
const policiesPath = configRoot + '/policies.json';
+const instantiatedChaincodesOnChannels = new Map();
+const installedChaincodesOnPeers = new Map();
+
module.exports = function () {
this.Then(/^I can create a channels from the (.+?) common connection profile$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (tlsType) => {
@@ -96,7 +99,14 @@ module.exports = function () {
tls = true;
profile = new CCP(path.join(__dirname, tlsCcpPath), true);
}
- return chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName);
+ if (!installedChaincodesOnPeers.has(orgName)) {
+ installedChaincodesOnPeers.set(orgName, []);
+ }
+ if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName);
+ installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
});
this.Then(/^I can install (.+?) chaincode named (.+?) to the (.+?) Fabric network$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (ccType, ccName, tlsType) => {
@@ -119,7 +129,14 @@ module.exports = function () {
// fixed version
const version = '1.0.0';
- return chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName);
+ if (!installedChaincodesOnPeers.has(orgName)) {
+ installedChaincodesOnPeers.set(orgName, []);
+ }
+ if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName);
+ installedChaincodesOnPeers.set(orgName, [installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
});
this.Then(/^I can install (.+?) chaincode named (.+?) as (.+?) to the (.+?) Fabric network$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (ccType, ccName, ccId, tlsType) => {
@@ -142,7 +159,14 @@ module.exports = function () {
// fixed version
const version = '1.0.0';
- return chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName);
+ if (!installedChaincodesOnPeers.has(orgName)) {
+ installedChaincodesOnPeers.set(orgName, []);
+ }
+ if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName);
+ installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
});
this.Then(/^I can instantiate the (.+?) installed (.+?) chaincode at version (.+?) named (.+?) on the (.+?) Fabric network as organization (.+?) on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (exisiting, ccType, version, ccName, tlsType, orgName, channelName, policyType, args) => {
@@ -164,7 +188,14 @@ module.exports = function () {
}
const policy = require(path.join(__dirname, policiesPath))[policyType];
- return chaincode_util.instantiateChaincode(ccName, ccName, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy);
+ if (!instantiatedChaincodesOnChannels.has(channelName)) {
+ instantiatedChaincodesOnChannels.set(channelName, []);
+ }
+ if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.instantiateChaincode(ccName, ccName, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy);
+ instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
});
this.Then(/^I can instantiate the (.+?) installed (.+?) chaincode at version (.+?) named (.+?) with identifier (.+?) on the (.+?) Fabric network as organization (.+?) on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (exisiting, ccType, version, ccName, ccId, tlsType, orgName, channelName, policyType, args) => {
@@ -186,7 +217,14 @@ module.exports = function () {
}
const policy = require(path.join(__dirname, policiesPath))[policyType];
- return chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy);
+ if (!instantiatedChaincodesOnChannels.has(channelName)) {
+ instantiatedChaincodesOnChannels.set(channelName, []);
+ }
+ if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy);
+ instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
});
this.Then(/^I can install\/instantiate (.+?) chaincode at version (.+?) named (.+?) to the (.+?) Fabric network for all organizations on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (ccType, version, ccName, tlsType, channelName, policyType, args) => {
@@ -234,10 +272,23 @@ module.exports = function () {
try {
for (const org in orgs) {
const orgName = orgs[org];
- await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName);
+ if (!installedChaincodesOnPeers.has(orgName)) {
+ installedChaincodesOnPeers.set(orgName, []);
+ }
+ if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName);
+ installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]);
+ }
}
- return chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, false, tls, profile, orgs[0], channelName, policy);
+ if (!instantiatedChaincodesOnChannels.has(channelName)) {
+ instantiatedChaincodesOnChannels.set(channelName, []);
+ }
+ if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) {
+ await chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, false, tls, profile, orgs[0], channelName, policy);
+ instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]);
+ }
+ return true;
} catch (err) {
testUtil.logError('Install/Instantiate failed with error: ', err);
throw err;
diff --git a/test/scenario/features/steps/docker_steps.js b/test/scenario/features/steps/docker_steps.js
index 98e8caccee..b0bab5e860 100644
--- a/test/scenario/features/steps/docker_steps.js
+++ b/test/scenario/features/steps/docker_steps.js
@@ -10,6 +10,7 @@ const path = require('path');
module.exports = function () {
this.Given(/^I have deployed a (.+?) Fabric network/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (type) => {
+ await testUtil.runShellCommand(undefined, 'rm -r ~/.hlf-checkpoint');
await testUtil.runShellCommand(undefined, 'docker kill $(docker ps -aq); docker rm $(docker ps -aq)');
if (type.localeCompare('non-tls') === 0) {
await testUtil.runShellCommand(true, 'docker-compose -f ' + path.join(__dirname, '../../../fixtures/docker-compose/docker-compose.yaml') + ' -p node up -d');
@@ -20,6 +21,7 @@ module.exports = function () {
});
this.Given(/^I have forcibly taken down all docker containers/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async () => {
+ await testUtil.runShellCommand(undefined, 'rm -r ~/.hlf-checkpoint');
await testUtil.runShellCommand(undefined, 'docker kill $(docker ps -aq); docker rm $(docker ps -aq)');
return await testUtil.sleep(testUtil.TIMEOUTS.SHORT_INC);
});
diff --git a/test/scenario/features/steps/network_steps.js b/test/scenario/features/steps/network_steps.js
index a4ef5be297..07f8fd7348 100644
--- a/test/scenario/features/steps/network_steps.js
+++ b/test/scenario/features/steps/network_steps.js
@@ -34,7 +34,6 @@ module.exports = function () {
this.Then(/^I use the gateway named (.+?) to evaluate transaction with args (.+?) for chaincode (.+?) instantiated on channel (.+?) with the response matching (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName, expected) => {
const result = await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, false);
-
if (result === expected) {
return Promise.resolve();
} else {
@@ -43,6 +42,111 @@ module.exports = function () {
});
+ this.Then(/^I use the gateway named (.+?) to submit (.+?) transactions with args (.+?) for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, numTransactions, args, ccName, channelName) => {
+ for (let i = 0; i < numTransactions; i++) {
+ await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, true);
+ }
+ });
+
+ this.Then(/^I use the gateway named (.+?) to submit (.+?) transactions with args (.+?) for chaincode (.+?) instantiated on fabric channel (.+?) disconnecting the event hub on listener (.+?) every (.+?) transactions$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, numTransactions, args, ccName, channelName, listenerName, disconnects) => {
+ const listener = network_util.getListenerInfo(listenerName).listener;
+ const eventHub = listener.eventHub;
+ for (let i = 0; i < numTransactions; i++) {
+ if (i % disconnects === 0) {
+ eventHub.disconnect();
+ }
+ await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, true);
+ }
+ });
+
+ this.Then(/^I use the gateway named (.+?) to submit a transaction with args (.+?) and listen for a commit event for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName) => {
+ return await network_util.performGatewayTransactionWithListener(gatewayName, ccName, channelName, args, true);
+ });
+
+ // Events
+ this.Then(/^I use the gateway named (.+?) to listen for (.+?) events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, eventName, listenerName, ccName, channelName) => {
+ return await network_util.createContractListener(gatewayName, channelName, ccName, eventName, listenerName);
+ });
+
+ this.Then(/^I use the gateway named (.+?) to listen for filtered_block_events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, listenerName, ccName, channelName) => {
+ return await network_util.createBlockListener(gatewayName, channelName, ccName, listenerName, true);
+ });
+
+ this.Then(/^I use the gateway named (.+?) to listen for unfiltered_block_events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, listenerName, ccName, channelName) => {
+ return await network_util.createBlockListener(gatewayName, channelName, ccName, listenerName, false);
+ });
+
+ this.Then(/^I receive ([0-9]+) events from the listener (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (calls, listenerName) => {
+ await new Promise(resolve => {
+ const interval = setInterval(() => {
+ const listenerInfo = network_util.getListenerInfo(listenerName);
+ if (Number(listenerInfo.calls) === Number(calls)) {
+ clearInterval(interval);
+ clearTimeout(timeout);
+ resolve();
+ }
+ }, 1000);
+ const timeout = setTimeout(() => {
+ resolve();
+ clearInterval(interval);
+ }, 60000);
+ });
+ const eventListenerInfo = network_util.getListenerInfo(listenerName);
+ if (Number(eventListenerInfo.calls) !== Number(calls)) {
+ throw new Error(`Expected ${listenerName} to be called ${calls} times, but called ${eventListenerInfo.calls} times`);
+ }
+ network_util.resetListenerCalls(listenerName);
+ return Promise.resolve();
+ });
+
+ this.Then(/^I use the transaction named (.+?) to create a commit listener called (.+?)$/, (transactionName, listenerName) => {
+ return network_util.createCommitListener(transactionName, listenerName);
+ });
+
+ this.Then(/^I use the transaction named (.+?) to submit a transaction with args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (transactionName, args) => {
+ return network_util.submitExistingTransaction(transactionName, args);
+ });
+
+ this.Then(/^I use the gateway named (.+?) to submit a transaction with args (.+?) and listen for a commit event for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName) => {
+ return await network_util.performGatewayTransactionWithListener(gatewayName, ccName, channelName, args, true);
+ });
+
+ this.Then(/^I receive at least ([0-9]+) events from the listener (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (calls, listenerName) => {
+ await new Promise(resolve => {
+ let timeout = null;
+ const interval = setInterval(() => {
+ const listenerInfo = network_util.getListenerInfo(listenerName);
+ if (Number(listenerInfo.calls) >= Number(calls)) {
+ clearInterval(interval);
+ clearTimeout(timeout);
+ resolve();
+ }
+ }, 1000);
+ timeout = setTimeout(() => {
+ resolve();
+ clearInterval(interval);
+ }, 60000);
+ });
+ const eventListenerInfo = network_util.getListenerInfo(listenerName);
+ if (Number(eventListenerInfo.calls) < Number(calls)) {
+ throw new Error(`Expected ${listenerName} to be called ${calls} times, but called ${eventListenerInfo.calls} times`);
+ }
+ network_util.resetListenerCalls(listenerName);
+ return Promise.resolve();
+ });
+
+ this.Then(/^I use the gateway named (.+?) to create a transaction named (.+?) that calls (.+?) using chaincode (.+?) instantiated on channel (.+?)$/, async (gatewayName, transactionName, fcnName, ccName, channelName) => {
+ return network_util.createTransaction(gatewayName, transactionName, fcnName, ccName, channelName);
+ });
+
+ this.Then(/^The gateway named (.+?) has a (.+?) type response$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, type) => {
+ if (network_util.lastTypeCompare(gatewayName, type)) {
+ return Promise.resolve();
+ } else {
+ throw new Error('Expected and actual result type from previous transaction did not match. Expected [' + type + '] but had [' + network_util.lastResult(gatewayName).type + ']');
+ }
+ });
+
this.Then(/^I can disconnect from the gateway named (.+?)$/, {timeout:testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName) => {
return await network_util.disconnectGateway(gatewayName);
});
diff --git a/test/typescript/integration/network-e2e/invoke.ts b/test/typescript/integration/network-e2e/invoke.ts
index 40178de0ba..50110e7c6a 100644
--- a/test/typescript/integration/network-e2e/invoke.ts
+++ b/test/typescript/integration/network-e2e/invoke.ts
@@ -82,8 +82,8 @@ async function getInternalEventHubForOrg(gateway: Gateway, orgMSP: string): Prom
const orgPeer = channel.getPeersForOrg(orgMSP)[0]; // Only one peer per org in the test configuration
// Using private functions to get hold of an internal event hub. Don't try this at home, kids!
- const eventHubFactory = (network as any).getEventHubFactory();
- return eventHubFactory.getEventHub(orgPeer);
+ const eventHubManager = (network as any).getEventHubManager();
+ return eventHubManager.getEventHub(orgPeer);
}
test('\n\n***** Network End-to-end flow: import identity into wallet and configure tls *****\n\n', async (t: any) => {