Skip to content
/ evt-bus Public

Event bus with powerful features: type inferring, async emitter, propagation stop, handler sorting, group off, etc.

License

Notifications You must be signed in to change notification settings

haelue/evt-bus

Repository files navigation

language: English | 中文

evt-bus

Evt-Bus · npm

An event bus tool with powerful features:

  • 🔑 Type inferring: support typescript, you can use the event bus like calling functions with type inferring
  • 📦 Async emitter: define async handler, and emit using Promise OR async-await
  • 🧡 Propagation stop: stop event propagation in handler-loop
  • 🔌 Handlers sorting: sort the handlers with order-index
  • ⛰️ Handlers group off: subscribe events with group-id, and unsubscribe them by group
  • ⚙️ Subscribe repeating: optional "allow/avoid repeat" subscribe

Table of Contents

Quick Start

Step1: This project uses node and npm to install.

$ npm install --save @haelue/evt-bus

Step2: Insert types in Global-Declare-File: src/global.d.ts (create one if non-exist)

/** file: src/global.d.ts */

declare type EvtGroupName = import("@haelue/evt-bus").EvtGroupName;
declare type EvtOrder = import("@haelue/evt-bus").EvtOrder;
declare type EvtRepeatable = import("@haelue/evt-bus").EvtRepeatable;
declare type EvtMessage = import("@haelue/evt-bus").EvtMessage;

Step3: Create a Channel-File: src/events/index.ts (OR anywhere)

/** file: src/events/index.ts */

import {
  EvtChannel,
  EvtChannelName,
  EvtOnMethod,
  EvtOffMethod,
  EvtOffAllMethod,
  EvtOnCountMethod,
  EvtEmitMethod,
  EvtEmitAsyncMethod,
  EvtWithExceptionHandlerMethod,
  EvtChannelOptions,
  evtDefaultOptions,
} from "@haelue/evt-bus";

interface UserEvtChannel {
  /** Add event-listenning */
  on: EvtOnMethod & EvtOnOffDictionary;

  /** Remove event-listenning of same handler(if defined) & same group(if defined) & same order(if defined) */
  off: EvtOffMethod & EvtOnOffDictionary;

  /** Remove all event-listenning of same group(if defined) & same order(if defined) */
  offAll: EvtOffAllMethod;

  /** Emit event */
  emit: EvtEmitMethod & EvtEmitDictionary;

  /** Emit event (async) */
  emitAsync: EvtEmitAsyncMethod & EvtEmitAsyncDictionary;

  /** Count of event-listenning */
  onCount: EvtOnCountMethod & EvtOnCountDictionary;

  /** Set exception-handler once for next emit */
  withExceptionHandler: EvtWithExceptionHandlerMethod<
    EvtEmitDictionary,
    EvtEmitAsyncDictionary
  >;
}

export const channelCached: Record<EvtChannelName, any> = {};

/** Get an event-channel of options (caching). */
export default function evt(
  options?: Partial<EvtChannelOptions>,
): UserEvtChannel {
  const name = options?.name ?? evtDefaultOptions.name;
  return (channelCached[name] ??= new EvtChannel(options));
}

Tip: It can both use in Browser.js OR Node.js; With both esm, cjs OR umd.

Step4: Create an Event-Declare-File: src/events/evt.d.ts (OR anywhere, but ends with evt.d.ts / emit.d.ts)

/** file: src/events/evt.d.ts */

declare interface EvtEmitDictionary {
  /** example emitter with type infer */
  fooTrigger(bar: { id: string; score: number; time: Date }): boolean;
}

Tip: Return type must be: boolean.

Step5: Run command below: (recommand insert into package.json)

$ npx evt-autogen

Other declares will generate in your Event-Declare-File (Step4). (see what happens in src/events/evt.d.ts)

Step6: Use the event "fooTrigger":

/** file: path/to/your/code.ts */

import evt from "src/events";
const { emit, on, off } = evt();

on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received! ${JSON.stringify(bar)}`);
});

emit.fooTrigger({ id: "foo", score: 1, time: new Date() });

// console print:
// event received! {"id":"foo","score":1,"time":"2024-11-01T06:48:50.865Z"}

off.fooTrigger();

emit.fooTrigger({ id: "foo2", score: 2, time: new Date() });

// console print:
// [nothing as expected]

Tip: evt() provides the same cached instance in different file.

Usage

The usage mode of evt-bus can be very flexible.

You can create many Event-Declare-File (Step4) in different modules, evt-autogen scan path ./src recursive.

You can use -p to change scan path.

$ npx evt-autogen -P [path/to/declare/files/root]
# "-p -path -PATH" is also ok

You can use export interface instead of declare interface in Event-Declare-File (Step4), and make the filename ends with evt.ts / emit.ts (without ".d."), and import symbols in Channel-File (Step3).

The first "e" argument in on-handler is like: { message: "fooTrigger", cancel: false }, you can use e.cancel = true to stop event propagation. But if you don't need this feature, and hate the first "e" argument, directions below would help:

/** file: src/events/index.ts */

import {
--- EvtChannel,
+++ EvtChannelSimple as EvtChannel,
...
} from "@haelue/evt-bus"
$ npx evt-autogen -S
# "-s -simple -SIMPLE" is also ok

Features

Channel options

import evt from "src/events";
const { emit, on, off } = evt({
  // each option below is ommitable
  name: "channel-1", // default "#"
  defaultGroup: "group-1", // default "*"
  defaultOrder: -1, // default 0
  defaultRepeatable: true, // default false
  defaultExceptionHandler: console.log, // default console.error
});

Async emitter

import evt from "src/events";
const { emitAsync, on } = evt();

on.fooTrigger(async (e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received! ${JSON.stringify(bar)}`);
});

await emitAsync.fooTrigger({ id: "foo", score: 1, time: new Date() });

Propagation stop & return

import evt from "src/events";
const { emit, on } = evt();

let changeFlag = false;
on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  e.cancel = true;
});
on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  changeFlag = true;
});

const result = emit.fooTrigger({ id: "foo", score: 1, time: new Date() });
console.log(`result: ${result}, flag changed: ${changeFlag}`);

// console print:
// result: false, flag changed: false

Handlers sorting

The default sort-order is 0, you can change it in channel-building, OR in on-method.

import evt from "src/events";
const { emit, on } = evt({ defaultOrder: 2 });

on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received: Order 2 (default)}`);
});
on.fooTrigger(
  (e, bar: { id: string; score: number; time: Date }) => {
    console.log(`event received: Order 3}`);
  },
  undefined,
  3,
);
on.fooTrigger(
  (e, bar: { id: string; score: number; time: Date }) => {
    console.log(`event received: Order 1`);
  },
  undefined,
  1,
);

emit.fooTrigger({ id: "foo", score: 1, time: new Date() });

// console print:
// event received: Order 3
// event received: Order 2 (default)
// event received: Order 1

Handlers group off

The default group name is *, you can change it in channel-building, OR in on-method.

import evt from "src/events";
const { on, offAll } = evt({ defaultGroup: "group-1" });

on.fooTrigger(
  (e, bar: { id: string; score: number; time: Date }) => {},
  "group-2",
);
on.eventBar((e, bar: string, tick: number) => {}, "group-2");

offAll("group-2");

Avoid/allow repeat subscribe

The default setting is avoid-repeat, you can change to allow-repeat in channel-building, OR in on-method.

import evt from "src/events";
const { emit, on } = evt({ defaultRepeatable: true });

const handler1 = (e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received in handler1`);
};
on.fooTrigger(handler1);
on.fooTrigger(handler1);
on.fooTrigger(handler1);

const handler2 = (e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received in handler2`);
};
on.fooTrigger(handler2, undefined, undefined, false);
on.fooTrigger(handler2, undefined, undefined, false);
on.fooTrigger(handler2, undefined, undefined, false);

emit.fooTrigger({ id: "foo", score: 1, time: new Date() });

// console print:
// event received in handler1
// event received in handler1
// event received in handler1
// event received in handler2

Tip: Each arrow-function creates different instance, which is not repeat

Handle exception

The default handler is console.error, you can change it in channel-building, OR using withExceptionHandler-method.

import evt from "src/events";
const { withExceptionHandler, emit, on } = evt({
  defaultExceptionHandler: () => console.log("Handle error throws"),
});

on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  throw new Error("Specified error");
});

emit.fooTrigger({ id: "foo", score: 1, time: new Date() });

// console print:
// Handle error throws

const avoidErrorLog = () => {};
withExceptionHandler(avoidErrorLog).emit.fooTrigger({
  id: "foo",
  score: 1,
  time: new Date(),
});

// console print:
// [nothing as expected]

Tip: If event-handler is async, use async emitter.

String message

String-message is also supported like a typical event bus.

import evt from "src/events";
const { emit, on, off } = evt();

on("fooTrigger", (e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received! ${JSON.stringify(bar)}`);
});

emit("fooTrigger", { id: "foo", score: 1, time: new Date() });

off("fooTrigger");

Tip: And it is also compatible to declare-type usage.

Advanced details

Advanced details that you may not use, but needs to be mentioned.

Count listeners

Use onCount-method to count subscribe listeners.

import evt from "src/events";
const { on, off, onCount } = evt();

on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received! ${JSON.stringify(bar)}`);
});
on.fooTrigger((e, bar: { id: string; score: number; time: Date }) => {
  console.log(`event received! ${JSON.stringify(bar)}`);
});

console.log("fooTrigger event subscribe count: ", onCount.fooTrigger());

// console print:
// fooTrigger event subscribe count: 2

off.fooTrigger();

console.log("fooTrigger event subscribe count: ", onCount.fooTrigger());

// console print:
// fooTrigger event subscribe count: 0

Methods: on, off, offAll, onCount

See the declares of methods: on, off, offAll, onCount:

interface onMethods {
  fooTrigger(handler, groupId?, sortOrder?, repeatable?): void;
}
interface offMethods {
  fooTrigger(handler?, groupId?, sortOrder?): void;
}
interface offAllMethod {
  (groupId?, sortOrder?): void;
}
interface onCountMethods {
  fooTrigger(handler?, groupId?, sortOrder?): number;
}

In on-method, if you ommit any parameter OR put undefined, the parameter will get default value.

But in off / offAll / onCount -method, if you ommit any parameter OR put undefined, it means this "Parameter-Match-Condition" will be ignored while searching listeners.

Debug tons of events

In a badly structed project, tons of events may interweaving here and there. A debug tool is designed to analyze them. Example of vue3 web project as belows:

/** file: main.ts */

import { nextTick } from "vue";
import { loadEvtDebug } from "@haelue/evt-bus";
loadEvtDebug(false, nextTick, ["mouseMoving", "keyboardPressing"]);

Run the web project, open the browser console, input evtDebug() and see what outputs.

About

Event bus with powerful features: type inferring, async emitter, propagation stop, handler sorting, group off, etc.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published