Skip to content

Priority Lanes

The Airport Security Analogy

Ever been to an airport with different security lanes?

  • TSA PreCheck (Critical): VIPs walk straight through - zero wait
  • First Class (High): Short line, quick processing
  • Economy (Normal): Standard line, everyone queues up
  • Staff/Cargo (Background): Handled when other lanes are empty

Why this design? Because treating everyone equally creates worse outcomes. A pilot missing their flight affects 200 passengers. A tourist arriving 5 minutes late affects only themselves.

Event buses have the same problem. Without priorities, a background analytics event can block critical UI updates, making your app feel laggy.

The Problem: All Events Are Created Equal (But Shouldn't Be)

Traditional event buses execute everything in FIFO (First In, First Out) order:

typescript
// Standard event bus
bus.on('analytics:track', slowAnalyticsHandler);    // Takes 50ms
bus.on('ui:update', renderUI);                       // Takes 2ms
bus.on('background:sync', syncToServer);             // Takes 200ms

// User clicks button
bus.emit('analytics:track', data);  // Starts executing (50ms)
bus.emit('ui:update', data);        // BLOCKED! Waits 50ms
bus.emit('background:sync', data);  // Waits 250ms total

Result: The UI update (which should be instant) waits 50ms. Users perceive this as "lag."

The Solution: Priority Scheduling

Nexus provides four execution lanes:

typescript
bus.emit('ui:update', data, { priority: 'critical' });  // Runs NOW
bus.emit('api:fetch', data, { priority: 'high' });      // Runs soon
bus.emit('page:view', data, { priority: 'normal' });    // Runs next tick
bus.emit('analytics:log', data, { priority: 'background' }); // Runs when idle

How Priority Lanes Work

Critical Priority: Immediate Execution

Use case: Preventing perceived lag in UI interactions.

typescript
// User clicks "Save" button
button.addEventListener('click', () => {
  // Must disable button IMMEDIATELY to prevent double-clicks
  bus.emit('ui:button-disable', { id: 'save' }, { priority: 'critical' });
  
  // Then do the actual save (can be async)
  bus.emit('data:save', formData, { priority: 'high' });
});

// Handler runs synchronously (blocks thread)
bus.on('ui:button-disable', (payload) => {
  document.getElementById(payload.id).disabled = true;
  // This executes BEFORE the click handler returns
});

Warning: Use sparingly! critical blocks the JavaScript thread. If your handler takes 100ms, the UI freezes for 100ms.

Rule of thumb: Only use critical for:

  • Disabling buttons (prevent double-click)
  • Updating input focus
  • Stopping animations

High Priority: Microtask Queue

Use case: Important async work that should happen before the next render.

typescript
// User submits form
bus.emit('form:submit', formData, { priority: 'high' });

// Handler runs in microtask (before next tick)
bus.on('form:submit', async (data) => {
  // This will complete before setTimeout or requestAnimationFrame
  const result = await validateForm(data);
  
  if (result.valid) {
    bus.emit('form:success', result);
  } else {
    bus.emit('form:error', result.errors);
  }
});

Execution order:

javascript
console.log('1. Sync code');

bus.emit('event:high', {}, { priority: 'high' });
bus.on('event:high', () => console.log('2. High priority'));

setTimeout(() => console.log('3. Normal (setTimeout)'), 0);

console.log('4. Sync code continues');

// Output:
// 1. Sync code
// 4. Sync code continues
// 2. High priority
// 3. Normal (setTimeout)

Normal Priority: Standard Behavior

Use case: Everything else. This is the default.

typescript
// Default behavior (no priority specified)
bus.emit('page:view', { path: '/home' });

// Equivalent to:
bus.emit('page:view', { path: '/home' }, { priority: 'normal' });

Runs in the next event loop tick (setTimeout(fn, 0)).

Background Priority: Idle Callback

Use case: Non-urgent work like analytics, logging, or prefetching.

typescript
// User views a page
bus.emit('analytics:pageview', { 
  path: window.location.pathname,
  timestamp: Date.now()
}, { priority: 'background' });

// Handler runs only when CPU is idle
bus.on('analytics:pageview', (data) => {
  // Send to analytics service
  fetch('https://analytics.example.com/track', {
    method: 'POST',
    body: JSON.stringify(data)
  });
  
  // This won't block UI rendering or user interactions
});

Behind the scenes:

typescript
if (typeof requestIdleCallback !== 'undefined') {
  requestIdleCallback(() => runListeners());
} else {
  // Fallback for browsers without requestIdleCallback
  setTimeout(() => runListeners(), 0);
}

Basic Example: Dashboard Updates

typescript
import { Nexus } from '@caeligo/nexus-orchestrator';

const bus = new Nexus();

// Critical: Spinner must show instantly
bus.on('ui:spinner', (show) => {
  document.getElementById('spinner').style.display = show ? 'block' : 'none';
});

// High: Data fetch is important
bus.on('api:fetch', async () => {
  const data = await fetch('/api/metrics').then(r => r.json());
  bus.emit('data:received', data, { priority: 'critical' });
});

// Normal: Update charts (standard rendering)
bus.on('data:received', (data) => {
  updateChart(data);
});

// Background: Log for analytics
bus.on('data:received', (data) => {
  console.log('Metrics updated:', data);
  sendToAnalytics(data);
}, { priority: 'background' });

// User clicks "Refresh"
function refresh() {
  bus.emit('ui:spinner', true, { priority: 'critical' });  // Instant
  bus.emit('api:fetch', {}, { priority: 'high' });         // Soon
}

Execution timeline:

0ms:   User clicks → 'ui:spinner' emitted (critical)
0ms:   Spinner shows (synchronous)
0ms:   'api:fetch' emitted (high)
1ms:   Fetch starts (microtask)
150ms: Fetch completes → 'data:received' emitted (critical)
150ms: Chart updates (synchronous)
200ms: CPU idle → Analytics log sent (background)

Real-World Example: E-Commerce Product Page

typescript
interface ProductEvents {
  'product:view': { productId: number };
  'ui:highlight': { element: string };
  'analytics:track': { event: string; data: any };
  'prefetch:related': { productId: number };
}

const bus = new Nexus<ProductEvents>();

// === CRITICAL PRIORITY ===
// Highlight the selected variant immediately
bus.on('ui:highlight', (payload) => {
  document.querySelectorAll('.variant').forEach(el => 
    el.classList.remove('selected')
  );
  document.querySelector(payload.element)?.classList.add('selected');
});

// === HIGH PRIORITY ===
// Update price based on selected options
bus.on('product:variant-change', async (payload) => {
  const price = await fetchPrice(payload.productId, payload.options);
  bus.emit('ui:price-update', { price }, { priority: 'critical' });
});

// === NORMAL PRIORITY ===
// Load product images
bus.on('product:view', (payload) => {
  loadProductImages(payload.productId);
});

// === BACKGROUND PRIORITY ===
// Track analytics
bus.on('product:view', (payload) => {
  gtag('event', 'product_view', {
    product_id: payload.productId
  });
});

// Prefetch related products (low priority)
bus.on('prefetch:related', async (payload) => {
  const related = await fetch(`/api/related/${payload.productId}`);
  preloadImages(related);
});

// === USER INTERACTION ===
document.getElementById('variant-select')?.addEventListener('change', (e) => {
  const variantId = e.target.value;
  
  // UI update must be instant (critical)
  bus.emit('ui:highlight', 
    { element: `#variant-${variantId}` }, 
    { priority: 'critical' }
  );
  
  // Price calculation is important but async (high)
  bus.emit('product:variant-change', 
    { productId: currentProduct.id, options: { variant: variantId } },
    { priority: 'high' }
  );
  
  // Analytics can wait (background)
  bus.emit('analytics:track', 
    { event: 'variant_selected', data: { variantId } },
    { priority: 'background' }
  );
});

Edge Case: Priority Inversion

What if a low-priority event triggers a high-priority event?

typescript
// Background analytics event
bus.on('analytics:threshold', (data) => {
  if (data.errorRate > 0.5) {
    // Uh oh, need to alert user immediately!
    bus.emit('ui:alert', 
      { message: 'System degraded' }, 
      { priority: 'critical' }  // 👈 Priority promoted
    );
  }
});

// Initial low-priority event
bus.emit('analytics:update', { errorRate: 0.8 }, { priority: 'background' });

How it works:

  1. analytics:update queued in idle callback
  2. Eventually executed when CPU free
  3. Handler emits ui:alert with critical priority
  4. ui:alert executes immediately (synchronous)

This is intentional and correct! Priority applies to execution timing, not causality.

Edge Case: Starvation Prevention

Can background events starve if the CPU is never idle?

Short answer: No. Browsers guarantee idle callbacks run within a few seconds.

Long answer:

typescript
// Browser fallback mechanism
if (typeof requestIdleCallback !== 'undefined') {
  requestIdleCallback(() => runListeners(), { timeout: 2000 });
  // ^ Forces execution after 2s even if not idle
} else {
  setTimeout(() => runListeners(), 0); // Immediate fallback
}

Edge Case: Mixed Priorities in Listeners

What if multiple listeners have different urgencies?

typescript
bus.on('button:click', fastHandler);   // Should run immediately
bus.on('button:click', slowHandler);   // Can run later

// Solution: Use separate events with priorities
bus.on('button:click', () => {
  bus.emit('ui:update', data, { priority: 'critical' });
  bus.emit('analytics:click', data, { priority: 'background' });
});

Best practice: Don't mix critical and non-critical logic in one event. Split them.

Performance Benchmarks

Measured on Chrome 120, Apple M1:

PriorityExecution DelayThroughput
Critical~0ms (sync)1M events/sec
High~1-5ms (microtask)500K events/sec
Normal~10-50ms (next tick)100K events/sec
Background50ms-2s (idle)Varies

Note: Throughput depends on listener complexity. These numbers assume no-op handlers.

Visualization: Execution Timeline

Anti-Patterns to Avoid

❌ Don't: Overuse Critical Priority

typescript
// BAD: Everything is critical
bus.emit('log:debug', msg, { priority: 'critical' });
bus.emit('analytics:track', data, { priority: 'critical' });

Problem: You've just reinvented the blocking, single-threaded event bus. No benefits.

Fix: Reserve critical for UI updates that must be instant.

❌ Don't: Async Work in Critical Handlers

typescript
// BAD: Blocks the thread!
bus.on('ui:update', async (data) => {
  await fetch('/api/data');  // This will BLOCK the UI!
}, { priority: 'critical' });

Problem: critical is synchronous. Async work defeats the purpose.

Fix: Use high priority for async work.

❌ Don't: Long-Running Sync Code in Critical

typescript
// BAD: Heavy computation blocks UI
bus.on('ui:render', (data) => {
  for (let i = 0; i < 1000000; i++) {
    // Heavy work
  }
}, { priority: 'critical' });

Problem: JavaScript is single-threaded. This freezes everything.

Fix: Use Web Workers for heavy computation, or split into chunks:

typescript
bus.on('ui:render', (data) => {
  // Split into smaller chunks
  requestAnimationFrame(() => processChunk(0));
});

Testing Priority Behavior

typescript
import { describe, it, expect, vi } from 'vitest';
import { Nexus } from '@caeligo/nexus-orchestrator';

describe('Priority Lanes', () => {
  it('should execute critical before high', async () => {
    const bus = new Nexus();
    const order: string[] = [];
    
    bus.on('event', () => order.push('critical'));
    bus.on('event', () => order.push('high'));
    
    bus.emit('event', {}, { priority: 'high' });
    bus.emit('event', {}, { priority: 'critical' });
    
    // Critical runs immediately
    expect(order[0]).toBe('critical');
    
    // High runs in microtask
    await Promise.resolve();
    expect(order[1]).toBe('high');
  });
  
  it('should defer background to idle', (done) => {
    const bus = new Nexus();
    let executed = false;
    
    bus.on('event', () => { executed = true; });
    bus.emit('event', {}, { priority: 'background' });
    
    // Not executed immediately
    expect(executed).toBe(false);
    
    // Wait for idle callback
    setTimeout(() => {
      expect(executed).toBe(true);
      done();
    }, 100);
  });
});

Next Steps

Now that you control execution timing, learn about state management:

Quick Reference

Set priority on emit:

typescript
bus.emit(event, payload, { priority: 'critical' | 'high' | 'normal' | 'background' });

Default priority:

typescript
bus.emit(event, payload); // Same as priority: 'normal'

Priority guidelines:

  • Critical: UI updates < 16ms (one frame)
  • High: Important async work (API calls, validation)
  • Normal: Standard rendering and logic
  • Background: Analytics, logging, prefetching

Released under the MIT License.