Skip to content

Commit

Permalink
Subscribe with delay (#55)
Browse files Browse the repository at this point in the history
* Introduce Delayed Subscription Feature in Emitter with Comprehensive Tests

- Implemented 'subscribeWithDelay' method in Emitter class, allowing delayed callback execution.
- Added extensive test suite for 'subscribeWithDelay', covering:
    - Basic delayed callback functionality.
    - Unsubscribing callbacks before and after delay periods.
    - Independent execution of multiple delayed subscriptions.
    - Correct execution order for callbacks with different delays.
    - Handling of the same callback registered with varying delays.
    - Behavior with zero delay (immediate execution).
- Ensured robustness of new feature with edge case handling and error scenarios.

This update significantly enhances the Emitter's capabilities, allowing for more flexible event handling scenarios.

* documentation updated

* documentation table of context improved

* style: Emitter imports

* style: imports single quote

* Doc. update

* test: Added tests for event emitter with delayed subscriptions, covering zero delay handling, multiple subscriptions with varying delays, correct execution order based on delay, and no execution on unsubscribe before delayed callback.

* test: remove duplication from subscribeWithDelay test

---------

Co-authored-by: Valeh ASADLI <[email protected]>
  • Loading branch information
emilQA and valehasadli authored Dec 19, 2023
1 parent 6676af5 commit 532935b
Show file tree
Hide file tree
Showing 16 changed files with 280 additions and 57 deletions.
113 changes: 73 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# BlinkHub Emitter

A type-safe event emitter library built with TypeScript, which provides an interface for subscribing to and emitting events.
A type-safe event emitter library built with TypeScript, which provides an interface for subscribing to and emitting events...

## Table of Contents

- [Installation](#installation)
- [Usage](#usage)
- [Basic Usage](#basic-usage)
- [Defining Events and Their Types](#defining-events-and-their-types)
- [Subscribing to Events](#subscribing-to-events)
- [Emitting Events](#emitting-events)
- [Unsubscribing from Events](#unsubscribing-from-events)
- [Error Handling](#error-handling)
- [Examples](#examples)
- [Advanced Features]()
- [The `once` method](#the-once-method)
- [Subscribing to Events with Delay](#subscribing-to-events-with-delay)
- [Subscribing to Multiple Events](#subscribing-to-multiple-events)
- [Emitter with Priority](#emitter-with-priority)
- [Channel-Based Event Handling](#channel-based-event-handling)
- [Error Handling](#error-handling)
- [Use Case Examples](#use-case-examples)
- [Simple Notification System](#simple-notification-system)
- [E-Commerce Cart System](#e-commerce-cart-system)
- [Additional Information](#additional-information)
- [FAQ for React Developers](#faq-for-react-developers)
- [Link to React Example](#link-to-react-example)

## Installation

`npm i blink-hub`

## Usage
## Basic Usage

### Defining Events and Their Types

Expand Down Expand Up @@ -60,7 +71,9 @@ unsubscribe(); // This will remove the callback from the event listeners.

### The `once` Method

The `once` method allows listeners to be invoked only once for the specified event. After the event has been emitted and the listener invoked, the listener is automatically removed. This can be useful for scenarios where you need to react to an event just a single time, rather than every time it's emitted.
The `once` method allows listeners to be invoked only once for the specified event.
After the event has been emitted and the listener invoked, the listener is automatically removed.
This can be useful for scenarios where you need to react to an event just a single time, rather than every time it's emitted.

```typescript
type UserEvents = {
Expand All @@ -79,20 +92,30 @@ userEmitter.emit('userFirstLogin', 'Alice'); // No output, as the listener has b
```

#### Note:
If an error occurs within a callback registered with `once`, the callback will not be re-invoked for subsequent events. Always handle errors adequately to prevent unforeseen behavior.
If an error occurs within a callback registered with `once`, the callback will not be re-invoked for subsequent events.
Always handle errors adequately to prevent unforeseen behavior.

### Error Handling
### Subscribing to Events with Delay

If an error occurs within a callback, the emitter will catch it and log it to the console. The emitter also pushes a null value to the results array in case of errors, though this behavior can be customized.
The `subscribeWithDelay` method allows you to subscribe to an event with a specified delay.
This means the callback function will only be executed after the delay period has passed, following the event emission.

```typescript
emitter.subscribe('eventName', () => {
throw new Error('Oops!');
});
const emitter = new Emitter<TestEvents>();

emitter.emit('eventName', 'Test', 'Error'); // Outputs: Error in callback for event 'eventName'
// Subscribe to an event with a delay
const delay = 1000; // Delay in milliseconds (1000ms = 1 second)
emitter.subscribeWithDelay('testEvent', (data: string) => {
console.log(`Received (after delay): ${data}`);
}, delay);

// Emit the event
emitter.emit('testEvent', 'Delayed Message');
// The callback will be executed after 1 second
```

This is particularly useful in scenarios where you want to defer the execution of an event handler,
such as debouncing user input or delaying a notification.

### Subscribing to Multiple Events
The subscribeList method allows you to subscribe to multiple events at once, providing a convenient way to manage event listeners when you need to react to different events with the same or different callbacks.
Expand Down Expand Up @@ -127,7 +150,19 @@ To unsubscribe from the user events:
unsubscribe();
```

### Examples
### Error Handling

If an error occurs within a callback, the emitter will catch it and log it to the console. The emitter also pushes a null value to the results array in case of errors, though this behavior can be customized.

```typescript
emitter.subscribe('eventName', () => {
throw new Error('Oops!');
});

emitter.emit('eventName', 'Test', 'Error'); // Outputs: Error in callback for event 'eventName'
```

### Use Case Examples

## Simple Notification System

Expand Down Expand Up @@ -284,44 +319,42 @@ notificationChannel.emit('channelEvent', 'You have 3 new notifications!');

This channel-based approach ensures that events are handled only by the listeners that are relevant to the particular context or module, improving modularity and maintainability.

## FAQ for React developers

### Why You Might Choose Event Emitters Over Context in React
## Additional Information

Note: Same reasons can/may apply for all framework/libraries.

While React's Context API offers a powerful way to manage and propagate state changes through your component tree, there are scenarios where an event emitter might be a more appropriate choice. Below, we detail some reasons why developers might opt for event emitters in certain situations.

### 1. Granularity

Event emitters allow you to listen to very specific events. With the Context API, any component consuming the context will re-render when the context value changes. If you're looking to react to specific events rather than broad state changes, an event emitter could be more suitable.
### FAQ for React developers

### 2. Decoupling
Why You Might Choose Event Emitters Over Context in React?

Event emitters facilitate a decoupled architecture. Components or services can emit events without knowing or caring about the listeners. This can lead to more modular and maintainable code, particularly in larger applications.

### 3. Cross-Framework Compatibility

In environments where different parts of your application use different frameworks or vanilla JavaScript, event emitters can provide a unified communication channel across these segments.
Note: Same reasons can/may apply for all framework/libraries.

### 4. Multiple Listeners
While React's Context API offers a powerful way to manage and propagate state changes through your component tree,
there are scenarios where an event emitter might be a more appropriate choice.
Below, we detail some reasons why developers might opt for event emitters in certain situations.

Event emitters inherently support having multiple listeners for a single event. This can be leveraged to trigger various side effects from one event, whereas with Context API, this would need manual management.
- Granularity
- Event emitters allow you to listen to very specific events. With the Context API, any component consuming the context will re-render when the context value changes. If you're looking to react to specific events rather than broad state changes, an event emitter could be more suitable.

### 5. Deeply Nested Components
- Decoupling
- Event emitters facilitate a decoupled architecture. Components or services can emit events without knowing or caring about the listeners. This can lead to more modular and maintainable code, particularly in larger applications.

In applications with deeply nested component structures, prop-drilling or managing context might become cumbersome. Event emitters can be an alternative to simplify state and event management in such cases.
- Cross-Framework Compatibility
- In environments where different parts of your application use different frameworks or vanilla JavaScript, event emitters can provide a unified communication channel across these segments.

### 6. Historical Reasons
- Multiple Listeners
- Event emitters inherently support having multiple listeners for a single event. This can be leveraged to trigger various side effects from one event, whereas with Context API, this would need manual management.

Older codebases developed before the advent of hooks and the newer Context API features might still employ event emitters, as they once provided a simpler solution to global state management in React.
- Deeply Nested Components
- In applications with deeply nested component structures, prop-drilling or managing context might become cumbersome. Event emitters can be an alternative to simplify state and event management in such cases.

### 7. Performance
- Historical Reasons
- Older codebases developed before the advent of hooks and the newer Context API features might still employ event emitters, as they once provided a simpler solution to global state management in React.

Event emitters might provide a performance edge in cases where the Context API might cause unnecessary re-renders. Since event emitters don't inherently lead to re-renders, they can be more performant in specific scenarios.
- Performance
- Event emitters might provide a performance edge in cases where the Context API might cause unnecessary re-renders. Since event emitters don't inherently lead to re-renders, they can be more performant in specific scenarios.

### 8. Non-UI Logic
- Non-UI Logic
- For parts of your application logic that reside outside the React component tree, event emitters can be beneficial, as they aren't tied to React's lifecycle or component hierarchy.

For parts of your application logic that reside outside the React component tree, event emitters can be beneficial, as they aren't tied to React's lifecycle or component hierarchy.
### Link to React Example

**For a practical use case in React using the BlinkHub Emitter, see the [React example on GitHub](https://github.com/valehasadli/blinkhub-react-example).**
24 changes: 24 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

// The directory where Jest should output its coverage files
coverageDirectory: "coverage",

// Indicates whether each individual test should be reported during the run
verbose: true,

// The test environment that will be used for testing
testEnvironment: "node",

// The glob patterns Jest uses to detect test files
testMatch: [
"<rootDir>/tests/**/*.[jt]s?(x)",
"<rootDir>/tests/?(*.)+(spec|test).[tj]s?(x)"
],

// A map from regular expressions to paths to transformers
transform: {
"^.+\\.(ts|tsx)?$": "babel-jest"
},
};
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blink-hub",
"version": "0.3.15",
"version": "0.4.0",
"description": "A versatile and efficient event-handling library designed for the modern JS/TS ecosystem.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -39,10 +39,5 @@
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"typescript": "^5.2.2"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx)?$": "babel-jest"
}
}
}
2 changes: 1 addition & 1 deletion src/lib/channels/ChannelRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Channel } from "./Channel";
import { Channel } from './Channel';

export class ChannelRegistry<T extends Record<string, (...args: any[]) => void>> {
private channels: Map<string, Channel<T>> = new Map();
Expand Down
13 changes: 12 additions & 1 deletion src/lib/events/Emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { IEventSubscriber } from '../interfaces/IEventSubscriber';
import { IEventOnceSubscriber } from '../interfaces/IEventOnceSubscriber';
import { IBulkEventSubscriber } from '../interfaces/IBulkEventSubscriber';
import { IChannelEventEmitter } from '../interfaces/IChannelEventEmitter';
import { IEventWithDelaySubscriber } from '../interfaces/IEventWithDelaySubscriber';

export default class Emitter<T extends Record<string, (...args: any[]) => void>>
implements IEventSubscriber<T>, IEventOnceSubscriber<T>, IBulkEventSubscriber<T>, IChannelEventEmitter<T> {
implements IEventSubscriber<T>, IEventOnceSubscriber<T>, IBulkEventSubscriber<T>, IChannelEventEmitter<T>,
IEventWithDelaySubscriber<T> {
private eventRegistry = new EventRegistry<T>();
private channelRegistry = new ChannelRegistry<T>();

Expand All @@ -30,4 +32,13 @@ export default class Emitter<T extends Record<string, (...args: any[]) => void>>
channel(name: string): Channel<T> {
return this.channelRegistry.channel(name);
}

subscribeWithDelay<K extends keyof T>(
name: K,
callback: T[K],
delay: number,
priority: number = 0
): () => void {
return this.eventRegistry.subscribeWithDelay(name, callback, delay, priority);
}
}
13 changes: 13 additions & 0 deletions src/lib/events/EventRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,17 @@ export class EventRegistry<T extends Record<string, (...args: any[]) => void>> {
unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
};
}

subscribeWithDelay<K extends keyof T>(
name: K,
callback: T[K],
delay: number,
priority: number = 0
): () => void {
const wrappedCallback = (...args: Parameters<T[K]>): void => {
setTimeout(() => callback(...args), delay);
};

return this.subscribe(name, wrappedCallback as any, priority);
}
}
8 changes: 8 additions & 0 deletions src/lib/interfaces/IEventWithDelaySubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface IEventWithDelaySubscriber<T extends Record<string, (...args: any[]) => void>> {
subscribeWithDelay<K extends keyof T>(
name: K,
callback: T[K],
delay: number,
priority?: number
): () => void;
}
4 changes: 2 additions & 2 deletions tests/channels/channel.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Emitter from "../../src";
import { Channel } from "../../src/lib/channels/Channel";
import Emitter from '../../src';
import { Channel } from '../../src/lib/channels/Channel';

type MyEvents = {
eventName: (arg: string) => void;
Expand Down
2 changes: 1 addition & 1 deletion tests/core/basicEvents.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Emitter from "../../src";
import Emitter from '../../src';

type MyEvents = {
event: (arg1: string, arg2?: string) => void;
Expand Down
2 changes: 1 addition & 1 deletion tests/core/emission.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Emitter from "../../src";
import Emitter from '../../src';

type MyEvents = {
event: (arg1: string, arg2?: string) => void;
Expand Down
2 changes: 1 addition & 1 deletion tests/core/priority.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Emitter from "../../src";
import Emitter from '../../src';

type MyEvents = {
testEvent: (val: string) => string;
Expand Down
2 changes: 1 addition & 1 deletion tests/core/subscribeList.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Emitter from "../../src";
import Emitter from '../../src';

describe('Emitter subscribe multiple events', () => {
type TestEvents = {
Expand Down
2 changes: 1 addition & 1 deletion tests/core/subscribeOnce.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Emitter from "../../src";
import Emitter from '../../src';

type MyEvents = {
sampleEvent: (msg: string) => void;
Expand Down
Loading

0 comments on commit 532935b

Please sign in to comment.