Skip to content

Commit

Permalink
feat: Allow to fully type events by extending the NextcloudEvents i…
Browse files Browse the repository at this point in the history
…nterface

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Apr 21, 2024
1 parent 44038c0 commit d8d88fa
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 26 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,35 @@ unsubscribe('a', h)
unsubscribe('b', h)
```

### Typed events

It is also possible to type events, which allows type infering on the event-bus methods like `emit`, `subscribe` and `unsubscribe`.
To register new events, simply extend the `NextcloudEvents` interface:

1. Create a file like `event-bus.d.ts`:
```ts
declare module '@nextcloud/event-bus' {
interface NextcloudEvents {
'example-app:awesomeness:increased': { level: number }
}
}

export {}
```
2. Now if you use any of the event bus functions, the parameters will automatically be typed correctly:
```ts
import { subscribe } from '@nextcloud/event-bus'

subscribe(
'example-app:awesomeness:increased',
(event) => {
// "event" automatically infers type { level: number}
console.log(event.level)
},
)
```


## Naming convention

To stay consistent, we encourage you to use the following syntax when declaring events
Expand Down
30 changes: 30 additions & 0 deletions lib/Event.ts
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
export type Event = object | number | string | boolean | null | undefined

/**
* Generic events mapping, fallback if no explicit types events are defined
* @see NextcloudEvents
*/
export type GenericEvents = Record<string | symbol, Event>

/**
* Nextcloud EventBus events
* This can be extended to allow typing of events like:
* @example
* ```ts
* // event-bus.d.ts
* // Extend the Nextcloud events interface for your custom event
* declare module '@nextcloud/event-bus' {
* export interface NextcloudEvents {
* // mapping of 'event name' => 'event type'
* 'my-event': { foo: number, bar: boolean }
* }
* }
*
* // your-code.ts
* import { emit } from '@nextcloud/event-bus'
* // Here the type of 'params' is infered automatically
* emit('my-event', (params) => { console.debug(params.foo, params.bar) })
* ```
*/
export interface NextcloudEvents {
[eventName: string | symbol]: Event
}
29 changes: 24 additions & 5 deletions lib/EventBus.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { Event } from "./Event";
import type { GenericEvents, NextcloudEvents } from "./Event";
import { EventHandler } from "./EventHandler";

export interface EventBus {
export interface EventBus<E extends GenericEvents = NextcloudEvents> {

/**
* Get the version of this event bus instance
* This is used for compatibility checking
*/
getVersion(): string;

subscribe(name: string, handler: EventHandler): void;
/**
* Subscribe the event bus
* @param name Name of the event to subscribe
* @param handler Handler ivoken when receiving the event
*/
subscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void;

unsubscribe(name: string, handler: EventHandler): void;
/**
* Unsubscribe a handler on one event from the event bus
* @param name Name of the event to unsubscribe
* @param handler Handler to unsubscribe
*/
unsubscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void;

emit(name: string, event: Event): void;
/**
* Emit an event on the event bus
* @param name Name of the event to emit
* @param event Event payload to emit
*/
emit<EventName extends keyof E>(name: EventName, event: E[EventName]): void;

}
4 changes: 2 additions & 2 deletions lib/EventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Event } from "./Event";

export interface EventHandler {
(event: Event): void
export interface EventHandler<T extends Event> {
(event: T): void
}
14 changes: 7 additions & 7 deletions lib/ProxyBus.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import valid from "semver/functions/valid.js";
import major from "semver/functions/major.js";

import { Event } from "./Event.js";
import type { Event, GenericEvents, NextcloudEvents } from "./Event.js";
import { EventBus } from "./EventBus.js";
import { EventHandler } from "./EventHandler.js";

export class ProxyBus implements EventBus {
export class ProxyBus<E extends GenericEvents = NextcloudEvents> implements EventBus<E> {

private bus: EventBus;
private bus: EventBus<E>;

constructor(bus: EventBus) {
constructor(bus: EventBus<E>) {
if (typeof bus.getVersion !== 'function' || !valid(bus.getVersion())) {
console.warn('Proxying an event bus with an unknown or invalid version')
} else if (major(bus.getVersion()) !== major(this.getVersion())) {
Expand All @@ -23,15 +23,15 @@ export class ProxyBus implements EventBus {
return globalThis.__pkg_version;
}

subscribe(name: string, handler: EventHandler): void {
subscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void {
this.bus.subscribe(name, handler);
}

unsubscribe(name: string, handler: EventHandler): void {
unsubscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void {
this.bus.unsubscribe(name, handler);
}

emit(name: string, event: Event): void {
emit<EventName extends keyof E>(name: EventName, event: E[EventName]): void {
this.bus.emit(name, event);
}

Expand Down
14 changes: 7 additions & 7 deletions lib/SimpleBus.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Event } from "./Event.js";
import { Event, GenericEvents, NextcloudEvents } from "./Event.js";
import { EventBus } from "./EventBus.js";
import { EventHandler } from "./EventHandler.js";

export class SimpleBus implements EventBus {
export class SimpleBus<E extends GenericEvents = NextcloudEvents> implements EventBus<E> {

private handlers = new Map<string, EventHandler[]>();
private handlers = new Map<keyof E, EventHandler<E[keyof E]>[]>();

getVersion(): string {
return globalThis.__pkg_version;
}

subscribe(name: string, handler: EventHandler): void {
this.handlers.set(name, (this.handlers.get(name) || []).concat(handler));
subscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void {
this.handlers.set(name, (this.handlers.get(name) || []).concat(handler as EventHandler<E[keyof E]>));
}

unsubscribe(name: string, handler: EventHandler): void {
unsubscribe<EventName extends keyof E>(name: EventName, handler: EventHandler<E[EventName]>): void {
this.handlers.set(name, (this.handlers.get(name) || []).filter(h => h != handler));
}

emit(name: string, event: Event): void {
emit<EventName extends keyof E>(name: EventName, event: E[EventName]): void {
(this.handlers.get(name) || []).forEach(h => {
try {
h(event)
Expand Down
10 changes: 5 additions & 5 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EventBus } from "./EventBus"
import { EventHandler } from "./EventHandler";
import { Event } from "./Event";
import { NextcloudEvents } from "./Event";
import { ProxyBus } from "./ProxyBus"
import { SimpleBus } from "./SimpleBus"

export type { EventBus } from './EventBus'
export type { EventHandler } from "./EventHandler";
export type { Event } from "./Event";
export type { Event, NextcloudEvents } from "./Event";

export { ProxyBus } from "./ProxyBus"
export { SimpleBus } from "./SimpleBus";
Expand Down Expand Up @@ -54,7 +54,7 @@ function getBus(): EventBus {
* @param name name of the event
* @param handler callback invoked for every matching event emitted on the bus
*/
export function subscribe(name: string, handler: EventHandler): void {
export function subscribe<K extends keyof NextcloudEvents>(name: K, handler: EventHandler<NextcloudEvents[K]>): void {
getBus().subscribe(name, handler)
}

Expand All @@ -66,7 +66,7 @@ export function subscribe(name: string, handler: EventHandler): void {
* @param name name of the event
* @param handler callback passed to `subscribed`
*/
export function unsubscribe(name: string, handler: EventHandler): void {
export function unsubscribe<K extends keyof NextcloudEvents>(name: K, handler: EventHandler<NextcloudEvents[K]>): void {
getBus().unsubscribe(name, handler)
}

Expand All @@ -76,6 +76,6 @@ export function unsubscribe(name: string, handler: EventHandler): void {
* @param name name of the event
* @param event event payload
*/
export function emit(name: string, event: Event): void {
export function emit<K extends keyof NextcloudEvents>(name: K, event: NextcloudEvents[K]): void {
getBus().emit(name, event)
}

0 comments on commit d8d88fa

Please sign in to comment.