UNPKG

ember-lifeline

Version:

Ember.js addon for lifecycle aware async tasks and DOM events.

224 lines (198 loc) 5.84 kB
import Ember from 'ember'; import getTask from './utils/get-task.js'; import { registerDestructor } from '@ember/destroyable'; function indexable(input) { return input; } /** * A map of instances/poller functions that allows us to * store poller tokens per instance. * * @private * */ let registeredPollers = new WeakMap(); /** * Test use only. Allows for swapping out the WeakMap to a Map, giving * us the ability to detect whether the pollers set is empty. * * @private * @param {*} mapForTesting A map used to ensure correctness when testing. */ function _setRegisteredPollers(mapForTesting) { registeredPollers = mapForTesting; } let token = 0; let _shouldPollOverride; function shouldPoll() { if (_shouldPollOverride) { return _shouldPollOverride(); } return !Ember.testing; } /** * Allows for overriding of the polling behavior to explicitly control * whether polling should occur or not. * * @param { Function } callback * @public */ function setShouldPoll(callback) { _shouldPollOverride = callback; } function getQueuedPollTasks() { let symbol = Symbol.for('LIFELINE_QUEUED_POLL_TASKS'); let globalObj = getGlobal(); let queuedPollTasks = globalObj[symbol]; if (!queuedPollTasks) { queuedPollTasks = globalObj[symbol] = new Map(); } return queuedPollTasks; } /** * Sets up a function that can perform polling logic in a testing safe way. * The task is invoked synchronously with an argument (generally called `next`). * In normal development/production when `next` is invoked, it will trigger the * task again (recursively). However, when in test mode the recursive polling * functionality is disabled, and usage of the `pollTaskFor` helper is required. * * Example: * * ```js * import Component from '@glimmer/component'; * import { pollTask, runTask } from 'ember-lifeline'; * * export default PollingComponent extends Component { * api: injectService(), * * init() { * this._super(...arguments); * * let token = pollTask(this, (next) => { * this.get('api').request('get', 'some/path') * .then(() => { * runTask(this, next, 1800); * }) * }); * * this._pollToken = token; * }, * }); * ``` * * Test Example: * * ```js * import { settled } from '@ember/test-helpers'; * import { pollTaskFor } from 'ember-lifeline'; * * * test('foo-bar watches things', async function(assert) { * await render(hbs`{{foo-bar}}`); * * assert.strictEqual(serverRequests, 1, 'called initially'); * * pollTaskFor(this._pollToken); * * await settled(); * * assert.strictEqual(serverRequests, 2, 'called again'); * * await settled(); * }); * ``` * * @method pollTask * @param { Destroyable } destroyable the entangled object that was provided with the original *Task call * @param { TaskOrName } taskOrName a function representing the task, or string * specifying a property representing the task, * which is run at the provided time specified * by timeout * @param { Token } token the Token for the pollTask, either a string or number * @public */ function pollTask(destroyable, taskOrName, token = getNextToken()) { let next; let task = getTask(destroyable, taskOrName, 'pollTask'); let tick = () => task.call(destroyable, next); let pollers = registeredPollers.get(destroyable); if (!pollers) { pollers = new Set(); registeredPollers.set(destroyable, pollers); registerDestructor(destroyable, getPollersDisposable(destroyable, pollers)); } pollers.add(token); if (shouldPoll()) { next = tick; } else { next = () => { getQueuedPollTasks().set(token, tick); }; } task.call(destroyable, next); return token; } /** * Clears a previously setup polling task. * * NOTE: This does not cancel any nested `runTask` calls. You're required to cancel any * cancelable behaviors, including any calls to `runTask` using `cancelTask`. * * Example: * * ```js * import Component from '@glimmer/component'; * import { inject as service } from '@ember/service'; * import { pollTask, runTask } from 'ember-lifeline'; * * export default AutoRefreshComponent extends Component { * @service api; * * enableAutoRefresh() { * this._pollToken = pollTask(this, (next) => { * this.get('api').request('get', 'some/path') * .then(() => { * runTask(this, next, 1800); * }) * }); * }, * * disableAutoRefresh() { * cancelPoll(this, this._pollToken); * }, * } * ``` * * @method cancelPoll * @param { Destroyable } destroyable the entangled object that was provided with the original *Task call * @param { Token } token the Token for the pollTask to be cleared, either a string or number * @public */ function cancelPoll(destroyable, token) { let pollToken; let pollers = registeredPollers.get(destroyable); pollToken = token; if (pollers !== undefined) { pollers.delete(pollToken); } getQueuedPollTasks().delete(pollToken); } function getGlobal() { // eslint-disable-next-line n/no-unsupported-features/es-builtins if (typeof globalThis !== 'undefined') return indexable(globalThis); if (typeof self !== 'undefined') return indexable(self); if (typeof window !== 'undefined') return indexable(window); if (typeof global !== 'undefined') return indexable(global); throw new Error('unable to locate global object'); } function getPollersDisposable(destroyable, pollers) { return function () { pollers.forEach(token => { cancelPoll(destroyable, token); }); }; } function getNextToken() { return token++; } export { _setRegisteredPollers, cancelPoll, getQueuedPollTasks, pollTask, setShouldPoll };