Home Reference Source

packages/remote-context/src/LocalContext.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 AssignableContext from './AssignableContext';
import RemoteSession from './RemoteSession';
import { RemoteSetAction, LocalReferenceAction } from './actions';
import LocalReference from './LocalReference';
import LocalSnapshot from './LocalSnapshot';

const kSession = Symbol('session');
const kSessionListener = Symbol('sessionListener');
const kDispatchSet = Symbol('dispatchSet');
const kDispatchSnapshots = Symbol('dispatchSnapshots');

/**
 * A local context that will represent the remote context of the other peer.
 */
export default class LocalContext extends AssignableContext {
  /**
   * @param {RemoteSession} session
   * @param {ReferenceContext} parentContext
   * @param {object} opts Options for {@link AssignableContext.constructor}
   */
  constructor(session, parentContext, opts = {}) {
    if (!(session instanceof RemoteSession)) {
      throw new TypeError(
        `Expect session to be instance of RemoteSession: ${session}`
      );
    }
    super({}, parentContext, opts);

    /**
     * @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 {Set}
     * @private
     */
    this[kDispatchSet] = new Set();

    /**
     * @type {Map}
     * @private
     */
    this[kDispatchSnapshots] = 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);
  }

  /**
   * Generate a remote action for dispatching the given value remotely.
   *
   * @param {*} value The given value
   * @return {Action}
   */
  dispatch(value) {
    if (!this[kSession]) {
      throw new TypeError('RemoteSession already closed');
    }

    const reference = this.assign(value);

    if (!this[kDispatchSet].has(reference)) {
      this[kDispatchSet].add(reference);

      const snapshot = new LocalSnapshot(value);
      this[kDispatchSnapshots].set(value, snapshot);

      return RemoteSetAction.fromSnapshot(this[kSession], reference, snapshot);
    }

    const localReference = new LocalReference(this[kSession], reference);
    this[kDispatchSnapshots].get(value).update(localReference);

    return new LocalReferenceAction(reference);
  }

  /**
   * @override
   */
  set(reference, value) {
    this[kDispatchSet].delete(reference);

    return super.set(reference, value);
  }

  /**
   * @override
   */
  delete(reference) {
    this[kDispatchSet].delete(reference);
    return super.delete(reference);
  }

  /**
   * @override
   */
  release(value) {
    this[kDispatchSnapshots].delete(value);
    return super.release(value);
  }

  /**
   * @override
   */
  clear() {
    this[kDispatchSet].clear();
    this[kDispatchSnapshots].clear();

    return super.clear();
  }

  /**
   * End the remote session.
   *
   * @return {LocalContext}
   */
  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 LocalContext)) {
        this[kSession].destroy();
      }
    }

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

    this.clear();
  }
}