UNPKG

ember-concurrency

Version:

Improved concurrency/async primitives for Ember.js

191 lines (185 loc) 6.42 kB
import { assert } from '@ember/debug'; import { schedule, cancel } from '@ember/runloop'; import { get } from '@ember/object'; import { addObserver, removeObserver } from '@ember/object/observers'; import { isEventedObject, EmberYieldable } from './utils.js'; class WaitForQueueYieldable extends EmberYieldable { constructor(queueName) { super(); this.queueName = queueName; } onYield(state) { let timerId; try { timerId = schedule(this.queueName, () => state.next()); } catch (error) { state.throw(error); } return () => cancel(timerId); } } class WaitForEventYieldable extends EmberYieldable { constructor(object, eventName) { super(); this.object = object; this.eventName = eventName; this.usesDOMEvents = false; } on(callback) { if (typeof this.object.addEventListener === 'function') { // assume that we're dealing with a DOM `EventTarget`. this.usesDOMEvents = true; this.object.addEventListener(this.eventName, callback); } else { this.object.on(this.eventName, callback); } } off(callback) { if (this.usesDOMEvents) { this.object.removeEventListener(this.eventName, callback); } else { this.object.off(this.eventName, callback); } } onYield(state) { let fn = null; let disposer = () => { fn && this.off(fn); fn = null; }; fn = event => { disposer(); state.next(event); }; this.on(fn); return disposer; } } class WaitForPropertyYieldable extends EmberYieldable { constructor(object, key, predicateCallback = Boolean) { super(); this.object = object; this.key = key; if (typeof predicateCallback === 'function') { this.predicateCallback = predicateCallback; } else { this.predicateCallback = v => v === predicateCallback; } } onYield(state) { let observerBound = false; let observerFn = () => { let value = get(this.object, this.key); let predicateValue = this.predicateCallback(value); if (predicateValue) { state.next(value); return true; } }; if (!observerFn()) { // eslint-disable-next-line ember/no-observers addObserver(this.object, this.key, null, observerFn); observerBound = true; } return () => { if (observerBound && observerFn) { removeObserver(this.object, this.key, null, observerFn); } }; } } /** * Use `waitForQueue` to pause the task until a certain run loop queue is reached. * * ```js * import { task, waitForQueue } from 'ember-concurrency'; * export default class MyComponent extends Component { * @task *myTask() { * yield waitForQueue('afterRender'); * console.log("now we're in the afterRender queue"); * } * } * ``` * * @param {string} queueName the name of the Ember run loop queue */ function waitForQueue(queueName) { return new WaitForQueueYieldable(queueName); } /** * Use `waitForEvent` to pause the task until an event is fired. The event * can either be a jQuery event or an Ember.Evented event (or any event system * where the object supports `.on()` `.one()` and `.off()`). * * ```js * import { task, waitForEvent } from 'ember-concurrency'; * export default class MyComponent extends Component { * @task *myTask() { * console.log("Please click anywhere.."); * let clickEvent = yield waitForEvent($('body'), 'click'); * console.log("Got event", clickEvent); * * let emberEvent = yield waitForEvent(this, 'foo'); * console.log("Got foo event", emberEvent); * * // somewhere else: component.trigger('foo', { value: 123 }); * } * } * ``` * * @param {object} object the Ember Object, jQuery element, or other object with .on() and .off() APIs * that the event fires from * @param {function} eventName the name of the event to wait for */ function waitForEvent(object, eventName) { assert(`${object} must include Ember.Evented (or support \`.on()\` and \`.off()\`) or DOM EventTarget (or support \`addEventListener\` and \`removeEventListener\`) to be able to use \`waitForEvent\``, isEventedObject(object)); return new WaitForEventYieldable(object, eventName); } /** * Use `waitForProperty` to pause the task until a property on an object * changes to some expected value. This can be used for a variety of use * cases, including synchronizing with another task by waiting for it * to become idle, or change state in some other way. If you omit the * callback, `waitForProperty` will resume execution when the observed * property becomes truthy. If you provide a callback, it'll be called * immediately with the observed property's current value, and multiple * times thereafter whenever the property changes, until you return * a truthy value from the callback, or the current task is canceled. * You can also pass in a non-Function value in place of the callback, * in which case the task will continue executing when the property's * value becomes the value that you passed in. * * ```js * import { task, waitForProperty } from 'ember-concurrency'; * export default class MyComponent extends Component { * @tracked foo = 0; * * @task *myTask() { * console.log("Waiting for `foo` to become 5"); * * yield waitForProperty(this, 'foo', v => v === 5); * // alternatively: yield waitForProperty(this, 'foo', 5); * * // somewhere else: this.foo = 5; * * console.log("`foo` is 5!"); * * // wait for another task to be idle before running: * yield waitForProperty(this, 'otherTask.isIdle'); * console.log("otherTask is idle!"); * } * } * ``` * * @param {object} object an object (most likely an Ember Object) * @param {string} key the property name that is observed for changes * @param {function} callbackOrValue a Function that should return a truthy value * when the task should continue executing, or * a non-Function value that the watched property * needs to equal before the task will continue running */ function waitForProperty(object, key, predicateCallback) { return new WaitForPropertyYieldable(object, key, predicateCallback); } export { waitForEvent, waitForProperty, waitForQueue }; //# sourceMappingURL=wait-for.js.map