@jucie-engine/message
v1.0.5
Published
Message bus system with event registry, channels, and subscriptions
Readme
Message Extension
A robust message passing system for the Jucie engine that enables typed, reliable communication between different parts of an application using Web Workers' MessageChannel API.
Overview
The Message extension provides a MessageBus (primary/central hub) and Channel (secondary/client) architecture for inter-process communication. It supports typed events, message routing, subscription management, and error handling.
Architecture
┌─────────────────┐ MessageChannel ┌─────────────────┐
│ MessageBus │◄────────────────────►│ Channel │
│ (Primary) │ │ (Secondary) │
│ │ │ │
│ • Manages ports │ │ • Connects to │
│ • Routes msgs │ │ MessageBus │
│ • Handles subs │ │ • Publishes │
│ • Validates │ │ • Subscribes │
└─────────────────┘ └─────────────────┘Key Components
- MessageBus: Central message router that manages multiple channels and routes messages between them
- Channel: Client that connects to MessageBus and can publish/subscribe to typed events
- Port: Low-level communication layer handling MessageChannel handshaking and message transport
- EventRegistry: Manages event schemas, validation, and symbol-to-string conversion
Installation
import { Engine } from '@jucie-engine/core';
import { MessageBus, Channel } from '@jucie-engine/message';
const engine = Engine.create()
.install(MessageBus.configure({
mode: 'development',
schemas: [userEvents, systemEvents]
}))
.install(Channel.configure());Event Schema Definition
Define typed events using the defineEvents function:
import { defineEvents } from '@brickworks/message';
const userEvents = defineEvents('user', () => ({
login: ['String', 'String'], // username, password
logout: ['String'], // username
profile: {
update: ['Object'],
delete: ['String']
}
}));
const systemEvents = defineEvents('system', () => ({
ready: '*', // wildcard - accepts any payload
error: ['String', 'Object'],
config: null // no payload validation
}));Schema Types
- Array:
['String', 'Object']- Validates argument count and types - Wildcard:
'*'- Accepts any payload - Null:
null- No payload validation - Nested: Objects create namespaced events
Usage
Basic Setup
// MessageBus (Primary) - typically on main thread
const messageBusEngine = Engine.create()
.install(MessageBus.configure({
mode: 'development',
schemas: [userEvents]
}))
.install(Channel.configure());
// Channel (Secondary) - typically in worker/iframe
const channelEngine = Engine.create()
.install(Channel.configure());Creating and Connecting Channels
// Create a channel from MessageBus
const port = messageBusEngine.messageBus.createChannel('worker1');
// Connect Channel to MessageBus
await channelEngine.channel.connect(port);
// Verify connection and event schemas are available
expect(channelEngine.channel.events).toBeDefined();Publishing and Subscribing
// Subscribe to events
channelEngine.channel.subscribe(
channelEngine.channel.events.user.login,
(username, password) => {
console.log('User login:', username);
}
);
// Publish events
channelEngine.channel.publish(
channelEngine.channel.events.user.login,
'john_doe',
'secret123'
);Advanced Features
One-time Subscriptions
channelEngine.channel.subscribeOnce(
channelEngine.channel.events.system.ready,
() => console.log('System is ready!')
);Namespaced Events
// Subscribe to namespaced event
channelEngine.channel.namespace('admin').subscribe(
channelEngine.channel.events.user.login,
(username) => console.log('Admin login:', username)
);
// Publish to namespaced event
channelEngine.channel.namespace('admin').publish(
channelEngine.channel.events.user.login,
'admin_user'
);Targeted Messaging
// Send message to specific channel only
channelEngine.channel.to('worker1').publish(
channelEngine.channel.events.user.logout,
'username'
);Multiple Subscribers
const subscriber1 = (data) => console.log('Sub1:', data);
const subscriber2 = (data) => console.log('Sub2:', data);
// Both subscribers will receive the message
channelEngine.channel.subscribe(event, subscriber1);
channelEngine.channel.subscribe(event, subscriber2);Channel Management
// Check if channel is active
const isActive = messageBusEngine.messageBus.isChannelActive('worker1');
// Enable/disable channels
await messageBusEngine.messageBus.disableChannel('worker1');
await messageBusEngine.messageBus.enableChannel('worker1');
// Use external MessageChannel
const { MessageChannel } = await import('worker_threads');
const messageChannel = new MessageChannel();
messageBusEngine.messageBus.useChannel('external', messageChannel.port1);Error Handling
The system provides comprehensive error handling:
// Unknown events throw errors
try {
channelEngine.channel.publish('unknown:event', 'data');
} catch (error) {
console.error('Unknown event:', error.message);
}
// Connection errors are handled gracefully
const result = await channelEngine.channel.connect(invalidPort);
if (result === false) {
console.error('Connection failed');
}Event Validation
Events are automatically validated against their schemas:
// This will pass validation
channelEngine.channel.publish(
channelEngine.channel.events.user.login,
'username', // String
'password' // String
);
// This will fail validation and log warnings
channelEngine.channel.publish(
channelEngine.channel.events.user.login,
'username', // String ✓
123 // Number ✗ (expected String)
);Configuration Options
MessageBus Configuration
MessageBus.configure({
mode: 'development', // 'development' | 'production' | 'test'
schemas: [userEvents, systemEvents] // Event schema definitions
})Channel Configuration
Channel.configure({
port: messagePort, // Optional: pre-configured MessagePort
subscriptions: [ // Optional: pre-configured subscriptions
{
name: 'user.login',
action: 'subscribe',
event: 'user:login',
namespace: 'admin',
subscriber: (username) => console.log(username)
}
]
})Testing
The extension includes comprehensive test coverage:
npm testTests cover:
- Basic connection flow
- Message publishing and subscribing
- Multiple subscribers and one-time subscriptions
- Namespace functionality
- Targeted messaging
- Error handling
- Channel management
- Event validation
- Complex event schemas
API Reference
MessageBus
createChannel(name, callback?)- Create a new channeluseChannel(name, port, callback?)- Use external MessageChanneldisableChannel(name)- Disable a channelenableChannel(name)- Enable a channelisChannelActive(name)- Check if channel is active
Channel
connect(port, callback?)- Connect to MessageBuspublish(event, ...payload)- Publish an eventsubscribe(event, subscriber)- Subscribe to an eventsubscribeOnce(event, subscriber)- Subscribe once to an eventunsubscribe(event, subscriber)- Unsubscribe from an eventnamespace(name)- Create namespaced channelto(...channels)- Create targeted channel
EventRegistry
processSchemas(schemas)- Process event schemasvalidateEvent(event, payload)- Validate event payloadgetEventObject()- Get nested event object with symbolsgetPath(symbol)- Convert symbol to string pathgetSymbol(path)- Convert string path to symbol
Best Practices
- Define schemas early - Set up event schemas before creating channels
- Use typed events - Leverage schema validation for better debugging
- Handle errors gracefully - Always wrap channel operations in try-catch
- Clean up subscriptions - Use unsubscribe functions to prevent memory leaks
- Use namespaces - Organize events with namespaces for better structure
- Test thoroughly - Use the comprehensive test suite as a reference
Examples
See the test files for complete working examples:
__tests__/MessageBus.test.js- Integration tests__tests__/EventRegistry.test.js- Event schema tests__tests__/Port.test.js- Low-level communication tests
