diff --git a/README.md b/README.md index 5a68e02..6da6b60 100644 --- a/README.md +++ b/README.md @@ -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 = 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 = createSubscriptionManager(); +``` + +### 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('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('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 { @@ -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 @@ -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 { @@ -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').forEach(h => h(data as ChatMessage)); - } else if (type === 'user') { - this.sm.handler('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', data); + } else if (type === 'user') { + this.sm.publish('onNewChatUser', data); + } + }); + } } ``` +That's it folks! + Stay tuned and **_keep on coding_**. diff --git a/src/SubscriptionManager.test.ts b/src/SubscriptionManager.test.ts index c5a8337..3fc5616 100644 --- a/src/SubscriptionManager.test.ts +++ b/src/SubscriptionManager.test.ts @@ -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('foo', 1); + sm.publish('foo', 2); + sm.publish('foo', 3); + + expect(handler).toBeCalledTimes(6); + + sub1.free(); + + handler.mockClear(); + + sm.publish('foo', 1); + sm.publish('foo', 2); + sm.publish('foo', 3); + + expect(handler).toBeCalledTimes(3); + + sub2.free(); + + handler.mockClear(); + + sm.publish('foo', 1); + sm.publish('foo', 2); + sm.publish('foo', 3); + + expect(handler).toBeCalledTimes(0); + }); }); diff --git a/src/SubscriptionManager.ts b/src/SubscriptionManager.ts index da0e7a6..314f23a 100644 --- a/src/SubscriptionManager.ts +++ b/src/SubscriptionManager.ts @@ -7,12 +7,18 @@ export interface Subscription { free(): void; } +// type ReturnType = T extends (... args: any[]) => infer T ? T : never; +export type Parameters = T extends (...args: infer T) => any ? T : never; export interface SubscriptionManager { - subscribe( + subscribe( channel: TChannel, eventHandler: TEventHandler, ): Subscription; - handler(channel: TChannel): TEventHandler[]; + handler(channel: TChannel): TEventHandler[]; + publish( + channel: TChannel, + ...args: Parameters + ): void; } class _Subscription implements Subscription { @@ -62,7 +68,7 @@ class _SubscriptionManager implements SubscriptionManager { } } - handler(channel: TChannel): TEventHandler[] { + handler(channel: TChannel): TEventHandler[] { const subscriptionIds = this.subscriptionForChannel(channel); if (subscriptionIds.length === 0) { return []; @@ -70,6 +76,13 @@ class _SubscriptionManager implements SubscriptionManager { return subscriptionIds.map((sub) => this.handlerForSubscriptionId(sub)); } + publish( + channel: TChannel, + ...args: Parameters + ): void { + this.handler(channel).forEach((h: TEventHandler) => h(...args)); + } + private handlerForSubscriptionId(subscriptionId: SubscriptionId): TEventHandler { return this._handlerForSubscription.get(subscriptionId)!; }