Skip to content

Commit

Permalink
Introducing the publish method.
Browse files Browse the repository at this point in the history
  • Loading branch information
pawsaw committed Nov 30, 2020
1 parent 39aca25 commit fe475ee
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 39 deletions.
162 changes: 126 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,106 @@ Subscription Manager - Lightweight JavaScript and TypeScript Utility for managin

## Why Subscription Manager?

- Designed with one goal in mind: **managing subscriptions**.
- **No dependencies** and an incredibly **small footprint**.
- Developed in TypeScript, usable in **TypeScript** and **JavaScript**.
- **Elegan**t and **easy to use**.

The best way to describe a situation is often by Example.

So here we go...
## Setup

```sh
npm i @pawsaw/subscription-manager
```

## Subscription Manager API

### Obtain an instance of Subscription Manager

At the beginning it is necessary to get an instance of the Subscription Manager.

```ts
import {
createSubscriptionManager,
SubscriptionManager,
Subscription,
} from '@pawsaw/subscription-manager';

### Example (TypeScript): ChatService
const sm: SubscriptionManager<string> = createSubscriptionManager();
```

Here _string_ is the data type of the channel, as default. Other data types are acceptable, but less unusual.

Here a Subscription Manager is created, where the channels are of the datatype _number_:

```ts
const sm: SubscriptionManager<number> = createSubscriptionManager<number>();
```

### subscribe

We **_subscribe_** to a certain channel and define a callback function that takes any parameters. The parameters must be the same as in the counterpart, the **_publish_** method.

```ts
function onChatMessageReceived(msg: string): void {
// ...
}

const sub = sm.subscribe('chatMessages', onChatMessageReceived);
```

The result of the _subscribe_ method is a _Subscription_.

### publish

To broadcast data, we use the publish method.

```ts
sm.publish('chatMessages', 'Hello World');
```

For all _Subscribers_ on this channel the stored callback is called.

### Typesafe publish and subscribe

In TypeScript it makes sense to get as much as possible out of static type checking.

```ts
interface OnChatMessageReceived {
(msg: string): void;
}

const onChatMessageReceived: OnChatMessageReceived = (msg: string) => {
// ...
};

const sub = sm.subscribe<OnChatMessageReceived>('chatMessages', onChatMessageReceived);
```

When publishing we can ensure the integrity of the parameters with respect to the type by explicitly specifying the type of the callback.

```ts
sm.publish<OnChatMessageReceived>('chatMessages', 'Hello World');
```

**Note:** The Subscription Manager may handle different types of callbacks on different channels.

### free

If a subscription is no longer needed, be sure to free it.

```ts
sub.free();
```

## Example (TypeScript): ChatService

We want to implement a _ChatService_ that looks like this:

```ts
export interface OnChatMessageReceived {
(msg: ChatMessage): void;
(msg: string): void;
}

export interface ChatService {
Expand All @@ -29,10 +118,10 @@ export interface ChatService {
... and a _Client_ using it:

```ts
const chatService = // ... access the instance somehow
const chatService = ChatService.instance(); // ... access the instance somehow

const onChatMessageReceived: OnChatMessageReceived = (msg: ChatMessage) => {
console.log(`Got new message: ${msg}`);
const onChatMessageReceived: OnChatMessageReceived = (msg: string) => {
console.log(`Got new message: ${msg}`);
};

// start listening for incomming messages
Expand All @@ -50,11 +139,11 @@ Maybe you want to offer another method in the ChatService to get information abo

```ts
export interface OnChatMessageReceived {
(msg: ChatMessage): void;
(msg: string): void;
}

export interface OnNewChatUser {
(user: ChatUser): void;
(user: string): void;
}

export interface ChatService {
Expand All @@ -67,50 +156,51 @@ It is easy to see that the implementation of subscription management is quite co

**Wouldn't it be nice to have a utility here that would make our work easier?**

## How to use the Subscription Manager?
### How to use the Subscription Manager?

```ts
import {
createSubscriptionManager,
SubscriptionManager,
Subscription
createSubscriptionManager,
SubscriptionManager,
Subscription,
} from '@pawsaw/subscription-manager';

// [ ... ]

export interface OnChatMessageReceived {
(msg: ChatMessage): void;
(msg: string): void;
}

export interface OnNewChatUser {
(user: ChatUser): void;
(user: string): void;
}

export class ChatService {

// Usually one SubscriptionManager per service
private sm = createSubscriptionManager();

private anyAsyncDatasource = // any async data source

onChatMessageReceived(listener: OnChatMessageReceived): Subscription {
return this.sm.subscribe('onChatMessageReceived', listener);
}

onNewChatUser(listener: OnNewChatUser): Subscription {
return this.sm.subscribe('onNewChatUser', listener);
}

private initDataSource(): void {
this.anyAsyncDatasource.receive((type, data) => {
if (type === 'message') {
this.sm.handler<OnChatMessageReceived>('onChatMessageReceived').forEach(h => h(data as ChatMessage));
} else if (type === 'user') {
this.sm.handler<OnNewChatUser>('onNewChatUser').forEach(h => h(data as ChatUser));
}
});
}
private sm = createSubscriptionManager();

private anyAsyncDatasource = any; // any async data source

onChatMessageReceived(listener: OnChatMessageReceived): Subscription {
return this.sm.subscribe('onChatMessageReceived', listener);
}

onNewChatUser(listener: OnNewChatUser): Subscription {
return this.sm.subscribe('onNewChatUser', listener);
}

private initDataSource(): void {
this.anyAsyncDatasource.receive((type: string, data: string) => {
if (type === 'message') {
this.sm.publish<OnChatMessageReceived>('onChatMessageReceived', data);
} else if (type === 'user') {
this.sm.publish<OnNewChatUser>('onNewChatUser', data);
}
});
}
}
```

That's it folks!

Stay tuned and **_keep on coding_**.
38 changes: 38 additions & 0 deletions src/SubscriptionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,42 @@ describe('Subscription Manager', () => {
expect(fooHandlersAfterUnsubscribe).toBeDefined();
expect(fooHandlersAfterUnsubscribe).toHaveLength(0);
});

it('The handler should be called numerous times while publishing.', () => {
const handlerImpl = (n: number) => {
expect(n).toBeDefined();
expect(n).toBeGreaterThan(0);
};

const handler = jest.fn().mockImplementation(handlerImpl);

const sub1 = sm.subscribe('foo', handler);
const sub2 = sm.subscribe('foo', handler);

sm.publish<typeof handlerImpl>('foo', 1);
sm.publish<typeof handlerImpl>('foo', 2);
sm.publish<typeof handlerImpl>('foo', 3);

expect(handler).toBeCalledTimes(6);

sub1.free();

handler.mockClear();

sm.publish<typeof handlerImpl>('foo', 1);
sm.publish<typeof handlerImpl>('foo', 2);
sm.publish<typeof handlerImpl>('foo', 3);

expect(handler).toBeCalledTimes(3);

sub2.free();

handler.mockClear();

sm.publish<typeof handlerImpl>('foo', 1);
sm.publish<typeof handlerImpl>('foo', 2);
sm.publish<typeof handlerImpl>('foo', 3);

expect(handler).toBeCalledTimes(0);
});
});
19 changes: 16 additions & 3 deletions src/SubscriptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ export interface Subscription {
free(): void;
}

// type ReturnType<T> = T extends (... args: any[]) => infer T ? T : never;
export type Parameters<T> = T extends (...args: infer T) => any ? T : never;
export interface SubscriptionManager<TChannel> {
subscribe<TEventHandler = Function>(
subscribe<TEventHandler extends Function = Function>(
channel: TChannel,
eventHandler: TEventHandler,
): Subscription;
handler<TEventHandler = Function>(channel: TChannel): TEventHandler[];
handler<TEventHandler extends Function = Function>(channel: TChannel): TEventHandler[];
publish<TEventHandler extends Function = Function>(
channel: TChannel,
...args: Parameters<TEventHandler>
): void;
}

class _Subscription<TChannel> implements Subscription {
Expand Down Expand Up @@ -62,14 +68,21 @@ class _SubscriptionManager<TChannel> implements SubscriptionManager<TChannel> {
}
}

handler<TEventHandler = Function>(channel: TChannel): TEventHandler[] {
handler<TEventHandler extends Function = Function>(channel: TChannel): TEventHandler[] {
const subscriptionIds = this.subscriptionForChannel(channel);
if (subscriptionIds.length === 0) {
return [];
}
return subscriptionIds.map((sub) => this.handlerForSubscriptionId<TEventHandler>(sub));
}

publish<TEventHandler extends Function = Function>(
channel: TChannel,
...args: Parameters<TEventHandler>
): void {
this.handler<TEventHandler>(channel).forEach((h: TEventHandler) => h(...args));
}

private handlerForSubscriptionId<TEventHandler>(subscriptionId: SubscriptionId): TEventHandler {
return this._handlerForSubscription.get(subscriptionId)!;
}
Expand Down

0 comments on commit fe475ee

Please sign in to comment.