Home Reference Source

packages/remote-context/src/RemoteContext.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 ReferenceContext from 'reference-context';
import RemoteSession from './RemoteSession';
import RemoteValue, { revoke, reveal } from './RemoteValue';
import RemotePromise from './RemotePromise';
import LocalPromise from './LocalPromise';

const { GetAction, DeleteAction, RemoteReferenceAction } = require('./actions');

const kSession = Symbol('session');
const kSessionListener = Symbol('sessionListener');
const kFetchRequests = Symbol('fetchRequests');

/**
 * A remote context that will represent the local context of the other peer.
 * @extends {ReferenceContext}
 * @example
 * import { RemoteContext } from 'remote-context';
 *
 * const remoteContext = new RemoteContext(session, parentContext);
 */
export default class RemoteContext extends ReferenceContext {
  /**
   * Create a remote context with given remote session and environment context.
   *
   * @param {RemoteSession} session The remote session
   * @param {ReferenceContext} parentContext The environment context
   */
  constructor(session, parentContext) {
    if (!(session instanceof RemoteSession)) {
      throw new TypeError(
        `Expect session to be instance of RemoteSession: ${session}`
      );
    }

    super({}, parentContext);

    /**
     * @type {RemoteSession}
     * @private
     */
    this[kSession] = session;

    /**
     * @type {function}
     * @private
     * @return {void}
     */
    this[kSessionListener] = () => {
      session.removeListener('close', this[kSessionListener]);

      this[kSessionListener] = null;
      this[kSession] = null;

      this.destroy();
    };
    session.on('close', this[kSessionListener]);

    /**
     * @type {Map}
     * @private
     */
    this[kFetchRequests] = new Map();
  }

  /**
   * Get the context remote session.
   *
   * @type {RemoteSession}
   */
  get session() {
    return this[kSession];
  }

  /**
   * @override
   */
  closure() {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    return new this.constructor(this[kSession], this);
  }

  /**
   * Fetch a remote reference and return a remote promise.
   * If the value already fetched, return a remote promise of the same value.
   *
   * @param {Reference} reference The remote reference to fetch
   * @param {object} [opts] {@link RemotePromise.constructor} options
   * @return {RemotePromise}
   */
  fetch(reference, opts = {}) {
    let promise = this[kFetchRequests].get(reference);
    if (promise) return promise;

    promise = new RemotePromise(this[kSession], new GetAction(reference), opts);
    this[kFetchRequests].set(reference, promise);

    return promise;
  }

  /**
   * Resolve a remote value and return a remote promise to that value.
   * If the value is not a remote value, return a local promise to the same value.
   *
   * @param {*} value The remote value to resolve
   * @param {object} [opts] {@link RemoteContext.fetch} options
   * @return {RemotePromise|LocalPromise}
   */
  resolve(value, opts = {}) {
    if (!(value instanceof Object) || !this.exists(value)) {
      return new LocalPromise(value);
    }

    const reference = this.lookup(value);
    this[kFetchRequests].delete(reference);

    return this.fetch(reference, opts);
  }

  /**
   * Generate a remote action for dispatching the given value remotely.
   *
   * @param {*} value The given value
   * @throws {ReferenceError} If the value is not {@link RemoteContext#exists}
   * @return {Action}
   */
  dispatch(value) {
    return new RemoteReferenceAction(this.lookup(value));
  }

  /**
   * Get an {@link RemoteValue} instance for the given reference value.
   *
   * @param {Reference} reference The remote reference
   * @throws {ReferenceError} If the context not {@link RemoteContext#has} the given reference
   * @return {RemoteValue}
   */
  getTarget(reference) {
    return reveal(this.get(reference));
  }

  /**
   * @override
   */
  set(reference, value) {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    const remoteValue = new RemoteValue(this[kSession], reference, value);
    return super.set(reference, remoteValue);
  }

  /**
   * @override
   */
  delete(reference) {
    this[kFetchRequests].delete(reference);
    if (!super.delete(reference)) return false;

    if (this[kSession] && !this[kSession].isEnded) {
      this[kSession].send(new DeleteAction(reference));
    }

    return true;
  }

  /**
   * @override
   */
  release(value) {
    if (!this.parent || !this.parent.exists(value)) {
      revoke(value);
    }

    return super.release(value);
  }

  /**
   * @override
   */
  clear() {
    this.forEach((value, reference) => {
      if (this[kSession] && !this[kSession].isEnded) {
        this[kSession].send(new DeleteAction(reference));
      }

      if (!this.parent || !this.parent.exists(value)) {
        revoke(value);
      }
    });

    this[kFetchRequests].clear();
    super.clear();
  }

  /**
   * End the remote session.
   *
   * @return {RemoteContext}
   */
  end() {
    if (this[kSession]) {
      this[kSession].end();
    }

    return this;
  }

  /**
   * Clear the context and destroy the context session only if this context IS NOT created via
   * closure.
   *
   * @return {void}
   */
  destroy() {
    if (this[kSession]) {
      if (this[kSessionListener]) {
        this[kSession].removeListener('close', this[kSessionListener]);
      }

      if (!(this.parent instanceof RemoteContext)) {
        this[kSession].destroy();
      }
    }

    delete this[kSession];
    delete this[kSessionListener];

    this.clear();
  }

  /**
   * Listen to an event on the remote session.
   *
   * @param {string} event The event to listen to
   * @param {function} listener The listener
   * @return {RemoteContext}
   */
  on(event, listener) {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    this[kSession].on(event, listener);
    return this;
  }

  /**
   * Listen once to an event on the remote session.
   *
   * @param {string} event The event to listen to
   * @param {function} listener The listener
   * @return {RemoteContext}
   */
  once(event, listener) {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    this[kSession].once(event, listener);
    return this;
  }

  /**
   * Remove listener from the remote session.
   *
   * @param {string} event The event on the listener
   * @param {function} listener The listener
   * @return {RemoteContext}
   */
  removeListener(event, listener) {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    this[kSession].removeListener(event, listener);
    return this;
  }
}