@mounaji_npm/event-core
v0.1.3
Published
Core event system — types, factory, EventBus, time formatting. Framework-agnostic.
Maintainers
Readme
@mounaji_npm/event-core
Framework-agnostic event system — typed constants, validated factory, extensible registry, lightweight pub/sub bus with interceptor support, and DOM bridge for server→client reactivity.
No React dependency. No build step. Works in Node.js, browsers, and any React framework.
Install
npm install @mounaji_npm/event-coreNo peer dependencies required.
Exports
import {
// Type system
EVENT_TYPES, EVENT_CATEGORIES, SEVERITY, PRIORITY,
getCategoryForType, getSeverityForType,
// Factory
createEvent,
// Client pub/sub
EventBus,
// Extensible registry
EventRegistry,
// DOM bridge
MN_EVENTS_UPDATED, MN_NOTIFICATION_TOAST,
signalEventsUpdate, signalNotificationToast,
// Time formatting
formatRelativeTime, formatAbsoluteTime,
} from '@mounaji_npm/event-core';Event Types
EVENT_TYPES
All built-in event type string constants, organized by domain:
import { EVENT_TYPES } from '@mounaji_npm/event-core';
// System
EVENT_TYPES.SYSTEM_STARTUP // 'system.startup'
EVENT_TYPES.SYSTEM_ERROR // 'system.error'
EVENT_TYPES.SYSTEM_CONFIG_CHANGED // 'system.config_changed'
// User
EVENT_TYPES.USER_CREATED // 'user.created'
EVENT_TYPES.USER_ROLE_CHANGED // 'user.role_changed'
EVENT_TYPES.USER_DEACTIVATED // 'user.deactivated'
// Menu
EVENT_TYPES.MENU_CREATED // 'menu.created'
EVENT_TYPES.MENU_UPDATED // 'menu.updated'
EVENT_TYPES.MENU_DELETED // 'menu.deleted'
EVENT_TYPES.PLANNING_CREATED // 'planning.created'
EVENT_TYPES.PLANNING_UPDATED // 'planning.updated'
// Stock
EVENT_TYPES.STOCK_LOW // 'stock.low'
EVENT_TYPES.STOCK_CRITICAL // 'stock.critical'
EVENT_TYPES.STOCK_REPLENISHED // 'stock.replenished'
// Production
EVENT_TYPES.PRODUCTION_STARTED // 'production.started'
EVENT_TYPES.PRODUCTION_COMPLETED // 'production.completed'
// Notification (system events about the notification pipeline itself)
EVENT_TYPES.NOTIFICATION_SENT // 'notification.sent'
EVENT_TYPES.NOTIFICATION_READ // 'notification.read'
EVENT_TYPES.NOTIFICATION_FAILED // 'notification.failed'EVENT_CATEGORIES
import { EVENT_CATEGORIES } from '@mounaji_npm/event-core';
EVENT_CATEGORIES.SYSTEM // 'system'
EVENT_CATEGORIES.USER // 'user'
EVENT_CATEGORIES.MENU // 'menu'
EVENT_CATEGORIES.STOCK // 'stock'
EVENT_CATEGORIES.PRODUCTION // 'production'
EVENT_CATEGORIES.NOTIFICATION // 'notification'SEVERITY
import { SEVERITY } from '@mounaji_npm/event-core';
SEVERITY.INFO // 'info'
SEVERITY.SUCCESS // 'success'
SEVERITY.WARNING // 'warning'
SEVERITY.DANGER // 'danger'PRIORITY
Indicates how urgently an event should be handled or delivered. Used by createEvent and respected by the notification pipeline.
import { PRIORITY } from '@mounaji_npm/event-core';
PRIORITY.CRITICAL // 'critical' — requires immediate attention (e.g. stock.critical)
PRIORITY.HIGH // 'high' — important, surface prominently
PRIORITY.NORMAL // 'normal' — default
PRIORITY.LOW // 'low' — informational, can be batchedAuto-derive helpers
getCategoryForType and getSeverityForType check the EventRegistry first, then fall back to built-in prefix/suffix matching.
import { getCategoryForType, getSeverityForType } from '@mounaji_npm/event-core';
getCategoryForType('menu.created') // 'menu'
getCategoryForType('stock.critical') // 'stock'
getCategoryForType('notification.sent')// 'notification'
getCategoryForType('invoice.paid') // 'billing' — if registered via EventRegistry
getCategoryForType('unknown.xyz') // 'system' — fallback
getSeverityForType('stock.critical') // 'danger'
getSeverityForType('menu.created') // 'success'
getSeverityForType('menu.deleted') // 'warning'
getSeverityForType('system.startup') // 'info'createEvent — Validated event factory
Builds a validated event payload ready for persistence. Auto-derives category, severity, and priority from the type when not explicitly provided.
import { createEvent, EVENT_TYPES, PRIORITY } from '@mounaji_npm/event-core';
// Minimal — all optional fields are auto-derived
const event = createEvent({
type: EVENT_TYPES.STOCK_CRITICAL,
title: 'Harina 000 por debajo del mínimo',
});
// → { type: 'stock.critical', category: 'stock', severity: 'danger',
// priority: 'normal', source: null, target: null, title: '...', metadata: {} }
// Full — explicit overrides and routing fields
const event = createEvent({
type: EVENT_TYPES.USER_CREATED,
title: 'Nuevo usuario registrado',
priority: PRIORITY.HIGH,
source: 'organization-team', // originating module (for tracing)
target: 'admin-user-id', // null = broadcast to all users
metadata: { name: 'Ana López', role: 'editor', email: '[email protected]' },
});Parameters:
| Param | Required | Description |
|---|---|---|
| type | ✅ | EVENT_TYPES constant or a custom type registered via EventRegistry |
| title | ✅ | Short human-readable description shown in notification lists |
| category | — | Auto-derived from type prefix if omitted |
| severity | — | Auto-derived from type suffix if omitted |
| priority | — | PRIORITY constant; defaults to PRIORITY.NORMAL |
| source | — | Originating module/component name (useful for tracing) |
| target | — | User ID to target; null = broadcast to all |
| metadata | — | Arbitrary key-value data shown in the notification expand panel |
EventBus — Client-side pub/sub
Lightweight singleton pub/sub bus. All modules in the same JS context share the same instance. Use it to signal UI state changes immediately after a successful mutation, without waiting for the next poll cycle.
import { EventBus, EVENT_TYPES } from '@mounaji_npm/event-core';
// Subscribe — returns unsubscribe function
const off = EventBus.on(EVENT_TYPES.MENU_CREATED, (payload) => {
console.log('Nuevo menú:', payload);
});
off(); // unsubscribe
// Subscribe to ALL events (useful for logging / analytics)
EventBus.on('*', ({ type, payload }) => {
analytics.track(type, payload);
});
// Emit (call after a successful API mutation)
EventBus.emit(EVENT_TYPES.MENU_CREATED, { title: 'Almuerzo semana 17', count: 12 });once(type, cb) — Fire once, then auto-unsubscribe
EventBus.once(EVENT_TYPES.SYSTEM_STARTUP, () => {
console.log('App inicializada — solo se ejecuta una vez');
});addInterceptor(fn) — Middleware before subscribers
Interceptors receive { type, payload } before any subscriber fires.
Return false to cancel the event. Return a new value to transform the payload.
Returns a cleanup function.
// Analytics interceptor — observes all events without blocking them
const removeLogger = EventBus.addInterceptor(({ type, payload }) => {
analytics.track(type, payload);
// return nothing → event continues with original payload
});
// Guard interceptor — cancels events during maintenance mode
const removeGuard = EventBus.addInterceptor(({ type }) => {
if (maintenanceMode && type !== EVENT_TYPES.SYSTEM_STARTUP) return false;
});
// Payload transformer — inject a timestamp into every event
const removeTimestamper = EventBus.addInterceptor(({ payload }) => {
return { ...payload, _emittedAt: Date.now() };
});
// Cleanup
removeLogger();
removeGuard();
removeTimestamper();getListenerCount(type) — Debug subscriber counts
console.log(EventBus.getListenerCount(EVENT_TYPES.MENU_CREATED)); // 2API reference
| Method | Signature | Description |
|---|---|---|
| on | (type, cb) → off | Subscribe; returns unsubscribe fn |
| once | (type, cb) → off | Subscribe for a single fire |
| off | (type, cb) | Unsubscribe a specific callback |
| emit | (type, payload) | Run interceptors, then dispatch to subscribers and '*' listeners |
| addInterceptor | (fn) → remove | Add middleware before subscribers |
| getListenerCount | (type) → number | Count active subscribers for a type |
| clear | () | Remove all listeners and interceptors (useful in test teardown) |
Note:
EventBusis purely in-memory and client-side. For cross-tab or server→client reactivity, use the DOM bridge below.
EventRegistry — Extend the type system
Register custom event types for your application domain. Custom types integrate seamlessly with createEvent, getCategoryForType, and getSeverityForType — the registry is checked before built-in prefix matching.
Register at app startup (e.g. in your bootstrap hook or module initializer).
import { EventRegistry, SEVERITY, createEvent } from '@mounaji_npm/event-core';
// Register at startup
EventRegistry.register({ type: 'invoice.paid', category: 'billing', severity: 'success' });
EventRegistry.register({ type: 'invoice.overdue', category: 'billing', severity: 'danger' });
EventRegistry.register({ type: 'shift.started', category: 'hr', severity: 'info' });
// Now createEvent understands your custom types
const event = createEvent({
type: 'invoice.paid',
title: 'Factura #1042 cobrada',
metadata: { amount: '$1,200', client: 'Empresa XYZ' },
});
// → { type: 'invoice.paid', category: 'billing', severity: 'success', ... }
// getCategoryForType also picks it up
getCategoryForType('invoice.overdue') // 'billing'
getSeverityForType('invoice.overdue') // 'danger'API:
| Method | Description |
|---|---|
| register({ type, category, severity }) | Register a custom event type |
| getCategory(type) | Returns registered category, or null if not found |
| getSeverity(type) | Returns registered severity, or null if not found |
| has(type) | Check if a type is registered |
| getAll() | Returns a snapshot of all registered custom types |
| clear() | Remove all registrations (useful in test teardown) |
formatRelativeTime / formatAbsoluteTime
import { formatRelativeTime, formatAbsoluteTime } from '@mounaji_npm/event-core';
formatRelativeTime('2026-05-05T09:59:00Z') // 'ahora' (< 1 min)
formatRelativeTime('2026-05-05T09:30:00Z') // 'hace 29 min'
formatRelativeTime('2026-05-05T05:00:00Z') // 'hace 5h'
formatRelativeTime('2026-05-03T10:00:00Z') // 'hace 2d'
formatRelativeTime('2026-04-10T10:00:00Z') // '10 abr' (≥ 7 days)
formatAbsoluteTime('2026-05-05T09:55:00Z') // '05/05/2026, 09:55'| Age | formatRelativeTime output |
|---|---|
| < 1 minute | ahora |
| < 1 hour | hace N min |
| < 24 hours | hace Nh |
| < 7 days | hace Nd |
| ≥ 7 days | 10 abr (short date, es-AR locale) |
DOM Event Bridge
Connects server-side event emission to the client UI without polling.
Flow
Server mutation succeeds
→ API route adds response header: x-events-updated: 1
→ App API client detects header → calls signalEventsUpdate()
→ window.dispatchEvent(new CustomEvent('mn:events-updated'))
→ NotificationProvider re-fetches silently
→ Bell badge + list + toast updated ✅Constants
| Constant | Value | When to use |
|---|---|---|
| MN_EVENTS_UPDATED | 'mn:events-updated' | Server signals new events exist; trigger a re-fetch |
| MN_NOTIFICATION_TOAST | 'mn:notification-toast' | Show a toast for a specific notification immediately |
signalEventsUpdate(detail?)
Dispatches mn:events-updated on window. Safe in SSR — guards with typeof window !== 'undefined'.
import { signalEventsUpdate } from '@mounaji_npm/event-core';
// In your API client response interceptor
if (response.headers.get('x-events-updated') === '1') {
signalEventsUpdate();
}signalNotificationToast(notification)
Triggers a real-time toast for a specific notification object — skips the fetch cycle. Useful for optimistic notifications or client-side-only events.
import { signalNotificationToast, createEvent, EVENT_TYPES } from '@mounaji_npm/event-core';
const event = createEvent({
type: EVENT_TYPES.STOCK_LOW,
title: 'Stock bajo: Harina 000',
});
signalNotificationToast(event);Wire-up in your API client
// lib/apiClient.js (app-level — not part of any npm package)
import { signalEventsUpdate } from '@mounaji_npm/event-core';
async function request(path, options = {}) {
const res = await fetch(path, options);
if (res.headers.get('x-events-updated') === '1') {
signalEventsUpdate();
}
// ...
}Wire-up in your API route handlers
// app/api/menus/route.js (Next.js App Router)
export async function POST(req) {
const body = await req.json();
const data = await createMenu(body); // emitEvent() called internally
return Response.json(data, {
status: 201,
headers: { 'x-events-updated': '1' }, // ← signal the client
});
}Event shape (Supabase events table)
The shape created by createEvent and expected by @mounaji_npm/notifications components:
{
id: string; // UUID — set by DB on insert
type: string; // EVENT_TYPES constant or custom registered type
category: string; // EVENT_CATEGORIES constant
severity: string; // SEVERITY constant
priority: string; // PRIORITY constant
source: string | null; // originating module name
target: string | null; // userId or null (broadcast)
title: string;
metadata: Record<string, unknown>;
read: boolean;
created_at: string; // ISO 8601 — set by DB on insert
}Integration with @mounaji_npm/notifications
event-core is the foundation layer. The notifications package builds the full delivery stack on top of it. Both can coexist in the same app.
// event-core alone — signal layer
import { EventBus, createEvent, EVENT_TYPES, signalEventsUpdate } from '@mounaji_npm/event-core';
// Full notification stack — add this in your app root
import { NotificationProvider, WebChannel, EmailChannel } from '@mounaji_npm/notifications';
// signalEventsUpdate() still lives in event-core and is called by your API clientSee @mounaji_npm/notifications for the complete notification system.
Complete server-to-UI example
// 1. Server service — emit an event after mutation
import { createEvent, EVENT_TYPES } from '@mounaji_npm/event-core';
export async function createMenu(data) {
const result = await db.from('menus').insert(data).select();
await emitEvent(createEvent({
type: EVENT_TYPES.MENU_CREATED,
title: `Nuevo menú cargado: ${data.name}`,
source: 'menu-service',
metadata: { nombre: data.name, tipo: data.meal_type, fecha: data.date },
})).catch(() => {});
return result;
}
// 2. API route — signal the client
return Response.json(data, {
status: 201,
headers: { 'x-events-updated': '1' },
});
// 3. API client — detect header, dispatch DOM event
if (res.headers.get('x-events-updated') === '1') {
signalEventsUpdate();
}
// 4. UI — NotificationProvider picks up the DOM event,
// silently re-fetches, and updates bell + toast automatically.