Skip to content

Implement and test getMultipleStates method for Stub API. #454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions libraries/fabric-shim/lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ class ChaincodeMessageHandler {
constructor(stream, chaincode) {
this._stream = stream;
this.chaincode = chaincode;
this.usePeerGetMultipleKeys = false;
this.maxSizeGetMultipleKeys = 0;
}

// this is a long-running method that does not return until
Expand Down Expand Up @@ -388,6 +390,65 @@ class ChaincodeMessageHandler {
return await this._askPeerAndListen(msg, 'GetState');
}

async handleGetMultipleStates(collection, keys, channel_id, txId) {
if (keys.length === 0) {
return [];
}

const responses = [];

if (!this.usePeerGetMultipleKeys) {
for (const key of keys) {
const resp = await this.handleGetState(collection, key, channel_id, txId);
responses.push(resp);
}
return responses;
}
Comment on lines +394 to +406
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be possible to simplify this, similar to the following:

Suggested change
if (keys.length === 0) {
return [];
}
const responses = [];
if (!this.usePeerGetMultipleKeys) {
for (const key of keys) {
const resp = await this.handleGetState(collection, key, channel_id, txId);
responses.push(resp);
}
return responses;
}
if (!this.usePeerGetMultipleKeys) {
const promises = keys.map((key) =>
this.handleGetState(collection, key, channel_id, tx_Id)
);
return Promise.all(promises);
}


let remainingKeys = [...keys];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you are doing this to take a copy and avoid modifying the keys array passed in by the caller? I don't think this will be necessary since slice will return a new array rather than modifying the original array. If it is a concern, it might be worth adding a unit test to confirm that the caller's array is not modified.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, remainingKeys is not needed as slice don't modify the original array.

while (remainingKeys.length > this.maxSizeGetMultipleKeys) {
const batch = remainingKeys.slice(0, this.maxSizeGetMultipleKeys);
const resp = await this.handleOneSendGetMultipleStates(collection, batch, channel_id, txId);
responses.push(...resp);
remainingKeys = remainingKeys.slice(this.maxSizeGetMultipleKeys);
}

if (remainingKeys.length > 0) {
const resp = await this.handleOneSendGetMultipleStates(collection, remainingKeys, channel_id, txId);
responses.push(...resp);
}

return responses.map(r => (r.length === 0 ? null : r));
Comment on lines +409 to +421
Copy link
Member

@bestbeforetoday bestbeforetoday May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slice handles out of bounds indexes cleanly so this should not need the multiple loops that the Go implementation uses. You might be able to simplify the implementation similar to the following:

Suggested change
while (remainingKeys.length > this.maxSizeGetMultipleKeys) {
const batch = remainingKeys.slice(0, this.maxSizeGetMultipleKeys);
const resp = await this.handleOneSendGetMultipleStates(collection, batch, channel_id, txId);
responses.push(...resp);
remainingKeys = remainingKeys.slice(this.maxSizeGetMultipleKeys);
}
if (remainingKeys.length > 0) {
const resp = await this.handleOneSendGetMultipleStates(collection, remainingKeys, channel_id, txId);
responses.push(...resp);
}
return responses.map(r => (r.length === 0 ? null : r));
const batchPromises = [];
for (
;
keys.length > 0;
keys = keys.slice(this.maxSizeGetMultipleKeys)
) {
const batch = keys.slice(0, this.maxSizeGetMultipleKeys);
const promise = this.handleOneSendGetMultipleStates(
collection,
batch,
channel_id,
txId
);
batchPromises.push(promise);
}
const batchResponses = await Promise.all(batchPromises);
return batchResponses.flat();

}

async handleOneSendGetMultipleStates(collection, keys, channel_id, txId) {
const msgPb = new peer.GetStateMultiple();
msgPb.setCollection(collection);
msgPb.setKeysList(keys);

const msg = mapToChaincodeMessage({
type: peer.ChaincodeMessage.Type.GET_STATE_MULTIPLE,
payload: msgPb.serializeBinary(),
txid: txId,
channel_id: channel_id
});

logger.debug('handleOneSendGetMultipleStates - keys:', keys);

const responseMsg = await this._askPeerAndListen(msg, 'GetMultipleStates');

if (responseMsg.getType() === peer.ChaincodeMessage.Type.RESPONSE) {
const result = peer.GetStateMultipleResult.deserializeBinary(responseMsg.getPayload());
return result.getValuesList();
}

if (responseMsg.getType() === peer.ChaincodeMessage.Type.ERROR) {
throw new Error(Buffer.from(responseMsg.getPayload()).toString());
}

throw new Error(`Unexpected message type ${responseMsg.getType()} received`);
}

async handlePutState(collection, key, value, channel_id, txId) {
const msgPb = new peer.PutState();
msgPb.setKey(key);
Expand Down
13 changes: 13 additions & 0 deletions libraries/fabric-shim/lib/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,19 @@ class ChaincodeStub {
return await this.handler.handleGetState(collection, key, this.channel_id, this.txId);
}

/**
* Retrieves the current values of the state variables for the given keys
* @async
* @param {...string} keys State variable keys to retrieve from the state store
* @returns {Promise<byte[][]>} Promise for the array of current values of the given keys
*/
async getMultipleStates(...keys) {
logger.debug('getMultipleStates called with keys:%o', keys);
// Access public data by setting the collection to empty string
const collection = '';
return await this.handler.handleGetMultipleStates(collection, keys, this.channel_id, this.txId);
}

/**
* Writes the state variable <code>key</code> of value <code>value</code>
* to the state store. If the variable already exists, the value will be
Expand Down
50 changes: 50 additions & 0 deletions libraries/fabric-shim/test/unit/stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,56 @@ describe('Stub', () => {
});
});

describe.only('getMultipleStates', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
describe.only('getMultipleStates', () => {
describe('getMultipleStates', () => {

it('should return handler.handleGetMultipleStates results', async () => {
const handleGetMultipleStatesStub = sinon.stub().resolves(['value1', 'value2', 'value3']);

const stub = new Stub({
handleGetMultipleStates: handleGetMultipleStatesStub
}, 'dummyChannelId', 'dummyTxid', chaincodeInput);

const result = await stub.getMultipleStates('key1', 'key2', 'key3');
expect(result).to.deep.equal(['value1', 'value2', 'value3']);

expect(handleGetMultipleStatesStub.calledOnce).to.be.true;
expect(handleGetMultipleStatesStub.firstCall.args).to.deep.equal([
'',
['key1', 'key2', 'key3'],
'dummyChannelId',
'dummyTxid'
]);
});

it('should return empty array if no keys passed', async () => {
const handleGetMultipleStatesStub = sinon.stub().resolves([]);

const stub = new Stub({
handleGetMultipleStates: handleGetMultipleStatesStub
}, 'dummyChannelId', 'dummyTxid', chaincodeInput);

const result = await stub.getMultipleStates();
expect(result).to.deep.equal([]);

expect(handleGetMultipleStatesStub.calledOnce).to.be.true;
expect(handleGetMultipleStatesStub.firstCall.args).to.deep.equal([
'',
[],
'dummyChannelId',
'dummyTxid'
]);
});

it('should throw error if handler rejects', async () => {
const handleGetMultipleStatesStub = sinon.stub().rejects(new Error('Something gone wrong'));

const stub = new Stub({
handleGetMultipleStates: handleGetMultipleStatesStub
}, 'dummyChannelId', 'dummyTxid', chaincodeInput);

await expect(stub.getMultipleStates('key1')).to.be.rejectedWith('Something gone wrong');
});
});

describe('putState', () => {
it ('should return handler.handlePutState', async () => {
const handlePutStateStub = sinon.stub().resolves('some state');
Expand Down
Loading