diff --git a/src/data/__tests__/indexHostSingleton.test.ts b/src/data/__tests__/indexHostSingleton.test.ts index 6ecdee97..d7381581 100644 --- a/src/data/__tests__/indexHostSingleton.test.ts +++ b/src/data/__tests__/indexHostSingleton.test.ts @@ -1,13 +1,14 @@ import { IndexHostSingleton } from '../indexHostSingleton'; const mockDescribeIndex = jest.fn(); +const mockIndexOperationsBuilder = jest.fn(); jest.mock('../../control', () => { const realControl = jest.requireActual('../../control'); - return { ...realControl, describeIndex: () => mockDescribeIndex, + indexOperationsBuilder: (config) => mockIndexOperationsBuilder(config), }; }); @@ -15,6 +16,7 @@ describe('IndexHostSingleton', () => { afterEach(() => { IndexHostSingleton._reset(); mockDescribeIndex.mockReset(); + mockIndexOperationsBuilder.mockReset(); }); test('calls describeIndex to resolve host for a specific apiKey and indexName, prepends protocol to host', async () => { @@ -140,4 +142,50 @@ describe('IndexHostSingleton', () => { await IndexHostSingleton.getHostUrl(pineconeConfig, 'test-index'); expect(mockDescribeIndex).toHaveBeenCalledTimes(1); }); + + test.only('calling getHostUrl with different apiKey configurations should instantiate new ManageIndexesApi classes', async () => { + const pineconeConfig1 = { apiKey: 'test-key-1' }; + const pineconeConfig2 = { apiKey: 'test-key-2' }; + + mockDescribeIndex + .mockResolvedValueOnce({ + name: 'index-1', + dimensions: 10, + metric: 'cosine', + host: 'test-host-1', + spec: { pod: { pods: 1, replicas: 1, shards: 1, podType: 'p1.x1' } }, + status: { ready: true, state: 'Ready' }, + }) + .mockResolvedValueOnce({ + name: 'index-2', + dimensions: 10, + metric: 'cosine', + host: 'test-host-2', + spec: { pod: { pods: 1, replicas: 1, shards: 1, podType: 'p1.x1' } }, + status: { ready: true, state: 'Ready' }, + }); + mockIndexOperationsBuilder.mockReturnValue({ test: 'one', test2: 'two' }); + + await IndexHostSingleton.getHostUrl(pineconeConfig1, 'index-1'); + await IndexHostSingleton.getHostUrl(pineconeConfig2, 'index-2'); + + expect(mockDescribeIndex).toHaveBeenCalledTimes(2); + expect(mockDescribeIndex).toHaveBeenNthCalledWith(1, 'index-1'); + expect(mockDescribeIndex).toHaveBeenNthCalledWith(2, 'index-2'); + + console.log( + 'mockIndexOperationsBuilder', + JSON.stringify(mockIndexOperationsBuilder.mock) + ); + + expect(mockIndexOperationsBuilder).toHaveBeenCalledTimes(2); + expect(mockIndexOperationsBuilder).toHaveBeenNthCalledWith( + 1, + pineconeConfig1 + ); + expect(mockIndexOperationsBuilder).toHaveBeenNthCalledWith( + 2, + pineconeConfig2 + ); + }); }); diff --git a/src/data/indexHostSingleton.ts b/src/data/indexHostSingleton.ts index fcd9e769..5bcddc86 100644 --- a/src/data/indexHostSingleton.ts +++ b/src/data/indexHostSingleton.ts @@ -1,4 +1,3 @@ -import { ManageIndexesApi } from '../pinecone-generated-ts-fetch'; import type { PineconeConfiguration } from './types'; import type { IndexName } from '../control'; import { describeIndex, indexOperationsBuilder } from '../control'; @@ -10,16 +9,12 @@ import { normalizeUrl } from '../utils'; // and index, so we cache them in a singleton for reuse. export const IndexHostSingleton = (function () { const hostUrls = {}; // map of apiKey-indexName to hostUrl - let indexOperationsApi: InstanceType | null = null; const _describeIndex = async ( config: PineconeConfiguration, indexName: IndexName ): Promise => { - if (!indexOperationsApi) { - indexOperationsApi = indexOperationsBuilder(config); - } - + const indexOperationsApi = indexOperationsBuilder(config); const describeResponse = await describeIndex(indexOperationsApi)(indexName); const host = describeResponse.host; @@ -34,19 +29,17 @@ export const IndexHostSingleton = (function () { } }; - const key = (config, indexName) => `${config.apiKey}-${indexName}`; + const _key = (config: PineconeConfiguration, indexName: string) => + `${config.apiKey}-${indexName}`; - return { - getHostUrl: async function ( - config: PineconeConfiguration, - indexName: IndexName - ) { - const cacheKey = key(config, indexName); + const singleton = { + getHostUrl: async (config: PineconeConfiguration, indexName: IndexName) => { + const cacheKey = _key(config, indexName); if (cacheKey in hostUrls) { return hostUrls[cacheKey]; } else { const hostUrl = await _describeIndex(config, indexName); - this._set(config, indexName, hostUrl); + singleton._set(config, indexName, hostUrl); if (!hostUrls[cacheKey]) { throw new PineconeUnableToResolveHostError( @@ -74,13 +67,15 @@ export const IndexHostSingleton = (function () { return; } - const cacheKey = key(config, indexName); + const cacheKey = _key(config, indexName); hostUrls[cacheKey] = normalizedHostUrl; }, _delete: (config: PineconeConfiguration, indexName: IndexName) => { - const cacheKey = key(config, indexName); + const cacheKey = _key(config, indexName); delete hostUrls[cacheKey]; }, }; + + return singleton; })(); diff --git a/src/integration/data/delete.test.ts b/src/integration/data/delete.test.ts index 30590029..df5cf3ff 100644 --- a/src/integration/data/delete.test.ts +++ b/src/integration/data/delete.test.ts @@ -69,7 +69,7 @@ describe('delete', () => { } }; - assertWithRetries(() => ns.fetch(['0']), fetchAssertions); + await assertWithRetries(() => ns.fetch(['0']), fetchAssertions); // Try deleting the record await ns.deleteOne('0'); @@ -79,7 +79,7 @@ describe('delete', () => { expect(stats.namespaces[namespace]).toBeUndefined(); }; - assertWithRetries(() => ns.describeIndexStats(), deleteAssertions); + await assertWithRetries(() => ns.describeIndexStats(), deleteAssertions); }); test('verify deleteMany with ids', async () => { @@ -113,7 +113,7 @@ describe('delete', () => { } }; - assertWithRetries(() => ns.fetch(['0']), fetchAssertions); + await assertWithRetries(() => ns.fetch(['0']), fetchAssertions); // Try deleting 2 of 3 records await ns.deleteMany(['0', '2']); @@ -129,7 +129,7 @@ describe('delete', () => { } }; - assertWithRetries(() => ns.describeIndexStats(), deleteAssertions); + await assertWithRetries(() => ns.describeIndexStats(), deleteAssertions); // Check that record id='1' still exists const fetchAssertions2 = (results) => { @@ -143,7 +143,7 @@ describe('delete', () => { } }; - assertWithRetries(() => ns.fetch(['1']), fetchAssertions2); + await assertWithRetries(() => ns.fetch(['1']), fetchAssertions2); // deleting non-existent records should not throw await ns.deleteMany(['0', '1', '2', '3']); @@ -159,6 +159,6 @@ describe('delete', () => { } }; - assertWithRetries(() => ns.describeIndexStats(), deleteAssertions2); + await assertWithRetries(() => ns.describeIndexStats(), deleteAssertions2); }); }); diff --git a/src/integration/errorHandling.test.ts b/src/integration/errorHandling.test.ts index e661344f..72f165c8 100644 --- a/src/integration/errorHandling.test.ts +++ b/src/integration/errorHandling.test.ts @@ -70,9 +70,9 @@ describe('Error handling', () => { await p.index('foo-index').query({ topK: 10, id: '1' }); } catch (e) { const err = e as PineconeConnectionError; - expect(err.name).toEqual('PineconeAuthorizationError'); + expect(err.name).toEqual('PineconeConnectionError'); expect(err.message).toEqual( - 'The API key you provided was rejected while calling https://api.pinecone.io/indexes/foo-index. Please check your configuration values and try again. You can find the configuration values for your project in the Pinecone developer console at https://app.pinecone.io' + 'Request failed to reach Pinecone. This can occur for reasons such as network problems that prevent the request from being completed, or a Pinecone API outage. Check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.' ); } });