Home Reference Source

packages/remote-context/src/RemoteSession.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 { Session, Action } from 'remote-protocol';
import Context from './Context';
import LocalContext from './LocalContext';
import RemoteContext from './RemoteContext';
import { UndefinedValueAction } from './actions';
import RemoteReferenceAction from './actions/RemoteReferenceAction';
import LocalReferenceAction from './actions/LocalReferenceAction';

const kLocal = Symbol('local');
const kRemote = Symbol('remote');
const kBinds = Symbol('binds');
const kIsWritable = Symbol('isWritable');
const kShowStack = Symbol('showStack');

/**
 * A remote context {@link Session} to another peer.
 *
 * @extends {Session}
 * @example
 * import { RemoteSession } from 'remote-context';
 *
 * const session = new RemoteSession(objectStream, context);
 */
export default class RemoteSession extends Session {
  /**
   * Create a session from an object stream.
   *
   * @param {Stream} objectStream an actions object stream
   * @param {Context} context The main context
   * @param {options} [opts] {@link Session} options with extensions
   * @param {boolean} [opts.writable=false] True if this context is writable to the remote peer
   * @param {boolean} [opts.showStack=false] True if errors on this context should include full
   * stack (The stack will be send to the remote peer as a string)
   */
  constructor(objectStream, context, opts = {}) {
    if (!(context instanceof Context)) {
      throw new TypeError(`Invalid context: ${context}`);
    }

    super(objectStream, opts);

    /**
     * @type {LocalContext}
     * @private
     */
    this[kLocal] = new LocalContext(this, context);

    /**
     * @type {RemoteContext}
     * @private
     */
    this[kRemote] = new RemoteContext(this, context.parent);

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

    /**
     * @type {boolean}
     * @private
     */
    this[kIsWritable] = opts.writable === true;

    /**
     * @type {boolean}
     * @private
     */
    this[kShowStack] = opts.showStack === true;
  }

  // Session methods

  /**
   * Get the remote context
   *
   * @type {RemoteContext}
   */
  get remote() {
    if (!this[kRemote]) {
      throw new Error(`${this} already destroyed`);
    }

    return this[kRemote];
  }

  /**
   * True if the remote peer should see stack errors.
   *
   * @type {boolean}
   */
  get showStack() {
    return this[kShowStack];
  }

  /**
   * Check if the given value is writable.
   * A value is writable if the local-context own it (ie. the value created elusively for this
   * peer) or if the context is writable and the main-context own it. Environment values are
   * always readonly unless there are on the context.
   *
   * @param {*} value The given value
   * @return {boolean}
   */
  isWritable(value) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    return (
      this[kLocal].own(value) ||
      (this[kIsWritable] && this[kLocal].parent.own(value))
    );
  }

  /**
   * Assign a remote or local reference to the given value and return the matching {@link Action}.
   * First check if the given value exists on the remote context and if so, return it's reference.
   * Otherwise assign a new reference for the given value on the local context.
   *
   * @param {*} value
   * @return {ReferenceAction}
   */
  assign(value) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    if (this[kRemote].exists(value)) {
      return new RemoteReferenceAction(this[kRemote].lookup(value));
    }

    return new LocalReferenceAction(this[kLocal].assign(value));
  }

  /**
   * Dispatch a value for the remote peer session.
   * Objects and functions will convert to actions, other primitive values such as string or numbers
   * will remain as-is.
   *
   * @override
   * @param {*} value
   * @return {Action|null|number|boolean|string}
   */
  dispatch(value) {
    switch (typeof value) {
      case 'symbol':
      case 'function': {
        // break and convert to reference
        break;
      }

      case 'object': {
        if (value === null) return null;

        if (
          value === this ||
          value === this[kLocal] ||
          value === this[kLocal].parent ||
          value === this[kRemote]
        ) {
          throw new ReferenceError(
            "[Safety Check] Can't dispatch internal context instances"
          );
        }

        if (value instanceof Action) {
          throw new TypeError(
            "Bad behaviour, shouldn't dispatch Action instances"
          );
        }

        if (value instanceof Error && !this[kShowStack]) {
          delete value.stack; // eslint-disable-line no-param-reassign
        }

        // break and convert to reference
        break;
      }

      case 'undefined':
      default: {
        // `undefined` is not a valid JSON/MsgPack value
        return new UndefinedValueAction();
      }

      case 'number':
      case 'string':
      case 'boolean': {
        return value;
      }
    }

    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    if (this[kRemote].exists(value)) {
      return this[kRemote].dispatch(value);
    }

    return this[kLocal].dispatch(value);
  }

  /**
   * @override
   */
  destroy(err) {
    super.destroy(err);
    if (!this[kLocal]) return;

    this[kLocal].clear();
    this[kRemote].clear();
    this[kBinds].clear();

    delete this[kLocal];
    delete this[kRemote];
    delete this[kBinds];
  }

  // Context methods

  /**
   * Get a local reference value from the local-context.
   *
   * @param {Reference} reference The local reference
   * @return {*}
   */
  get(reference) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    return this[kLocal].get(reference);
  }

  /**
   * Set a local reference to the local-context.
   *
   * @param {Reference} reference The local reference
   * @param {*} value The local value
   * @return {RemoteSession}
   */
  set(reference, value) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    this[kLocal].set(reference, value);
    return this;
  }

  /**
   * Check if the given value exists on the local-context.
   *
   * @param {*} value The given value
   * @return {boolean} True if exists
   */
  exists(value) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    return this[kLocal].exists(value);
  }

  /**
   * Delete a local reference from the local-context.
   *
   * @param {Reference} reference The local reference
   * @return {boolean} True if the reference has been removed
   */
  delete(reference) {
    if (!this[kLocal]) {
      throw new Error(`${this} already destroyed`);
    }

    return this[kLocal].delete(reference);
  }
}