| name | debug-event-communication |
| description | Debug Thymian's RxJS-based event system. USE WHEN analyzing event flow issues, investigating missing responses, debugging action/event handlers, tracing event communication between plugins, or when tests fail due to event system problems. EXAMPLES: 'event not received', 'action timeout', 'no response from handler', 'plugin communication broken', 'correlation ID mismatch', 'event listener not triggered'. |
Debug Event Communication
This skill helps debug and analyze Thymian's event-driven architecture based on RxJS Subjects and ThymianEmitter.
Thymian Event System Architecture
Core Components
ThymianEmitter (packages/core/src/emitter/thymian-emitter.ts):
- Central event bus using RxJS
Subject streams
- Three internal subjects:
#events: Regular events and action events (fire-and-forget + request-response)
#responses: Response events from action handlers
#errors: Error events with correlation support
- Each emitter has a
source identifier (typically plugin name)
- Child emitters share the same subject instances but have different sources
Event Types
-
ThymianEvent (Fire-and-Forget):
- Structure:
{ id, name, payload, timestamp, source }
- Registered with:
emitter.on(eventName, handler)
- Emitted with:
emitter.emit(eventName, payload)
- Examples:
core.error, core.register, core.report, core.exit
- No response expected, handler errors are caught and emitted as error events
-
ThymianActionEvent (Request-Response):
- Structure:
{ id, name, payload, timestamp, source }
- Registered with:
emitter.onAction(actionName, (payload, ctx) => { ctx.reply(response) })
- Emitted with:
await emitter.emitAction(actionName, payload, options)
- Examples:
core.ready, core.close, core.format.load, core.lint, core.test, core.analyze, core.report.flush
- Waits for response(s) from handler(s) with timeout
- Strategies:
'first', 'collect' (default), 'deep-merge'
-
ThymianResponseEvent:
- Structure:
{ id, name, correlationId, payload, timestamp, source }
- Created by calling
ctx.reply(payload) in action handler
correlationId links response to original action event's id
-
ThymianErrorEvent:
- Structure:
{ id, name, error, timestamp, source, correlationId? }
- Created by calling
ctx.error(err) or thrown errors in handlers
- Can be correlated to events/actions via
correlationId
Key Mechanisms
- Correlation: Response events contain
correlationId matching the action event's id
- Timeout: Default 1000ms, configurable per action via
options.timeout
- Listener Tracking:
#listeners Map tracks number of registered handlers per action
- Response Collection:
emitAction waits for N responses (N = number of listeners)
- Error Handling: All handler errors are caught and emitted as error events
- Tracing: Enable via
traceEvents: true in constructor options
Common Event Communication Issues
1. Action Timeout / No Response
Symptoms:
emitAction hangs or times out
- Warning: "No response event received for action X within Yms"
Root Causes:
- Handler forgot to call
ctx.reply() or ctx.error()
- Handler threw error before calling
ctx.reply()
- Timeout too short for async operations
- No handler registered (check
strict option)
Debug Steps:
emitter.onAction('my.action', (payload, ctx) => {
console.log('Handler called with:', payload);
ctx.reply(result);
});
const result = await emitter.emitAction('my.action', data, {
timeout: 5000,
strict: true,
});
const emitter = new ThymianEmitter(logger, state, {
traceEvents: true,
timeout: 5000,
});
2. Missing Event / Listener Not Triggered
Symptoms:
- Event handler never executes
- Expected side effects don't occur
Root Causes:
- Listener registered after event was emitted (race condition)
- Event name typo (string literal mismatch)
- Handler subscribed on different emitter instance
- Subject already completed
Debug Steps:
emitter.on('core.register', (payload) => {
console.log('Received event:', payload);
});
emitter.on('my.event', handler);
emitter.emit('my.event', data);
const childEmitter = emitter.child('plugin-name');
childEmitter.on('event', handler);
3. Correlation ID Mismatch
Symptoms:
- Response not linked to action
- Wrong response received
Root Causes:
- Handler called
ctx.reply() multiple times
- Multiple action emissions without awaiting
correlationId manually modified
Debug Steps:
const actionEventId = randomUUID();
emitter.onAction('my.action', (payload, ctx) => {
ctx.reply(result);
});
emitter.onError((errorEvent) => {
console.log('Error correlation:', errorEvent.correlationId);
});
4. Multiple Responses / Incomplete Collection
Symptoms:
- Warning: "Expected N response events but got M"
- Only partial results returned
Root Causes:
- One handler times out while others succeed
- Handler count changed between registration and emission
strategy: 'first' used with multiple handlers
Debug Steps:
console.log('Handlers registered:', emitter['#listeners'].get('my.action'));
await emitter.emitAction('my.action', data, {
strategy: 'collect',
timeout: 5000,
});
await emitter.emitAction('my.action', data, {
strategy: 'first',
});
await emitter.emitAction('my.action', data, {
strategy: 'deep-merge',
});
5. Error Events Not Handled
Symptoms:
- Silent failures
- Errors not propagated
- Unhandled promise rejections
Root Causes:
- No
onError listener registered
- Error thrown in handler but not caught
ctx.error() not awaited
Debug Steps:
emitter.onError((errorEvent) => {
console.error('Event error:', {
name: errorEvent.name,
source: errorEvent.source,
correlationId: errorEvent.correlationId,
error: errorEvent.error,
});
});
emitter.onAction('my.action', (payload, ctx) => {
try {
const result = processPayload(payload);
ctx.reply(result);
} catch (err) {
ctx.error(err);
}
});
try {
await emitter.emitAction('my.action', data);
} catch (err) {
}
Test Case Analysis Checklist
When analyzing a failed test:
Output Format
When debugging an event issue, provide:
- Summary: One-sentence description of the issue
- Event Flow Map: Producer → Event/Action → Handler(s) → Response
- Timing Analysis: Emission timestamps, response times, timeout values
- Root Cause: Specific line/code causing the issue
- Fix: Concrete code change to resolve the issue