# @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

```bash
npm install @mounaji_npm/event-core
```

No peer dependencies required.

---

## Exports

```js
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:

```js
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`

```js
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`

```js
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.

```js
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 batched
```

### Auto-derive helpers

`getCategoryForType` and `getSeverityForType` check the `EventRegistry` first, then fall back to built-in prefix/suffix matching.

```js
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.

```js
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: 'ana@domain.com' },
});
```

**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.

```js
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

```js
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.

```js
// 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

```js
console.log(EventBus.getListenerCount(EVENT_TYPES.MENU_CREATED)); // 2
```

### API 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:** `EventBus` is 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).

```js
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`

```js
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'`.

```js
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.

```js
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

```js
// 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

```js
// 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:

```ts
{
  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.

```js
// 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 client
```

See [`@mounaji_npm/notifications`](../notifications/README.md) for the complete notification system.

---

## Complete server-to-UI example

```js
// 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.
```
