Home Reference Source

packages/remote-context/src/actions/RemoteSetObjectAction.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 { trimArgumentsList } from 'remote-instance';
import RemoteSetAction from './RemoteSetAction';
import PropertyDescriptorAction from './PropertyDescriptorAction';
import { setCachedGetter } from '../helpers/descriptors';

const ARRAY_PROTOTYPE_CODE = 1;

function toPropertiesArray(session, obj, mapFn) {
  return Object.keys(obj).reduce((arr, property) => {
    arr.push(session.dispatch(property), mapFn(obj[property], property));
    return arr;
  }, []);
}

function isValidPropertiesArray(propsArr) {
  return Array.isArray(propsArr) && propsArr.length % 2 === 0;
}

function fromPropertiesArray(session, propsArr, forEachFn) {
  for (let i = 0; i < propsArr.length; i += 2) {
    const property = session.fetch(propsArr[i]);
    const value = session.fetch(propsArr[i + 1]);

    forEachFn(value, property);
  }
}

export default class RemoteSetObjectAction extends RemoteSetAction {
  static fromSnapshot(session, reference, snapshot) {
    const obj = snapshot.value;
    if (obj === null) return null;

    // Fetch order should be the same: first prototype then the descriptors
    const proto = session.dispatch(this.getPrototype(snapshot));

    const descriptors = toPropertiesArray(
      session,
      snapshot.ownPropertyDescriptors,
      desc => PropertyDescriptorAction.fromPropertyDescriptor(session, desc)
    );

    const isExtensible = Object.isExtensible(obj);

    const cachedGetters = toPropertiesArray(
      session,
      snapshot.cachedGetters,
      cachedValue => session.dispatch(cachedValue)
    );

    return new this(reference, descriptors, proto, isExtensible, cachedGetters);
  }

  static getPrototype(snapshot) {
    const proto = snapshot.prototype;

    if (proto === Object.prototype) {
      return null;
    }

    if (proto === Array.prototype) {
      return ARRAY_PROTOTYPE_CODE;
    }

    return proto;
  }

  constructor(
    reference,
    descriptors = [],
    proto = null,
    isExtensible = true,
    cachedGetters = []
  ) {
    if (!isValidPropertiesArray(descriptors)) {
      throw new TypeError(`Invalid descriptors: ${descriptors}`);
    }

    if (typeof proto !== 'object') {
      if (proto !== ARRAY_PROTOTYPE_CODE) {
        throw new TypeError('Object prototype may only be an Object or null');
      }
    }

    if (typeof isExtensible !== 'boolean') {
      throw new TypeError('Expect "isExtensible" to be boolean');
    }

    if (!isValidPropertiesArray(cachedGetters)) {
      throw new TypeError(`Invalid "cachedGetters" param: ${cachedGetters}`);
    }

    super(reference);

    this.descriptors = descriptors;
    this.proto = proto;
    this.isExtensible = isExtensible;
    this.cachedGetters = cachedGetters;
  }

  // eslint-disable-next-line class-methods-use-this
  createInstance() {
    if (this.proto === ARRAY_PROTOTYPE_CODE) {
      return [];
    }

    return {};
  }

  populateInstance(session, instance) {
    // Fetch order the same as dispatch order (ie. first prototype then descriptors)
    // @see {@link RemoteSetObjectAction.fromValue}

    const proto = session.fetch(this.proto);
    if (proto !== null && typeof proto === 'object') {
      Object.setPrototypeOf(instance, proto);
    }

    fromPropertiesArray(session, this.descriptors, (descriptor, property) => {
      Object.defineProperty(instance, property, descriptor);
    });

    fromPropertiesArray(session, this.cachedGetters, (value, property) => {
      setCachedGetter(instance, property, value);
    });
  }

  toArgumentsList() {
    const argumentsList = [
      this.reference,
      this.descriptors,
      this.proto,
      this.isExtensible,
      this.cachedGetters,
    ];
    const defaults = [undefined, [], null, true, []];

    return trimArgumentsList(argumentsList, defaults);
  }
}

RemoteSetAction.types.object = RemoteSetObjectAction;