Home Reference Source

packages/remote-context/src/RemotePromise.js

/**
 * Copyright 2017 Moshe Simantov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Action } from 'remote-protocol';
import RemoteSession from './RemoteSession';
import RemoteValue from './RemoteValue';
import {
  ReflectApplyAction,
  ReflectConstructAction,
  ReflectGetAction,
  ReflectPromiseAction,
} from './actions';

const kAction = Symbol('action');

/**
 * @example
 * const promise = new RemotePromise(remoteSession, action);
 *
 * // resolve the action value
 * promise.then(value => {
 *   // `value` is the resolve remote action value
 * });
 *
 * // resolve property of the action value
 * promise.find().then(value => {
 *   // `value` === resolveOnRemote(action.fetch().find())
 * });
 */
export default class RemotePromise {
  /**
   * Create a remote promise from a given action.
   *
   * @param {RemoteSession} session The remote session
   * @param {Action} action The given action
   * @param {object} [opts] {@link Session#request} options
   * @return {function} The remote promise
   */
  constructor(session, action, opts = {}) {
    if (!(session instanceof RemoteSession)) {
      throw new TypeError(
        `Expect "session" param to be instance of RemoteSession: ${session}`
      );
    }
    if (!(action instanceof Action)) {
      throw new TypeError(
        `Expect "action" param to be instance of Action: ${action}`
      );
    }

    // eslint-disable-next-line func-names
    const func = function() {
      throw new ReferenceError(
        "This is a remote-promise function and it should'nt be execute"
      );
    };

    func[kAction] = action;
    let promise;

    func.then = function then(onFulfilled, onRejected) {
      if (!(kAction in this)) {
        throw new ReferenceError(
          `Invalid 'this' bind to RemotePromise#then: ${this}`
        );
      }

      if (!promise) {
        promise = new Promise((resolve, reject) => {
          let reflectAction;
          const rejectOnSessionClose = () => {
            reflectAction.release(session);
            reject(new Error('Session closed before promise resolved'));
          };

          reflectAction = ReflectPromiseAction.fromValue(
            session,
            this[kAction],
            value => {
              session.removeListener('close', rejectOnSessionClose);
              reflectAction.release(session);

              RemoteValue.resolve(value).then(resolve, reject);
            },
            error => {
              session.removeListener('close', rejectOnSessionClose);
              reflectAction.release(session);

              reject(error);
            }
          );

          session.request(
            reflectAction,
            opts,
            () => {
              session.once('close', rejectOnSessionClose);
            },
            error => {
              reflectAction.release(session);
              reject(error);
            }
          );
        });
      }

      return promise.then(onFulfilled, onRejected);
    };

    func.catch = function promiseCatch(onRejected) {
      return this.then(null, onRejected);
    };

    return new Proxy(func, {
      get(target, property) {
        if (property in target) return target[property];

        return new RemotePromise(
          session,
          ReflectGetAction.fromProxy(session, action, property),
          opts
        );
      },

      apply(target, thisArg, argumentsList) {
        return new RemotePromise(
          session,
          ReflectApplyAction.fromProxy(session, action, thisArg, argumentsList),
          opts
        );
      },

      construct(target, argumentsList) {
        return new RemotePromise(
          session,
          ReflectConstructAction.fromProxy(session, action, argumentsList),
          opts
        );
      },
    });
  }
}