@ferrow/notification-service
v1.0.0
Published
Channel-agnostic notification service with templating, retry, and fan-out
Maintainers
Readme
notification-service
Channel-agnostic notification system. Register any channel (email, SMS, Slack, webhook, etc.). Template-driven messages with variable interpolation. Fan-out to multiple channels with per-channel retry and result reporting.
Quickstart
import { NotificationService, ConsoleChannel, InMemoryChannel } from 'notification-service';
const service = new NotificationService();
// Register channels
service.registerChannel('console', new ConsoleChannel('alerts'));
service.registerChannel('log', new InMemoryChannel());
// Register template with {{var}} placeholders
service.registerTemplate('welcome', 'Hello {{name}}, welcome to {{app}}!');
// Send to all channels
const results = await service.send('welcome', {
name: 'Alice',
app: 'MyApp'
});
// results[0].fulfilled == true
// results[1].fulfilled == trueAPI
Constructor
new NotificationService()Methods
registerChannel(name, channel)
Register a channel by name. Channel must implement:
interface Channel {
send(message: string): Promise<void>;
}service.registerChannel('email', new EmailChannel(apiKey));
service.registerChannel('slack', new SlackChannel(webhookUrl));registerTemplate(name, template)
Register a message template with {{varName}} placeholders.
service.registerTemplate('order-shipped', 'Order {{orderId}} shipped to {{address}}');send(templateName, vars, channelNames?)
Render template and send to channels. Returns array of per-channel results.
const results = await service.send('order-shipped', {
orderId: '12345',
address: '123 Main St'
}, ['email', 'sms']); // Omit channelNames to send to all
// results = [
// { channelName: 'email', fulfilled: true },
// { channelName: 'sms', fulfilled: false, error: 'Rate limited' }
// ]Throws:
Error('Template not found: ...')if template not registeredError('Template variable not provided: ...')if a {{var}} is missingError('No channels registered')if channelNames is emptyError('Channel not found: ...')if named channel not registered
setRetryOptions(options)
Configure retry behavior (default: 2 attempts, 100ms backoff).
service.setRetryOptions({ maxAttempts: 3, backoffMs: 500 });getChannels() / getTemplates()
List registered channels and templates.
console.log(service.getChannels()); // ['console', 'log']
console.log(service.getTemplates()); // ['welcome', 'order-shipped']Built-in Channels
ConsoleChannel — Logs to console
new ConsoleChannel('prefix')InMemoryChannel — Stores messages for testing
const mem = new InMemoryChannel();
await service.send(...);
console.log(mem.getMessages()); // ['Hello Alice, ...']
mem.clear();Scope & Limits
- No built-in email/SMS/Slack — implement as a
Channeland register it - Template-only messaging — raw text placeholders; no markdown/HTML rendering
- Retry per-channel — 2 attempts by default, exponential backoff; no circuit breaker
- Fan-out only — no queuing, persistence, or scheduled delivery
- Missing variables throw — no silent defaults; templates must be complete
- No async template loading — templates registered synchronously
Example: Custom Channel
class EmailChannel implements Channel {
constructor(private apiKey: string) {}
async send(message: string): Promise<void> {
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
personalizations: [{ to: [{ email: '[email protected]' }] }],
from: { email: '[email protected]' },
subject: 'Notification',
content: [{ type: 'text/plain', value: message }]
})
});
if (!res.ok) throw new Error(`SendGrid: ${res.statusText}`);
}
}
service.registerChannel('email', new EmailChannel(process.env.SENDGRID_API_KEY!));License
MIT
Sponsored by Ferrow
Part of the ferrow-toolkit collection · Sponsored by Ferrow
