This guide covers hub-based messaging between twins using Events and Actions. For real-time streaming and low-latency communication, see Real-Time Channels.
What Is Twin Messaging?
Twin messaging routes messages through the PhyStack hub. Every message has:
- A type (string) - identifies what kind of message this is
- A payload (object) - the data being sent
There are two patterns:
| Pattern | Description | Use Case |
|---|---|---|
| Events | Fire-and-forget messages | Status updates, notifications, telemetry |
| Actions | Request-response with callback | Commands that need confirmation, device control |
Events vs Actions
Events (Fire-and-Forget)
Events are simple messages that don't expect a response. Use events when you just need to notify other twins about something.
// Broadcast to all subscribers
instance.emit('statusUpdate', { status: 'ready', temperature: 25 });
// Send to a specific twin
instance.to(targetTwinId).emit('statusUpdate', { status: 'ready' });
// Receiving an event
instance.on('statusUpdate', (data) => {
console.log('Status:', data.status);
});Actions (Request-Response)
Actions are messages that expect a response. Use actions when you need confirmation that something completed successfully. Actions require targeting a specific twin using to().
// Sending an action - to().emit() with callback returns a Promise
try {
const result = await instance.to(targetTwinId).emit('runDiagnostics', { level: 'full' }, () => {});
console.log('Diagnostics completed:', result.message);
} catch (error) {
console.error('Diagnostics failed:', error.message);
}
// Handling an action and responding
instance.on('runDiagnostics', async (data, respond) => {
try {
const diagnosticResult = await device.runDiagnostics(data.level);
respond({ status: 'success', message: `All ${diagnosticResult.testsRun} tests passed` });
} catch (error) {
respond({ status: 'error', message: error.message });
}
});The response object follows this structure:
interface TwinMessageResult {
status: 'success' | 'error' | 'warning';
message?: string;
}Getting Started
Prerequisites
- PhyStack device or hub-client connected
- Understanding of twin IDs and instances
Basic Setup
import { PhyHubClient } from '@phystack/hub-client';
const client = await PhyHubClient.connect();
// Get your instance
const instance = await client.getInstance();Sending Events
Broadcast Events
Send events to all subscribers of your twin:
const instance = await client.getInstance();
// Send to all subscribers (fire-and-forget)
instance.emit('statusUpdate', { status: 'ready' });
instance.emit('sensorData', { temperature: 25.5, humidity: 60 });Targeted Events
Send events to a specific twin:
const targetTwinId = 'screen-abc123';
// Send to specific twin (fire-and-forget)
instance.to(targetTwinId).emit('displayMessage', {
title: 'System Alert',
content: 'Maintenance scheduled for 10:00 PM'
});Receiving Events
Register handlers to receive events:
const instance = await client.getInstance();
// Handle incoming events
instance.on('command', (data) => {
console.log('Received command:', data);
});
// Handle multiple event types
instance.on('sensorReading', handleSensorData);
instance.on('configUpdate', handleConfigChange);
instance.on('alertTriggered', handleAlert);Sending Actions
Actions wait for a response from the handler. Use to(targetId).emit() with a callback to get a Promise back:
const instance = await client.getInstance();
const targetTwinId = 'edge-abc123';
// to().emit() with callback returns a Promise
try {
const result = await instance.to(targetTwinId).emit('calibrateSensor', {
sensorId: 'temp-001',
targetValue: 25.0
}, () => {});
console.log('Calibration complete:', result.message);
} catch (error) {
console.error('Calibration failed:', error.message);
}Actions have a 10-second timeout. If no response is received, the Promise rejects with a timeout error.
Note: Actions require a target twin. Use
instance.to(targetId).emit()for request-response patterns. The broadcastinstance.emit()is fire-and-forget only.
Handling Actions
When handling actions, you receive a respond function to send back results:
instance.on('restartService', async (data, respond) => {
try {
await serviceManager.restart(data.serviceName);
respond({
status: 'success',
message: `Service ${data.serviceName} restarted`
});
} catch (error) {
respond({
status: 'error',
message: error.message
});
}
});Common Patterns
Edge to Screen Communication
Edge app sends commands to screen, screen responds:
// Edge app - fire-and-forget event
const screenTwinId = 'screen-abc123';
instance.to(screenTwinId).emit('updateDisplay', {
mode: 'dashboard',
data: { temperature: 25.5, humidity: 60 }
});
// Screen app
instance.on('updateDisplay', (data) => {
renderDashboard(data.mode, data.data);
});Screen to Edge with Response
Screen requests action from edge, edge confirms:
// Screen app - action with response
const edgeTwinId = 'edge-abc123';
try {
const result = await instance.to(edgeTwinId).emit('executeCommand', {
command: 'capture-snapshot',
params: { resolution: '1080p' }
}, () => {});
if (result.status === 'success') {
showConfirmation('Snapshot captured');
}
} catch (error) {
console.error('Command failed:', error.message);
}
// Edge app
instance.on('executeCommand', async (data, respond) => {
const success = await commandExecutor.run(data.command, data.params);
respond({
status: success ? 'success' : 'error',
message: success ? 'Command executed' : 'Execution failed'
});
});Peripheral Communication
Communicate with peripheral twins (printers, sensors, displays, etc.). Peripheral emit supports callbacks since it's targeted communication:
// Get a peripheral
const display = await instance.getPeripheralInstance('display-001');
// Send command (action) - peripheral.emit with callback
try {
const result = await display.emit('SET_BRIGHTNESS', {
level: 80
}, () => {});
console.log('Display updated:', result.status);
} catch (error) {
console.error('Update failed:', error.message);
}
// Fire-and-forget to peripheral
display.emit('LOG_EVENT', { event: 'button_pressed' });
// Listen for peripheral events
display.on('buttonPressed', (data) => {
console.log('Button pressed:', data.buttonId);
});Twin Properties
In addition to events and actions, you can update and observe twin properties:
// Update reported properties (what the twin is)
await instance.updateReported({
status: 'online',
firmware: '1.2.3'
});
// Update desired properties (what the twin should do)
await peripheral.updateDesired({
brightness: 100,
volume: 50
});
// Listen for property updates
peripheral.onUpdateDesired((desired) => {
console.log('New desired state:', desired);
// Apply the desired configuration
});
peripheral.onUpdateReported((reported) => {
console.log('Peripheral reported:', reported);
});Instance API Reference
| Method | Description |
|---|---|
emit(type, data) | Broadcast event to subscribers (fire-and-forget) |
to(twinId).emit(type, data) | Send event to specific twin (fire-and-forget) |
to(twinId).emit(type, data, cb) | Send action to specific twin (returns Promise) |
on(type, callback) | Listen for events/actions |
updateReported(props) | Update reported properties |
updateDesired(props) | Update desired properties |
onUpdateReported(cb) | Listen for reported changes |
onUpdateDesired(cb) | Listen for desired changes |
Peripheral API Reference
| Method | Description |
|---|---|
emit(type, data) | Send event to peripheral (fire-and-forget) |
emit(type, data, cb) | Send action to peripheral (returns Promise) |
on(type, callback) | Listen for peripheral events/actions |
to(twinId).emit(...) | Send to a different twin |
When to Use Twin Messaging
Twin Messaging is ideal when:
- Messages need to work across any network (VPNs, firewalls, cellular)
- You need reliable delivery through the hub
- Latency of 50-200ms is acceptable
- You're sending commands, status updates, or configuration
Consider Real-Time Channels when:
- You need sub-10ms latency
- You're streaming video or audio
- You're sending high-frequency sensor data (100+ messages/sec)
- You're transferring large binary data
Checkpoint
You should now understand:
- The difference between Events and Actions
- When to use
emit()vsto().emit() - How to send and receive messages using the Instance API
- Common communication patterns between twins
Next Steps
- Communication Overview - Compare messaging approaches
- Real-Time Channels - WebRTC DataChannels and MediaStreams
- Peripherals and Descriptors - Hardware integration