@quazardous/quarkernel
v2.4.0
Published
Micro Custom Events Kernel with dependency ordering, shared context, and composite events
Maintainers
Readme
QuarKernel
Event orchestration: dependency ordering, shared context and composite events.
TypeScript-first. Zero dependencies. ~4.8 kB gzipped for the kernel, state machines in a separate entry (sizes).
Why QuarKernel?
// Other event libs: fire and pray
emitter.emit('user:login', data);
// QuarKernel: orchestrate with confidence
qk.on('user:login', fetchUser, { id: 'fetch' });
qk.on('user:login', logAnalytics, { after: ['fetch'] }); // Guaranteed order
qk.on('user:login', (e) => greet(e.context.user)); // Shared contextWhat makes it different:
mitt and eventemitter3 are simple synchronous event buses, and much smaller. emittery adds async listeners. QuarKernel is for when listeners must be orchestrated: run in a given order, share data, and react to combinations of events.
| Feature | mitt | eventemitter3 | emittery | QuarKernel |
|---------|:----:|:-------------:|:--------:|:--------------:|
| Awaitable emit() (async listeners) | - | - | Yes | Yes |
| Serial emit | - | - | Yes | Yes |
| Promise-based once() | - | - | Yes | Yes |
| Catch-all listener | Yes (*) | - | Yes (onAny) | Yes |
| Wildcard patterns (user:*) | - | - | - | Yes |
| Listener priority | - | - | - | Yes |
| Dependency ordering (after) | - | - | - | Yes |
| Shared context per emit | - | - | - | Yes |
| Composite events | - | - | - | Yes |
| State machines | - | - | - | Yes (/fsm) |
| TypeScript types | Yes | Yes | Yes | Yes |
| Size (min + gzip) | 0.2 kB | 1.3 kB | 2.2 kB | 4.8 kB (createKernel) |
Sizes measured with esbuild (minified ESM) + gzip: mitt 3.0.1, eventemitter3 5.0.4, emittery 1.2.0, QuarKernel 2.4.0.
Install
npm install @quazardous/quarkernel<!-- CDN -->
<script src="https://unpkg.com/@quazardous/quarkernel@2/dist/index.umd.js"></script>Requirements
- Node.js 18+, or any modern browser (the code uses ES2022 syntax)
- ES modules (
import), CommonJS (require), and aQuarKernelglobal from the UMD build for<script>tags - TypeScript types included
- Release notes in the CHANGELOG
Size
Minified + gzipped, measured with esbuild on 2.4.0. The publish workflow enforces a budget per entry point (npm run size --workspace=packages/quarkernel).
| Import | min + gzip |
|--------|-----------:|
| @quazardous/quarkernel, createKernel only | 4.8 kB |
| @quazardous/quarkernel, whole entry | 7.6 kB |
| @quazardous/quarkernel/fsm | 6.1 kB (includes the kernel, shared with the core entry) |
| @quazardous/quarkernel/xstate | 1.1 kB |
The ES module entries share a single copy of the kernel, so importing both the core and /fsm adds nothing on top of /fsm. The CommonJS builds (require) are self-contained: requiring both loads the kernel twice.
Latest published version: core,
fsm
Quick Start
import { createKernel } from '@quazardous/quarkernel';
const qk = createKernel();
// 1. Dependency ordering - control execution sequence
qk.on('checkout', async (e) => {
e.context.inventory = await checkStock(e.data.items);
}, { id: 'stock' });
qk.on('checkout', async (e) => {
// Runs AFTER stock check - guaranteed
await processPayment(e.data.card, e.context.inventory);
}, { after: ['stock'] });
await qk.emit('checkout', { items: ['sku-123'], card: 'tok_visa' });// 2. Composite events - react to event combinations
const checkout = qk.when(['cart:ready', 'payment:confirmed']);
checkout.onComposed(() => {
console.log('Both events fired - proceed to shipping!');
});
// Promise form: await checkout.once(), then checkout.dispose() when done
// Across several kernels: new Composition([[kernelA, 'a'], [kernelB, 'b']])// 3. Wildcards - catch event patterns
qk.on('user:*', (e) => console.log('User action:', e.name));
// Matches: user:login, user:logout, user:signup...Semantics
Execution order. emit() runs listeners level by level. Listeners without after form the first level; any other listener runs in the level right after its deepest dependency. Listeners of the same level run in parallel and start in priority order (highest first). Each level waits until the previous one has finished, including async work. emitSerial() follows the same order, one listener at a time.
Context. Every emit creates a fresh event.context object. All listeners of that emit share it by reference (it is not cloned), and no other emit sees it, so concurrent emits of the same event never mix their context.
Errors. By default (errorBoundary: true), a failing listener is reported to onError (console.error unless you pass your own) and the other listeners keep running, including the ones that depend on it; emit() resolves with the errors of that emit (also available to listeners as event.errors). With errorBoundary: false, emit() still runs every listener and then rejects with an AggregateError, while emitSerial() stops at the first failure and rejects with that error. Anything a listener wrote to context before throwing stays visible to later listeners. A missing or cyclic after dependency always rejects the emit.
const qk = createKernel({
errorBoundary: true, // default
onError: (error, event) => report(error, event.name),
});
const errors = await qk.emit('import:run', file); // [] when every listener succeeded
errors.forEach(({ listenerId, error }) => console.warn(listenerId, error.message));Concurrency. Emits are not queued: overlapping emits run their listeners interleaved, each with its own context. An emit keeps the listeners it started with; listeners added or removed during an emit only take effect on the next one. event.stopPropagation() skips the listeners that have not started yet.
Cleanup
const off = qk.on('user:login', handler);
off(); // remove this listener
qk.off('user:login', handler); // same, by reference
qk.off('user:login'); // every listener of an event
qk.offAll(); // every listener
// Remove several listeners at once with an AbortSignal
const controller = new AbortController();
qk.on('tick', onTick, { signal: controller.signal });
qk.on('tock', onTock, { signal: controller.signal });
controller.abort();
// From inside a listener
qk.on('job:progress', (e, ctx) => {
if (e.data.done) ctx.off();
});
// Run only once
qk.on('app:ready', init, { once: true });Promises
import { createMachine } from '@quazardous/quarkernel/fsm';
// Next occurrence of an event (rejects after the timeout)
const login = await qk.once('user:login', { timeout: 5000 });
console.log(login.data);
// A combination of events
const ready = qk.when(['user:ready', 'config:ready']);
const { data } = await ready.once({ timeout: 5000 });
ready.dispose();
// A state machine reaching a state
const order = createMachine({ /* ... */ });
const { from, context } = await order.waitFor('confirmed', { timeout: 10000 });State Machines (optional)
Need explicit states on top of events? The optional @quazardous/quarkernel/fsm entry adds finite state machines built on the kernel, using an XState-compatible config. It is a separate import: it adds nothing to your bundle unless you use it.
import { createMachine } from '@quazardous/quarkernel/fsm';
const order = createMachine({
id: 'order',
initial: 'draft',
states: {
draft: { on: { SUBMIT: 'pending' } },
pending: { on: { APPROVE: 'confirmed', REJECT: 'draft' } },
confirmed: {}
}
});
await order.send('SUBMIT');
console.log(order.state); // 'pending'Behavior helpers, auto-timers and XState import/export are covered in Async Patterns and Advanced Patterns. Try machines visually in FSM Studio.
Framework Adapters
Official bindings with auto-cleanup on unmount:
| Package | Framework | Docs |
|---------|-----------|------|
| @quazardous/quarkernel-vue | Vue 3 | README |
| @quazardous/quarkernel-react | React 18+ | README |
| @quazardous/quarkernel-svelte | Svelte 5 | README |
# Vue
npm install @quazardous/quarkernel @quazardous/quarkernel-vue
# React
npm install @quazardous/quarkernel @quazardous/quarkernel-react
# Svelte
npm install @quazardous/quarkernel @quazardous/quarkernel-svelteDocumentation
Guides:
- Getting Started - Installation, first event, basic usage
- API Reference - Complete API documentation
- Advanced Patterns - Multi-machine, composition, sagas
- Async Integration - QK + FSM + Promises synergies
- Migration v1 to v2 - Upgrade guide
Packages:
- Core package - Main QuarKernel library
Resources:
- Benchmarks - Performance comparisons
- Demos - Live examples
- Changelog - Release notes
- Contributing - Setup, tests and release process
Use Cases
Request pipeline - Auth, validate, transform, respond in guaranteed order
Game events - Combo detection with composite events
Form wizards - Step dependencies with shared validation context
Order workflows - State machines for order lifecycle (draft → pending → confirmed → shipped)
Analytics - Wildcard listeners for all track:* events
Microservices - Event choreography with dependency graphs
UI flows - FSM-driven modals, wizards, and multi-step forms
License
MIT - Made by quazardous
