UNPKG

@wiko/subscribable

Version:

Helpers for creating subscription-based event emitters

122 lines (93 loc) 5.46 kB
[![npm][npm-image]][npm-url] [![npm-downloads][npm-downloads-image]][npm-url] <br /> [![code-style-prettier][code-style-prettier-image]][code-style-prettier-url] [code-style-prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square [code-style-prettier-url]: https://github.com/prettier/prettier [npm-downloads-image]: https://img.shields.io/npm/dm/@wiko/subscribable?style=flat [npm-image]: https://img.shields.io/npm/v/@wiko/subscribable?style=flat [npm-url]: https://www.npmjs.com/package/@wiko/subscribable # @wiko/subscribable This package contains utilities for creating subscription-based event targets. These differ from the `EventTarget` interface in that the method you use to add a listener returns an unsubscribe function. It is primarily intended for internal use &ndash; particularly for those building `RpcSubscriptionChannels` and associated infrastructure. ## Types ### `DataPublisher<TDataByChannelName>` This type represents an object with an `on` function that you can call to subscribe to certain data over a named channel. ```ts let dataPublisher: DataPublisher<{ error: WikoError }>; dataPublisher.on('data', handleData); // ERROR. `data` is not a known channel name. dataPublisher.on('error', e => { console.error(e); }); // OK. ``` ### `TypedEventEmitter<TEventMap>` This type allows you to type `addEventListener` and `removeEventListener` so that the call signature of the listener matches the event type given. ```ts const emitter: TypedEventEmitter<{ message: MessageEvent }> = new WebSocket('wss://rpc-devnet.wiko.org'); emitter.addEventListener('data', handleData); // ERROR. `data` is not a known event type. emitter.addEventListener('message', message => { console.log(message.origin); // OK. `message` is a `MessageEvent` so it has an `origin` property. }); ``` ### `TypedEventTarget<TEventMap>` This type is a superset of `TypedEventEmitter` that allows you to constrain calls to `dispatchEvent`. ```ts const target: TypedEventTarget<{ candyVended: CustomEvent<{ flavour: string }> }> = new EventTarget(); target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavour: 'raspberry' } })); // OK. target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavor: 'raspberry' } })); // ERROR. Misspelling in detail. ``` ## Functions ### `createAsyncIterableFromDataPublisher({ abortSignal, dataChannelName, dataPublisher, errorChannelName })` Returns an `AsyncIterable` given a data publisher. The iterable will produce iterators that vend messages published to `dataChannelName` and will throw the first time a message is published to `errorChannelName`. Triggering the abort signal will cause all iterators spawned from this iterator to return once they have published all queued messages. ```ts const iterable = createAsyncIterableFromDataPublisher({ abortSignal: AbortSignal.timeout(10_000), dataChannelName: 'message', dataPublisher, errorChannelName: 'error', }); try { for await (const message of iterable) { console.log('Got message', message); } } catch (e) { console.error('An error was published to the error channel', e); } finally { console.log("It's been 10 seconds; that's enough for now."); } ``` Things to note: - If a message is published over a channel before the `AsyncIterator` attached to it has polled for the next result, the message will be queued in memory. - Messages only begin to be queued after the first time an iterator begins to poll. Channel messages published before that time will be dropped. - If there are messages in the queue and an error occurs, all queued messages will be vended to the iterator before the error is thrown. - If there are messages in the queue and the abort signal fires, all queued messages will be vended to the iterator after which it will return. - Any new iterators created after the first error is encountered will reject with that error when polled. ### `demultiplexDataPublisher(publisher, sourceChannelName, messageTransformer)` Given a channel that carries messages for multiple subscribers on a single channel name, this function returns a new `DataPublisher` that splits them into multiple channel names. Imagine a channel that carries multiple notifications whose destination is contained within the message itself. ```ts const demuxedDataPublisher = demultiplexDataPublisher(channel, 'message', message => { const destinationChannelName = `notification-for:${message.subscriberId}`; return [destinationChannelName, message]; }); ``` Now you can subscribe to _only_ the messages you are interested in, without having to subscribe to the entire `'message'` channel and filter out the messages that are not for you. ```ts demuxedDataPublisher.on( 'notification-for:123', message => { console.log('Got a message for subscriber 123', message); }, { signal: AbortSignal.timeout(5_000) }, ); ``` ### `getDataPublisherFromEventEmitter(emitter)` Returns an object with an `on` function that you can call to subscribe to certain data over a named channel. The `on` function returns an unsubscribe function. ```ts const socketDataPublisher = getDataPublisherFromEventEmitter(new WebSocket('wss://rpc-devnet.wiko.org')); const unsubscribe = socketDataPublisher.on('message', message => { if (JSON.parse(message.data).id === 42) { console.log('Got response 42'); unsubscribe(); } }); ```