Home Reference Source

core/observable.js

import Promise from 'es6-promise';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import { rxSubscriber } from 'rxjs/symbol/rxSubscriber';
import isFunction from 'lodash/isFunction';
import { isDefined, isPromiseLike, wrapInPromise } from './utils';

/**
 * @private
 */
const emptyObserver = {
  closed: true,
  next() { /* noop */ },
  error(err) { throw err; },
  complete() { /* noop */ },
  status() { /* noop */ },
  presence() { /* noop */ }
};

/**
 * @private
 */
class SafeSubscriber extends Subscriber {
  constructor(_parentSubscriber, observerOrNext, error, complete, status, presence) {
    super();
    let next;
    let context = this;
    this._parentSubscriber = _parentSubscriber;

    if (isFunction(observerOrNext)) {
      next = observerOrNext;
    } else if (observerOrNext) {
      next = observerOrNext.next;
      error = observerOrNext.error;
      complete = observerOrNext.complete;
      if (observerOrNext !== emptyObserver) {
        context = Object.create(observerOrNext);
        if (isFunction(context.unsubscribe)) {
          this.add(context.unsubscribe.bind(context));
        }
        context.unsubscribe = this.unsubscribe.bind(this);
      }
    }

    this._context = context;
    this._next = next;
    this._error = error;
    this._complete = complete;
    this._status = status;
    this._presence = presence;
  }

  next(value) {
    if (!this.isStopped && this._next) {
      const { _parentSubscriber } = this;
      if (!_parentSubscriber.syncErrorThrowable) {
        this.__tryOrUnsub(this._next, value);
      } else if (this.__tryOrSetError(_parentSubscriber, this._next, value)) {
        this.unsubscribe();
      }
    }
  }

  error(err) {
    if (!this.isStopped) {
      const { _parentSubscriber } = this;
      if (this._error) {
        if (!_parentSubscriber.syncErrorThrowable) {
          this.__tryOrUnsub(this._error, err);
          this.unsubscribe();
        } else {
          this.__tryOrSetError(_parentSubscriber, this._error, err);
          this.unsubscribe();
        }
      } else if (!_parentSubscriber.syncErrorThrowable) {
        this.unsubscribe();
        throw err;
      } else {
        _parentSubscriber.syncErrorValue = err;
        _parentSubscriber.syncErrorThrown = true;
        this.unsubscribe();
      }
    }
  }

  complete() {
    if (!this.isStopped) {
      const { _parentSubscriber } = this;
      if (this._complete) {
        const wrappedComplete = () => this._complete.call(this._context);

        if (!_parentSubscriber.syncErrorThrowable) {
          this.__tryOrUnsub(wrappedComplete);
          this.unsubscribe();
        } else {
          this.__tryOrSetError(_parentSubscriber, wrappedComplete);
          this.unsubscribe();
        }
      } else {
        this.unsubscribe();
      }
    }
  }

  status(value) {
    if (!this.isStopped && this._status) {
      const { _parentSubscriber } = this;
      if (!_parentSubscriber.syncErrorThrowable) {
        this.__tryOrUnsub(this._status, value);
      } else if (this.__tryOrSetError(_parentSubscriber, this._status, value)) {
        this.unsubscribe();
      }
    }
  }

  presence(value) {
    if (!this.isStopped && this._presence) {
      const { _parentSubscriber } = this;
      if (!_parentSubscriber.syncErrorThrowable) {
        this.__tryOrUnsub(this._presence, value);
      } else if (this.__tryOrSetError(_parentSubscriber, this._presence, value)) {
        this.unsubscribe();
      }
    }
  }

  __tryOrUnsub(fn, value) {
    try {
      fn.call(this._context, value);
    } catch (err) {
      this.error(err);
      throw err;
    }
  }

  __tryOrSetError(parent, fn, value) {
    try {
      fn.call(this._context, value);
    } catch (err) {
      parent.syncErrorValue = err;
      parent.syncErrorThrown = true;
      return true;
    }
    return false;
  }

  _unsubscribe() {
    const { _parentSubscriber } = this;
    this._context = null;
    this._parentSubscriber = null;
    _parentSubscriber.unsubscribe();
  }
}

/**
 * @private
 */
class KinveySubscriber extends Subscriber {
  constructor(observerOrNext, error, complete, status, presence) {
    super(observerOrNext, error, complete);

    switch (arguments.length) {
      case 0:
        this.destination = emptyObserver;
        break;
      case 1:
        if (isDefined(observerOrNext) === false) {
          this.destination = emptyObserver;
          break;
        }
        if (typeof observerOrNext === 'object') {
          if (observerOrNext instanceof Subscriber) {
            this.destination = observerOrNext;
            this.destination.add(this);
          } else {
            this.syncErrorThrowable = true;
            this.destination = new SafeSubscriber(this, observerOrNext);
          }
          break;
        }
      default:
        this.syncErrorThrowable = true;
        this.destination = new SafeSubscriber(this, observerOrNext, error, complete, status, presence);
        break;
    }
  }

  status(value) {
    if (!this.isStopped) {
      this._status(value);
    }
  }

  presence(value) {
    if (!this.isStopped) {
      this._presence(value);
    }
  }

  _status(value) {
    this.destination.status(value);
  }

  _presence(value) {
    this.destination.presence(value);
  }
}

/**
 * @private
 */
function toSubscriber(observerOrNext, error, complete, status, presence) {
  if (observerOrNext) {
    if (observerOrNext instanceof KinveySubscriber) {
      return observerOrNext;
    }

    if (observerOrNext[rxSubscriber]) {
      return observerOrNext[rxSubscriber]();
    }
  }

  if (!observerOrNext && !error && !complete && !status && !presence) {
    return new KinveySubscriber(emptyObserver);
  }

  return new KinveySubscriber(observerOrNext, error, complete, status, presence);
}


/**
 * @private
 */
export class KinveyObservable extends Observable {
  subscribe(observerOrNext, error, complete, status, presence) {
    const { operator } = this;
    const sink = toSubscriber(observerOrNext, error, complete, status, presence);

    if (operator) {
      operator.call(sink, this.source);
    } else {
      sink.add(this._trySubscribe(sink));
    }

    if (sink.syncErrorThrowable) {
      sink.syncErrorThrowable = false;
      if (sink.syncErrorThrown) {
        throw sink.syncErrorValue;
      }
    }

    return sink;
  }

  toPromise() {
    return new Promise((resolve, reject) => {
      let value;
      this.subscribe((v) => {
        value = v;
      }, reject, () => {
        resolve(value);
      });
    });
  }

  static create(subscriber) {
    return new KinveyObservable(subscriber);
  }
}

/**
 * @private
 */
export function wrapInObservable(promiseOrFunc, completeAfter = true) {
  const argIsPromise = isPromiseLike(promiseOrFunc);

  const stream = KinveyObservable.create((observer) => {
    let promise;
    if (argIsPromise) {
      promise = promiseOrFunc;
    } else {
      const funcResult = promiseOrFunc(observer);
      promise = wrapInPromise(funcResult); // func can be sync or async
    }

    promise
      .then((result) => {
        if (argIsPromise) {
          observer.next(result);
        }
        if (completeAfter) {
          observer.complete();
        }
      })
      .catch(err => observer.error(err));
  });

  return stream;
}