From 42cecde03f90461d3e45ddae2eea54c7ccba20b8 Mon Sep 17 00:00:00 2001 From: Tomasz Swistak Date: Wed, 16 Apr 2025 09:02:42 +0200 Subject: [PATCH] Implements command interpreter for event handling Adds a core command interpreter for managing system events. This implementation provides functionality for registering, emitting, and unregistering event callbacks. It enables decoupling of components by allowing them to subscribe to specific event types and react accordingly, improving the modularity and maintainability of the system. Also, adds .idea/ to .gitignore. --- .gitignore | 3 +- packages/core/src/command-interpreter.test.ts | 159 ++++++++++++++++++ packages/core/src/command-interpreter.ts | 51 ++++++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/command-interpreter.test.ts create mode 100644 packages/core/src/command-interpreter.ts diff --git a/.gitignore b/.gitignore index 32802cd8d..592190115 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ yarn-error.log* *.pem # System files -.vscode/* \ No newline at end of file +.vscode/* +.idea/* diff --git a/packages/core/src/command-interpreter.test.ts b/packages/core/src/command-interpreter.test.ts new file mode 100644 index 000000000..54238e080 --- /dev/null +++ b/packages/core/src/command-interpreter.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { CoreCommandInterpreter } from './command-interpreter'; +import type { SystemEvent } from './types/command-interpreter.interface'; + +describe('CoreCommandInterpreter', () => { + let interpreter: CoreCommandInterpreter; + + beforeEach(() => { + interpreter = new CoreCommandInterpreter(); + }); + + describe('emit', () => { + it('should call all registered callbacks for the event type', () => { + const commandCallback = vi.fn(); + const modelChangeCallback = vi.fn(); + const otherCommandCallback = vi.fn(); + + interpreter.register('command', commandCallback); + interpreter.register('modelChange', modelChangeCallback); + interpreter.register('command', otherCommandCallback); + + const commandEvent: SystemEvent = { type: 'command', name: 'test' }; + interpreter.emit(commandEvent); + + expect(commandCallback).toHaveBeenCalledWith(commandEvent); + expect(otherCommandCallback).toHaveBeenCalledWith(commandEvent); + expect(modelChangeCallback).not.toHaveBeenCalled(); + }); + + it('should not call any callbacks if none are registered for the event type', () => { + const callback = vi.fn(); + interpreter.register('modelChange', callback); + + const commandEvent: SystemEvent = { type: 'command', name: 'test' }; + interpreter.emit(commandEvent); + + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('register', () => { + it('should allow registering multiple callbacks for the same event type', () => { + const callback1 = vi.fn(); + const callback2 = vi.fn(); + + interpreter.register('command', callback1); + interpreter.register('command', callback2); + + const event: SystemEvent = { type: 'command', name: 'test' }; + interpreter.emit(event); + + expect(callback1).toHaveBeenCalledWith(event); + expect(callback2).toHaveBeenCalledWith(event); + }); + + it('should allow registering the same callback multiple times', () => { + const callback = vi.fn(); + + interpreter.register('command', callback); + interpreter.register('command', callback); + + const event: SystemEvent = { type: 'command', name: 'test' }; + interpreter.emit(event); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should preserve callback order', () => { + const calls: string[] = []; + const callback1 = () => calls.push('1'); + const callback2 = () => calls.push('2'); + + interpreter.register('command', callback1); + interpreter.register('command', callback2); + + interpreter.emit({ type: 'command', name: 'test' }); + + expect(calls).toEqual(['1', '2']); + }); + }); + + describe('unregister', () => { + it('should remove the callback when unregister is called', () => { + const callback = vi.fn(); + const unregister = interpreter.register('command', callback); + + unregister(); + interpreter.emit({ type: 'command', name: 'test' }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should only remove the specific callback that was unregistered', () => { + const callback1 = vi.fn(); + const callback2 = vi.fn(); + + const unregister1 = interpreter.register('command', callback1); + interpreter.register('command', callback2); + + unregister1(); + interpreter.emit({ type: 'command', name: 'test' }); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).toHaveBeenCalled(); + }); + + it('should remove the event type from the map when no callbacks remain', () => { + const callback = vi.fn(); + const unregister = interpreter.register('command', callback); + + unregister(); + interpreter.emit({ type: 'command', name: 'test' }); + + // @ts-expect-error - accessing private property for testing + expect(interpreter.callbacks.has('command')).toBe(false); + }); + + it('should handle unregistering a callback that was already removed', () => { + const callback = vi.fn(); + const unregister = interpreter.register('command', callback); + + unregister(); + unregister(); // Call again, should not throw + + interpreter.emit({ type: 'command', name: 'test' }); + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('event data', () => { + it('should pass command event data to callbacks', () => { + const callback = vi.fn(); + interpreter.register('command', callback); + + const eventData = { some: 'data' }; + interpreter.emit({ type: 'command', name: 'test', data: eventData }); + + expect(callback).toHaveBeenCalledWith({ + type: 'command', + name: 'test', + data: eventData + }); + }); + + it('should pass model change event data to callbacks', () => { + const callback = vi.fn(); + interpreter.register('modelChange', callback); + + const eventData = { some: 'data' }; + interpreter.emit({ type: 'modelChange', action: 'update', data: eventData }); + + expect(callback).toHaveBeenCalledWith({ + type: 'modelChange', + action: 'update', + data: eventData + }); + }); + }); +}); diff --git a/packages/core/src/command-interpreter.ts b/packages/core/src/command-interpreter.ts new file mode 100644 index 000000000..112699af6 --- /dev/null +++ b/packages/core/src/command-interpreter.ts @@ -0,0 +1,51 @@ +import type { CommandInterpreter, SystemEvent, SystemEventCallback } from './types/command-interpreter.interface'; + +/** + * Core implementation of CommandInterpreter interface + * Handles event emission and registration of callbacks for system events + */ +export class CoreCommandInterpreter implements CommandInterpreter { + private callbacks: Map = new Map(); + + /** + * Emit a system event to all registered callbacks for the event type + * @param event Event to emit + */ + emit(event: SystemEvent): void { + const callbacks = this.callbacks.get(event.type); + if (callbacks) { + for (const callback of callbacks) { + callback(event); + } + } + } + + /** + * Register a callback for specific event types + * @param eventType Type of event to listen for + * @param callback Function to be called when event occurs + * @returns Function to unregister the callback + */ + register(eventType: SystemEvent['type'], callback: SystemEventCallback): () => void { + if (!this.callbacks.has(eventType)) { + this.callbacks.set(eventType, []); + } + + const callbacks = this.callbacks.get(eventType) as SystemEventCallback[]; + callbacks.push(callback); + + // Return unregister function + return () => { + const callbacks = this.callbacks.get(eventType); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + if (callbacks.length === 0) { + this.callbacks.delete(eventType); + } + } + } + }; + } +}