From 59eee0af7c32f47a763d0ac5e6a419ae47d7f4c0 Mon Sep 17 00:00:00 2001 From: Sourav Date: Mon, 9 Mar 2026 18:02:40 +0530 Subject: [PATCH] feat: implement stub API fallbacks for read/write batching. Added fallback methods, updated types, and achieved 100% coverage. Ref: #453 Signed-off-by: Sourav --- libraries/fabric-shim/lib/stub.js | 56 +++++++++++++++++++ libraries/fabric-shim/test/unit/stub.js | 73 +++++++++++++++++++++++++ libraries/fabric-shim/types/index.d.ts | 5 ++ 3 files changed, 134 insertions(+) diff --git a/libraries/fabric-shim/lib/stub.js b/libraries/fabric-shim/lib/stub.js index 1dae7847..d455c503 100644 --- a/libraries/fabric-shim/lib/stub.js +++ b/libraries/fabric-shim/lib/stub.js @@ -937,6 +937,62 @@ class ChaincodeStub { return await this.handler.handleGetState(collection, key, this.channel_id, this.txId); } + /** + * Retrieves the current values of the state variables for multiple keys. + * This is a fallback implementation that processes reads without actual gRPC batching. + * @async + * @param {string[]} keys Array of state variable keys to retrieve from the state store + * @returns {Promise} Promise for an array of the current values of the state variables + */ + async getMultipleStates(keys) { + logger.debug('getMultipleStates called with keys:%j', keys); + if (!Array.isArray(keys)) { + throw new Error('keys must be an array of strings'); + } + + const promises = keys.map(key => this.getState(key)); + return await Promise.all(promises); + } + + /** + * getMultiplePrivateData returns the values of the specified `keys` from the specified `collection`. + * This is a fallback implementation that processes reads without actual gRPC batching. + * @async + * @param {string} collection The collection name + * @param {string[]} keys Array of private data variable keys to retrieve from the state store + * @returns {Promise} Promise for an array of private values from the state store + */ + async getMultiplePrivateData(collection, keys) { + logger.debug('getMultiplePrivateData called with collection:%s, keys:%j', collection, keys); + if (!collection || typeof collection !== 'string') { + throw new Error('collection must be a valid string'); + } + if (!Array.isArray(keys)) { + throw new Error('keys must be an array of strings'); + } + + const promises = keys.map(key => this.getPrivateData(collection, key)); + return await Promise.all(promises); + } + + /** + * startWriteBatch indicates the beginning of a block of PutState/PutPrivateData calls + * that should be batched together. + * (Fallback behavior: no-op) + */ + startWriteBatch() { + logger.debug('startWriteBatch called'); + } + + /** + * finishWriteBatch sends the currently accumulated batch of state writes to the peer. + * (Fallback behavior: no-op) + * @async + */ + async finishWriteBatch() { + logger.debug('finishWriteBatch called'); + } + /** * getPrivateDataHash returns the hash of the value of the specified `key` from * the specified `collection`. diff --git a/libraries/fabric-shim/test/unit/stub.js b/libraries/fabric-shim/test/unit/stub.js index b907fc29..15429051 100644 --- a/libraries/fabric-shim/test/unit/stub.js +++ b/libraries/fabric-shim/test/unit/stub.js @@ -1103,6 +1103,79 @@ describe('Stub', () => { }); }); + describe('getMultipleStates', () => { + let stub; + + beforeEach(() => { + stub = new Stub({ + handleGetState: sinon.stub().resolves(Buffer.from('value')) + }, 'dummyChannelId', 'dummyTxid', chaincodeInput); + }); + + it('should throw an error if keys is not an array', async () => { + await expect(stub.getMultipleStates('not-an-array')).to.be.rejectedWith(/keys must be an array of strings/); + }); + + it('should call getState for each key and return the results', async () => { + sandbox.stub(stub, 'getState').callsFake(async (key) => { + if (key === 'key1') { + return Buffer.from('value1'); + } else if (key === 'key2') { + return Buffer.from('value2'); + } + }); + + const results = await stub.getMultipleStates(['key1', 'key2']); + expect(results).to.deep.equal([Buffer.from('value1'), Buffer.from('value2')]); + sinon.assert.calledTwice(stub.getState); + }); + }); + + describe('getMultiplePrivateData', () => { + let stub; + + beforeEach(() => { + stub = new Stub({ + handleGetState: sinon.stub().resolves(Buffer.from('value')) + }, 'dummyChannelId', 'dummyTxid', chaincodeInput); + }); + + it('should throw an error if collection is missing', async () => { + await expect(stub.getMultiplePrivateData(null, ['key1'])).to.be.rejectedWith(/collection must be a valid string/); + }); + + it('should throw an error if keys is not an array', async () => { + await expect(stub.getMultiplePrivateData('collection', 'not-an-array')).to.be.rejectedWith(/keys must be an array of strings/); + }); + + it('should call getPrivateData for each key and return the results', async () => { + sandbox.stub(stub, 'getPrivateData').callsFake(async (collection, key) => { + if (key === 'key1') { + return Buffer.from('value1'); + } else if (key === 'key2') { + return Buffer.from('value2'); + } + }); + + const results = await stub.getMultiplePrivateData('collection', ['key1', 'key2']); + expect(results).to.deep.equal([Buffer.from('value1'), Buffer.from('value2')]); + sinon.assert.calledTwice(stub.getPrivateData); + }); + }); + + describe('Write Batching Fallbacks', () => { + it('should execute startWriteBatch as a no-op', () => { + const stub = new Stub('dummyClient', 'dummyChannelId', 'dummyTxid', chaincodeInput); + stub.startWriteBatch(); + }); + + it('should execute finishWriteBatch as a no-op', async () => { + const stub = new Stub('dummyClient', 'dummyChannelId', 'dummyTxid', chaincodeInput); + await stub.finishWriteBatch(); + }); + }); + + describe('getPrivateDataHash', () => { let handleGetPrivateDataHashStub; let stub; diff --git a/libraries/fabric-shim/types/index.d.ts b/libraries/fabric-shim/types/index.d.ts index bafe7dd4..face0327 100644 --- a/libraries/fabric-shim/types/index.d.ts +++ b/libraries/fabric-shim/types/index.d.ts @@ -142,6 +142,11 @@ declare module 'fabric-shim' { getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise & AsyncIterable; getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise & AsyncIterable; getPrivateDataQueryResult(collection: string, query: string): Promise & AsyncIterable; + + getMultipleStates(keys: string[]): Promise; + getMultiplePrivateData(collection: string, keys: string[]): Promise; + startWriteBatch(): void; + finishWriteBatch(): Promise; } export class KeyEndorsementPolicy {