UNPKG

send-and-receive

Version:

Two small helper methods that simplify communication between nodes in different subtrees of the browser DOM.

226 lines (224 loc) 6.78 kB
function defineProperty(obj, name, getter) { Object.defineProperty(obj, name, { get: getter, enumerable: true, configurable: false, }); } function createSubsription(context) { const subscription = {}; defineProperty(subscription, 'received', () => context.received); defineProperty(subscription, 'remaining', () => context.remaining); defineProperty(subscription, 'cancelled', () => context.cancelled); defineProperty(subscription, 'cancel', () => context.cancel); defineProperty(subscription, 'paused', () => context.paused); defineProperty(subscription, 'pause', () => context.pause); defineProperty(subscription, 'resume', () => context.resume); return subscription; } /** * Dispatches an event of the specified `type` with the * specified `data` (optional). * * @param type The name of the event. * @param data The optional payload of the event. * @example * * send('player:play', { src: 'song.mp3' }); */ function send(type, data) { return dispatchEvent(new CustomEvent(type, { detail: data })); } const DEFAULT_RECEIVE_OPTIONS = { limit: Number.POSITIVE_INFINITY, }; /** * Listens on dispatched events of the specified `type` * and, when it receives one, invokes `callback` with the * data passed when sending. * * @param type The name of the event. * @param callback The function to invoke. * @param options The configuration options. * @returns A subscription object. * @example * * const subscription = receive('player:play', (data) => { * doSomethingWith(data); * }); * * @description * * Use the returned subscription object to retrieve some * metadata or to cancel receiving further events: * * @example * * subscription.received //=> How often has the event been received? * subscription.remaining //=> How many remaining events can it receive? * * subscription.cancelled //=> Did we completely opt out of receiving further events? * subscription.cancel() //=> Unlisten from the event and set cancelled status. * * subscription.paused //=> Did we temporarily stop receiving further events? * subscription.pause() //=> Pause listening and set paused status. * subscription.resume() //=> Resume listening and unset paused status. * * @description * * Note that both `subscription.pause()` and `subscription.resume()` * will throw an error if the subscription has been cancelled. * * By default, the number of events it can receive is not limited, which * means `subscription.remaining` will always return *positive infinity*. * * Besides calling `subscription.cancel()` in order to stop listening to * further events, you can also restrict the number of times the event * will be received by supplying the `limit` option: * * @example * * receive('player:play', callback, { limit: 1 }); * * @description * * Here, after the event has been received once, it will be auto-cancelled. * Furthermore, the subscription's `received` property will have changed * from `0` to `1`, and the `remaining` property from `1` to `0`. */ function receive(type, callback, { limit } = DEFAULT_RECEIVE_OPTIONS) { if (limit <= 0) { throw new RangeError('limit must be greater than 0'); } const handler = (event) => { if (context.received < limit) { context.received++; context.remaining--; if (context.remaining === 0) { context.cancel(); } callback(event.detail); } }; const context = { received: 0, remaining: limit, cancelled: false, cancel() { if (context.cancelled) return; removeEventListener(type, handler, false); context.cancelled = true; }, paused: false, pause() { if (context.cancelled) { throw new Error('cannot pause a cancelled subscription'); } if (context.paused) return; removeEventListener(type, handler, false); context.paused = true; }, resume() { if (context.cancelled) { throw new Error('cannot resume a cancelled subscription'); } if (!context.paused) return; addEventListener(type, handler, false); context.paused = false; }, }; addEventListener(type, handler, false); return createSubsription(context); } /** * A convenience method for the case when you want to receive * the event only once. * * @param type The name of the event. * @param callback The function to invoke. * @returns A subscription object. * @example * * receiveOnce('player:play', callback); * * @description * * This is semantically the same as calling `receive` with * `{ limit: 1 }` as options. */ function receiveOnce(type, callback) { return receive(type, callback, { limit: 1 }); } /** * A convenience method to create both a sender function and * a receiver function for the specified `type`. * * @param type The name of the event. * @returns An array of size 2, where the first element is the * send function and the second element the receive function. * * @description * * This method is especially useful when coding in TypeScript, * as it allows strict-typing the `data`: * * @example * * // a.ts * import { create } from 'send-and-receive'; * * const [sendPlay, receivePlay] = create<Song>('player:play'); * * export { receivePlay }; * * // later on (button click, etc.) * sendPlay({ src: 'song.mp3' }); * * // b.ts * import { receivePlay } from './a.js'; * * receivePlay((song) => { * doSomethingWith(song.src); * }); * * @description * * Optionally, you can pass a function as the second argument which * transforms the arguments passed to `send` into the data structure * supplied to the `receive` callback: * * @example * * interface Options { * action: "push" | "replace"; * } * * const [navigateTo, receiveNavigateTo] = create( * "navigate-to", * (url: string, options: Options = { action: "push" }) => ({ * ...options, * url, * }) * ); * * // send... * navigateTo("/foo", { action: "replace" }); * * // receive... * receiveNavigateTo(({ url, action }) => history[action](url)); */ function create(type, buildData = identity) { return [ function createdSend(...args) { return send(type, buildData(...args)); }, function createdReceive(callback, options = DEFAULT_RECEIVE_OPTIONS) { return receive(type, callback, options); }, ]; } const identity = (...args) => args[0]; export { create, receive, receiveOnce, send };