FastEvent is a well-designed, powerful, type-safe, and thoroughly tested event emitter that provides robust event subscription and publishing mechanisms, suitable for both nodejs/browser environments.
npm install fastevent
yarn add fastevent
pnpm add fastevent
bun add fasteventFastEvent provides complete event emission and subscription functionality, with an API design inspired by eventemitter2.
import { FastEvent } from 'fastevent';
const events = new FastEvent();
// Basic event publishing
const results = events.emit('user/login', { id: 1 });
// Asynchronous event emission
const results = await events.emitAsync('data/process', { items: [...] });
// Event subscription
events.on('user/login', (message) => {
console.log('User login:', message.payload);
});
// One-time listener
events.once('startup', () => console.log('Application has started'));
// Listener with options
events.on('data/update', handler, {
count: 3, // Maximum trigger count
prepend: true, // Add to the beginning of the queue
filter: (msg) => msg.payload.important // Only process important updates
});
// Global listener
events.onAny((message) => {
console.log('Event occurred:', message.type);
});Listener functions receive a Message object that contains the following properties:
events.on("user/login", (message) => {
// {
// type: 'user/login', // Event name
// payload: { id: 1 }, // Event data
// meta: {...} // Event metadata
// }
});Retain the last event data, so subsequent subscribers can immediately receive the event value upon subscription:
const events = new FastEvent();
// Publish and retain event
events.emit("config/theme", { dark: true }, true);
// Equivalent to
events.emit("config/theme", { dark: true }, { retain: true });
// Subsequent subscribers immediately receive the retained value
events.on("config/theme", (message) => {
console.log("Theme:", message.payload); // Immediately outputs: { dark: true }
});FastEvent supports hierarchical event publishing and subscription.
- The default event hierarchy delimiter is
/, which can be modified viaoptions.delimiter - Two types of wildcards are supported when subscribing to events:
*matches a single path level,**matches multiple path levels (only used at the end of event names)
const events = new FastEvent();
// Match user/*/login
events.on("user/*/login", (message) => {
console.log("Any user type login:", message.payload);
});
// Match all events under user
events.on("user/**", (message) => {
console.log("All user-related events:", message.payload);
});
// Trigger events
events.emit("user/admin/login", { id: 1 }); // Both handlers will be called
events.emit("user/admin/profile/update", { name: "New" }); // Only the ** handler will be calledFastEvent provides multiple ways to remove listeners:
// Return a subscriber object to remove the listener, recommended approach
const subscriber = events.on("user/login", handler);
subscriber.off();
// Remove a specific listener
events.off(listener);
// Remove all listeners for a specific event
events.off("user/login");
// Remove a specific listener for a specific event
events.off("user/login", listener);
// Remove listeners using wildcard patterns
events.off("user/*");
// Remove all listeners
events.offAll();
// Remove all listeners under a specific prefix
events.offAll("user");Scopes allow you to handle events within a specific namespace.
Note that scopes share the same listener table with the parent event emitter:
const events = new FastEvent();
// Create a user-related scope
const userScope = events.scope("user");
// The following two approaches are equivalent:
userScope.on("login", handler);
events.on("user/login", handler);
// The following two approaches are also equivalent:
userScope.emit("login", data);
events.emit("user/login", data);
// Clear all listeners in the scope
userScope.offAll(); // Equivalent to events.offAll('user')Use waitFor to wait for a specific event to occur, with timeout support.
const events = new FastEvent();
async function waitForLogin() {
try {
// Wait for login event with a 5-second timeout
const userData = await events.waitFor("user/login", 5000);
console.log("User logged in:", userData);
} catch (error) {
console.log("Login wait timeout");
}
}
waitForLogin();
// Later trigger the login event
events.emit("user/login", { id: 1, name: "Alice" });Write an asynchronous event iterator called FastEventIterator that returns events in the form of for await (const messages of emitter. when no valid listener function is specified when using on/onAy.
import { FastEvent } from "@fastevent/core";
const emitter = new FastEvent();
emitter.emit("user/login", { userId: 123 });
for await (const message of emitter.on("user/login")) {
console.log(message.payload);
}use using for automatic cleanup when leaving scope.
const emitter = new FastEvent();
const events: string[] = [];
{
using subscriber = emitter.on("test", ({ type }) => {
events.push(type);
});
emitter.emit("test");
// has a subscriber
expect(emitter.getListeners("test").length).toBe(1);
}
emitter.emit("test");
expect(events).toEqual(["test"]);
expect(events.length).toBe(1);
// subscribe is cancel
expect(emitter.getListeners("test").length).toBe(0);FastEvent provides multiple hook functions for operations at different stages of the event emitter lifecycle.
const otherEvents = new FastEvent();
const events = new FastEvent({
// Called when a new listener is added
onAddListener: (type, listener, options) => {
console.log("Added new listener:", type);
// Return false to prevent the listener from being added
return false;
// Can directly return a FastEventSubscriber
// For example: transfer events starting with `@` to another FastEvent
if (type.startsWith("@")) {
return otherEvents.on(type, listener, options);
}
},
// Called when a listener is removed
onRemoveListener: (type, listener) => {
console.log("Removed listener:", type);
},
// Called when listeners are cleared
onClearListeners: () => {
console.log("All listeners cleared");
},
// Called when a listener throws an error
onListenerError: (error, listener, message, args) => {
console.error(`Error in listener for event ${message.type}:`, error);
},
// Called before a listener executes
onBeforeExecuteListener: (message, args) => {
console.log("Before executing event listener");
// Return false to prevent listener execution
return false;
// Forward events to another FastEvent
// For example: forward events starting with `@` to another FastEvent
if (type.startsWith("@")) {
return otherEvents.emit(message.type);
}
},
// Called after a listener executes
onAfterExecuteListener: (message, returns, listeners) => {
console.log("After executing event listener");
// Can intercept and modify return values here
},
});By default, all listeners are executed in parallel when an event is triggered.
FastEvent provides powerful listener execution mechanisms that allow developers to control how listeners are executed.
import { race } from "fastevent/executors";
const events = new FastEvent({
executor: race(),
});
events.on("task/start", async () => {
/* Time-consuming operation 1 */
});
events.on("task/start", async () => {
/* Time-consuming operation 2 */
});
// The two listeners will execute in parallel, returning the fastest result
await events.emitAsync("task/start");Built-in Support:
| Executor | Description |
|---|---|
parallel |
Default, concurrent execution |
race |
Parallel executor, uses Promise.race for parallel execution |
balance |
Evenly distributed executor |
first |
Execute only the first listener |
last |
Execute only the last listener |
random |
Randomly select a listener |
series |
Serial executor, execute listeners in sequence and return the last result |
waterfall |
Execute listeners in sequence and return the last result, abort on error |
(listeners,message,args,execute)=>any[] |
Custom executor |
Listener pipes are used to wrap listener functions during event subscription to implement various common advanced features.
import { queue } from "fastevent/pipes";
const events = new FastEvent();
// default queue size is 10
events.on(
"data/update",
(data) => {
console.log("Processing data:", data);
},
{
pipes: [queue({ size: 10 })],
},
);Built-in Support:
| Pipe | Description |
|---|---|
queue |
Queue listener, process messages in queue, supports priority and timeout control |
throttle |
Throttle listener |
debounce |
Debounce listener |
timeout |
Timeout listener |
retry |
Retry listener, for controlling retries after listener execution failure |
memorize |
Cache listener, cache listener execution results |
FastEvent can elegantly forward publishing and subscription to another FastEvent instance.
import { expandable } from "fastevent";
const otherEmitter = new FastEvent();
const emitter = new FastEvent({
onAddListener: (type, listener, options) => {
// Subscription forwarding rule: when event name starts with `@/`, forward subscription to another `FastEvent` instance
if (type.startsWith("@/")) {
return otherEmitter.on(type.substring(2), listener, options);
}
},
onBeforeExecuteListener: (message, args) => {
// Event forwarding rule: when event name starts with `@/`, publish to another `FastEvent` instance
if (message.type.startsWith("@/")) {
message.type = message.type.substring(2);
return expandable(otherEmitter.emit(message, args));
}
},
});
const events: any[] = [];
otherEmitter.on("data", ({ payload }) => {
events.push(payload);
});
// Subscribe to otherEmitter's data event
emitter.on("@/data", ({ payload }) => {
expect(payload).toBe(1);
events.push(payload);
});
// Publish data event to otherEmitter
const subscriber = emitter.emit("@/data", 1);
subscriber.off();Metadata is a mechanism for providing additional contextual information for events.
You can set metadata at different levels: global, scope level, or event-specific level.
const events = new FastEvent({
meta: {
version: "1.0",
environment: "production",
},
});
events.on("user/login", (message) => {
console.log("Event data:", message.payload);
console.log("Metadata:", message.meta); // Includes type, version, and environment
});
// Using scope-level metadata
const userScope = events.scope("user", {
meta: { domain: "user" },
});
// Add specific metadata when publishing events
userScope.emit(
"login",
{ userId: "123" },
{
meta: { timestamp: Date.now() }, // Event-specific metadata
},
);
// Listeners receive merged metadata
userScope.on("login", (message) => {
console.log("Metadata:", message.meta);
});FastEvent has complete TypeScript type support.
// Define events with different payload types
interface ComplexEvents {
"data/number": number;
"data/string": string;
"data/object": { value: any };
}
const events = new FastEvent<ComplexEvents>();
// TypeScript ensures type safety for each event
events.on("data/number", (message) => {
const sum = message.payload + 1; // payload type is number
});
// All event emissions are type-checked
events.emit("data/number", 42);
events.emit("data/string", "hello");
events.emit("data/object", { value: true });By default, the FastEvent listener receives messages in the format of FastEventMessage, which includes three fields: type, payload, and optional meta.
However, FastEvent also provides customization capabilities, allowing each event to receive different messages with corresponding type prompts.
import { FastEvent, FastEventOptions, NotPayload } from "fastevent";
// {<event type>:<payload>}
type CustomEvents = {
// NotPayload is used to indicate that it is not a payload, but a complete message body
click: NotPayload<{ x: number; y: number }>;
"div/mousemove": boolean;
"div/scroll": number;
"div/focus": string;
};
const emitter = new FastEvent<CustomEvents>({
//Convert standard FastEventMessage to the format you need
transform: (message: FastEventMessage) => {
if (["div/click", "div/mousemove"].includes(message.type)) {
return message.payload;
}
return message;
},
});
emitter.on("click", (message) => {
// typeof message === { x: number; y: number } ✅
});NotPayload is only used to identify some events, and Transformed can also be used to message all events.
import { FastEvent, FastEventOptions, TransformedEvents } from "fastevent";
// {<event type>:<message>}
type CustomEvents = TransformedEvents<{
click: { x: number; y: number };
"div/mousemove": boolean;
"div/scroll": number;
"div/focus": string;
}>;transformis used to convert standard FastEventMessage into the format you needNotPayloadandTransformonEventsare used to declare types, in order to provide type declarations for listeners whenon/once.
fastevent-viewer component to debuging.
FastEvent has been thoroughly unit tested, with over 320+ cumulative test cases and 99%+ test coverage.
MIT
For more detailed documentation, see WebSite
