diff --git a/src/LocalIndex.ts b/src/LocalIndex.ts index 3a3628d..e14c2cc 100644 --- a/src/LocalIndex.ts +++ b/src/LocalIndex.ts @@ -66,7 +66,7 @@ export class LocalIndex = Record< } await this.loadIndexData(); - this._update = Object.assign({}, this._data); + this._update = structuredClone(this._data); } /** @@ -207,6 +207,32 @@ export class LocalIndex = Record< } } + /** + * Adds a batch of items to the index. + * @remarks + * Batch update requires no update to be in progress. This is necessary so that if any one + * insert operation fails, the entire update can be safely cancelled. This prevents partial + * updates from being applied to the local index. + * @param items Items to insert. + * @returns Inserted items. + */ + public async batchInsertItems(items: Partial>[]): Promise> { + await this.beginUpdate(); + try { + const newItems: any = []; + for (const item of items) { + const newItem = await this.addItemToUpdate(item, true); + newItems.push(newItem); + } + await this.endUpdate(); + return newItems; + } catch (e) { + // cancels this update to prevent partial batch updates. allows error to bubble up. + await this.cancelUpdate(); + throw e; + } + } + /** * Returns true if the index exists. */ diff --git a/tests/LocalIndex.test.ts b/tests/LocalIndex.test.ts new file mode 100644 index 0000000..2eb44b5 --- /dev/null +++ b/tests/LocalIndex.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert' +import { LocalIndex, IndexItem } from '../lib/LocalIndex' +import fs from 'fs/promises' +import path from 'path' + +describe('LocalIndex', () => { + const testIndexDir = path.join(__dirname, 'test_index'); + + beforeEach(async () => { + await fs.rm(testIndexDir, { recursive: true, force: true }); + }); + + afterEach(async () => { + await fs.rm(testIndexDir, { recursive: true, force: true }); + }); + + it('should create a new index', async () => { + const index = new LocalIndex(testIndexDir); + await index.createIndex(); + const created = await index.isIndexCreated(); + assert.equal(created, true); + }); + + describe('batchInsertItems', () => { + const indexItems: Partial[] = [ + { id: '1', vector: [1, 2, 3] }, + { id: '2', vector: [2, 3, 4] }, + { id: '3', vector: [3, 4, 5] } + ]; + + it('should insert provided items', async () => { + const index = new LocalIndex(testIndexDir); + await index.createIndex(); + + const newItems = await index.batchInsertItems(indexItems); + + assert.equal(newItems.length, 3); + + const retrievedItems = await index.listItems(); + assert.equal(retrievedItems.length, 3); + }); + + it('on id collision - cancel batch insert & bubble up error', async () => { + const index = new LocalIndex(testIndexDir); + await index.createIndex(); + + await index.insertItem({ id: '2', vector: [9, 9, 9] }); + + // ensures insert error is bubbled up to batchIndexItems caller + await assert.rejects( + async () => { + await index.batchInsertItems(indexItems); + }, + { + name: 'Error', + message: 'Item with id 2 already exists' + } + ); + + // ensures no partial update is applied + const storedItems = await index.listItems(); + assert.equal(storedItems.length, 1); + }); + }); +});