diff --git a/.gitignore b/.gitignore
index 7a1360f9a9..1b492eabc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,6 @@ packages/composer-website/jekylldocs/jsdoc/
packages/composer-playground/src/assets/npmlist.json
packages/composer-systests/systestv1/tls/ca/fabric-ca-server.db
+packages/composer-runtime-hlfv1/vendor/github.com/
+packages/composer-runtime-hlfv1/vendor/gopkg.in/sourcemap.v1/
+
diff --git a/packages/composer-admin/lib/adminconnection.js b/packages/composer-admin/lib/adminconnection.js
index aa7d81525e..a1a40dd459 100644
--- a/packages/composer-admin/lib/adminconnection.js
+++ b/packages/composer-admin/lib/adminconnection.js
@@ -91,9 +91,6 @@ class AdminConnection {
if (businessNetworkIdentifier) {
return this.connection.ping(this.securityContext);
}
- })
- .then(() => {
-
});
}
diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js
index 0f32e425ce..1f4a97c215 100644
--- a/packages/composer-client/lib/businessnetworkconnection.js
+++ b/packages/composer-client/lib/businessnetworkconnection.js
@@ -282,8 +282,16 @@ class BusinessNetworkConnection extends EventEmitter {
* @return {Promise} A promise to a BusinessNetworkDefinition that indicates the connection is complete
*/
connect(connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions) {
+ const method = 'connect';
+ LOG.entry(method, connectionProfile, businessNetwork, enrollmentID, enrollmentSecret, additionalConnectOptions);
return this.connectionProfileManager.connect(connectionProfile, businessNetwork, additionalConnectOptions)
.then((connection) => {
+ connection.on('events', (events) => {
+ events.forEach((event) => {
+ let serializedEvent = this.getBusinessNetwork().getSerializer().fromJSON(event);
+ this.emit('event', serializedEvent);
+ });
+ });
this.connection = connection;
return connection.login(enrollmentID, enrollmentSecret);
})
@@ -301,6 +309,7 @@ class BusinessNetworkConnection extends EventEmitter {
})
.then((businessNetwork) => {
this.businessNetwork = businessNetwork;
+ LOG.exit(method);
return this.businessNetwork;
});
}
@@ -321,14 +330,20 @@ class BusinessNetworkConnection extends EventEmitter {
* terminated.
*/
disconnect() {
+ const method = 'disconnect';
+ LOG.entry(method);
if (!this.connection) {
return Promise.resolve();
}
return this.connection.disconnect()
.then(() => {
+ this.connection.removeListener('events', () => {
+ LOG.debug(method, 'removeLisener');
+ });
this.connection = null;
this.securityContext = null;
this.businessNetwork = null;
+ LOG.exit(method);
});
}
diff --git a/packages/composer-client/scripts/tsgen.js b/packages/composer-client/scripts/tsgen.js
index c8f0d933be..50b2b942c7 100755
--- a/packages/composer-client/scripts/tsgen.js
+++ b/packages/composer-client/scripts/tsgen.js
@@ -41,22 +41,30 @@ function renderClass(key, clazz) {
fileContents += ` static ${statick}(${insert}): any;\n`;
}
});
- let members = Object.getOwnPropertyNames(clazz.prototype);
- members.forEach((member) => {
- let func = clazz.prototype[member];
- if (typeof func === 'function') {
- const args = new Array(func.length).fill('temp');
- args.forEach((value, index, array) => {
- args[index] = `arg${index}?: any`;
- });
- const insert = args.join(', ');
- if (member === 'constructor') {
- fileContents += ` ${member}(${insert});\n`;
- } else {
- fileContents += ` ${member}(${insert}): any;\n`;
+ let foundConstructor = false;
+ let prototype = clazz.prototype;
+ while(prototype) {
+ let members = Object.getOwnPropertyNames(prototype);
+ members.forEach((member) => {
+ let func = prototype[member];
+ if (typeof func === 'function') {
+ const args = new Array(func.length).fill('temp');
+ args.forEach((value, index, array) => {
+ args[index] = `arg${index}?: any`;
+ });
+ const insert = args.join(', ');
+ if (member === 'constructor') {
+ if (!foundConstructor) {
+ foundConstructor = true;
+ fileContents += ` ${member}(${insert});\n`;
+ }
+ } else {
+ fileContents += ` ${member}(${insert}): any;\n`;
+ }
}
- }
- });
+ });
+ prototype = Object.getPrototypeOf(prototype);
+ }
fileContents += '}\n';
}
diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js
index afa1c79b2a..a00df3c9c7 100644
--- a/packages/composer-client/test/businessnetworkconnection.js
+++ b/packages/composer-client/test/businessnetworkconnection.js
@@ -155,6 +155,28 @@ describe('BusinessNetworkConnection', () => {
});
});
+ it('should create a connection, listen for events, and emit the events it detects individually', () => {
+ sandbox.stub(businessNetworkConnection.connectionProfileManager, 'connect').resolves(mockConnection);
+ mockConnection.login.resolves(mockSecurityContext);
+ mockConnection.ping.resolves();
+ const buffer = Buffer.from(JSON.stringify({
+ data: 'aGVsbG8='
+ }));
+ sandbox.stub(Util, 'queryChainCode').withArgs(mockSecurityContext, 'getBusinessNetwork', []).resolves(buffer);
+ sandbox.stub(BusinessNetworkDefinition, 'fromArchive').resolves(mockBusinessNetworkDefinition);
+ const cb = sinon.stub();
+ businessNetworkConnection.on('event', cb);
+ mockConnection.on.withArgs('events', sinon.match.func).yields(['event1', 'event2']);
+ mockSerializer.fromJSON.onCall(0).returns('event1#serialized');
+ mockSerializer.fromJSON.onCall(1).returns('event2#serialized');
+
+ return businessNetworkConnection.connect('testprofile', 'testnetwork', 'enrollmentID', 'enrollmentSecret', { some: 'other', options: true })
+ .then((result) => {
+ sinon.assert.calledTwice(cb); // two events
+ sinon.assert.calledWith(cb, 'event1#serialized');
+ sinon.assert.calledWith(cb, 'event2#serialized');
+ });
+ });
});
describe('#disconnect', () => {
@@ -169,9 +191,11 @@ describe('BusinessNetworkConnection', () => {
return businessNetworkConnection.disconnect()
.then(() => {
sinon.assert.calledOnce(mockConnection.disconnect);
+ sinon.assert.calledOnce(mockConnection.removeListener);
return businessNetworkConnection.disconnect();
})
.then(() => {
+ mockConnection.removeListener.withArgs('events', sinon.match.func).yield(['event1', 'event2']);
should.equal(businessNetworkConnection.connection, null);
sinon.assert.calledOnce(mockConnection.disconnect);
});
diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt
index 9fc8952c97..e10ea85c7a 100644
--- a/packages/composer-common/api.txt
+++ b/packages/composer-common/api.txt
@@ -31,6 +31,7 @@ class Factory {
+ Resource newConcept(string,string,Object,boolean,string) throws ModelException
+ Relationship newRelationship(string,string,string) throws ModelException
+ Resource newTransaction(string,string,string,Object,string)
+ + Resource newEvent(string,string,Object,string)
+ Object toJSON()
}
class FileWallet extends Wallet {
diff --git a/packages/composer-common/changelog.txt b/packages/composer-common/changelog.txt
index dde1baff07..1d025c8b0b 100644
--- a/packages/composer-common/changelog.txt
+++ b/packages/composer-common/changelog.txt
@@ -12,6 +12,9 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#
+Version 0.7.1 {90a630e8b408292357ee801e5b79512c} 2017-05-04
+- Added Factory.newEvent
+
Version 0.7.0 {632a80837e835bbe0343d4b37ce12742} 2017-05-01
- Added Typed.instanceOf
diff --git a/packages/composer-common/lib/connection.js b/packages/composer-common/lib/connection.js
index 4b9c4bc4a9..f28884f67f 100644
--- a/packages/composer-common/lib/connection.js
+++ b/packages/composer-common/lib/connection.js
@@ -15,6 +15,7 @@
'use strict';
const ConnectionManager = require('./connectionmanager');
+const EventEmitter = require('events');
/**
* Base class representing a connection to a business network.
@@ -23,7 +24,7 @@ const ConnectionManager = require('./connectionmanager');
* @class
* @memberof module:composer-common
*/
-class Connection {
+class Connection extends EventEmitter {
/**
* Constructor.
@@ -32,6 +33,7 @@ class Connection {
* @param {string} businessNetworkIdentifier The identifier of the business network for this connection, or null if an admin connection
*/
constructor(connectionManager, connectionProfile, businessNetworkIdentifier) {
+ super();
if (!(connectionManager instanceof ConnectionManager)) {
throw new Error('connectionManager not specified');
} else if (!connectionProfile) {
diff --git a/packages/composer-common/lib/factory.js b/packages/composer-common/lib/factory.js
index b464dc64f5..7e26f44187 100644
--- a/packages/composer-common/lib/factory.js
+++ b/packages/composer-common/lib/factory.js
@@ -29,6 +29,7 @@ const Concept = require('./model/concept');
const ValidatedConcept = require('./model/validatedconcept');
const TransactionDeclaration = require('./introspect/transactiondeclaration');
+const EventDeclaration = require('./introspect/eventdeclaration');
const uuid = require('uuid');
@@ -304,6 +305,36 @@ class Factory {
return transaction;
}
+ /**
+ * Create a new event object. The identifier of the event is
+ * set to a UUID.
+ * @param {string} ns - the namespace of the event.
+ * @param {string} type - the type of the event.
+ * @param {Object} [options] - an optional set of options
+ * @param {string} [options.generate] - Pass one of:
+ * - sample
- return a resource instance with generated sample data.
+ * - empty
- return a resource instance with empty property values.
+ * @return {Resource} A resource for the new event.
+ */
+ newEvent(ns, type, options) {
+ if (!ns) {
+ throw new Error('ns not specified');
+ } else if (!type) {
+ throw new Error('type not specified');
+ }
+ const id = 'valid';
+ let event = this.newResource(ns, type, id, options);
+ const classDeclaration = event.getClassDeclaration();
+ if (!(classDeclaration instanceof EventDeclaration)) {
+ throw new Error(event.getClassDeclaration().getFullyQualifiedName() + ' is not an event');
+ }
+
+ // set the timestamp
+ event.timestamp = new Date();
+
+ return event;
+ }
+
/**
* Stop serialization of this object.
* @return {Object} An empty object.
diff --git a/packages/composer-common/test/factory.js b/packages/composer-common/test/factory.js
index a0cabed8e5..e33029fa32 100644
--- a/packages/composer-common/test/factory.js
+++ b/packages/composer-common/test/factory.js
@@ -44,6 +44,10 @@ describe('Factory', () => {
transaction MyTransaction identified by transactionId {
o String transactionId
o String newValue
+ }
+ event MyEvent identified by eventId {
+ o String eventId
+ o String value
}`);
factory = new Factory(modelManager);
sandbox = sinon.sandbox.create();
@@ -217,6 +221,39 @@ describe('Factory', () => {
});
+ describe('#newEvent', () => {
+ it('should throw if ns not specified', () => {
+ (() => {
+ factory.newEvent(null, 'MyEvent');
+ }).should.throw(/ns not specified/);
+ });
+
+ it('should throw if type not specified', () => {
+ (() => {
+ factory.newEvent('org.acme.test', null);
+ }).should.throw(/type not specified/);
+ });
+
+ it('should throw if a non event type was specified', () => {
+ (() => {
+ factory.newEvent('org.acme.test', 'MyTransaction');
+ }).should.throw(/not an event/);
+ });
+
+ it('should create a new instance with a generated ID', () => {
+ let resource = factory.newEvent('org.acme.test', 'MyEvent');
+ resource.eventId.should.equal('valid');
+ resource.timestamp.should.be.an.instanceOf(Date);
+ });
+
+ it('should pass options onto newEvent', () => {
+ let spy = sandbox.spy(factory, 'newResource');
+ factory.newEvent('org.acme.test', 'MyEvent', { hello: 'world' });
+ sinon.assert.calledOnce(spy);
+ sinon.assert.calledWith(spy, 'org.acme.test', 'MyEvent', 'valid', { hello: 'world' });
+ });
+ });
+
describe('#toJSON', () => {
it('should return an empty object', () => {
diff --git a/packages/composer-connector-embedded/lib/embeddedconnection.js b/packages/composer-connector-embedded/lib/embeddedconnection.js
index 540b737b63..fdd08697e9 100644
--- a/packages/composer-connector-embedded/lib/embeddedconnection.js
+++ b/packages/composer-connector-embedded/lib/embeddedconnection.js
@@ -182,7 +182,7 @@ class EmbeddedConnection extends Connection {
let engine = EmbeddedConnection.createEngine(container);
EmbeddedConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeUUID);
EmbeddedConnection.addChaincode(chaincodeUUID, container, engine);
- let context = new EmbeddedContext(engine, userID);
+ let context = new EmbeddedContext(engine, userID, this);
return businessNetwork.toArchive()
.then((businessNetworkArchive) => {
return engine.init(context, 'init', [businessNetworkArchive.toString('base64')]);
@@ -246,7 +246,7 @@ class EmbeddedConnection extends Connection {
let userID = securityContext.getUserID();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
- let context = new EmbeddedContext(chaincode.engine, userID);
+ let context = new EmbeddedContext(chaincode.engine, userID, this);
return chaincode.engine.query(context, functionName, args)
.then((data) => {
return Buffer.from(JSON.stringify(data));
@@ -265,7 +265,7 @@ class EmbeddedConnection extends Connection {
let userID = securityContext.getUserID();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
- let context = new EmbeddedContext(chaincode.engine, userID);
+ let context = new EmbeddedContext(chaincode.engine, userID, this);
return chaincode.engine.invoke(context, functionName, args)
.then((data) => {
return undefined;
diff --git a/packages/composer-connector-hlf/lib/hfcconnection.js b/packages/composer-connector-hlf/lib/hfcconnection.js
index ffcbcd677f..53be1b04aa 100644
--- a/packages/composer-connector-hlf/lib/hfcconnection.js
+++ b/packages/composer-connector-hlf/lib/hfcconnection.js
@@ -50,6 +50,8 @@ class HFCConnection extends Connection {
LOG.info('constructor', 'Creating connection', this.getIdentifier());
this.chain = chain;
this.connectOptions = connectOptions;
+
+ this.composerEventId = null;
}
/**
@@ -66,6 +68,7 @@ class HFCConnection extends Connection {
* terminated, or rejected with an error.
*/
disconnect() {
+ // this.chain.getEventHub().unregisterChaincodeEvent(this.composerEventId);
this.chain.eventHubDisconnect();
this.businessNetworkIdentifier = null;
this.connectionProfile = null;
@@ -99,6 +102,7 @@ class HFCConnection extends Connection {
result.setUser(enrollmentID);
result.setEnrolledMember(enrolledMember);
result.setEventHub(self.chain.getEventHub());
+
LOG.info('login', 'Successful login', self.getIdentifier());
resolve(result);
});
@@ -126,6 +130,7 @@ class HFCConnection extends Connection {
}
})
.then(() => {
+ this.subscribeToEvents(securityContext.getChaincodeID());
return securityContext;
});
});
@@ -336,6 +341,20 @@ class HFCConnection extends Connection {
});
}
+ /**
+ * Subscribe to events emitted by transactions
+ * @param {String} chaincodeID The chaincode ID
+ */
+ subscribeToEvents(chaincodeID) {
+ if (this.chain.getEventHub() && chaincodeID) {
+ LOG.entry('registerChaincodeEvent', chaincodeID, 'composer');
+ this.composerEventId = this.chain.getEventHub().registerChaincodeEvent(chaincodeID, 'composer', (event) => {
+ const jsonEvent = JSON.parse(event.payload.toString('utf8'));
+ this.emit('events', jsonEvent);
+ });
+ }
+ }
+
}
module.exports = HFCConnection;
diff --git a/packages/composer-connector-hlf/test/hfcconnection.js b/packages/composer-connector-hlf/test/hfcconnection.js
index 282dcdf3d7..957da1433e 100644
--- a/packages/composer-connector-hlf/test/hfcconnection.js
+++ b/packages/composer-connector-hlf/test/hfcconnection.js
@@ -130,17 +130,28 @@ describe('HFCConnection', () => {
});
it('should enroll against the Hyperledger Fabric', function() {
-
+ const events = {
+ payload: {
+ toString: () => {
+ return '{"events": "events"}';
+ }
+ }
+ };
+ connection.emit = sinon.stub();
// Login to the Hyperledger Fabric using the mock hfc.
let enrollmentID = 'doge';
let enrollmentSecret = 'suchsecret';
return connection
.login('doge', 'suchsecret')
.then(function(securityContext) {
+ mockEventHub.registerChaincodeEvent.withArgs('123', 'composer', sinon.match.func).yield(events);
sinon.assert.calledOnce(mockChain.enroll);
sinon.assert.calledWith(mockChain.enroll, enrollmentID, enrollmentSecret);
sinon.assert.calledOnce(mockChain.setRegistrar);
sinon.assert.calledWith(mockChain.setRegistrar, mockMember);
+ sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent);
+ sinon.assert.calledOnce(connection.emit);
+ sinon.assert.calledWith(connection.emit, 'events', {'events':'events'});
securityContext.should.be.a.instanceOf(HFCSecurityContext);
securityContext.getEnrolledMember().should.equal(mockMember);
securityContext.getEventHub().should.equal(mockEventHub);
diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js
index 2dd94ca9b5..9d1edadecc 100644
--- a/packages/composer-connector-hlfv1/lib/hlfconnection.js
+++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js
@@ -82,7 +82,6 @@ class HLFConnection extends Connection {
super(connectionManager, connectionProfile, businessNetworkIdentifier);
const method = 'constructor';
LOG.entry(method, connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, chain, eventHubs, caClient);
-
// Validate all the arguments.
if (!connectOptions) {
throw new Error('connectOptions not specified');
@@ -100,7 +99,17 @@ class HLFConnection extends Connection {
this.connectOptions = connectOptions;
this.client = client;
this.chain = chain;
+ this.businessNetworkIdentifier = businessNetworkIdentifier;
+
this.eventHubs = eventHubs;
+
+ if (businessNetworkIdentifier) {
+ LOG.entry(method, 'registerChaincodeEvent', businessNetworkIdentifier, 'composer');
+ eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => {
+ this.emit('events', JSON.parse(event.payload.toString('utf8')));
+ });
+ }
+
this.caClient = caClient;
// We create promisified versions of these APIs.
@@ -134,6 +143,7 @@ class HLFConnection extends Connection {
if (eventHub.isconnected()) {
eventHub.disconnect();
}
+ this.eventHubs[0].unregisterChaincodeEvent(this.businessNetworkIdentifier);
});
LOG.exit(method);
})
diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js
index 1d8adc0d70..385d4a9fd1 100644
--- a/packages/composer-connector-hlfv1/test/hlfconnection.js
+++ b/packages/composer-connector-hlfv1/test/hlfconnection.js
@@ -92,6 +92,21 @@ describe('HLFConnection', () => {
describe('#constructor', () => {
+ it('should subscribe to the eventHub and emit events', () => {
+ const events = {
+ payload: {
+ toString: () => {
+ return '{"event":"event"}';
+ }
+ }
+ };
+ connection.emit = sandbox.stub();
+ mockEventHub.registerChaincodeEvent.withArgs('org.acme.biznet', 'composer', sinon.match.func).yield(events);
+ sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent);
+ sinon.assert.calledWith(mockEventHub.registerChaincodeEvent, 'org.acme.biznet', 'composer', sinon.match.func);
+ sinon.assert.calledOnce(connection.emit);
+ });
+
it('should throw if connectOptions not specified', () => {
(() => {
new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', null, mockClient, mockChain, mockEventHub, mockCAClient);
@@ -128,7 +143,6 @@ describe('HLFConnection', () => {
new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChain, [mockEventHub], null);
}).should.throw(/caClient not specified/);
});
-
});
describe('#getConnectionOptions', () => {
diff --git a/packages/composer-connector-proxy/lib/proxyconnection.js b/packages/composer-connector-proxy/lib/proxyconnection.js
index 4c0afde38a..3cc045d52c 100644
--- a/packages/composer-connector-proxy/lib/proxyconnection.js
+++ b/packages/composer-connector-proxy/lib/proxyconnection.js
@@ -17,7 +17,9 @@
const Connection = require('composer-common').Connection;
const ProxyUtil = require('./proxyutil');
const ProxySecurityContext = require('./proxysecuritycontext');
+const Logger = require('composer-common').Logger;
+const LOG = Logger.getLog('ProxyConnection');
/**
* Base class representing a connection to a business network.
* @protected
@@ -45,6 +47,8 @@ class ProxyConnection extends Connection {
* terminated, or rejected with an error.
*/
disconnect() {
+ const method = 'disconnect';
+ LOG.entry(method);
return new Promise((resolve, reject) => {
this.socket.emit('/api/connectionDisconnect', this.connectionID, (error) => {
if (error) {
@@ -52,6 +56,10 @@ class ProxyConnection extends Connection {
}
resolve();
});
+ })
+ .then(() => {
+ this.socket.removeListener('events', () => {});
+ LOG.exit(method);
});
}
diff --git a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js
index 5ee0a5be47..9f3df3db9a 100644
--- a/packages/composer-connector-proxy/lib/proxyconnectionmanager.js
+++ b/packages/composer-connector-proxy/lib/proxyconnectionmanager.js
@@ -18,6 +18,9 @@ const ConnectionManager = require('composer-common').ConnectionManager;
const ProxyConnection = require('./proxyconnection');
const ProxyUtil = require('./proxyutil');
const socketIOClient = require('socket.io-client');
+const Logger = require('composer-common').Logger;
+
+const LOG = Logger.getLog('ProxyConnectionManager');
let connectorServerURL = 'http://localhost:15699';
@@ -37,6 +40,20 @@ class ProxyConnectionManager extends ConnectionManager {
connectorServerURL = url;
}
+ /**
+ * Create a connection for ease of unit testing
+ * @param {ProxyConnectionManager} _this The ConnectionManaget
+ * @param {String} connectionProfile The connection profile to use
+ * @param {String} businessNetworkIdentifier The network identifier to use
+ * @param {Socket.io} socket The socket to use
+ * @param {String} connectionID The connection ID to use
+ * @returns {ProxyConnection} The connection
+ */
+ static createConnection(_this, connectionProfile, businessNetworkIdentifier, socket, connectionID) {
+ return new ProxyConnection(_this, connectionProfile, businessNetworkIdentifier, socket, connectionID);
+
+ }
+
/**
* Creates a new ProxyConnectionManager
* @param {ConnectionProfileManager} connectionProfileManager
@@ -80,6 +97,8 @@ class ProxyConnectionManager extends ConnectionManager {
* object once the connection is established, or rejected with a connection error.
*/
connect(connectionProfile, businessNetworkIdentifier, connectionOptions) {
+ const method = 'connect';
+ LOG.entry(method, connectionProfile, businessNetworkIdentifier, connectionOptions);
return this.ensureConnected()
.then(() => {
return new Promise((resolve, reject) => {
@@ -87,7 +106,15 @@ class ProxyConnectionManager extends ConnectionManager {
if (error) {
return reject(ProxyUtil.inflaterr(error));
}
- let connection = new ProxyConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID);
+ let connection = ProxyConnectionManager.createConnection(this, connectionProfile, businessNetworkIdentifier, this.socket, connectionID);
+ // Only emit when client
+ this.socket.on('events', (myConnectionID, events) => {
+ LOG.debug(method, events);
+ if (myConnectionID === connectionID) {
+ connection.emit('events', events);
+ }
+ });
+ LOG.exit(method);
resolve(connection);
});
});
diff --git a/packages/composer-connector-proxy/test/proxyconnection.js b/packages/composer-connector-proxy/test/proxyconnection.js
index 4661f3751a..e10c1725f7 100644
--- a/packages/composer-connector-proxy/test/proxyconnection.js
+++ b/packages/composer-connector-proxy/test/proxyconnection.js
@@ -44,7 +44,8 @@ describe('ProxyConnection', () => {
beforeEach(() => {
mockConnectionManager = sinon.createStubInstance(ConnectionManager);
mockSocket = {
- emit: sinon.stub()
+ emit: sinon.stub(),
+ removeListener: sinon.stub()
};
mockSocket.emit.throws(new Error('unexpected call'));
connection = new ProxyConnection(mockConnectionManager, connectionProfile, businessNetworkIdentifier, mockSocket, connectionID);
@@ -59,6 +60,7 @@ describe('ProxyConnection', () => {
.then(() => {
sinon.assert.calledOnce(mockSocket.emit);
sinon.assert.calledWith(mockSocket.emit, '/api/connectionDisconnect', connectionID, sinon.match.func);
+ mockSocket.removeListener.withArgs('events', sinon.match.func).yield();
});
});
diff --git a/packages/composer-connector-proxy/test/proxyconnectionmanager.js b/packages/composer-connector-proxy/test/proxyconnectionmanager.js
index 742a94bd2a..c7d2a7c2db 100644
--- a/packages/composer-connector-proxy/test/proxyconnectionmanager.js
+++ b/packages/composer-connector-proxy/test/proxyconnectionmanager.js
@@ -38,6 +38,9 @@ describe('ProxyConnectionManager', () => {
let mockConnectionProfileManager;
let ProxyConnectionManager;
+ let connectionManager;
+ let mockConnection;
+
beforeEach(() => {
mockConnectionProfileManager = sinon.createStubInstance(ConnectionProfileManager);
mockSocket = {
@@ -45,9 +48,9 @@ describe('ProxyConnectionManager', () => {
once: sinon.stub(),
on: sinon.stub()
};
- mockSocket.emit.throws(new Error('unexpected call'));
- mockSocket.once.throws(new Error('unexpected call'));
- mockSocket.on.throws(new Error('unexpected call'));
+ // mockSocket.emit.throws(new Error('unexpected call'));
+ // mockSocket.once.throws(new Error('unexpected call'));
+ // mockSocket.on.throws(new Error('unexpected call'));
mockSocketFactory = sinon.stub().returns(mockSocket);
ProxyConnectionManager = proxyquire('../lib/proxyconnectionmanager', {
'socket.io-client': mockSocketFactory
@@ -128,9 +131,10 @@ describe('ProxyConnectionManager', () => {
describe('#connect', () => {
- let connectionManager;
-
beforeEach(() => {
+ mockConnection = sinon.createStubInstance(ProxyConnection);
+ mockConnection.connectionID = connectionID;
+ mockConnection.socket = mockSocket;
mockSocket.on.withArgs('connect').returns();
mockSocket.on.withArgs('disconnect').returns();
connectionManager = new ProxyConnectionManager(mockConnectionProfileManager);
@@ -139,6 +143,8 @@ describe('ProxyConnectionManager', () => {
it('should send a connect call to the connector server', () => {
mockSocket.emit.withArgs('/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func).yields(null, connectionID);
+ sinon.stub(ProxyConnectionManager, 'createConnection').returns(mockConnection);
+ mockSocket.on.withArgs('events', sinon.match.func).yields(connectionID, [{'event': 'event1'}, {'evnet': 'event2'}]);
return connectionManager.connect(connectionProfile, businessNetworkIdentifier, connectionOptions)
.then((connection) => {
sinon.assert.calledOnce(mockSocket.emit);
@@ -146,6 +152,26 @@ describe('ProxyConnectionManager', () => {
connection.should.be.an.instanceOf(ProxyConnection);
connection.socket.should.equal(mockSocket);
connection.connectionID.should.equal(connectionID);
+ sinon.assert.calledThrice(mockSocket.on);
+ sinon.assert.calledWith(mockSocket.on, 'events', sinon.match.func);
+ sinon.assert.calledWith(mockConnection.emit, 'events', [{'event': 'event1'}, {'evnet': 'event2'}]);
+ });
+ });
+
+ it('should not emit events if connectionID and myConnectionID dont match', () => {
+ mockSocket.emit.withArgs('/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func).yields(null, connectionID);
+ sinon.stub(ProxyConnectionManager, 'createConnection').returns(mockConnection);
+ mockSocket.on.withArgs('events', sinon.match.func).yields('myConnectionID', '[{"event": "event1"}, {"evnet": "event2"}]');
+ return connectionManager.connect(connectionProfile, businessNetworkIdentifier, connectionOptions)
+ .then((connection) => {
+ sinon.assert.calledOnce(mockSocket.emit);
+ sinon.assert.calledWith(mockSocket.emit, '/api/connectionManagerConnect', connectionProfile, businessNetworkIdentifier, connectionOptions, sinon.match.func);
+ connection.should.be.an.instanceOf(ProxyConnection);
+ connection.socket.should.equal(mockSocket);
+ connection.connectionID.should.equal(connectionID);
+ sinon.assert.calledThrice(mockSocket.on);
+ sinon.assert.calledWith(mockSocket.on, 'events', sinon.match.func);
+ sinon.assert.notCalled(connection.emit);
});
});
@@ -157,4 +183,11 @@ describe('ProxyConnectionManager', () => {
});
+ describe('#createConnection', () => {
+ it('should create an instance of ProxyConnection', () => {
+ let cm = ProxyConnectionManager.createConnection(connectionManager, 'profile', 'businessNetworkIdentifier', mockSocket, connectionID);
+ cm.should.be.an.instanceOf(ProxyConnection);
+ });
+ });
+
});
diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js
index a5d1867482..972f0f8fc2 100644
--- a/packages/composer-connector-server/lib/connectorserver.js
+++ b/packages/composer-connector-server/lib/connectorserver.js
@@ -123,6 +123,9 @@ class ConnectorServer {
return Promise.resolve();
}
delete this.connections[connectionID];
+
+ connection.removeListener('events', () => {});
+
return connection.disconnect()
.then(() => {
callback(null);
@@ -161,6 +164,12 @@ class ConnectorServer {
callback(null, securityContextID);
LOG.exit(method, securityContextID);
})
+ .then(() => {
+ connection.on('events', (events) => {
+ LOG.debug(method, events);
+ this.socket.emit('events', connectionID, events);
+ });
+ })
.catch((error) => {
LOG.error(error);
callback(ConnectorServer.serializerr(error));
diff --git a/packages/composer-connector-server/test/connectorserver.js b/packages/composer-connector-server/test/connectorserver.js
index 927dbb990b..4ecd700809 100644
--- a/packages/composer-connector-server/test/connectorserver.js
+++ b/packages/composer-connector-server/test/connectorserver.js
@@ -63,7 +63,8 @@ describe('ConnectorServer', () => {
mockConnectionProfileStore.load.throws(new Error('unexpected call'));
mockConnectionProfileStore.save.throws(new Error('unexpected call'));
mockSocket = {
- on: sinon.stub()
+ on: sinon.stub(),
+ emit: sinon.stub()
};
mockConnection = sinon.createStubInstance(Connection);
mockSecurityContext = sinon.createStubInstance(SecurityContext);
@@ -200,7 +201,9 @@ describe('ConnectorServer', () => {
const cb = sinon.stub();
return connectorServer.connectionDisconnect(connectionID, cb)
.then(() => {
+ mockConnection.removeListener.withArgs('events', sinon.match.func).yield(['event1', 'event2']);
should.equal(connectorServer.connections[connectionID], undefined);
+ sinon.assert.calledOnce(mockConnection.removeListener);
sinon.assert.calledOnce(cb);
sinon.assert.calledWith(cb, null);
});
@@ -249,6 +252,9 @@ describe('ConnectorServer', () => {
sinon.assert.calledOnce(cb);
sinon.assert.calledWith(cb, null);
connectorServer.securityContexts[securityContextID].should.equal(mockSecurityContext);
+ mockConnection.on.withArgs('events', sinon.match.func).yield(['event1', 'event2']);
+ sinon.assert.calledOnce(mockSocket.emit);
+ sinon.assert.calledWith(mockSocket.emit, 'events', connectionID, ['event1', 'event2']);
});
});
diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js
index 747661ec5f..1658787b9f 100644
--- a/packages/composer-connector-web/lib/webconnection.js
+++ b/packages/composer-connector-web/lib/webconnection.js
@@ -14,6 +14,7 @@
'use strict';
+// const Resource = require('composer-common').Resource;
const Connection = require('composer-common').Connection;
const Engine = require('composer-runtime').Engine;
const uuid = require('uuid');
@@ -192,7 +193,7 @@ class WebConnection extends Connection {
let engine = WebConnection.createEngine(container);
WebConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeID);
WebConnection.addChaincode(chaincodeID, container, engine);
- let context = new WebContext(engine, userID);
+ let context = new WebContext(engine, userID, this);
return businessNetwork.toArchive()
.then((businessNetworkArchive) => {
return engine.init(context, 'init', [businessNetworkArchive.toString('base64')]);
@@ -259,7 +260,7 @@ class WebConnection extends Connection {
let userID = securityContext.getUserID();
let chaincodeID = securityContext.getChaincodeID();
let chaincode = WebConnection.getChaincode(chaincodeID);
- let context = new WebContext(chaincode.engine, userID);
+ let context = new WebContext(chaincode.engine, userID, this);
return chaincode.engine.query(context, functionName, args)
.then((data) => {
return Buffer.from(JSON.stringify(data));
@@ -278,7 +279,7 @@ class WebConnection extends Connection {
let userID = securityContext.getUserID();
let chaincodeID = securityContext.getChaincodeID();
let chaincode = WebConnection.getChaincode(chaincodeID);
- let context = new WebContext(chaincode.engine, userID);
+ let context = new WebContext(chaincode.engine, userID, this);
return chaincode.engine.invoke(context, functionName, args)
.then((data) => {
return undefined;
diff --git a/packages/composer-connector-web/lib/webconnectionmanager.js b/packages/composer-connector-web/lib/webconnectionmanager.js
index 43406c94cc..4300796b53 100644
--- a/packages/composer-connector-web/lib/webconnectionmanager.js
+++ b/packages/composer-connector-web/lib/webconnectionmanager.js
@@ -43,7 +43,8 @@ class WebConnectionManager extends ConnectionManager {
* object once the connection is established, or rejected with a connection error.
*/
connect(connectionProfile, businessNetworkIdentifier, connectionOptions) {
- return Promise.resolve(new WebConnection(this, connectionProfile, businessNetworkIdentifier));
+ let connection = new WebConnection(this, connectionProfile, businessNetworkIdentifier);
+ return Promise.resolve(connection);
}
}
diff --git a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts
index 68e83c36dc..2b9e52ccd9 100644
--- a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts
+++ b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.ts
@@ -192,6 +192,7 @@ export class ConnectionProfileDataComponent {
this.profileUpdated.emit({updated: true});
}, (reason) => {
+ console.log(reason);
if (reason && reason !== 1) { // someone hasn't pressed escape
this.alertService.errorStatus$.next(reason);
}
diff --git a/packages/composer-playground/src/app/test/test.component.ts b/packages/composer-playground/src/app/test/test.component.ts
index ab8616f5a8..ae4fba4cd2 100644
--- a/packages/composer-playground/src/app/test/test.component.ts
+++ b/packages/composer-playground/src/app/test/test.component.ts
@@ -1,3 +1,4 @@
+
import { Component, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ClientService } from '../services/client.service';
diff --git a/packages/composer-runtime-embedded/index.js b/packages/composer-runtime-embedded/index.js
index d833536e23..7771b32bbd 100644
--- a/packages/composer-runtime-embedded/index.js
+++ b/packages/composer-runtime-embedded/index.js
@@ -18,5 +18,6 @@ module.exports.EmbeddedContainer = require('./lib/embeddedcontainer');
module.exports.EmbeddedContext = require('./lib/embeddedcontext');
module.exports.EmbeddedDataCollection = require('./lib/embeddeddatacollection');
module.exports.EmbeddedDataService = require('./lib/embeddeddataservice');
+module.exports.EmbeddedEventService = require('./lib/embeddedeventservice');
module.exports.EmbeddedIdentityService = require('./lib/embeddedidentityservice');
module.exports.EmbeddedLoggingService = require('./lib/embeddedloggingservice');
diff --git a/packages/composer-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js
index b7433069ce..02f03fa4d8 100644
--- a/packages/composer-runtime-embedded/lib/embeddedcontext.js
+++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js
@@ -16,6 +16,7 @@
const Context = require('composer-runtime').Context;
const EmbeddedIdentityService = require('./embeddedidentityservice');
+const EmbeddedEventService = require('./embeddedeventservice');
/**
* A class representing the current request being handled by the JavaScript engine.
@@ -27,11 +28,13 @@ class EmbeddedContext extends Context {
* Constructor.
* @param {Engine} engine The owning engine.
* @param {String} userID The current user ID.
+ * @param {EventEmitter} eventSink The event emitter
*/
- constructor(engine, userID) {
+ constructor(engine, userID, eventSink) {
super(engine);
this.dataService = engine.getContainer().getDataService();
this.identityService = new EmbeddedIdentityService(userID);
+ this.eventSink = eventSink;
}
/**
@@ -50,6 +53,17 @@ class EmbeddedContext extends Context {
return this.identityService;
}
+
+ /**
+ * Get the event service provided by the chaincode container.
+ * @return {EventService} The event service provided by the chaincode container.
+ */
+ getEventService() {
+ if (!this.eventService) {
+ this.eventService = new EmbeddedEventService(this.eventSink);
+ }
+ return this.eventService;
+ }
}
module.exports = EmbeddedContext;
diff --git a/packages/composer-runtime-embedded/lib/embeddedeventservice.js b/packages/composer-runtime-embedded/lib/embeddedeventservice.js
new file mode 100644
index 0000000000..f0de0c4b49
--- /dev/null
+++ b/packages/composer-runtime-embedded/lib/embeddedeventservice.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const EventService = require('composer-runtime').EventService;
+const Logger = require('composer-common').Logger;
+
+const LOG = Logger.getLog('WebDataService');
+
+/**
+ * Base class representing the event service provided by a {@link Container}.
+ * @protected
+ */
+class EmbeddedEventService extends EventService {
+
+ /**
+ * Constructor.
+ * @param {EventEmitter} eventSink the event emitter
+ */
+ constructor(eventSink) {
+ super();
+ const method = 'constructor';
+
+ this.eventSink = eventSink;
+
+ LOG.exit(method);
+ }
+
+ /**
+ * Emit the events stored in eventBuffer
+ */
+ commit() {
+ const jsonEvent = JSON.parse(this.serializeBuffer());
+ this.eventSink.emit('events', jsonEvent);
+ }
+}
+
+module.exports = EmbeddedEventService;
diff --git a/packages/composer-runtime-embedded/test/embeddedcontext.js b/packages/composer-runtime-embedded/test/embeddedcontext.js
index 2ea470790a..4200d9d81f 100644
--- a/packages/composer-runtime-embedded/test/embeddedcontext.js
+++ b/packages/composer-runtime-embedded/test/embeddedcontext.js
@@ -14,12 +14,14 @@
'use strict';
+const Serializer = require('composer-common').Serializer;
const Context = require('composer-runtime').Context;
const DataService = require('composer-runtime').DataService;
const Engine = require('composer-runtime').Engine;
const EmbeddedContainer = require('..').EmbeddedContainer;
const EmbeddedContext = require('..').EmbeddedContext;
const IdentityService = require('composer-runtime').IdentityService;
+const EventService = require('composer-runtime').EventService;
require('chai').should();
const sinon = require('sinon');
@@ -28,6 +30,7 @@ describe('EmbeddedContext', () => {
let mockEmbeddedContainer;
let mockDataService;
+ let mockSerializer;
let mockEngine;
beforeEach(() => {
@@ -36,6 +39,7 @@ describe('EmbeddedContext', () => {
mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockEmbeddedContainer);
mockEmbeddedContainer.getDataService.returns(mockDataService);
+ mockSerializer = sinon.createStubInstance(Serializer);
});
describe('#constructor', () => {
@@ -66,4 +70,19 @@ describe('EmbeddedContext', () => {
});
+ describe('#getEventService', () => {
+
+ it('should return the container event service', () => {
+ let context = new EmbeddedContext(mockEngine, 'bob1');
+ context.getSerializer = sinon.stub().returns(mockSerializer);
+ context.getEventService().should.be.an.instanceOf(EventService);
+ });
+
+ it('should return this.eventService if it is set', () => {
+ let context = new EmbeddedContext(mockEngine, 'bob1');
+ context.eventService = {};
+ context.getEventService().should.deep.equal({});
+ });
+ });
+
});
diff --git a/packages/composer-runtime-embedded/test/embeddedeventservice.js b/packages/composer-runtime-embedded/test/embeddedeventservice.js
new file mode 100644
index 0000000000..ef6c6a0f2d
--- /dev/null
+++ b/packages/composer-runtime-embedded/test/embeddedeventservice.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const EmbeddedEventService = require('..').EmbeddedEventService;
+const EventEmitter = require('events').EventEmitter;
+
+const chai = require('chai');
+chai.should();
+chai.use(require('chai-as-promised'));
+const sinon = require('sinon');
+require('sinon-as-promised');
+
+describe('EmbeddedEventService', () => {
+
+ let eventService;
+ let mockEventEmitter;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ mockEventEmitter = sinon.createStubInstance(EventEmitter);
+ eventService = new EmbeddedEventService(mockEventEmitter);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe('#constructor', () => {
+ it('should assign a default event emitter', () => {
+ eventService = new EmbeddedEventService(mockEventEmitter);
+ eventService.eventSink.should.be.an.instanceOf(EventEmitter);
+ });
+ });
+
+ describe('#commit', () => {
+ it ('should emit a list of events', () => {
+ eventService.serializeBuffer = sinon.stub();
+ eventService.serializeBuffer.returns('[{"event":"event"}]');
+ eventService.commit();
+ sinon.assert.calledOnce(eventService.serializeBuffer);
+ sinon.assert.calledOnce(mockEventEmitter.emit);
+ sinon.assert.calledWith(mockEventEmitter.emit, 'events', [{'event':'event'}]);
+ });
+ });
+});
diff --git a/packages/composer-runtime-hlf/context.go b/packages/composer-runtime-hlf/context.go
index 21629277d4..3e5369363d 100644
--- a/packages/composer-runtime-hlf/context.go
+++ b/packages/composer-runtime-hlf/context.go
@@ -26,6 +26,7 @@ type Context struct {
This *otto.Object
DataService *DataService
IdentityService *IdentityService
+ EventService *EventService
}
// NewContext creates a Go wrapper around a new instance of the Context JavaScript class.
@@ -52,10 +53,12 @@ func NewContext(vm *otto.Otto, engine *Engine, stub shim.ChaincodeStubInterface)
// Create the services.
result.DataService = NewDataService(vm, result, stub)
result.IdentityService = NewIdentityService(vm, result, stub)
+ result.EventService = NewEventService(vm, result, stub)
// Bind the methods into the JavaScript object.
result.This.Set("getDataService", result.getDataService)
result.This.Set("getIdentityService", result.getIdentityService)
+ result.This.Set("getEventService", result.getEventService)
return result
}
@@ -75,3 +78,11 @@ func (context *Context) getIdentityService(call otto.FunctionCall) (result otto.
return context.IdentityService.This.Value()
}
+
+// getEventService ...
+func (context *Context) getEventService(call otto.FunctionCall) (result otto.Value) {
+ logger.Debug("Entering Context.getEventService", call)
+ defer func() { logger.Debug("Exiting Context.getEventService", result) }()
+
+ return context.EventService.This.Value()
+}
diff --git a/packages/composer-runtime-hlf/eventservice.go b/packages/composer-runtime-hlf/eventservice.go
new file mode 100644
index 0000000000..7b5a278ca9
--- /dev/null
+++ b/packages/composer-runtime-hlf/eventservice.go
@@ -0,0 +1,80 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/hyperledger/fabric/core/chaincode/shim"
+ "github.com/robertkrimen/otto"
+)
+
+// EventService is a go wrapper around the EventService JavaScript class
+type EventService struct {
+ This *otto.Object
+ Stub shim.ChaincodeStubInterface
+}
+
+// NewEventService creates a Go wrapper around a new instance of the EventService JavaScript class.
+func NewEventService(vm *otto.Otto, context *Context, stub shim.ChaincodeStubInterface) (result *EventService) {
+ logger.Debug("Entering NewEventService", vm, context, &stub)
+ defer func() { logger.Debug("Exiting NewEventServce", result) }()
+
+ // Create a new instance of the JavaScript chaincode class.
+ temp, err := vm.Call("new concerto.EventService", nil, context.This)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create new instance of EventService JavaScript class: %v", err))
+ } else if !temp.IsObject() {
+ panic("New instance of EventService JavaScript class is not an object")
+ }
+ object := temp.Object()
+
+ // Add a pointer to the Go object into the JavaScript object.
+ result = &EventService{This: temp.Object(), Stub: stub}
+ err = object.Set("$this", result)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to store Go object in EventService JavaScript object: %v", err))
+ }
+
+ // Bind the methods into the JavaScript object.
+ result.This.Set("_commit", result.commit)
+ return result
+}
+
+// Serializes the buffered events and emits them
+func (eventService *EventService) commit(call otto.FunctionCall) (result otto.Value) {
+ logger.Debug("Entering EventService.commit", call)
+ defer func() { logger.Debug("Exiting EventService.commit", result) }()
+
+ callback := call.Argument(0)
+
+ value, err := call.This.Object().Call("serializeBuffer")
+
+ if err != nil {
+ panic(err)
+ }
+
+ if len(value.String()) > 0 {
+ logger.Debug("Emitting event from EventService.commit", value.String())
+ eventService.Stub.SetEvent("composer", []byte(value.String()))
+ }
+
+ _, err = callback.Call(callback, nil, eventService.This)
+ if err != nil {
+ panic(err)
+ }
+
+ return otto.UndefinedValue()
+}
diff --git a/packages/composer-runtime-hlfv1/chaincode_test.go b/packages/composer-runtime-hlfv1/chaincode_test.go
deleted file mode 100644
index 534d0658c9..0000000000
--- a/packages/composer-runtime-hlfv1/chaincode_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package main
diff --git a/packages/composer-runtime-hlfv1/context.go b/packages/composer-runtime-hlfv1/context.go
index eaf4fa5349..a52e197ab0 100644
--- a/packages/composer-runtime-hlfv1/context.go
+++ b/packages/composer-runtime-hlfv1/context.go
@@ -24,6 +24,7 @@ type Context struct {
VM *duktape.Context
DataService *DataService
IdentityService *IdentityService
+ EventService *EventService
}
// NewContext creates a Go wrapper around a new instance of the Context JavaScript class.
@@ -40,6 +41,7 @@ func NewContext(vm *duktape.Context, engine *Engine, stub shim.ChaincodeStubInte
// Create the services.
result.DataService = NewDataService(vm, result, stub)
result.IdentityService = NewIdentityService(vm, result, stub)
+ result.EventService = NewEventService(vm, result, stub)
// Find the JavaScript engine object.
vm.PushGlobalStash() // [ stash ]
@@ -64,6 +66,8 @@ func NewContext(vm *duktape.Context, engine *Engine, stub shim.ChaincodeStubInte
vm.PutPropString(-2, "getDataService") // [ stash theEngine global composer theContext ]
vm.PushGoFunction(result.getIdentityService) // [ stash theEngine global composer theContext getIdentityService ]
vm.PutPropString(-2, "getIdentityService") // [ stash theEngine global composer theContext ]
+ vm.PushGoFunction(result.getEventService) // [ stash theEngine global composer theContext getEventService ]
+ vm.PutPropString(-2, "getEventService") // [ stash theEngine global composer theContext ]
// Return the new context.
return result
@@ -90,3 +94,14 @@ func (context *Context) getIdentityService(vm *duktape.Context) (result int) {
vm.GetPropString(-1, "identityService")
return 1
}
+
+// getEventService returns the event service to use.
+func (context *Context) getEventService(vm *duktape.Context) (result int) {
+ logger.Debug("Entering Context.getEventService", vm)
+ defer func() { logger.Debug("Exiting Context.getEventService", result) }()
+
+ // Return the JavaScript object from the global stash.
+ vm.PushGlobalStash()
+ vm.GetPropString(-1, "eventService")
+ return 1
+}
diff --git a/packages/composer-runtime-hlfv1/eventservice.go b/packages/composer-runtime-hlfv1/eventservice.go
new file mode 100644
index 0000000000..3dc2e15b96
--- /dev/null
+++ b/packages/composer-runtime-hlfv1/eventservice.go
@@ -0,0 +1,95 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+ duktape "gopkg.in/olebedev/go-duktape.v3"
+
+ "github.com/hyperledger/fabric/core/chaincode/shim"
+)
+
+// EventService is a go wrapper around the EventService JavaScript class
+type EventService struct {
+ VM *duktape.Context
+ Stub shim.ChaincodeStubInterface
+}
+
+// NewEventService creates a Go wrapper around a new instance of the EventService JavaScript class.
+func NewEventService(vm *duktape.Context, context *Context, stub shim.ChaincodeStubInterface) (result *EventService) {
+ logger.Debug("Entering NewEventService", vm, context, &stub)
+ defer func() { logger.Debug("Exiting NewEventServce", result) }()
+
+ // Ensure the JavaScript stack is reset.
+ defer vm.SetTop(vm.GetTop())
+
+ // Create a new event service
+ result = &EventService{VM: vm, Stub: stub}
+
+ //Create a new instance of the JavaScript EventService class.
+ vm.PushGlobalObject() // [ global ]
+ vm.GetPropString(-1, "composer") // [ global composer ]
+ vm.GetPropString(-1, "EventService") // [ global composer EventService ]
+ err := vm.Pnew(0) // [ global composer theEventService ]
+ if err != nil {
+ panic(err)
+ }
+
+ // Store the event service into the global stash.
+ vm.PushGlobalStash() // [ global composer theEventService stash ]
+ vm.Dup(-2) // [ global composer theEventService stash theEventService ]
+ vm.PutPropString(-2, "eventService") // [ global composer theEventService stash ]
+ vm.Pop() // [ global composer theEventService ]
+
+ // Bind the methods into the JavaScript object.
+ vm.PushGoFunction(result.commit) // [ global composer theEventService commit ]
+ vm.PushString("bind") // [ global composer theEventService commit "bind" ]
+ vm.Dup(-3) // [ global composer theEventService commit "bind" theEventService ]
+ vm.PcallProp(-3, 1) // [ global composer theEventService commit boundCommit ]
+ vm.PutPropString(-3, "_commit") // [ global composer theEventService commit ]
+
+ // Return a new event service
+ return result
+}
+
+// Serializes the buffered events and emits them
+func (eventService *EventService) commit(vm *duktape.Context) (result int) {
+ logger.Debug("Entering EventService.commit", vm)
+ defer func() { logger.Debug("Exiting EventService.commit", result) }()
+
+ // Validate the arguments from JavaScript.
+ vm.RequireFunction(0)
+
+ vm.PushThis() // [ theEventService ]
+ vm.GetPropString(-1, "serializeBuffer") // [ theEventService, serializeBuffer ]
+ vm.RequireFunction(-1) // [ theEventService, serializeBuffer ]
+ vm.Dup(-2) // [ theEventService, serializeBuffer, theEventService ]
+ vm.CallMethod(0) // [ theEventService, returnValue ]
+ vm.RequireObjectCoercible(-1) // [ theEventService, returnValue ]
+ vm.JsonEncode(-1) // [ theEventService, returnValue ]
+ value := vm.RequireString(-1) // [ theEventService, returnValue ]
+
+ if len(value) > 0 {
+ logger.Debug("Emitting event from EventService.commit", value)
+ eventService.Stub.SetEvent("composer", []byte(value))
+ }
+
+ // Call the callback.
+ vm.Dup(0)
+ vm.PushNull()
+ if vm.Pcall(1) == duktape.ExecError {
+ panic(vm.ToString(-1))
+ }
+ return 0
+}
diff --git a/packages/composer-runtime-web/index.js b/packages/composer-runtime-web/index.js
index 1b3191ba95..62e1c82779 100644
--- a/packages/composer-runtime-web/index.js
+++ b/packages/composer-runtime-web/index.js
@@ -18,5 +18,6 @@ module.exports.WebContainer = require('./lib/webcontainer');
module.exports.WebContext = require('./lib/webcontext');
module.exports.WebDataCollection = require('./lib/webdatacollection');
module.exports.WebDataService = require('./lib/webdataservice');
+module.exports.WebEventService = require('./lib/webeventservice');
module.exports.WebIdentityService = require('./lib/webidentityservice');
module.exports.WebLoggingService = require('./lib/webloggingservice');
diff --git a/packages/composer-runtime-web/lib/webcontainer.js b/packages/composer-runtime-web/lib/webcontainer.js
index 074945cb23..fcc13fd75f 100644
--- a/packages/composer-runtime-web/lib/webcontainer.js
+++ b/packages/composer-runtime-web/lib/webcontainer.js
@@ -19,6 +19,7 @@ const uuidv4 = require('uuid');
const version = require('../package.json').version;
const WebDataService = require('./webdataservice');
const WebLoggingService = require('./webloggingservice');
+const WebEventService = require('./webeventservice');
/**
* A class representing the chaincode container hosting the JavaScript engine.
@@ -35,6 +36,7 @@ class WebContainer extends Container {
this.uuid = uuid || uuidv4.v4();
this.dataService = new WebDataService(this.uuid);
this.loggingService = new WebLoggingService();
+ this.eventService = new WebEventService();
}
/**
diff --git a/packages/composer-runtime-web/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js
index 7e6874996a..fa5b527fc2 100644
--- a/packages/composer-runtime-web/lib/webcontext.js
+++ b/packages/composer-runtime-web/lib/webcontext.js
@@ -16,6 +16,7 @@
const Context = require('composer-runtime').Context;
const WebIdentityService = require('./webidentityservice');
+const WebEventService = require('./webeventservice');
/**
* A class representing the current request being handled by the JavaScript engine.
@@ -27,11 +28,13 @@ class WebContext extends Context {
* Constructor.
* @param {Engine} engine The owning engine.
* @param {String} userID The current user ID.
+ * @param {EventEmitter} eventSink The event emitter
*/
- constructor(engine, userID) {
+ constructor(engine, userID, eventSink) {
super(engine);
this.dataService = engine.getContainer().getDataService();
this.identityService = new WebIdentityService(userID);
+ this.eventSink = eventSink;
}
/**
@@ -50,6 +53,17 @@ class WebContext extends Context {
return this.identityService;
}
+ /**
+ * Get the event service provided by the chaincode container.
+ * @return {EventService} The event service provided by the chaincode container.
+ */
+ getEventService() {
+ if (!this.eventService) {
+ this.eventService = new WebEventService(this.eventSink);
+ }
+ return this.eventService;
+ }
+
}
module.exports = WebContext;
diff --git a/packages/composer-runtime-web/lib/webeventservice.js b/packages/composer-runtime-web/lib/webeventservice.js
new file mode 100644
index 0000000000..28ab7e94d6
--- /dev/null
+++ b/packages/composer-runtime-web/lib/webeventservice.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const EventService = require('composer-runtime').EventService;
+const Logger = require('composer-common').Logger;
+
+const LOG = Logger.getLog('WebDataService');
+
+/**
+ * Base class representing the event service provided by a {@link Container}.
+ * @protected
+ */
+class WebEventService extends EventService {
+
+ /**
+ * Constructor.
+ * @param {EventEmitter} eventSink the event emitter
+ */
+ constructor(eventSink) {
+ super();
+ const method = 'constructor';
+ this.eventSink = eventSink;
+
+ LOG.exit(method);
+ }
+
+ /**
+ * Emit the events stored in eventBuffer
+ */
+ commit() {
+ const jsonEvent = JSON.parse(this.serializeBuffer());
+ this.eventSink.emit('events', jsonEvent);
+ }
+}
+
+module.exports = WebEventService;
diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js
index 235482ce12..7f82c3736a 100644
--- a/packages/composer-runtime-web/test/webcontext.js
+++ b/packages/composer-runtime-web/test/webcontext.js
@@ -14,10 +14,12 @@
'use strict';
+const Serializer = require('composer-common').Serializer;
const Context = require('composer-runtime').Context;
const DataService = require('composer-runtime').DataService;
const Engine = require('composer-runtime').Engine;
const IdentityService = require('composer-runtime').IdentityService;
+const EventService = require('composer-runtime').EventService;
const WebContainer = require('..').WebContainer;
const WebContext = require('..').WebContext;
@@ -28,6 +30,7 @@ describe('WebContext', () => {
let mockWebContainer;
let mockDataService;
+ let mockSerializer;
let mockEngine;
beforeEach(() => {
@@ -36,6 +39,7 @@ describe('WebContext', () => {
mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockWebContainer);
mockWebContainer.getDataService.returns(mockDataService);
+ mockSerializer = sinon.createStubInstance(Serializer);
});
describe('#constructor', () => {
@@ -66,4 +70,19 @@ describe('WebContext', () => {
});
+ describe('#getEventService', () => {
+
+ it('should return the container event service', () => {
+ let context = new WebContext(mockEngine, 'bob1');
+ context.getSerializer = sinon.stub().returns(mockSerializer);
+ context.getEventService().should.be.an.instanceOf(EventService);
+ });
+
+ it('should return this.eventService if it is set', () => {
+ let context = new WebContext(mockEngine, 'bob1');
+ context.eventService = {};
+ context.getEventService().should.deep.equal({});
+ });
+ });
+
});
diff --git a/packages/composer-runtime-web/test/webeventservice.js b/packages/composer-runtime-web/test/webeventservice.js
new file mode 100644
index 0000000000..c85f397f68
--- /dev/null
+++ b/packages/composer-runtime-web/test/webeventservice.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const WebEventService = require('..').WebEventService;
+const EventEmitter = require('events').EventEmitter;
+
+const chai = require('chai');
+chai.should();
+chai.use(require('chai-as-promised'));
+const sinon = require('sinon');
+require('sinon-as-promised');
+
+describe('WebEventService', () => {
+
+ let eventService;
+ let mockEventEmitter;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ mockEventEmitter = sinon.createStubInstance(EventEmitter);
+ eventService = new WebEventService(mockEventEmitter);
+ eventService.getEventEmitter = sinon.stub();
+ eventService.getEventEmitter.returns(mockEventEmitter);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe('#constructor', () => {
+ it('should assign a default event emitter', () => {
+ eventService = new WebEventService(mockEventEmitter);
+ (eventService.eventSink instanceof EventEmitter).should.be.true;
+ });
+ });
+
+ describe('#commit', () => {
+ it ('should emit a list of events', () => {
+ eventService.serializeBuffer = sinon.stub();
+ eventService.serializeBuffer.returns('[{"event": "event"}]');
+ eventService.commit();
+ sinon.assert.calledOnce(eventService.serializeBuffer);
+ sinon.assert.calledOnce(mockEventEmitter.emit);
+ sinon.assert.calledWith(mockEventEmitter.emit, 'events', [{'event': 'event'}]);
+ });
+ });
+});
diff --git a/packages/composer-runtime/index.js b/packages/composer-runtime/index.js
index 999ec33662..08967a5f37 100644
--- a/packages/composer-runtime/index.js
+++ b/packages/composer-runtime/index.js
@@ -25,6 +25,7 @@ module.exports.Context = require('./lib/context');
module.exports.DataCollection = require('./lib/datacollection');
module.exports.DataService = require('./lib/dataservice');
module.exports.Engine = require('./lib/engine');
+module.exports.EventService = require('./lib/eventservice');
module.exports.IdentityService = require('./lib/identityservice');
module.exports.JSTransactionExecutor = require('./lib/jstransactionexecutor');
module.exports.LoggingService = require('./lib/loggingservice');
diff --git a/packages/composer-runtime/lib/api.js b/packages/composer-runtime/lib/api.js
index 534f0af053..da9386eff5 100644
--- a/packages/composer-runtime/lib/api.js
+++ b/packages/composer-runtime/lib/api.js
@@ -35,13 +35,16 @@ class Api {
/**
* Constructor.
* @param {Factory} factory The factory to use.
+ * @param {Serializer} serializer The serializer to use.
* @param {Resource} participant The current participant.
* @param {RegistryManager} registryManager The registry manager to use.
+ * @param {EventService} eventService The event service to use.
+ * @param {Context} context The transaction context.
* @private
*/
- constructor(factory, participant, registryManager) {
+ constructor(factory, serializer, participant, registryManager, eventService, context) {
const method = 'constructor';
- LOG.entry(method, factory, participant, registryManager);
+ LOG.entry(method, factory, serializer, participant, registryManager, eventService, context);
/**
* Get the factory. The factory can be used to create new instances of
@@ -161,6 +164,23 @@ class Api {
return result;
};
+ /**
+ * Emit an event defined in the transaction
+ * @method module:composer-runtime#emit
+ * @param {Resource} event The event to be emitted
+ * @public
+ */
+ this.emit = function emit(event) {
+ const method = 'emit';
+ LOG.entry(method);
+ event.setIdentifier(context.getTransaction().getIdentifier() + '#' + context.getEventNumber());
+ let serializedEvent = serializer.toJSON(event);
+ context.incrementEventNumber();
+ LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent);
+ eventService.emit(serializedEvent);
+ LOG.exit(method);
+ };
+
Object.freeze(this);
LOG.exit(method);
}
diff --git a/packages/composer-runtime/lib/api/factory.js b/packages/composer-runtime/lib/api/factory.js
index 744bb4b704..be00e2570e 100644
--- a/packages/composer-runtime/lib/api/factory.js
+++ b/packages/composer-runtime/lib/api/factory.js
@@ -139,7 +139,7 @@ class Factory {
* // Get the factory.
* var factory = getFactory();
* // Create a new relationship to the vehicle.
- * var record = factory.newConcept('org.acme', 'Record', 'RECORD_1');
+ * var record = factory.newConcept('org.acme', 'Record');
* // Add the record to the persons array of records.
* person.records.push(record);
* @public
@@ -154,6 +154,20 @@ class Factory {
return factory.newConcept(ns, type);
};
+ /**
+ * Create a new type with a given namespace and type
+ * @public
+ * @method module:composer-runtime.Factory#newEvent
+ * @param {string} ns The namespace of the event.
+ * @param {string} type The type of the event.
+ * @return {Resource} The new instance of the event.
+ * @throws {Error} If the specified type (specified by the namespace and
+ * type) is not defined in the current version of the business network.
+ */
+ this.newEvent = function newEvent(ns, type) {
+ return factory.newEvent(ns, type);
+ };
+
Object.freeze(this);
LOG.exit(method);
}
diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js
index 79c3fa4299..b190ae83e9 100644
--- a/packages/composer-runtime/lib/context.js
+++ b/packages/composer-runtime/lib/context.js
@@ -68,6 +68,7 @@ class Context {
this.accessController = null;
this.sysregistries = null;
this.sysidentities = null;
+ this.eventNumber = 0;
}
/**
@@ -237,6 +238,15 @@ class Context {
throw new Error('abstract function called');
}
+ /**
+ * Get the event service provided by the chaincode container.
+ * @abstract
+ * @return {EventService} The event service provided by the chaincode container.
+ */
+ getEventService() {
+ throw new Error('abstract function called');
+ }
+
/**
* Get the model manager.
* @return {ModelManager} The model manager.
@@ -331,7 +341,7 @@ class Context {
*/
getApi() {
if (!this.api) {
- this.api = new Api(this.getFactory(), this.getParticipant(), this.getRegistryManager());
+ this.api = new Api(this.getFactory(), this.getSerializer(), this.getParticipant(), this.getRegistryManager(), this.getEventService(), this);
}
return this.api;
}
@@ -463,6 +473,22 @@ class Context {
return this.sysidentities;
}
+ /**
+ * Get the next event number
+ * @return {integer} the event number.
+ */
+ getEventNumber() {
+ return this.eventNumber;
+ }
+
+ /**
+ * Incrememnt the event number by 1
+ * @return {integer} the event number.
+ */
+ incrementEventNumber() {
+ return this.eventNumber++;
+ }
+
/**
* Stop serialization of this object.
* @return {Object} An empty object.
diff --git a/packages/composer-runtime/lib/engine.transactions.js b/packages/composer-runtime/lib/engine.transactions.js
index e3e3ab2f42..397021dd55 100644
--- a/packages/composer-runtime/lib/engine.transactions.js
+++ b/packages/composer-runtime/lib/engine.transactions.js
@@ -93,6 +93,10 @@ class EngineTransactions {
LOG.debug(method, 'Storing executed transaction in transaction registry');
return transactionRegistry.add(transaction);
+ })
+ .then(() => {
+ // Commit all transactions
+ return context.getEventService().commit();
});
}
diff --git a/packages/composer-runtime/lib/eventservice.js b/packages/composer-runtime/lib/eventservice.js
new file mode 100644
index 0000000000..b3bf518443
--- /dev/null
+++ b/packages/composer-runtime/lib/eventservice.js
@@ -0,0 +1,95 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const Logger = require('composer-common').Logger;
+const LOG = Logger.getLog('EventService');
+
+/**
+ * Base class representing the event service provided by a {@link Container}.
+ * @protected
+ * @abstract
+ * @memberof module:composer-runtime
+ */
+class EventService {
+
+ /**
+ * Constructor.
+ */
+ constructor() {
+ this.eventBuffer = [];
+ }
+
+ /**
+ * Add an event to the buffer
+ * @param {Resource} event The event to be emitted
+ * when complete, or rejected with an error.
+ */
+ emit(event) {
+ const method = 'emit';
+ LOG.entry(method, event);
+ this.eventBuffer.push(event);
+ LOG.debug(method, this.eventBuffer);
+ LOG.exit(method);
+ }
+
+ /**
+ * Emit all buffered events
+ * @abstract
+ * @return {Promise} A promise that will be resolved with a {@link DataCollection}
+ */
+ commit() {
+ return new Promise((resolve, reject) => {
+ this._commit((error) => {
+ if (error) {
+ return reject(error);
+ }
+ return resolve();
+ });
+ });
+ }
+
+ /**
+ * Emit all buffered events
+ * @abstract
+ *
+ * @param {commitCallback} callback The callback function to call when complete.
+ */
+ _commit(callback) {
+ throw new Error('abstract function called');
+ }
+
+ /**
+ * Get an array of events as a string
+ * @return {String} - An array of serialized events
+ */
+ serializeBuffer() {
+ const method = 'serializeBuffer';
+ LOG.entry(method);
+ LOG.exit(method, this.eventBuffer);
+ return JSON.stringify(this.eventBuffer);
+ }
+
+ /**
+ * Stop serialization of this object.
+ * @return {Object} An empty object.
+ */
+ toJSON() {
+ return {};
+ }
+
+}
+
+module.exports = EventService;
diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js
index 9cd7cd4706..069da4db67 100644
--- a/packages/composer-runtime/test/api.js
+++ b/packages/composer-runtime/test/api.js
@@ -22,6 +22,9 @@ const realFactory = require('composer-common').Factory;
const Registry = require('../lib/registry');
const RegistryManager = require('../lib/registrymanager');
const Resource = require('composer-common').Resource;
+const EventService = require('../lib/eventservice');
+const Context = require('../lib/context');
+const Serializer = require('composer-common').Serializer;
const chai = require('chai');
chai.should();
@@ -33,15 +36,21 @@ require('sinon-as-promised');
describe('Api', () => {
let mockFactory;
+ let mockSerializer;
let mockParticipant;
let mockRegistryManager;
+ let mockEventService;
+ let mockContext;
let api;
beforeEach(() => {
mockFactory = sinon.createStubInstance(realFactory);
+ mockSerializer = sinon.createStubInstance(Serializer);
mockParticipant = sinon.createStubInstance(Resource);
mockRegistryManager = sinon.createStubInstance(RegistryManager);
- api = new Api(mockFactory, mockParticipant, mockRegistryManager);
+ mockEventService = sinon.createStubInstance(EventService);
+ mockContext = sinon.createStubInstance(Context);
+ api = new Api(mockFactory, mockSerializer, mockParticipant, mockRegistryManager, mockEventService, mockContext);
});
describe('#constructor', () => {
@@ -106,4 +115,23 @@ describe('Api', () => {
});
+ describe('#emit', () => {
+ let mockTransaction;
+ let mockEvent;
+
+ beforeEach(() => {
+ mockTransaction = sinon.createStubInstance(Resource);
+ mockEvent = sinon.createStubInstance(Resource);
+ mockTransaction.getIdentifier.returns('much.wow');
+ mockContext.getTransaction.returns(mockTransaction);
+ mockContext.getEventNumber.returns(0);
+ });
+
+ it('should call eventService.emit', () => {
+ api.emit(mockEvent);
+ sinon.assert.calledOnce(mockEventService.emit);
+ // sinon.assert.calledWith(mockEventService.emit, mockEvent);
+ });
+ });
+
});
diff --git a/packages/composer-runtime/test/api/factory.js b/packages/composer-runtime/test/api/factory.js
index f413101cdc..fa1ed5e5ff 100644
--- a/packages/composer-runtime/test/api/factory.js
+++ b/packages/composer-runtime/test/api/factory.js
@@ -91,4 +91,13 @@ describe('AssetRegistry', () => {
});
+ describe('#newEvent', () => {
+
+ it('should proxy to the registry', () => {
+ mockFactory.newEvent.withArgs('org.acme', 'Doge').returns(mockResource);
+ factory.newEvent('org.acme', 'Doge').should.equal(mockResource);
+ });
+
+ });
+
});
diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js
index 88ae5c427a..d0e350b9eb 100644
--- a/packages/composer-runtime/test/context.js
+++ b/packages/composer-runtime/test/context.js
@@ -22,6 +22,7 @@ const Context = require('../lib/context');
const DataCollection = require('../lib/datacollection');
const DataService = require('../lib/dataservice');
const Engine = require('../lib/engine');
+const EventService = require('../lib/eventservice');
const Factory = require('composer-common').Factory;
const IdentityManager = require('../lib/identitymanager');
const IdentityService = require('../lib/identityservice');
@@ -45,12 +46,14 @@ require('sinon-as-promised');
describe('Context', () => {
+ let mockBusinessNetworkDefinition;
let mockEngine;
let context;
let sandbox;
beforeEach(() => {
mockEngine = sinon.createStubInstance(Engine);
+ mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition);
context = new Context(mockEngine);
sandbox = sinon.sandbox.create();
});
@@ -266,6 +269,16 @@ describe('Context', () => {
});
+ describe('#getEventService', () => {
+
+ it('should throw as abstract method', () => {
+ (() => {
+ context.getEventService();
+ }).should.throw(/abstract function called/);
+ });
+
+ });
+
describe('#getModelManager', () => {
it('should throw if not initialized', () => {
@@ -419,6 +432,9 @@ describe('Context', () => {
sinon.stub(context, 'getParticipant').returns(mockParticipant);
let mockRegistryManager = sinon.createStubInstance(RegistryManager);
sinon.stub(context, 'getRegistryManager').returns(mockRegistryManager);
+ let mockEventService = sinon.createStubInstance(EventService);
+ sinon.stub(context, 'getEventService').returns(mockEventService);
+ context.businessNetworkDefinition = mockBusinessNetworkDefinition;
context.getApi().should.be.an.instanceOf(Api);
});
@@ -648,6 +664,19 @@ describe('Context', () => {
});
+ describe('#getEventNumber', () => {
+ it('should get the current event number', () => {
+ context.getEventNumber().should.equal(0);
+ });
+ });
+
+ describe('#incrementEventNumber', () => {
+ it('should get the incremenet current event number', () => {
+ context.incrementEventNumber();
+ context.getEventNumber().should.equal(1);
+ });
+ });
+
describe('#toJSON', () => {
it('should return an empty object', () => {
diff --git a/packages/composer-runtime/test/engine.transactions.js b/packages/composer-runtime/test/engine.transactions.js
index be9ab1c5d7..e76e59d23f 100644
--- a/packages/composer-runtime/test/engine.transactions.js
+++ b/packages/composer-runtime/test/engine.transactions.js
@@ -18,6 +18,7 @@ const Api = require('../lib/api');
const Container = require('../lib/container');
const Context = require('../lib/context');
const Engine = require('../lib/engine');
+const EventService = require('../lib/eventservice');
const LoggingService = require('../lib/loggingservice');
const Registry = require('../lib/registry');
const RegistryManager = require('../lib/registrymanager');
@@ -38,6 +39,7 @@ describe('EngineTransactions', () => {
let mockContainer;
let mockLoggingService;
+ let mockEventService;
let mockContext;
let engine;
let mockRegistryManager;
@@ -71,6 +73,8 @@ describe('EngineTransactions', () => {
mockContext.getTransactionExecutors.returns([mockTransactionExecutor]);
mockRegistry = sinon.createStubInstance(Registry);
mockRegistryManager.get.withArgs('Transaction', 'default').resolves(mockRegistry);
+ mockEventService = sinon.createStubInstance(EventService);
+ mockContext.getEventService.returns(mockEventService);
});
describe('#submitTransaction', () => {
@@ -114,6 +118,7 @@ describe('EngineTransactions', () => {
should.equal(transaction.$resolved, undefined);
return true;
}));
+ sinon.assert.calledOnce(mockEventService.commit);
});
});
@@ -160,6 +165,7 @@ describe('EngineTransactions', () => {
should.equal(transaction.$resolved, undefined);
return true;
}));
+ sinon.assert.calledOnce(mockEventService.commit);
});
});
diff --git a/packages/composer-runtime/test/eventservice.js b/packages/composer-runtime/test/eventservice.js
new file mode 100644
index 0000000000..f2a8d437f1
--- /dev/null
+++ b/packages/composer-runtime/test/eventservice.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+const EventService = require('../lib/eventservice');
+const Serializer = require('composer-common').Serializer;
+
+const should = require('chai').should();
+require('chai-as-promised');
+const sinon = require('sinon');
+
+describe('EventService', () => {
+
+ let mockSerializer;
+ let eventService;
+
+ beforeEach(() => {
+ mockSerializer = sinon.createStubInstance(Serializer);
+ eventService = new EventService(mockSerializer);
+ });
+
+ describe('#constructor', () => {
+ it('should have a property for buffering events', () => {
+ should.exist(eventService.eventBuffer);
+ });
+ });
+
+ describe('#emit', () => {
+ it('should add the event to the data buffer', () => {
+ eventService.emit({});
+ eventService.eventBuffer[0].should.deep.equal({});
+ });
+ });
+
+ describe('#eventService', () => {
+
+ it('should call _commit and handle no error', () => {
+ sinon.stub(eventService, '_commit').yields(null, {});
+ return eventService.commit()
+ .then(() => {
+ sinon.assert.calledWith(eventService._commit);
+ });
+ });
+
+
+ it('should call _commit and handle an error', () => {
+ sinon.stub(eventService, '_commit').yields(new Error('error'));
+ return eventService.commit()
+ .then((result) => {
+ throw new Error('should not get here');
+ })
+ .catch((error) => {
+ sinon.assert.calledWith(eventService._commit);
+ error.should.match(/error/);
+ });
+ });
+
+ });
+
+ describe('#_commit', () => {
+
+ it('should throw as abstract method', () => {
+ (() => {
+ eventService._commit();
+ }).should.throw(/abstract function called/);
+ });
+
+ });
+
+ describe('#serializeBuffer', () => {
+ it('should return the list of events that are to be comitted', () => {
+ let event = {'$class': 'much.wow'};
+ eventService.eventBuffer = [ event ];
+
+ eventService.serializeBuffer().should.equal('[{"$class":"much.wow"}]');
+ });
+ });
+
+ describe('#toJSON', () => {
+
+ it('should return an empty object', () => {
+ eventService.toJSON().should.deep.equal({});
+ });
+
+ });
+
+});
diff --git a/packages/composer-runtime/test/jstransactionexecutor.js b/packages/composer-runtime/test/jstransactionexecutor.js
index 65fd096b79..3e828f2955 100644
--- a/packages/composer-runtime/test/jstransactionexecutor.js
+++ b/packages/composer-runtime/test/jstransactionexecutor.js
@@ -20,6 +20,7 @@ const JSTransactionExecutor = require('../lib/jstransactionexecutor');
const ModelManager = require('composer-common').ModelManager;
const RegistryManager = require('../lib/registrymanager');
const ScriptManager = require('composer-common').ScriptManager;
+const Serializer = require('composer-common').Serializer;
const chai = require('chai');
chai.should();
@@ -37,6 +38,7 @@ describe('JSTransactionExecutor', () => {
let participant;
let scriptManager;
let mockRegistryManager;
+ let mockSerializer;
let api;
beforeEach(() => {
@@ -59,7 +61,8 @@ describe('JSTransactionExecutor', () => {
participant = factory.newResource('org.acme', 'TestParticipant', '1');
scriptManager = new ScriptManager(modelManager);
mockRegistryManager = sinon.createStubInstance(RegistryManager);
- api = new Api(factory, participant, mockRegistryManager);
+ mockSerializer = sinon.createStubInstance(Serializer);
+ api = new Api(factory, mockSerializer, participant, mockRegistryManager);
});
afterEach(() => {