Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
barneycarroll opened this issue Nov 13, 2018 · 3 comments
Open

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

barneycarroll opened this issue Nov 13, 2018 · 3 comments

Comments

@barneycarroll
Copy link
Owner

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
Copy link

kristianmandrup commented Jan 10, 2019

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
Copy link

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
Copy link

kristianmandrup commented Jan 10, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants