Skip to content

Commit e5cb0cd

Browse files
authored
feat: event-bus to be async to allow for non-blocking callbacks (#106)
* feat: rewrote event-bus to be asynchronous to allow for non-blocking callbacks BREAKING CHANGE: `subscribe` is now asynchronous and requires `await` before call to `unsubscribe` * feat: update dependencies * fix: deps * ci: update node versions * feat: wildcard channels BREAKING CHANGE: wildcard channels now get the original channel * fix: logs and tests
1 parent d89b8bb commit e5cb0cd

File tree

11 files changed

+3781
-1484
lines changed

11 files changed

+3781
-1484
lines changed

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88

99
jobs:
1010
delivery:
11-
name: Node 20.x
11+
name: Node 24.x
1212
runs-on: ubuntu-latest
1313

1414
steps:

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919

2020
strategy:
2121
matrix:
22-
node: [18.x, 20.x]
22+
node: [20.x, 22.x, 24.x]
2323

2424
steps:
2525
- name: ☁️ Check out source code

README.md

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,28 @@ Simple typesafe cross-platform pubsub communication between different single pag
99

1010
## Table of Contents
1111

12-
- [Purpose](#purpose)
13-
- [Installation](#installation)
14-
- [Usage](#usage)
15-
- [Advanced Schema](#advanced-schema)
16-
- [API](#api)
17-
- [Register](#register)
18-
- [Unregister](#unregister)
19-
- [Subscribe](#subscribe)
20-
- [Publish](#publish)
21-
- [Get Latest](#get-latest)
22-
- [Get Schema](#get-schema)
12+
- [Event Bus](#event-bus)
13+
- [Table of Contents](#table-of-contents)
14+
- [Purpose](#purpose)
15+
- [Installation](#installation)
16+
- [Usage](#usage)
17+
- [Advanced Schema](#advanced-schema)
18+
- [API](#api)
19+
- [Constructor Options](#constructor-options)
20+
- [Register](#register)
21+
- [Parameters](#parameters)
22+
- [Unregister](#unregister)
23+
- [Parameters](#parameters-1)
24+
- [Subscribe](#subscribe)
25+
- [Parameters](#parameters-2)
26+
- [Publish](#publish)
27+
- [Parameters](#parameters-3)
28+
- [Get Latest](#get-latest)
29+
- [Parameters](#parameters-4)
30+
- [Get Schema](#get-schema)
31+
- [Parameters](#parameters-5)
32+
- [Clear Replay](#clear-replay)
33+
- [Parameters](#parameters-6)
2334

2435
---
2536

@@ -103,8 +114,8 @@ class PublisherElement extends HTMLElement {
103114
this.render();
104115
this.firstChild && this.firstChild.addEventListener('click', this.send);
105116
}
106-
send() {
107-
eventBus.publish('namespace:eventName', true);
117+
async send() {
118+
await eventBus.publish('namespace:eventName', true);
108119
}
109120
render() {
110121
this.innerHTML = `<button type="button">send</button>`;
@@ -118,22 +129,35 @@ class PublisherElement extends HTMLElement {
118129
`Fragment Two - React Component`
119130

120131
```typescript
121-
import React, { useState, useEffect } from 'react';
132+
import React from 'react';
122133

123-
function SubscriberComponent() {
124-
const [isFavorite, setFavorite] = useState(false);
134+
// Custom hook for event bus subscriptions
135+
function useEventSubscription<T>(eventName: string, schema: object) {
136+
const [value, setValue] = React.useState<T>();
137+
138+
React.useEffect(() => {
139+
let sub: { unsubscribe(): void };
125140

126-
useEffect(() => {
127-
function handleSubscribe(favorite: boolean) {
128-
setFavorite(favorite);
141+
async function init() {
142+
try {
143+
eventBus.register(eventName, schema);
144+
sub = await eventBus.subscribe<T>(eventName, (event) => setValue(event.payload));
145+
} catch (e) {
146+
console.error(`Failed to subscribe to ${eventName}:`, e);
147+
}
129148
}
130-
eventBus.register('namespace:eventName', { type: 'boolean' });
131-
const sub = eventBus.subscribe<boolean>('namespace:eventName', handleSubscribe);
132-
return function cleanup() {
133-
sub.unsubscribe();
149+
150+
init();
151+
return () => {
152+
if (sub) sub.unsubscribe();
134153
};
135-
}, []);
154+
}, [eventName, schema]);
155+
156+
return value;
157+
}
136158

159+
function SubscriberComponent() {
160+
const isFavorite = useEventSubscription<boolean>('namespace:eventName', { type: 'boolean' });
137161
return isFavorite ? 'This is a favorite' : 'This is not interesting';
138162
}
139163
```
@@ -205,8 +229,8 @@ export class StoreComponent implements OnInit, OnDestroy {
205229
/* handle new deals ... */
206230
}
207231

208-
onSend() {
209-
eventBus.publish('store:addToCart', {
232+
async onSend() {
233+
await eventBus.publish('store:addToCart', {
210234
name: 'Milk',
211235
amount: '1000 ml',
212236
price: 0.99,
@@ -228,20 +252,37 @@ export class StoreComponent implements OnInit, OnDestroy {
228252

229253
## API
230254

255+
### Constructor Options
256+
257+
The EventBus constructor accepts an options object with the following properties:
258+
259+
```typescript
260+
interface EventBusOptions {
261+
/**
262+
* The logging level for the event bus
263+
* - 'none': No logging
264+
* - 'error': Only log errors (default)
265+
* - 'warn': Log warnings and errors
266+
* - 'info': Log everything
267+
*/
268+
logLevel?: 'none' | 'error' | 'warn' | 'info';
269+
}
270+
```
271+
231272
### Register
232273

233274
Register a schema for the specified event type and equality checking on subsequent registers. Subsequent registers must use an equal schema or an error will be thrown.
234275

235276
```typescript
236-
register(channel: string, schema: object): boolean;
277+
register(channel: string, schema: Record<string, unknown>): boolean;
237278
```
238279

239280
#### Parameters
240281

241-
| Name | Type | Description |
242-
| ------- | -------- | ---------------------------------------------------- |
243-
| channel | `string` | name of event channel to register schema to |
244-
| schema | `object` | all communication on channel must follow this schema |
282+
| Name | Type | Description |
283+
| ------- | ------------------------- | ---------------------------------------------------- |
284+
| channel | `string` | name of event channel to register schema to |
285+
| schema | `Record<string, unknown>` | all communication on channel must follow this schema |
245286

246287
**Returns** - returns true if event channel already existed of false if a new one was created.
247288

@@ -272,11 +313,13 @@ with an optional replay of last event at initial subscription.
272313
The channel may be the wildcard `'*'` to subscribe to all channels.
273314

274315
```typescript
275-
subscribe<T>(channel: string, callback: Callback<T>): { unsubscribe(): void };
316+
async subscribe<T>(channel: string, callback: Callback<T>): Promise<{ unsubscribe(): void }>;
276317

277-
subscribe<T>(channel: string, replay: boolean, callback: Callback<T>): { unsubscribe(): void };
318+
async subscribe<T>(channel: string, replay: boolean, callback: Callback<T>): Promise<{ unsubscribe(): void }>;
278319
```
279320

321+
Note: Subscribe is an async function that returns a Promise with the subscription.
322+
280323
Callbacks will be fired when event is published on a subscribed channel with the argument:
281324

282325
```typescript
@@ -300,10 +343,10 @@ Callbacks will be fired when event is published on a subscribed channel with the
300343

301344
### Publish
302345

303-
Publish to event channel with an optional payload triggering all subscription callbacks.
346+
Publish to event channel with an optional payload triggering all subscription callbacks. Returns a Promise that resolves when all callbacks have completed.
304347

305348
```typescript
306-
publish<T>(channel: string, payload?: T): void;
349+
async publish<T>(channel: string, payload?: T): Promise<void>;
307350
```
308351

309352
#### Parameters
@@ -313,7 +356,7 @@ publish<T>(channel: string, payload?: T): void;
313356
| channel | `string` | name of event channel to send payload on |
314357
| payload | `any` | payload to be sent |
315358

316-
**Returns** - void
359+
**Returns** - Promise that resolves when all callbacks have completed
317360

318361
---
319362

@@ -350,3 +393,21 @@ getSchema<T>(channel: string): any | undefined;
350393
| channel | `string` | name of the event channel to fetch the schema from |
351394

352395
**Returns** - the schema or `undefined`
396+
397+
---
398+
399+
### Clear Replay
400+
401+
Clears the replay event for the specified channel.
402+
403+
```typescript
404+
clearReplay(channel: string): boolean;
405+
```
406+
407+
#### Parameters
408+
409+
| Name | Type | Description |
410+
| ------- | -------- | -------------------------------------------------- |
411+
| channel | `string` | name of the event channel to clear the replay from |
412+
413+
**Returns** - returns true if the replay event was cleared, false otherwise.

docs/react-component.js

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,44 @@
11
const e = React.createElement;
22

3-
function ReactComponent() {
4-
const [isFavorite, setFavorite] = React.useState(false);
3+
function useEventSubscription(eventName, schema) {
4+
const [value, setValue] = React.useState(false);
55

66
React.useEffect(() => {
7-
function handleSubscribe(favorite) {
8-
setFavorite(favorite);
9-
}
7+
let sub;
108

11-
eventBus.register('namespace:eventName', { type: 'boolean' });
12-
const sub = eventBus.subscribe('namespace:eventName', handleSubscribe);
13-
14-
try {
15-
// Re-registering again with another schema is not allowed
16-
eventBus.register('namespace:eventName', { type: 'string' });
17-
} catch (e) {
18-
console.warn(e);
19-
console.debug('eventType:', e.eventType);
20-
console.debug('schema:', e.schema);
21-
console.debug('newSchema:', e.newSchema);
9+
async function init() {
10+
try {
11+
eventBus.register(eventName, schema);
12+
sub = await eventBus.subscribe(eventName, (event) => setValue(event.payload));
13+
} catch (e) {
14+
console.warn(`Failed to subscribe to ${eventName}:`, e);
15+
console.debug('channel:', e.channel);
16+
console.debug('schema:', e.schema);
17+
console.debug('newSchema:', e.newSchema);
18+
}
2219
}
2320

24-
return function cleanup() {
25-
sub.unsubscribe();
21+
init();
22+
23+
return () => {
24+
if (sub) sub.unsubscribe();
2625
};
27-
}, []);
26+
}, [eventName, schema]);
27+
28+
return value;
29+
}
30+
31+
function ReactComponent() {
32+
const isFavorite = useEventSubscription('namespace:eventName', { type: 'boolean' });
33+
// Re-registering again with another schema is not allowed
34+
useEventSubscription('namespace:eventName', { type: 'string' });
2835

29-
return e('article', { style: { padding: '1rem', backgroundColor: 'DeepSkyBlue' } },
36+
return e(
37+
'article',
38+
{ style: { padding: '1rem', backgroundColor: 'DeepSkyBlue' } },
3039
e('header', null, 'React Component'),
31-
e('p', null, isFavorite ? 'This is a favorite' : 'This is not interesting')
32-
)
40+
e('p', null, isFavorite ? 'This is a favorite' : 'This is not interesting'),
41+
);
3342
}
3443

3544
const domContainer = document.querySelector('#react-component');

docs/web-component.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class WebComponent extends HTMLElement {
2323
eventBus.publish('namespace:eventName', { nested: true });
2424
} catch (e) {
2525
console.warn(e);
26-
console.debug('eventType:', e.eventType);
26+
console.debug('channel:', e.channel);
2727
console.debug('schema:', e.schema);
28-
console.debug('detail:', e.detail);
28+
console.debug('payload:', e.payload);
2929
}
3030
}
3131

0 commit comments

Comments
 (0)