Events for the IXO Oracles
npm install @ixo/oracles-events@ixo/oracles-eventsReal-time event system for the IXO Oracles Framework. Handles streaming communication between your oracle (server) and client applications via Server-Sent Events (SSE) and WebSocket (WS) connections.
Table of Contents
- Overview
- Communication Channels
- Event Types
- Installation
- Creating a New Event
- Advanced Usage
- Testing
- Contributing
---
The @ixo/oracles-events package enables real-time communication between your oracle and client applications. While your LLM processes conversations using LangGraph, events stream live updates to clients showing tool execution, rendering custom UI components, and enabling browser-side interactions.
Key Features:
- Dual Communication: SSE for one-way streaming, WebSocket for bidirectional communication
- Type-Safe Events: Full TypeScript support with payload validation
- Session Management: Automatic session isolation and request tracking
- Client Integration: Seamless integration with @ixo/oracles-client-sdk
- Browser Tools: Reverse tool calls that execute in the user's browser
---
Use Case: One-way streaming from server to client
Best For: Tool call updates, component rendering, status notifications
Endpoint: /sse/events?sessionId={sessionId}
Use Case: Bidirectional real-time communication
Best For: Browser tool calls, interactive features, immediate responses
Implementation: Socket.IO with session-based rooms
---
Purpose: Notifies clients when LLM executes tools on the server
Use Cases: API calls, database queries, long-running operations
``typescript
// Start tool execution
new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
args: { location: 'New York' },
status: 'isRunning',
}).emit();
// Tool completes
new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
status: 'done',
output: 'Temperature: 72°F, Sunny',
}).emit();
`
Purpose: Instructs client to render custom UI components
Use Cases: Dynamic lists, charts, forms, interactive elements
`typescript`
new RenderComponentEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
componentName: 'ProductList',
args: {
products: [
{ name: 'Product A', price: 29.99 },
{ name: 'Product B', price: 39.99 },
],
},
status: 'done',
}).emit();
Purpose: Enables LLM to execute tools directly in user's browser
Use Cases: UI navigation, DOM manipulation, browser-specific operations
`typescript
// Server-side: Call browser tool
import { callBrowserTool } from '@ixo/common';
const result = await callBrowserTool({
sessionId: 'user-session-123',
toolCallId: 'tool-call-789',
toolName: 'navigateToPage',
args: { url: '/products' },
});
`
Purpose: Notifies clients to refresh message cache
Use Cases: New messages, conversation state changes
Purpose: Manages conversation flow and navigation state
Use Cases: Multi-step conversations, wizard flows
---
`bash`
pnpm install @ixo/oracles-events
---
Create your event by extending BaseEvent:
`typescript
// src/events/my-custom-event/types.ts
export const EVENT_NAME = 'my_custom_event' as const;
export interface IMyCustomEvent {
message: string;
priority: 'high' | 'normal' | 'low';
metadata?: Record
}
`
`typescript
// src/events/my-custom-event/my-custom-event.ts
import {
BaseEvent,
shouldHaveSessionId,
WithRequiredEventProps,
} from '../base-event/base-event';
import { EVENT_NAME, IMyCustomEvent } from './types';
export class MyCustomEvent extends BaseEvent
constructor(public payload: WithRequiredEventProps
super();
shouldHaveSessionId(payload);
}
public eventName = EVENT_NAME;
static eventName = EVENT_NAME;
}
`
`typescript
// src/graph-event-emitter.ts
import { MyCustomEvent } from './events/my-custom-event/my-custom-event';
export class GraphEventEmitter {
static registerEventHandlers(server: Server): void {
MyCustomEvent.registerEventHandlers(server);
// ... other events
}
}
`
`typescript`
// src/events/index.ts
export { MyCustomEvent } from './my-custom-event/my-custom-event';
export type { IMyCustomEvent } from './my-custom-event/types';
`typescript
// src/types/index.ts
import type { MyCustomEvent } from '../events';
export type AllEvents =
| ToolCallEvent
| RenderComponentEvent
| BrowserToolCallEvent
| MessageCacheInvalidationEvent
| RouterEvent
| MyCustomEvent; // Add your event here
export type MyCustomEventPayload = WithRequiredEventProps
`
There are two ways to emit events depending on your use case:
#### Method 1: Standard Event Emission (Recommended)
Use Case: Standard event flow that automatically reaches clients via WebSocket
How it works: event.emit() → rootEventEmitter → GraphEventEmitter (if registered) → WebSocket clients
`typescript
// In your LangGraph node or service
import { ToolCallEvent } from '@ixo/oracles-events';
const event = new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
args: { location: 'New York' },
status: 'isRunning',
});
// This follows the complete flow:
// 1. event.emit() → rootEventEmitter.emit(eventName, payload)
// 2. GraphEventEmitter (registered in ws.gateway.ts) listens to rootEventEmitter
// 3. Automatically broadcasts to WebSocket clients in the session
event.emit();
`
Complete Event Flow:
``
Your Code → event.emit() → rootEventEmitter → GraphEventEmitter → WebSocket Server → Client
Registered Events: ToolCallEvent, RenderComponentEvent, BrowserToolCallEvent, MessageCacheInvalidationEvent, RouterEvent
#### Method 2: Direct Service Emission
Use Case: Direct emission to specific communication channels
How it works: Bypass event system and emit directly to SSE/WebSocket services
`typescript
// Direct SSE emission (like in messages.service.ts)
import { SseService } from 'src/sse/sse.service';
@Injectable()
export class MyService {
constructor(private readonly sseService: SseService) {}
sendEventToClient(sessionId: string) {
const event = new MyCustomEvent({
sessionId,
requestId: 'request-456',
message: 'Direct emission',
priority: 'high',
});
// Direct emission to SSE clients
this.sseService.publishToSession(sessionId, event);
}
}
`
#### Method 3: Internal Communication
Use Case: Emit events back into the internal events system
How it works: Direct rootEventEmitter usage for service-to-service communication
`typescript
// Internal emission (like in ws.gateway.ts)
import { rootEventEmitter } from '@ixo/oracles-events';
// Emit back into the events system for internal processing
rootEventEmitter.emit('browser_tool_result', {
sessionId: 'user-session-123',
toolCallId: 'tool-123',
result: { success: true },
timestamp: new Date().toISOString(),
});
`
When to use each method:
- Method 1: Standard registered events that automatically reach WebSocket clients (ToolCallEvent, RenderComponentEvent, etc.)
- Method 2: Real-time streaming scenarios, direct SSE client communication
- Method 3: Internal service communication, responding to WebSocket events and triggering internal workflows
---
The following diagram shows the complete flow from your code to the client:
`mermaid
graph LR
A["🧠 Your Code
event.emit()"] --> B["📡 rootEventEmitter"]
B --> C["🎯 GraphEventEmitter
(listens to rootEventEmitter)"]
C --> D["📱 WebSocket → Client"]
style A fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style B fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style C fill:#e8f5e8,stroke:#388e3c,stroke-width:2px
style D fill:#fce4ec,stroke:#d81b60,stroke-width:2px
`
How it works:
1. You call event.emit() in your coderootEventEmitter
2. This triggers (internal event bus)GraphEventEmitter
3. listens to rootEventEmitter and forwards to WebSocket
4. Client receives the event automatically
That's it! Once GraphEventEmitter is registered in ws.gateway.ts, all events automatically reach clients.
Many events support status tracking:
`typescript
// Start long-running operation
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'isRunning',
}).emit();
// Update with progress (optional)
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'isRunning',
output: 'Progress: 50% complete',
}).emit();
// Complete operation
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'done',
output: 'Analysis complete: Found 25 insights',
}).emit();
`
Chain multiple events for complex workflows:
`typescript
// 1. Show tool execution
new ToolCallEvent({
sessionId,
requestId,
toolName: 'data_analysis',
status: 'isRunning',
}).emit();
// 2. Complete tool and render results
const data = await performAnalysis();
new ToolCallEvent({
sessionId,
requestId,
toolName: 'data_analysis',
status: 'done',
output: Analyzed ${data.length} records,
}).emit();
// 3. Render custom component with results
new RenderComponentEvent({
sessionId,
requestId,
componentName: 'AnalysisChart',
args: { data, chartType: 'bar' },
status: 'done',
}).emit();
`
Every event payload must include:
`typescript`
interface RequiredEventProps {
sessionId: string; // Chat session identifier
requestId: string; // Request identifier (for tracking)
}
`typescript
// Use consistent IDs for related events
const sessionId = 'user-session-123';
const requestId = 'message-request-456';
// Multiple events for the same request
new ToolCallEvent({ sessionId, requestId / ... / });
new RenderComponentEvent({ sessionId, requestId / ... / });
`
---
`typescript
import { MyCustomEvent } from '../my-custom-event';
import { rootEventEmitter } from '../../root-event-emitter';
describe('MyCustomEvent', () => {
it('should emit with correct payload', () => {
const payload = {
sessionId: 'test-session',
requestId: 'test-request',
message: 'test message',
priority: 'high' as const,
};
const event = new MyCustomEvent(payload);
const emitSpy = jest.spyOn(rootEventEmitter, 'emit');
event.emit();
expect(emitSpy).toHaveBeenCalledWith(MyCustomEvent.eventName, payload);
});
it('should require sessionId', () => {
expect(() => {
new MyCustomEvent({
requestId: 'test-request',
message: 'test',
priority: 'normal',
} as any);
}).toThrow();
});
});
`
`typescript
import { createTestEvent } from '../test-utils';
it('should register event handlers correctly', () => {
const mockServer = {
on: jest.fn(),
emit: jest.fn(),
};
MyCustomEvent.registerEventHandlers(mockServer as any);
expect(mockServer.on).toHaveBeenCalledWith(
MyCustomEvent.eventName,
expect.any(Function),
);
});
`
---
- Use snake_case: tool_call, render_component, my_custom_eventuser_preferences_updated
- Be descriptive: not updatepayment_status_changed
- Include domain if needed:
- Always define interfaces for payload types
- Use WithRequiredEventProps for constructor parameters
- Export both the event class and interface
- Add proper JSDoc comments
- Batch events when possible instead of emitting many small events
- Use appropriate channels: SSE for notifications, WS for interactions
- Validate payloads before emission to catch errors early
- Avoid large payloads: Keep event data focused and minimal
- Validate all inputs in event payloads
- Sanitize user data before including in events
- Ensure session isolation - events only go to correct sessions
- Never include sensitive data in event payloads
---
For comprehensive system architecture, event flows, and integration patterns, see Events System Architecture.
---
Thank you for using @ixo/oracles-events`!