UNPKG

remix-nlux

Version:

Remix IDE NLUX integration. Remix IDE is the leading IDE for building and deploying smart contracts on Ethereum. NLUX is a JavaScript and React library for building conversational AI experiences.

337 lines (278 loc) 10.5 kB
import {ContextAdapter} from '@shared/types/adapters/context/contextAdapter'; import {ContextDataAdapter} from '@shared/types/adapters/context/contextDataAdapter'; import {DestroyContextResult, InitializeContextResult} from '@shared/types/aiContext/contextResults'; import {ContextItemDataType, ContextItems} from '@shared/types/aiContext/data'; import {warn} from '@shared/utils/warn'; type UpdateQueueItem = { operation: 'set'; data: ContextItemDataType; description: string; } | { operation: 'update'; data?: ContextItemDataType; description?: string; } | { operation: 'delete'; }; export class DataSyncService { private actionToPerformWhenIdle: 'flush' | 'reset' | 'none' = 'none'; private dataAdapter: ContextDataAdapter; private readonly itemIds = new Set<string>(); private status: 'idle' | 'updating' | 'destroyed' = 'idle'; private theContextId: string | null = null; private readonly updateQueueByItemId: Map<string, UpdateQueueItem> = new Map(); constructor(adapter: ContextDataAdapter | ContextAdapter) { this.dataAdapter = adapter; } get contextId(): string | null { return this.theContextId; } async createContext(initialItems?: ContextItems): Promise<InitializeContextResult> { if (this.status === 'destroyed') { return { success: false, error: 'The context has been destroyed', }; } const contextItems = initialItems ?? null; this.itemIds.clear(); if (contextItems !== null) { const itemIds = Object.keys(contextItems); itemIds.forEach((itemId) => { this.itemIds.add(itemId); }); } this.actionToPerformWhenIdle = 'none'; try { // TODO - Handle retry on failure. const result = await this.dataAdapter.create(contextItems ?? {}); if (result.success) { this.theContextId = result.contextId; return { success: true, contextId: result.contextId, }; } else { return { success: false, error: 'Failed to set the context', }; } } catch (error) { return { success: false, error: `${error}`, }; } } async destroy(): Promise<DestroyContextResult> { if (this.status === 'destroyed') { warn(`Context.DataSyncService.destroy() called on a state that has already been destroyed`); return { success: true, }; } if (this.status === 'updating' && !this.contextId) { warn(`Context.DataSyncService.destroy() called with no contextId!`); } if (this.contextId) { this.status = 'updating'; await this.dataAdapter.discard(this.contextId); // TODO - Handle failure to reset context data. } this.status = 'destroyed'; this.theContextId = null; this.updateQueueByItemId.clear(); this.actionToPerformWhenIdle = 'none'; return { success: true, }; } async flush(): Promise<void> { if (!this.contextId) { throw new Error('Context not initialized'); } if (this.status === 'updating') { this.actionToPerformWhenIdle = 'flush'; return; } this.status = 'updating'; const itemsInQueue = this.updateQueueByItemId.keys(); const itemsToUpdate: string[] = []; const itemsToDelete: string[] = []; for (const itemId of itemsInQueue) { const item = this.updateQueueByItemId.get(itemId); if (!item) { continue; } if (item.operation === 'delete') { itemsToDelete.push(itemId); continue; } if (['set', 'update'].includes(item.operation)) { itemsToUpdate.push(itemId); } } const itemsUpdateObject: Partial<ContextItems> = itemsToUpdate.reduce( (acc: Partial<ContextItems>, itemId) => { const op = this.updateQueueByItemId.get(itemId); if (!op) { return acc; } if (op.operation === 'set') { acc[itemId] = { value: op.data, description: op.description, }; } if ( op.operation === 'update' && (op.data !== undefined || op.description !== undefined) ) { // We know that at least one of the two arguments is not undefined. acc[itemId] = {value: op.data!, description: op.description!}; } return acc; }, {}, ); if (Object.keys(itemsUpdateObject).length > 0) { Object.keys(itemsUpdateObject).forEach((itemId) => { this.updateQueueByItemId.delete(itemId); }); try { await this.dataAdapter.updateItems(this.contextId, itemsUpdateObject); } catch (error) { warn(`Failed to update context data: ${error}`); // Reset the items that failed to update. Object.keys(itemsUpdateObject).forEach((itemId) => { const item = itemsUpdateObject[itemId]; if (!item) { return; } this.updateQueueByItemId.set(itemId, { operation: 'update', data: item.value, description: item.description, }); }); } } if (itemsToDelete.length > 0) { itemsToDelete.forEach((itemId) => { this.itemIds.delete(itemId); this.updateQueueByItemId.delete(itemId); }); try { await this.dataAdapter.removeItems(this.contextId, itemsToDelete); } catch (error) { warn(`Failed to delete context data: ${error}`); // Reset the items that failed to delete. itemsToDelete.forEach((itemId) => { this.itemIds.add(itemId); this.updateQueueByItemId.set(itemId, {operation: 'delete'}); }); } } await this.backToIdle(); } hasActiveItemWithId(itemId: string): boolean { return this.itemIds.has(itemId) && ( !this.updateQueueByItemId.has(itemId) || this.updateQueueByItemId.get(itemId)?.operation !== 'delete' ); } hasItemWithId(itemId: string): boolean { return this.itemIds.has(itemId); } removeItem(itemId: string) { if (this.status === 'destroyed') { throw new Error('The context has been destroyed'); } if (!this.contextId) { throw new Error('Context not initialized'); } if (!this.itemIds.has(itemId)) { throw new Error('Item not found'); } this.updateQueueByItemId.set(itemId, {operation: 'delete'}); } async resetContextData(newInitialData?: ContextItems): Promise<void> { const victimContextId = this.contextId; this.itemIds.clear(); this.updateQueueByItemId.clear(); if (this.status === 'updating') { this.actionToPerformWhenIdle = 'reset'; return; } if (!victimContextId) { warn(`resetContextData() called with no contextId!`); await this.backToIdle(); return; } try { this.status = 'updating'; await this.dataAdapter.resetItems(victimContextId, newInitialData); } catch (error) { warn(`Failed to reset context data: ${error}`); // TODO - Handle retry on failure. } this.updateQueueByItemId.clear(); if (newInitialData) { this.itemIds.clear(); const newItems = Object.keys(newInitialData); newItems.forEach((itemId) => { this.itemIds.add(itemId); }); } else { this.itemIds.clear(); } await this.backToIdle(); } setItemData(itemId: string, description: string, data: ContextItemDataType) { if (this.status === 'destroyed') { throw new Error('The context has been destroyed'); } this.updateQueueByItemId.set( itemId, { operation: 'set', description, data, }, ); this.itemIds.add(itemId); } updateItemData(itemId: string, description?: string, data?: ContextItemDataType) { if (this.status === 'destroyed') { throw new Error('The context has been destroyed'); } if (data === undefined && description === undefined) { return; } const currentInQueue: UpdateQueueItem | undefined = this.updateQueueByItemId.get(itemId); if (currentInQueue?.operation === 'delete') { throw new Error('Item has been deleted'); } const updatedData = data ?? currentInQueue?.data ?? undefined; const updatedDescription = description ?? currentInQueue?.description ?? undefined; // We know that at least one of the two arguments is not undefined, which is what's required by // updateQueueByItemId[itemId] type. this.updateQueueByItemId.set(itemId, { operation: 'update', data: updatedData, description: updatedDescription, }); } private async backToIdle() { this.status = 'idle'; const actionToPerformWhenIdle = this.actionToPerformWhenIdle; this.actionToPerformWhenIdle = 'none'; if (actionToPerformWhenIdle === 'flush') { await this.flush(); return; } if (actionToPerformWhenIdle === 'reset') { await this.resetContextData(); return; } } }