diff --git a/benchmarks/storage.ts b/benchmarks/storage.ts index 46c6c0c..37b9e2d 100644 --- a/benchmarks/storage.ts +++ b/benchmarks/storage.ts @@ -1,6 +1,5 @@ import { AESKey } from '../src/crypto/aes-key.js'; -import { BaseStorage } from '../src/storage/base-storage.js'; -import { StorageMemory, StorageMemorySynced, EncryptedStorage } from '../src/storage/index.js'; +import { StorageMemory, EncryptedStorage } from '../src/storage/index.js'; const storage = StorageMemory.from(); // const storageSynced = StorageMemorySynced.from(storage); @@ -8,6 +7,7 @@ const storage = StorageMemory.from(); const currentDate = new Date(); const data = { + id: 'test', name: 'test', age: 20, email: 'test@test.com', @@ -24,9 +24,9 @@ storageEncryptedBase.on('insert', (event) => { }); // Store data in storage -await storage.insertOne('test', data); +await storage.insertOne(data); // storageSynced.insertOne('test', data); -await storageEncrypted.insertOne('test', data); +await storageEncrypted.insertOne(data); // Retrieve data from storage const retrievedData = await storage.findOne({ name: 'test' }); diff --git a/src/storage/base-storage.ts b/src/storage/base-storage.ts index 3b7067b..5fbcd4d 100644 --- a/src/storage/base-storage.ts +++ b/src/storage/base-storage.ts @@ -11,15 +11,13 @@ export type FindOptions = { export type StorageEvent> = { insert: { - key: string; value: T; }; update: { - key: string; value: T; }; delete: { - key: string; + value: T; }; clear: undefined; }; @@ -29,22 +27,26 @@ export abstract class BaseStorage< > extends EventEmitter> { /** * Insert a document into the store - * @param id Unique identifier for the document * @param document The document to insert */ - abstract insertOne(id: string, document: T): Promise; + async insertOne(document: T): Promise { + await this.insertMany([document]); + } /** * Insert multiple documents into the store - * @param documents Map of ID to document + * @param documents Array of documents to insert */ - abstract insertMany(documents: Map): Promise; + abstract insertMany(documents: Array): Promise; /** * Find a single document that matches the filter * @param filter MongoDB-like query filter */ - abstract findOne(filter?: Partial): Promise; + async findOne(filter?: Partial): Promise { + const results = await this.find(filter); + return results.length > 0 ? results[0] : null; + } /** * Find all documents that match the filter @@ -59,7 +61,10 @@ export abstract class BaseStorage< * @param update Document or fields to update * @returns True if a document was updated, false otherwise */ - abstract updateOne(filter: Partial, update: Partial): Promise; + async updateOne(filter: Partial, update?: Partial): Promise { + const results = await this.updateMany(filter, update, { limit: 1 }); + return results > 0; + } /** * Update all documents that match the filter @@ -71,7 +76,7 @@ export abstract class BaseStorage< abstract updateMany( filter: Partial, update: Partial, - options: Partial, + options?: Partial, ): Promise; /** @@ -79,7 +84,10 @@ export abstract class BaseStorage< * @param filter Query to match the document to delete * @returns True if a document was deleted, false otherwise */ - abstract deleteOne(filter: Partial): Promise; + async deleteOne(filter: Partial): Promise { + const results = await this.deleteMany(filter, { limit: 1 }); + return results > 0; + } /** * Delete all documents that match the filter with options diff --git a/src/storage/encrypted-storage.ts b/src/storage/encrypted-storage.ts index 871a6b7..9e2a9d4 100644 --- a/src/storage/encrypted-storage.ts +++ b/src/storage/encrypted-storage.ts @@ -8,7 +8,7 @@ import { BaseStorage, type FindOptions } from './base-storage.js'; export class EncryptedStorage< T extends Record = Record, > extends BaseStorage { - static from(storage: BaseStorage, key: AESKey) { + static from(storage: BaseStorage>, key: AESKey) { return new EncryptedStorage(storage, key); } @@ -18,24 +18,34 @@ export class EncryptedStorage< }); constructor( - private readonly storage: BaseStorage, + private readonly storage: BaseStorage>, private readonly key: AESKey, ) { super(); // Forward events from the underlying storage, decrypting the data this.storage.on('insert', async (event) => { + // De-crypt the value before emitting the event. const decryptedValue = await this.convertToDecrypted(event.value as Record); - this.emit('insert', { key: event.key, value: decryptedValue }); + + // Re-emit the insert event with the original payload. + this.emit('insert', { value: decryptedValue }); }); this.storage.on('update', async (event) => { + // De-crypt the value before emitting the event. const decryptedValue = await this.convertToDecrypted(event.value as Record); - this.emit('update', { key: event.key, value: decryptedValue }); + + // Re-emit the update event with the original payload. + this.emit('update', { value: decryptedValue }); }); - this.storage.on('delete', (event) => { - this.emit('delete', event); + this.storage.on('delete', async (event) => { + // De-crypt the value before emitting the event. + const decryptedValue = await this.convertToDecrypted(event.value as Record); + + // Re-emit the delete event with the original payload. + this.emit('delete', { value: decryptedValue }); }); this.storage.on('clear', (event) => { @@ -43,43 +53,22 @@ export class EncryptedStorage< }); } - async insertOne(id: string, document: T): Promise { - const encrypted = await this.convertToEncrypted(document); - await this.storage.insertOne(id, encrypted); - } - - async insertMany(documents: Map): Promise { - const encrypted = new Map>(); - for (const [key, value] of documents.entries()) { - encrypted.set(key, await this.convertToEncrypted(value)); + async insertMany(documents: Array): Promise { + const encrypted = []; + for (const document of documents) { + encrypted.push(await this.convertToEncrypted(document)); } await this.storage.insertMany(encrypted); } - async findOne(filter: Partial): Promise { + async find(filter?: Partial, options?: FindOptions): Promise { const encryptedFilter = await this.convertToEncrypted(filter); - - console.log('encryptedFilter', encryptedFilter); - - const document = await this.storage.findOne(encryptedFilter); - if (!document) return null; - return this.convertToDecrypted(document); - } - - async find(filter: Partial): Promise { - const encryptedFilter = await this.convertToEncrypted(filter); - const documents = await this.storage.find(encryptedFilter); + const documents = await this.storage.find(encryptedFilter, options); return Promise.all( documents.map(async (document) => this.convertToDecrypted(document)), ); } - async updateOne(filter: Partial, update: Partial): Promise { - const encryptedFilter = await this.convertToEncrypted(filter); - const encryptedUpdate = await this.convertToEncrypted(update); - return this.storage.updateOne(encryptedFilter, encryptedUpdate); - } - async updateMany( filter: Partial, update: Partial, @@ -90,11 +79,6 @@ export class EncryptedStorage< return this.storage.updateMany(encryptedFilter, encryptedUpdate, options); } - async deleteOne(filter: Partial): Promise { - const encryptedFilter = await this.convertToEncrypted(filter); - return this.storage.deleteOne(encryptedFilter); - } - async deleteMany( filter: Partial, options: Partial = {}, diff --git a/src/storage/storage-memory-synced.ts b/src/storage/storage-memory-synced.ts index 09425e5..006cee4 100644 --- a/src/storage/storage-memory-synced.ts +++ b/src/storage/storage-memory-synced.ts @@ -15,34 +15,34 @@ export class StorageMemorySynced = Record { - await this.inMemoryCache.insertOne(payload.key, payload.value); - this.emit('insert', { key: payload.key, value: payload.value }); + await this.inMemoryCache.insertOne(payload.value); + this.emit('insert', payload); }); this.store.on('update', async (payload) => { // For update events, we need to find and update the document in memory // Since we don't have the filter, we'll update by key - await this.inMemoryCache.insertOne(payload.key, payload.value); - this.emit('update', { key: payload.key, value: payload.value }); + const filter = { + id: payload.value.id, + } as unknown as Partial + + // Update the document in memory by ID. + await this.inMemoryCache.updateOne(filter, payload.value); + this.emit('update', payload); }); this.store.on('delete', async (payload) => { - // For delete events, we need to find and delete the document by key - const allDocs = await this.inMemoryCache.find(); - for (const doc of allDocs) { - if ('id' in doc && doc.id === payload.key) { - const filter = {} as Partial; - (filter as any).id = payload.key; - await this.inMemoryCache.deleteOne(filter); - break; - } - } - this.emit('delete', { key: payload.key }); + await this.inMemoryCache.deleteOne(payload.value); + + // Re-emit the delete event with the original payload. + this.emit('delete', payload); }); this.store.on('clear', async () => { // Clear all documents from memory cache - await this.inMemoryCache.deleteMany({} as Partial); + await this.inMemoryCache.deleteMany({}); + + // Re-emit the clear event with the original payload. this.emit('clear', undefined); }); } @@ -57,35 +57,21 @@ export class StorageMemorySynced = Record { - await this.store.insertOne(id, document); - } - - async insertMany(documents: Map): Promise { + async insertMany(documents: Array): Promise { await this.store.insertMany(documents); } - async findOne(filter?: Partial): Promise { - return await this.inMemoryCache.findOne(filter); - } - async find(filter?: Partial, options?: FindOptions): Promise { return await this.inMemoryCache.find(filter, options); } - async updateOne(filter: Partial, update: Partial): Promise { - return await this.store.updateOne(filter, update); - } - async updateMany( filter: Partial, update: Partial, @@ -94,10 +80,6 @@ export class StorageMemorySynced = Record): Promise { - return await this.store.deleteOne(filter); - } - async deleteMany(filter: Partial, options: FindOptions = {} as FindOptions): Promise { return await this.store.deleteMany(filter, options); } diff --git a/src/storage/storage-memory.ts b/src/storage/storage-memory.ts index 663b4df..e410b7b 100644 --- a/src/storage/storage-memory.ts +++ b/src/storage/storage-memory.ts @@ -10,7 +10,7 @@ export class StorageMemory< T extends Record = Record, > extends BaseStorage { // TODO: Eventually this may accept indexes as an argument. - static from(): StorageMemory { + static from>(): StorageMemory { return new StorageMemory(); } @@ -24,15 +24,10 @@ export class StorageMemory< this.children = new Map(); } - async insertOne(id: string, document: T): Promise { - this.store.set(id, document); - this.emit('insert', { key: id, value: document }); - } - - async insertMany(documents: Map): Promise { - for (const [key, value] of documents.entries()) { - this.store.set(key, value); - this.emit('insert', { key, value }); + async insertMany(documents: Array): Promise { + for (const document of documents) { + this.store.set(document.id, document); + this.emit('insert', { value: document }); } } @@ -49,16 +44,7 @@ export class StorageMemory< return true; } - async findOne(filter?: Partial): Promise { - for (const [, value] of this.store) { - if (this.matchesFilter(value, filter)) { - return value; - } - } - return null; - } - - async find(filter?: Partial): Promise { + async find(filter?: Partial, options?: FindOptions): Promise { const results: T[] = []; for (const [, value] of this.store) { if (this.matchesFilter(value, filter)) { @@ -68,18 +54,6 @@ export class StorageMemory< return results; } - async updateOne(filter: Partial, update: Partial): Promise { - for (const [key, value] of this.store) { - if (this.matchesFilter(value, filter)) { - const updated = { ...value, ...update }; - this.store.set(key, updated); - this.emit('update', { key, value: updated }); - return true; - } - } - return false; - } - async updateMany( filter: Partial, update: Partial, @@ -106,35 +80,24 @@ export class StorageMemory< for (const [key, oldValue] of itemsToProcess) { const updatedValue = { ...oldValue, ...update }; this.store.set(key, updatedValue); - this.emit('update', { key, value: updatedValue }); + this.emit('update', { value: updatedValue }); updated++; } return updated; } - async deleteOne(filter: Partial): Promise { - for (const [key, value] of this.store) { - if (this.matchesFilter(value, filter)) { - this.store.delete(key); - this.emit('delete', { key }); - return true; - } - } - return false; - } - async deleteMany( filter: Partial, options: Partial = {}, ): Promise { let deleted = 0; - const keysToDelete: string[] = []; + const rowsToDelete: Array = []; // Collect all matching keys for (const [key, value] of this.store) { if (this.matchesFilter(value, filter)) { - keysToDelete.push(key); + rowsToDelete.push(value); } } @@ -142,13 +105,13 @@ export class StorageMemory< const startIndex = options.skip || 0; const endIndex = options.limit ? startIndex + options.limit - : keysToDelete.length; - const keysToProcess = keysToDelete.slice(startIndex, endIndex); + : rowsToDelete.length; + const rowsToProcess = rowsToDelete.slice(startIndex, endIndex); // Delete items - for (const key of keysToProcess) { - this.store.delete(key); - this.emit('delete', { key }); + for (const row of rowsToProcess) { + this.store.delete(row.id); + this.emit('delete', { value: row }); deleted++; }