Guide for migrating from other event systems to RailJS.
const EventEmitter = require('events');
const emitter = new EventEmitter();
// Listen
emitter.on('data', (data) => {
console.log(data);
});
// Emit
emitter.emit('data', { value: 123 });import { Rail } from '@railjs/core';
const rail = new Rail();
// Listen (with module name for cleanup)
rail.on('data', (data) => {
console.log(data);
}, 'my-module');
// Emit
rail.emit('data', { value: 123 });| Feature | EventEmitter | RailJS |
|---|---|---|
| Data safety | No cloning | Deep cloning by default |
| Module concept | No | Built-in modules |
| Async support | Limited | Full emitAsync() |
| Cleanup | Manual | Automatic via detach() |
// Action creators
const addTodo = (text) => ({
type: 'ADD_TODO',
text
});
// Reducer
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { text: action.text, completed: false }];
default:
return state;
}
}
// Store
const store = createStore(todosReducer);
// Subscribe
store.subscribe(() => {
console.log(store.getState());
});
// Dispatch
store.dispatch(addTodo('Learn RailJS'));import { Rail } from '@railjs/core';
const rail = new Rail();
// State module
const stateModule = {
name: 'state',
state: { todos: [] },
connect(rail) {
rail.on('todo.add', (data) => {
this.state.todos = [...this.state.todos, {
text: data.text,
completed: false
}];
rail.emit('state.changed', this.state);
}, 'state');
}
};
rail.attach(stateModule);
// Subscribe to changes
rail.on('state.changed', (state) => {
console.log(state);
}, 'logger');
// Dispatch (emit event)
rail.emit('todo.add', { text: 'Learn RailJS' });| Feature | Redux | RailJS |
|---|---|---|
| Centralized state | Yes | Optional (per module) |
| Reducers | Required | Not needed |
| Actions | Type strings | Event names |
| Side effects | Middleware (thunk/saga) | Built-in async |
| DevTools | Redux DevTools | Debug mode + history |
import PubSub from 'pubsub-js';
// Subscribe
const token = PubSub.subscribe('MESSAGE', (msg, data) => {
console.log(msg, data);
});
// Publish
PubSub.publish('MESSAGE', { text: 'hello' });
// Unsubscribe
PubSub.unsubscribe(token);import { Rail } from '@railjs/core';
const rail = new Rail();
// Subscribe
const unsubscribe = rail.on('MESSAGE', (data) => {
console.log(data);
}, 'my-module');
// Publish
rail.emit('MESSAGE', { text: 'hello' });
// Unsubscribe
unsubscribe();| Feature | PubSub | RailJS |
|---|---|---|
| Topic/Event names | Strings | Strings (same) |
| Namespacing | Manual | Via module names |
| Data isolation | No | Yes (deep clone) |
| Module lifecycle | No | attach()/detach() |
-
Replace global emitter with Rail instance:
// Before const emitter = new MyEmitter(); // After const rail = new Rail();
-
Wrap listeners in modules:
// Before emitter.on('event', handler); // After const myModule = { name: 'my-module', connect(rail) { rail.on('event', handler, 'my-module'); } }; rail.attach(myModule);
-
Use
emitAsync()for async operations:// Before emitter.on('fetch', async (data) => { const result = await api.fetch(data); emitter.emit('fetch-done', result); }); // After rail.on('fetch', async (data) => { return await api.fetch(data); }, 'api-module'); const results = await rail.emitAsync('fetch', data);
-
Enable performance mode if needed:
// If you don't need data isolation const rail = new Rail({ clone: false });
-
Add debug logging during migration:
const rail = new Rail({ debug: true });
Before (callbacks):
emitter.on('get-user', (id, callback) => {
const user = db.getUser(id);
callback(user);
});
emitter.emit('get-user', 123, (user) => {
console.log(user);
});After (RailJS):
// Option 1: Event-based
rail.on('get-user', (data) => {
const user = db.getUser(data.id);
rail.emit('user-result', { id: data.id, user });
}, 'db');
// Option 2: Async (recommended)
rail.on('get-user', async (data) => {
return await db.getUser(data.id);
}, 'db');
const results = await rail.emitAsync('get-user', { id: 123 });
const user = results[0].result;Before (middleware chain):
app.use((data, next) => {
console.log('Log:', data);
next();
});
app.use((data, next) => {
data.timestamp = Date.now();
next();
});After (RailJS):
// Logger module
const loggerModule = {
name: 'logger',
connect(rail) {
rail.on('request', (data) => {
console.log('Log:', data);
}, 'logger');
}
};
// Timestamp module
const timestampModule = {
name: 'timestamp',
connect(rail) {
rail.on('request', (data) => {
// Note: Don't mutate data, emit new event instead
rail.emit('request.timestamped', {
...data,
timestamp: Date.now()
});
}, 'timestamp');
}
};-
Benchmark before and after:
npm run benchmark
-
Consider disabling cloning for hot paths:
const rail = new Rail({ clone: false });
-
Use
emit()instead ofemitAsync()when possible:// Faster (fire-and-forget) rail.emit('event', data); // Slower (waits for all handlers) await rail.emitAsync('event', data);
Make sure cloning is enabled (default):
const rail = new Rail({ clone: true }); // ExplicitTry disabling cloning for performance-critical paths:
const rail = new Rail({ clone: false });Or use selective cloning:
const rail = new Rail({ clone: true });
// Disable for specific hot path
rail.setClone(false);
rail.emit('hot-path-event', data);
rail.setClone(true);RailJS prevents circular dependencies by design. Instead:
// Don't: Direct import
import moduleB from './module-b';
// Do: Event-based communication
rail.on('module-b.request', (data) => {
rail.emit('module-a.response', result);
}, 'module-a');