barneycarroll / patchinko

A terse API for performing deep patching on JavaScript structures
MIT License
73 stars 5 forks source link

Skip patching for Array-input patch-scopes, replace instead #21

Open barneycarroll opened 5 years ago

barneycarroll commented 5 years ago

The kitchen sink demo shows a contrived example whereby within constant mode we use patch-scope to copy then patch an array using a numerically-keyed object.

At the time this was meant to illustrate the low-level simplicity of the underlying algorithm, but when using Patchinko in flavoured mode (constant or immutable), for the purposes of holistic state management, this makes no sense at all.

Lists in application data models tend to be holistic entities: the idea that one would simply merge a short array into a longer array would be a very particular use case; far more intuitive that one would want to replace the array contents wholesale, with immutable replacing the array itself and constant simply splicing out every item.

Before: treat arrays as hashes, iterate over input and patch corresponding keys After: immutable: replace arrays; constant: target.splice(0, Infinity, input)

kristianmandrup commented 5 years ago

Would love to see patchinko in combination with atama (proxy based change management) https://github.com/franciscop/atama

Engine: https://github.com/franciscop/atama/blob/master/src/engine.js

See how the proxify method proxies each part of the state recursively... thus also handling array in an elegant non-intrusive way that "just works"

    if (Array.isArray(value)) {
      value = value.map((value, property, target) => {
        return proxify(value, [...stack, { target, property, value }]);
      });
    }
kristianmandrup commented 5 years ago

To make Atama configurable to suit your valid scenario of having the array as "one logical unit", we could make the Atama engine configurable with factory functions:

const $createProxify = target => {
  return (value, stack) => {
    if (basicTypes.includes(typeof value) || value === null) {
      return value;
    }
    if (Array.isArray(value)) {
      value = value.map((value, property, target) => {
        return proxify(value, [...stack, { target, property, value }]);
      });
    }
    if (/^\{/.test(JSON.stringify(value))) {
      for (let property in value) {
        const current = { target, property, value: value[property] };
        value[property] = proxify(value[property], [...stack, current]);
      }
    }

    return new Proxy(value, {
      get: getProxy(stack),
      set: setProxy(stack),
      deleteProperty: delProxy(stack)
    });
  };
};

// Set values
const createSetProxy = (options = {}) => {
  const createProxify = options.createProxify || $createProxify;
  return (stack = []) => (target, property, value) => {
    // Log it into the history
    const type = typeof target[property] === "undefined" ? "create" : "update";
    history.add({ type, key: getKey([...stack, property]), value });

    // First of all set it in the beginning
    target[property] = value;

    const proxify = createProxify(target, options);

    // Proxify the value in-depth
    target[property] = proxify(value, [...stack, { target, property, value }]);

    // Trigger the root listener for any change
    listeners.forEach(one => one(state));

    return true;
  };
};

const $createState = (options = {}) => {
  return new Proxy(
    {},
    {
      get: getProxy(),
      set: createSetProxy(options),
      deleteProperty: delProxy()
    }
  );
};

const createEngine = (options = {}) => {
  const engine = {};
  const createState = options.createState || $createState;
  const state = createState(options);

  engine.attach = arg => {
    if (detached.length) {
      listeners.push(...detached.splice(0, detached.length));
    }
    return state;
  };

  engine.detatch = temp => {
    detached.push(...listeners.splice(0, listeners.length));
    // TODO: build the raw tree here
    return state;
  };

  engine.listen = cb => listeners.push(cb);
};

export default createEngine;

Then we can supply our own createProxify where it handles the array as a single unit. Your thoughts?

kristianmandrup commented 5 years ago

Made it a little easier to customize:

const $isPrimitive = value =>
  basicTypes.includes(typeof value) || value === null;

const createProxifyPrimitive = (options = {}) => {
  const isPrimitive = options.isPrimitive || $isPrimitive;
  return value => {
    if (!isPrimitive) return;
    return value;
  };
};

const isArray = !Array.isArray(value);

const $proxifyValues = (value, stack) => {
  return value.map((value, property, target) => {
    return proxify(value, [...stack, { target, property, value }]);
  });
};

const createProxifyArray = (options = {}) => {
  const proxifyValues = options.proxifyValues || $proxifyValues;
  return (value, stack) => {
    if (!isArray(value)) return;
    return proxifyValues(value, stack);
  };
};

const isJson = value => /^\{/.test(JSON.stringify(value));

const $proxifyJsonValues = (value, stack) => {
  for (let property in value) {
    const current = { target, property, value: value[property] };
    value[property] = proxify(value[property], [...stack, current]);
  }
};

const createProxifyJson = (options = {}) => {
  const proxifyJsonValues = options.proxifyJsonValues || $proxifyJsonValues;
  return value => {
    if (!isJson(value)) return;
    proxifyJsonValues(value);
  };
};

const createProxifyComplex = (options = {}) => {
  const createProxy = options.createProxy || $createProxy;
  return (value, stack, options) => {
    const { createProxifyJson } = Object.assign(typeProxifiers, options);
    const proxifyJson = createProxifyJson(options);
    proxifyJson(value);
    return createProxy(value, stack, options);
  };
};

const typeProxifiers = {
  createProxifyPrimitive,
  createProxifyArray,
  createProxifyJson,
  createProxifyComplex
};

const $createProxy = (value, stack, options) => {
  return new Proxy(value, {
    get: getProxy(stack),
    set: createSetProxy(stack, options),
    deleteProperty: delProxy(stack)
  });
};

const $createProxify = (options = {}) => {
  const {
    createProxifyPrimitive,
    createProxifyArray,
    createProxifyComplex
  } = Object.assign(typeProxifiers, options);
  const proxifyPrimitive = createProxifyPrimitive(options);
  const proxifyArray = createProxifyArray(options);
  const proxifyComplex = createProxifyComplex(options);

  return (value, stack) => {
    return (
      proxifyPrimitive(value) ||
      proxifyArray(value, options) ||
      proxifyComplex(value, stack, options)
    );
  };
};

// Set values
const createSetProxy = (options = {}) => {
  const createProxify = options.createProxify || $createProxify;
  return (stack = []) => (target, property, value) => {
    // Log it into the history
    const type = typeof target[property] === "undefined" ? "create" : "update";
    history.add({ type, key: getKey([...stack, property]), value });

    // First of all set it in the beginning
    target[property] = value;

    const proxify = createProxify(options);

    // Proxify the value in-depth
    target[property] = proxify(value, [...stack, { target, property, value }]);

    // Trigger the root listener for any change
    listeners.forEach(one => one(state));

    return true;
  };
};

const $createState = (options = {}) => {
  const createProxy = options.createProxy || $createProxy;
  return createProxy({}, undefined, options);
};

const createEngine = (options = {}) => {
  const engine = {};
  const createState = options.createState || $createState;
  const state = createState(options);

  engine.attach = arg => {
    if (detached.length) {
      listeners.push(...detached.splice(0, detached.length));
    }
    return state;
  };

  engine.detatch = temp => {
    detached.push(...listeners.splice(0, listeners.length));
    // TODO: build the raw tree here
    return state;
  };

  engine.listen = cb => listeners.push(cb);
};

export default createEngine;

To customize using array as single entity

createEngine({
  proxifyValues: (value) => value
})

Otherwise each value in the array is proxified as well